//! Sub-cycle A smoke coverage for the three Phase 5 ports.
//!
//! Verifies the trait contracts at the in-memory adapter boundary —
//! happy / reject / transient path per axis. Sub-cycle B (5.1-5.4)
//! reuses these adapters to drive engine integration tests; this file
//! is the standalone substrate-only verification.

mod common;

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

use common::{MemoryEpochRevocation, MemoryReplayDefense, MemorySessionRevocation};
use ppoppo_token::{
    EpochRevocation, EpochRevocationError, ReplayDefense, ReplayDefenseError, SessionRevocation,
    SessionRevocationError,
};

// ── M35 ReplayDefense ────────────────────────────────────────────────────

#[tokio::test]
async fn replay_first_observation_admits() {
    let port = MemoryReplayDefense::new();
    let result = port
        .check_and_record("hash-A", Duration::from_secs(60))
        .await;
    assert!(result.is_ok(), "first observation must admit, got {result:?}");
}

#[tokio::test]
async fn replay_second_observation_within_ttl_rejects() {
    let port = MemoryReplayDefense::new();
    let _ = port
        .check_and_record("hash-A", Duration::from_secs(60))
        .await;
    let result = port
        .check_and_record("hash-A", Duration::from_secs(60))
        .await;
    assert!(
        matches!(result, Err(ReplayDefenseError::Replayed)),
        "second observation must reject as Replayed, got {result:?}",
    );
}

#[tokio::test]
async fn replay_substrate_transient_surfaces_typed_error() {
    let port = MemoryReplayDefense::failing();
    let result = port
        .check_and_record("hash-A", Duration::from_secs(60))
        .await;
    assert!(
        matches!(result, Err(ReplayDefenseError::Transient(_))),
        "failing substrate must surface Transient (engine fail-closes), got {result:?}",
    );
}

// ── M36 SessionRevocation ────────────────────────────────────────────────

#[tokio::test]
async fn session_default_returns_active() {
    let port = MemorySessionRevocation::new();
    let result = port.is_active("sub-X", "sid-Y").await;
    assert_eq!(
        result.ok(),
        Some(true),
        "default = active when not explicitly revoked",
    );
}

#[tokio::test]
async fn session_revoked_returns_inactive() {
    let port = MemorySessionRevocation::new();
    port.revoke("sub-X", "sid-Y");
    let result = port.is_active("sub-X", "sid-Y").await;
    assert_eq!(
        result.ok(),
        Some(false),
        "revoke() must flip to false (row deletion = revocation)",
    );
}

#[tokio::test]
async fn session_revoke_does_not_affect_siblings() {
    // M36 must kick exactly the targeted (sub, sid) pair — sibling
    // sessions stay alive. This is the axis distinction from sv
    // (account-wide epoch); a sibling test guards against accidental
    // collapse.
    let port = MemorySessionRevocation::new();
    port.revoke("sub-X", "sid-Y");
    let sibling = port.is_active("sub-X", "sid-Z").await;
    assert_eq!(sibling.ok(), Some(true), "sibling session must stay alive");
}

#[tokio::test]
async fn session_substrate_transient_surfaces_typed_error() {
    let port = MemorySessionRevocation::failing();
    let result = port.is_active("sub-X", "sid-Y").await;
    assert!(
        matches!(result, Err(SessionRevocationError::Transient(_))),
        "failing substrate must surface Transient, got {result:?}",
    );
}

// ── sv-port EpochRevocation ──────────────────────────────────────────────

#[tokio::test]
async fn epoch_default_zero() {
    let port = MemoryEpochRevocation::new();
    let result = port.current("sub-X").await;
    assert_eq!(result.ok(), Some(0), "genesis convention = 0 for unknown sub");
}

#[tokio::test]
async fn epoch_bump_returns_new_value() {
    let port = MemoryEpochRevocation::new();
    port.bump("sub-X", 5);
    let result = port.current("sub-X").await;
    assert_eq!(
        result.ok(),
        Some(5),
        "after bump, current() returns the new epoch",
    );
}

#[tokio::test]
async fn epoch_substrate_transient_surfaces_typed_error() {
    let port = MemoryEpochRevocation::failing();
    let result = port.current("sub-X").await;
    assert!(
        matches!(result, Err(EpochRevocationError::Transient(_))),
        "failing substrate must surface Transient, got {result:?}",
    );
}

// ── Cross-port: dyn-compatibility / Arc<dyn ...> compile guard ───────────

#[tokio::test]
async fn ports_are_dyn_compatible_via_arc() {
    // Compile-time guard: each trait can be erased behind `Arc<dyn ...>`
    // so VerifyConfig's slot type works. RPIT-in-trait would have broken
    // this — `async_trait` is the right choice for Phase 5.
    let _r: Arc<dyn ReplayDefense> = Arc::new(MemoryReplayDefense::new());
    let _s: Arc<dyn SessionRevocation> = Arc::new(MemorySessionRevocation::new());
    let _e: Arc<dyn EpochRevocation> = Arc::new(MemoryEpochRevocation::new());
}
