//! M38 transport equivalence — engine layer (Phase 5 commit 5.4).
//!
//! Per STANDARDS_JWT_DETAILS_MITIGATION §E: "Cookie + Bearer header
//! treated as the same surface (no privileged cookie)". The engine
//! enforces this *structurally* — `verify(token: &str, cfg, key_set)`
//! has no transport-hint parameter, no `check_*` submodule reads
//! anything beyond those three inputs.
//!
//! This file is the regression guard: tests that demonstrate the
//! engine has neither auto-stripping logic (Bearer / cookie attrs) nor
//! hidden transport-aware state (deterministic across calls).
//!
//! ── Why these tests live in ppoppo-token (not chat-auth) ───────────────
//!
//! The *real* M38 test — "cookie extraction path and Bearer extraction
//! path produce identical Claims" — uses chat-auth's and pas-external's
//! actual extraction helpers, which import ppoppo-token (not the other
//! way around). That property test belongs in those crates' test
//! trees. Here, we lock the *engine* invariant: bare token in →
//! Claims out, byte-identical regardless of imagined transport origin.

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

use std::time::Duration;

use ppoppo_token::{IssueConfig, IssueRequest, 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";

fn mint_token(signer: &SigningKey) -> String {
    let issue_cfg = IssueConfig::access_token(ISSUER, AUDIENCE, signer.kid());
    let req = IssueRequest::new(TEST_SUB, TEST_CLIENT_ID, TTL_15M);
    issue(&req, &issue_cfg, signer).expect("issue should succeed")
}

#[tokio::test]
async fn engine_verify_is_deterministic_across_repeated_calls() {
    // M38 supports the no-hidden-state invariant: two calls with
    // identical (token, cfg, key_set) produce identical Claims. If the
    // engine cached a "last seen transport" or otherwise carried
    // request-scoped state, future requests through different transports
    // could observe stale state — that's the M38 violation.
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    let claims_a = verify(&token, &cfg, &key_set).await.expect("first verify");
    let claims_b = verify(&token, &cfg, &key_set).await.expect("second verify");
    let claims_c = verify(&token, &cfg, &key_set).await.expect("third verify");

    assert_eq!(claims_a, claims_b, "engine must be deterministic across calls");
    assert_eq!(claims_b, claims_c, "engine must be deterministic across calls");
}

#[tokio::test]
async fn engine_does_not_strip_bearer_prefix() {
    // The bare token verifies; the same token wrapped in "Bearer "
    // (the wire form an HTTP middleware would receive in
    // `Authorization: Bearer <token>`) does NOT verify, because the
    // engine treats input verbatim. Stripping is the consumer
    // middleware's responsibility — confirmed by this test failing if
    // someone ever adds `token.strip_prefix("Bearer ")` to verify().
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    verify(&token, &cfg, &key_set).await.expect("bare token must verify");

    let bearer_wrapped = format!("Bearer {token}");
    let result = verify(&bearer_wrapped, &cfg, &key_set).await;
    assert!(
        result.is_err(),
        "engine MUST NOT auto-strip 'Bearer ' prefix — consumer middleware extracts \
         the bare token before calling verify(). got Ok: {result:?}",
    );
}

#[tokio::test]
async fn engine_does_not_strip_cookie_attribute_prefix() {
    // Cookie wire form: `access_token=<token>; Path=/; Secure`. The
    // engine rejects this because it treats input verbatim — the
    // consumer middleware (chat-auth's cookie extractor) is responsible
    // for parsing the cookie header and handing the bare token down.
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    let cookie_wrapped = format!("access_token={token}; Path=/; Secure");
    let result = verify(&cookie_wrapped, &cfg, &key_set).await;
    assert!(
        result.is_err(),
        "engine MUST NOT parse cookie attributes — consumer middleware does that. \
         got Ok: {result:?}",
    );
}

#[tokio::test]
async fn engine_does_not_strip_url_query_prefix() {
    // Query-string wire form: `?access_token=<token>` (anti-pattern but
    // possible). Engine rejects — consumer middleware decides whether
    // to even support query-string token transport (RCW/CTW do not).
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    let query_wrapped = format!("?access_token={token}");
    let result = verify(&query_wrapped, &cfg, &key_set).await;
    assert!(
        result.is_err(),
        "engine MUST NOT URL-decode or strip query attrs. got Ok: {result:?}",
    );
}

/// Compile-time signature witness — existence proves `verify` accepts
/// exactly `(&str, &VerifyConfig, &KeySet)` and returns the right
/// future type. If anyone refactors verify to accept a transport-hint
/// argument (or otherwise changes the signature), this witness fails
/// to compile and forces an update of the M38 invariant doc on
/// `engine::verify` first. The function is intentionally never
/// called — its compile success is the regression guard.
#[allow(dead_code)]
async fn _verify_signature_witness_for_m38<'a>(
    token: &'a str,
    cfg: &'a VerifyConfig,
    key_set: &'a ppoppo_token::KeySet,
) -> Result<ppoppo_token::Claims, ppoppo_token::AuthError> {
    verify(token, cfg, key_set).await
}

#[tokio::test]
async fn engine_does_not_strip_whitespace() {
    // Whitespace prefix (e.g. " eyJ..." from a sloppy header parser
    // upstream) is not silently stripped — the engine treats input
    // verbatim. If a future change adds `token.trim()` inside verify,
    // this test catches it; the engine should remain transport-blind
    // and let the consumer middleware normalize.
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);

    let leading_space = format!(" {token}");
    let result = verify(&leading_space, &cfg, &key_set).await;
    assert!(
        result.is_err(),
        "engine MUST NOT trim whitespace — consumer middleware does that. \
         got Ok: {result:?}",
    );
}
