//! End-to-end JWKS round-trip — sign with `SigningKey`, publish the
//! public half via `Jwks`, deserialize it back into `KeySet`, verify
//! the token through `engine::verify`.
//!
//! This is the integration that Phase 6.4 (chat-auth) and the future
//! pas-external SDK both rely on at runtime: PAS publishes a JWKS,
//! consumers fetch + parse + populate `KeySet`, the engine verifies.
//! A regression here would silently break every cross-service auth
//! call after the cutover.

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use std::time::Duration;

use base64::Engine;
use ppoppo_token::{
    IssueConfig, IssueRequest, Jwks, SigningKey, VerifyConfig, issue, verify,
};

const TEST_SUB: &str = "01HSAB00000000000000000000";
const TEST_CLIENT_ID: &str = "ppoppo-internal";
const TTL_15M: Duration = Duration::from_secs(900);
const ISSUER: &str = "https://accounts.ppoppo.com";
const AUDIENCE: &str = "ppoppo";

/// Same public-key DER as `SigningKey::test_pair` exposes via the
/// matching PEM constant. JWKS-side flow needs the raw 32-byte half
/// (last 32 bytes of the 44-byte SPKI DER).
fn test_pair_public_key_bytes() -> [u8; 32] {
    const TEST_PUBLIC_KEY_DER_B64: &str = "MCowBQYDK2VwAyEAh//e6j3It3xhjghg8Kpn2pM0jMCH/cvemGu4vv7D1Q4=";
    let der = base64::engine::general_purpose::STANDARD
        .decode(TEST_PUBLIC_KEY_DER_B64)
        .unwrap();
    der[12..].try_into().unwrap()
}

#[tokio::test]
async fn sign_publish_via_jwks_then_verify_round_trips() {
    // 1. Mint a token with the test_pair signing half.
    let (signer, _direct_key_set) = SigningKey::test_pair();
    let issue_cfg = IssueConfig::access_token(ISSUER, AUDIENCE, signer.kid());
    let req = IssueRequest::new(TEST_SUB, TEST_CLIENT_ID, TTL_15M);
    let token = issue(&req, &issue_cfg, &signer).expect("issue must succeed");

    // 2. Build a JWKS document carrying ONLY the corresponding public half.
    //    This simulates the consumer-side path: chat-auth never sees the
    //    private key — it fetches JWKS from PAS and trusts whatever lands.
    let jwks = Jwks::from_ed25519_keys(&[(signer.kid(), &test_pair_public_key_bytes())]);

    // 3. Serialize the JWKS to JSON, then back — proves the over-the-wire
    //    shape is what the engine consumes (no in-memory shortcut).
    let on_wire = serde_json::to_string(&jwks).unwrap();
    let parsed: Jwks = serde_json::from_str(&on_wire).unwrap();

    // 4. Convert into engine `KeySet` and verify the token.
    let key_set = parsed
        .into_key_set()
        .expect("JWKS must convert to KeySet");
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    let claims = verify(&token, &cfg, &key_set)
        .await
        .expect("token signed by test_pair must verify against JWKS-derived KeySet");

    assert_eq!(claims.sub, TEST_SUB);
    assert_eq!(claims.client_id, TEST_CLIENT_ID);
}

#[tokio::test]
async fn jwks_with_unknown_kid_rejects_token() {
    // Negative path: PAS rotates a key out of the JWKS but a stale token
    // with the old kid arrives. Engine must refuse with M12 (KidUnknown)
    // because the consumer's KeySet (built from the current JWKS) no
    // longer carries that kid.
    let (signer, _) = SigningKey::test_pair();
    let issue_cfg = IssueConfig::access_token(ISSUER, AUDIENCE, signer.kid());
    let req = IssueRequest::new(TEST_SUB, TEST_CLIENT_ID, TTL_15M);
    let token = issue(&req, &issue_cfg, &signer).expect("issue");

    // Publish a JWKS that does NOT contain the signing kid (rotation case).
    let jwks = Jwks::from_ed25519_keys(&[("k4.different-kid", &test_pair_public_key_bytes())]);
    let key_set = jwks.into_key_set().expect("convert");
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    let result = verify(&token, &cfg, &key_set).await;
    assert_eq!(
        result,
        Err(ppoppo_token::AuthError::KidUnknown),
        "rotated-out kid must reject as KidUnknown",
    );
}
