//! End-to-end round-trip — `issue()` output must verify cleanly under
//! `verify()` for every surface the engine serves.
//!
//! Three surfaces in production:
//!
//! - **PAS self** — PAS validates its own access token (introspection,
//!   `/userinfo`, admin endpoints). aud = `https://accounts.ppoppo.com`.
//! - **PCS** — PCS validates an access token issued by PAS for chat
//!   operations. aud = `ppoppo`.
//! - **External** — RCW / CTW / external developer apps validate access
//!   tokens issued for their OAuth client. aud = the registered
//!   client_id (e.g. `ppoppo-external-app-id`).
//!
//! Each surface gets its own test so an audience-binding regression on
//! ONE surface fails its dedicated test rather than masking inside a
//! shared fixture. The 4th test (`round_trip_multi_aud`) exercises the
//! M22 array-form acceptance path that the verify side already covers
//! in `tests/jwt_negative.rs::test_accept_aud_array_match`.
//!
//! Pin: `IssueConfig::audiences: Vec<String>` and
//! `VerifyConfig::audience: String` are intentionally asymmetric. The
//! verify side accepts a single expected audience; the issue side can
//! emit one OR many; M21 (string match) and M22 (array contains) cover
//! both shapes. Single-aud round-trips emit RFC 9068's string form;
//! multi-aud emits the array form.

#![allow(clippy::unwrap_used)]

use std::time::Duration;

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

// Phase 3 used `01HSUB...` because "SUB" spells the concept, but `U` is
// excluded from Crockford base32 (look-alike of `V`). Phase 4 commit 4.1
// (M39) enforces ULID format, so the fixture must actually parse. `S A B`
// are all valid Crockford characters.
const TEST_SUB: &str = "01HSAB00000000000000000000";
const TEST_CLIENT_ID: &str = "ppoppo-internal";
const TTL_15M: Duration = Duration::from_secs(900);

#[tokio::test]
async fn round_trip_pas_self() {
    // PAS validates its own access tokens — `/userinfo`, introspection,
    // admin endpoints. Issuer matches verifier. aud is the PAS canonical
    // self-audience.
    let (signer, key_set) = SigningKey::test_pair();
    let issuer = "https://accounts.ppoppo.com";
    let audience = "https://accounts.ppoppo.com";

    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 should succeed");

    let verify_cfg = VerifyConfig::access_token(issuer, audience);
    let claims = verify(&token, &verify_cfg, &key_set)
        .await
        .expect("verify should succeed");

    assert_eq!(claims.iss, issuer);
    assert_eq!(claims.sub, TEST_SUB);
    assert_eq!(claims.client_id, TEST_CLIENT_ID);
    assert_eq!(claims.exp - claims.iat, 900);
    assert_eq!(claims.nbf, Some(claims.iat));
    // jti must be a 26-char ULID. Fully audited via `Ulid::from_string`
    // would be redundant — the issue path generates it via `Ulid::new`,
    // and `engine::encode::tests::pinned_jti_overrides_engine_generation`
    // already pins the generation behavior.
    assert_eq!(claims.jti.len(), 26);
}

#[tokio::test]
async fn round_trip_pcs() {
    // PCS validates a PAS-issued access token for chat operations.
    // Issuer = PAS, aud = `ppoppo`. This is the canonical 1st-party
    // PAS-to-PCS handoff.
    let (signer, key_set) = SigningKey::test_pair();
    let issuer = "https://accounts.ppoppo.com";
    let audience = "ppoppo";

    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 should succeed");

    let verify_cfg = VerifyConfig::access_token(issuer, audience);
    let claims = verify(&token, &verify_cfg, &key_set)
        .await
        .expect("verify should succeed");

    assert_eq!(claims.iss, issuer);
    assert_eq!(claims.sub, TEST_SUB);
    // The cross-surface contract: the verifier's accepted aud must
    // round-trip cleanly without leaking on the public Claims surface.
    // (Phase 2 Decision 1 — Claims has no aud field; the binding is
    // proven implicitly by verify's success.)
}

#[tokio::test]
async fn round_trip_external() {
    // External developer app (RCW / CTW / 3rd-party) validates a token
    // issued for their OAuth client. Issuer = PAS, aud = registered
    // client identifier. client_id matches aud for self-issued tokens
    // but they remain conceptually distinct (aud = "where this token
    // is valid", client_id = "who originated this token").
    let (signer, key_set) = SigningKey::test_pair();
    let issuer = "https://accounts.ppoppo.com";
    let audience = "ppoppo-external-app-id";
    let client_id = "ppoppo-external-app-id";

    let issue_cfg = IssueConfig::access_token(issuer, audience, signer.kid());
    let req = IssueRequest::new(TEST_SUB, client_id, TTL_15M);
    let token = issue(&req, &issue_cfg, &signer).expect("issue should succeed");

    let verify_cfg = VerifyConfig::access_token(issuer, audience);
    let claims = verify(&token, &verify_cfg, &key_set)
        .await
        .expect("verify should succeed");

    assert_eq!(claims.client_id, client_id);
}

#[tokio::test]
async fn round_trip_multi_aud_array_form() {
    // Multi-audience tokens (rare, but supported). The issue side emits
    // an aud array; the verify side accepts the array via M22 if any
    // element matches `cfg.audience`.
    let (signer, key_set) = SigningKey::test_pair();
    let issuer = "https://accounts.ppoppo.com";

    let issue_cfg = IssueConfig::access_token(issuer, "primary", signer.kid())
        .with_audiences(vec!["primary".to_string(), "secondary".to_string()]);
    let req = IssueRequest::new(TEST_SUB, TEST_CLIENT_ID, TTL_15M);
    let token = issue(&req, &issue_cfg, &signer).expect("issue should succeed");

    // Verifier configured for `primary` accepts the array because
    // `primary` is in it.
    let verify_primary = VerifyConfig::access_token(issuer, "primary");
    verify(&token, &verify_primary, &key_set)
        .await
        .expect("primary in aud array — must verify");

    // Verifier configured for `secondary` also accepts.
    let verify_secondary = VerifyConfig::access_token(issuer, "secondary");
    verify(&token, &verify_secondary, &key_set)
        .await
        .expect("secondary in aud array — must verify");

    // Verifier configured for an audience NOT in the array rejects.
    let verify_bystander = VerifyConfig::access_token(issuer, "bystander");
    let result = verify(&token, &verify_bystander, &key_set).await;
    assert!(result.is_err(), "bystander not in aud — must reject");
}
