//! Engine integration — M35 jti replay defense (Phase 5 commit 5.1).
//!
//! Verifies the `engine::verify` ↔ `cfg.replay` wiring at the public
//! crate boundary. The standalone port behaviour (substrate-only) is
//! covered in `tests/ports_smoke.rs`; this file covers the integration
//! surface — `verify(token, cfg, key_set)` consults `cfg.replay` after
//! `check_domain` and surfaces typed `AuthError` variants.

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

mod common;

use std::sync::Arc;
use std::time::Duration;

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

/// Mint a fresh, valid token. Each call generates a distinct `jti`
/// (engine `Ulid::new`) so callers that want a sibling-guard fixture
/// get distinct tokens automatically.
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 replay_admits_when_cfg_replay_is_none() {
    // Port not wired = gate disabled. This is the "legacy" or
    // "sibling-test" config — VerifyConfig admits without consulting
    // any substrate. Same token verified twice both succeed.
    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("first verify");
    verify(&token, &cfg, &key_set).await.expect("second verify (no port = no gate)");
}

#[tokio::test]
async fn replay_admits_first_observation() {
    // Port wired, fresh substrate — first verify of a token succeeds
    // and records the jti hash. Engine returns `Claims` unchanged
    // (replay enforcement is a side-effect on the substrate).
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let port = Arc::new(MemoryReplayDefense::new());
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_replay_defense(port);

    let claims = verify(&token, &cfg, &key_set).await.expect("first verify must admit");
    assert_eq!(claims.sub, TEST_SUB);
}

#[tokio::test]
async fn replay_blocks_second_observation_with_jti_replayed() {
    // The TDD red-signal test: second verify of the same token within
    // TTL must reject with `JtiReplayed`. Port wires the substrate
    // state; engine consults it; reject path maps to the typed variant.
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let port = Arc::new(MemoryReplayDefense::new());
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_replay_defense(port);

    verify(&token, &cfg, &key_set).await.expect("first verify");
    let result = verify(&token, &cfg, &key_set).await;

    assert_eq!(
        result,
        Err(AuthError::JtiReplayed),
        "second verify of same token within TTL must reject as JtiReplayed",
    );
}

#[tokio::test]
async fn replay_admits_distinct_tokens_independently() {
    // Sibling guard: two distinct tokens (each with its own ULID jti +
    // distinct signature → distinct sha256(token)) verified through the
    // same substrate must both succeed. Regression against accidental
    // axis collapse (e.g. a misimplementation that keys by `sub` would
    // reject the second token here even though it's a different jti).
    let (signer, key_set) = SigningKey::test_pair();
    let token_a = mint_token(&signer);
    let token_b = mint_token(&signer);
    assert_ne!(token_a, token_b, "test fixture: distinct tokens required");

    let port = Arc::new(MemoryReplayDefense::new());
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_replay_defense(port);

    verify(&token_a, &cfg, &key_set).await.expect("token A");
    verify(&token_b, &cfg, &key_set).await.expect("token B (independent jti)");
}

#[tokio::test]
async fn replay_substrate_transient_maps_to_replay_cache_unavailable() {
    // Fail-closed posture verification: when the replay substrate is
    // unreachable, engine refuses (NOT admit-on-failure). The variant
    // is `ReplayCacheUnavailable`, distinct from `JtiReplayed` — audit
    // logs distinguish "active attack" from "infrastructure issue".
    let (signer, key_set) = SigningKey::test_pair();
    let token = mint_token(&signer);
    let port = Arc::new(MemoryReplayDefense::failing());
    let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_replay_defense(port);

    let result = verify(&token, &cfg, &key_set).await;
    assert_eq!(
        result,
        Err(AuthError::ReplayCacheUnavailable),
        "failing substrate must map to ReplayCacheUnavailable, not admit",
    );
}

#[tokio::test]
async fn replay_dyn_compatibility_via_arc_in_verify_config() {
    // Type-system regression guard: `Arc<dyn ReplayDefense>` is the
    // VerifyConfig slot type. If someone refactors the trait to
    // dyn-incompatible (e.g. RPIT-in-trait), this fails to compile.
    let port: Arc<dyn ReplayDefense> = Arc::new(MemoryReplayDefense::new());
    let _cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_replay_defense(port);
}
