//! Zero-trust authentication primitives.
//!
//! Provides challenge-response authentication with zero-knowledge proofs,
//! proof-of-possession, and continuous session verification.
//!
//! # Session State Machine
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────────────┐
//! │                    ZERO-TRUST SESSION LIFECYCLE                         │
//! ├─────────────────────────────────────────────────────────────────────────┤
//! │                                                                         │
//! │                          ┌─────────────┐                                │
//! │                          │   Created   │                                │
//! │                          │  (Initial)  │                                │
//! │                          └──────┬──────┘                                │
//! │                                 │ generate_challenge()                  │
//! │                                 ▼                                       │
//! │                          ┌─────────────┐                                │
//! │                          │  Challenged │                                │
//! │                          │ (Awaiting   │                                │
//! │                          │  Response)  │                                │
//! │                          └──────┬──────┘                                │
//! │                                 │                                       │
//! │              ┌──────────────────┼──────────────────┐                    │
//! │              │ verify_proof()   │                  │ timeout            │
//! │              │ SUCCESS          │                  │                    │
//! │              ▼                  │                  ▼                    │
//! │       ┌─────────────┐           │           ┌─────────────┐             │
//! │       │   Active    │           │           │   Failed    │             │
//! │       │ (Verified)  │           │           │ (Rejected)  │             │
//! │       └──────┬──────┘           │           └─────────────┘             │
//! │              │                  │                                       │
//! │              │ needs_verification()?                                    │
//! │              │ (interval elapsed)                                       │
//! │              ▼                  │                                       │
//! │       ┌─────────────┐           │                                       │
//! │       │  Reverify   │───────────┘                                       │
//! │       │  (Pending)  │  re-challenge and verify                          │
//! │       └──────┬──────┘                                                   │
//! │              │                                                          │
//! │       ┌──────┴──────┐                                                   │
//! │       │ verify()    │                                                   │
//! │       ▼             ▼                                                   │
//! │ ┌───────────┐ ┌───────────┐                                             │
//! │ │ Upgraded  │ │Downgraded │                                             │
//! │ │  Trust    │ │  Trust    │                                             │
//! │ └─────┬─────┘ └─────┬─────┘                                             │
//! │       │             │                                                   │
//! │       └──────┬──────┘                                                   │
//! │              ▼                                                          │
//! │       ┌─────────────┐                                                   │
//! │       │   Expired   │  session_duration > max_lifetime                  │
//! │       │ (Terminal)  │                                                   │
//! │       └─────────────┘                                                   │
//! │                                                                         │
//! └─────────────────────────────────────────────────────────────────────────┘
//!
//! ┌─────────────────────────────────────────────────────────────────────────┐
//! │                         TRUST LEVEL TRANSITIONS                         │
//! ├─────────────────────────────────────────────────────────────────────────┤
//! │                                                                         │
//! │   ┌───────────┐    verify()     ┌───────────┐    verify()    ┌────────┐│
//! │   │ Untrusted │ ───────────────►│  Partial  │ ──────────────►│Trusted ││
//! │   │   (0)     │  (success)      │   (1)     │  (success)     │  (2)   ││
//! │   └─────┬─────┘                 └─────┬─────┘                └────┬───┘│
//! │         ▲                             ▲                          │    │
//! │         │         downgrade()         │        downgrade()       │    │
//! │         │◄────────────────────────────┼──────────────────────────┘    │
//! │         │        (verification fail)  │                               │
//! │         │                             │         verify()              │
//! │         │                             │         (success)             │
//! │         │                             ▼                               │
//! │         │                      ┌─────────────┐                        │
//! │         │                      │   Fully     │                        │
//! │         └──────────────────────│  Trusted    │                        │
//! │             (revoke)           │    (3)      │                        │
//! │                                └─────────────┘                        │
//! │                                                                       │
//! │   Trust Score: 0 (none) → 1 (partial) → 2 (trusted) → 3 (full)       │
//! └─────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! # Challenge-Response Protocol
//!
//! 1. **Challenge Generation**: Server generates random nonce with complexity level
//! 2. **Proof Construction**: Client creates ZK proof using private key + nonce
//! 3. **Verification**: Server verifies proof without learning private key
//! 4. **Session Update**: Trust level adjusted based on verification result

#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]

use crate::primitives::ec::ed25519::ED25519_PUBLIC_KEY_LEN;
use crate::types::traits::{
    ContinuousVerifiable, ProofOfPossession, VerificationStatus, ZeroTrustAuthenticable,
};
use crate::unified_api::{
    ProofComplexity, ZeroTrustConfig,
    error::{CoreError, Result},
};
use crate::{
    log_zero_trust_auth_failure, log_zero_trust_auth_success, log_zero_trust_challenge_generated,
    log_zero_trust_proof_verified, log_zero_trust_session_created, log_zero_trust_session_expired,
    log_zero_trust_session_verification_failed, log_zero_trust_session_verified,
    log_zero_trust_unverified_mode,
    types::{PrivateKey, PublicKey},
};

// Re-export TrustLevel from types module (pure Rust, no FFI deps)
pub use crate::types::zero_trust::TrustLevel;
use chrono::{DateTime, Duration, Utc};
use std::sync::Mutex;
use subtle::ConstantTimeEq;

// ============================================================================
// Security Mode for Unified API
// ============================================================================

/// Security mode for cryptographic operations.
///
/// This enum gates cryptographic operations on authentication state. Its purpose is
/// **session validation** — ensuring the caller has proven possession of a private key
/// before being allowed to perform crypto operations. It does NOT control algorithm
/// selection (that is handled by `CryptoConfig` or the explicit algorithm parameter).
///
/// This follows the industry pattern where authentication and algorithm selection are
/// separate concerns. No major crypto library (ring, RustCrypto, Tink, OpenSSL) couples
/// "trust level" to algorithm choice — the key type or config determines the algorithm,
/// and authentication is an orthogonal layer.
///
/// The `validate()` call IS the core purpose: verifying session validity before allowing
/// crypto operations. The `_unverified()` convenience functions exist for scenarios where
/// Zero Trust verification is not applicable (e.g., batch processing, testing, or systems
/// that handle authentication at a different layer).
///
/// # Usage
///
/// ```rust,no_run
/// # use latticearc::unified_api::{encrypt_aes_gcm, SecurityMode, VerifiedSession, generate_keypair};
/// # fn main() -> Result<(), latticearc::unified_api::error::CoreError> {
/// let (pk, sk) = generate_keypair()?;
///
/// // With Zero Trust verification (recommended)
/// let session = VerifiedSession::establish(pk.as_slice(), sk.expose_secret())?;
/// # let data = b"secret";
/// # let key = latticearc::primitives::rand::random_bytes(32);
/// let encrypted = encrypt_aes_gcm(data, &key, SecurityMode::Verified(&session))?;
///
/// // Without verification (opt-out)
/// let encrypted = encrypt_aes_gcm(data, &key, SecurityMode::Unverified)?;
/// # Ok(())
/// # }
/// ```
///
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum SecurityMode<'a> {
    /// Use a verified session for Zero Trust security.
    ///
    /// This mode:
    /// - Validates the session is not expired
    /// - Provides audit trail with session context
    /// - Recommended for all production use
    Verified(&'a VerifiedSession),

    /// Operate without session verification.
    ///
    /// This mode:
    /// - Skips session validation
    /// - Use only when Zero Trust is not applicable
    Unverified,
}

impl<'a> SecurityMode<'a> {
    /// Returns `true` if this is a verified security mode.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use latticearc::unified_api::{SecurityMode, VerifiedSession, generate_keypair};
    /// # fn main() -> Result<(), latticearc::unified_api::error::CoreError> {
    /// # let (pk, sk) = generate_keypair()?;
    /// # let session = VerifiedSession::establish(pk.as_slice(), sk.expose_secret())?;
    /// let mode = SecurityMode::Verified(&session);
    /// assert!(mode.is_verified());
    ///
    /// let mode = SecurityMode::Unverified;
    /// assert!(!mode.is_verified());
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn is_verified(&self) -> bool {
        matches!(self, Self::Verified(_))
    }

    /// Returns `true` if this is an unverified security mode.
    #[must_use]
    pub fn is_unverified(&self) -> bool {
        matches!(self, Self::Unverified)
    }

    /// Get the verified session if this is a `Verified` mode.
    ///
    /// Returns `None` for `Unverified` mode.
    #[must_use]
    pub fn session(&self) -> Option<&'a VerifiedSession> {
        match self {
            Self::Verified(session) => Some(session),
            Self::Unverified => None,
        }
    }

    /// Validate the security mode, checking session validity if verified.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::ZeroTrustVerificationFailed` if the session
    /// has been downgraded to `TrustLevel::Untrusted`.
    /// Returns `CoreError::SessionExpired` if the session is still
    /// trusted but past its policy lifetime.
    ///
    /// Returns `Ok(())` for `Unverified` mode (no validation performed).
    pub fn validate(&self) -> Result<()> {
        match self {
            Self::Verified(session) => {
                // trust_level must gate validation; otherwise
                // downgrade_trust_level() has no observable effect.
                if session.trust_level() == TrustLevel::Untrusted {
                    // Downgrade-rejection is NOT clock expiry; emitting
                    // `session_expired` here would inflate SIEM expiry
                    // counters on every policy-driven trust drop.
                    log_zero_trust_session_verification_failed!(
                        hex::encode(session.session_id()),
                        "trust_level downgraded to Untrusted"
                    );
                    // Generic public string; the discriminator
                    // ("trust_level downgraded to Untrusted") is in
                    // the tracing event above. Pattern-6 posture: the
                    // user-visible Err shape does not distinguish
                    // reject reasons.
                    return Err(CoreError::ZeroTrustVerificationFailed(
                        "zero-trust validation failed".to_string(),
                    ));
                }
                // `verify_valid()` already logs verified/expired
                // internally; do not re-log here.
                session.verify_valid()
            }
            Self::Unverified => {
                log_zero_trust_unverified_mode!("validate");
                Ok(())
            }
        }
    }
}

impl<'a> From<&'a VerifiedSession> for SecurityMode<'a> {
    fn from(session: &'a VerifiedSession) -> Self {
        Self::Verified(session)
    }
}

// `impl Default for SecurityMode` was REMOVED. The previous default
// returned `Unverified`, which let production code using
// `..Default::default()` silently disable Zero Trust validation —
// exactly the failure mode the type was designed to make impossible.
// Callers must now choose explicitly: `SecurityMode::Verified(&session)`
// for Zero Trust enforcement (recommended) or `SecurityMode::Unverified`
// for opt-out paths where ZT is not applicable. The choice now
// appears in source diffs and code review can catch accidents.

/// A verified session that proves Zero Trust authentication has been completed.
///
/// This type provides compile-time enforcement that Zero Trust verification has been
/// performed. It can only be created through successful authentication, ensuring
/// that cryptographic operations requiring a session have been properly authorized.
///
/// # Example
///
/// ```rust,no_run
/// # use latticearc::unified_api::{VerifiedSession, encrypt_aes_gcm, SecurityMode, generate_keypair};
/// # fn main() -> Result<(), latticearc::unified_api::error::CoreError> {
/// # let (public_key, private_key) = generate_keypair()?;
/// // Establish a verified session (performs challenge-response)
/// let session = VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
///
/// // Use the session for cryptographic operations
/// # let data = b"secret";
/// # let key = latticearc::primitives::rand::random_bytes(32);
/// let result = encrypt_aes_gcm(data, &key, SecurityMode::Verified(&session))?;
/// # Ok(())
/// # }
/// ```
pub struct VerifiedSession {
    /// Unique session identifier.
    session_id: [u8; 32],
    /// Timestamp when authentication was completed (wall-clock, for
    /// audit display only — never used for validity decisions).
    authenticated_at: DateTime<Utc>,
    /// Current trust level.
    trust_level: TrustLevel,
    /// Public key that was verified.
    public_key: PublicKey,
    /// When this session expires (wall-clock, for audit display only).
    expires_at: DateTime<Utc>,
    /// monotonic instant + lifetime drive `is_valid()`.
    /// NTP rollback can't extend a session past its policy lifetime.
    issued_at_monotonic: std::time::Instant,
    lifetime: std::time::Duration,
}

impl std::fmt::Debug for VerifiedSession {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("VerifiedSession")
            .field("session_id", &"[REDACTED]")
            .field("authenticated_at", &self.authenticated_at)
            .field("trust_level", &self.trust_level)
            .field("public_key", &"[REDACTED]")
            .field("expires_at", &self.expires_at)
            .finish()
    }
}

/// Default session lifetime in seconds (30 minutes).
const DEFAULT_SESSION_LIFETIME_SECS: i64 = 30 * 60;

// A zero or negative lifetime would silently become `u64::MAX`
// (i.e. non-expiring) after the i64→u64 cast in `from_authenticated`.
// Pin the invariant at compile time.
const _: () =
    assert!(DEFAULT_SESSION_LIFETIME_SECS > 0, "DEFAULT_SESSION_LIFETIME_SECS must be > 0",);

impl VerifiedSession {
    /// Quick session establishment for the common case.
    ///
    /// Performs a complete challenge-response authentication automatically,
    /// creating a verified session ready for use.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The default Zero Trust configuration is invalid
    /// - Random bytes for session ID or challenge cannot be generated
    /// - Proof generation or verification fails
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use latticearc::unified_api::{VerifiedSession, generate_keypair};
    /// # fn main() -> Result<(), latticearc::unified_api::error::CoreError> {
    /// # let (public_key, private_key) = generate_keypair()?;
    /// let session = VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
    /// assert!(session.is_valid());
    /// # Ok(())
    /// # }
    /// ```
    pub fn establish(public_key: &[u8], private_key: &[u8]) -> Result<Self> {
        // Create owned copies for the auth handler
        let pk: PublicKey = PublicKey::new(public_key.to_vec());
        let sk: PrivateKey = PrivateKey::new(private_key.to_vec());

        let auth = ZeroTrustAuth::new(pk, sk)?;
        let mut session = ZeroTrustSession::new(auth);

        // Self-authentication: prove we possess the private key.
        let challenge = session.initiate_authentication()?;
        // Reject a captured challenge-response replay on age grounds before
        // the proof is verified. Without this gate a captured pair could be
        // replayed indefinitely against the same `establish` call site.
        if !session.auth.verify_challenge_age(&challenge)? {
            return Err(CoreError::AuthenticationRequired(
                "Challenge expired during establish (replay protection)".to_string(),
            ));
        }
        let proof = session.auth.generate_proof(challenge.data())?;
        session.verify_response(&proof)?;

        session.into_verified()
    }

    /// Create a verified session from an authenticated `ZeroTrustSession`.
    ///
    /// This is the internal constructor used by `ZeroTrustSession::into_verified()`.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationRequired` if the session has not been
    /// successfully authenticated.
    pub(crate) fn from_authenticated(session: &ZeroTrustSession) -> Result<Self> {
        if !session.is_authenticated() {
            log_zero_trust_auth_failure!(
                "pending",
                "Session must be authenticated before creating VerifiedSession"
            );
            return Err(CoreError::AuthenticationRequired(
                "Session must be authenticated before creating VerifiedSession".to_string(),
            ));
        }

        // Generate unique session ID via the primitives layer.
        let session_id_vec = crate::primitives::security::generate_secure_random_bytes(32)
            .map_err(|_e| CoreError::EntropyDepleted {
                message: "Failed to generate session ID".to_string(),
                action: "Check system entropy source".to_string(),
            })?;
        let mut session_id = [0u8; 32];
        session_id.copy_from_slice(&session_id_vec);

        let now = Utc::now();
        let expires_at = now
            .checked_add_signed(Duration::seconds(DEFAULT_SESSION_LIFETIME_SECS))
            .ok_or_else(|| {
            CoreError::ConfigurationError(
                "Cannot compute session expiry: timestamp overflow".to_string(),
            )
        })?;

        let trust_level = TrustLevel::Trusted;

        // Log successful session creation
        let session_id_hex = hex::encode(session_id);
        log_zero_trust_session_created!(session_id_hex, trust_level, expires_at);
        log_zero_trust_auth_success!(session_id_hex, trust_level);

        // Capture a monotonic instant at construction. The wall-clock
        // `expires_at` above remains for audit display, but `is_valid`
        // consults `Instant::elapsed()` instead. The const_assert above
        // pins the constant > 0, so `as u64` produces the same numeric
        // value with no saturation surprise.
        let lifetime = std::time::Duration::from_secs(DEFAULT_SESSION_LIFETIME_SECS as u64);
        Ok(Self {
            session_id,
            authenticated_at: now,
            trust_level,
            public_key: session.auth.public_key.clone(),
            expires_at,
            issued_at_monotonic: std::time::Instant::now(),
            lifetime,
        })
    }

    /// Check if the session is still valid (not expired).
    ///
    /// A session is valid if the elapsed time since construction is
    /// strictly less than the configured lifetime.
    ///
    /// Uses a monotonic `Instant` so NTP rollback / system-clock
    /// manipulation cannot extend a session past its policy lifetime.
    /// `expires_at` / `authenticated_at` are wall-clock and kept for
    /// audit display only.
    #[must_use]
    pub fn is_valid(&self) -> bool {
        self.issued_at_monotonic.elapsed() < self.lifetime
    }

    /// Get the current trust level of this session.
    #[must_use]
    pub fn trust_level(&self) -> TrustLevel {
        self.trust_level
    }

    /// Downgrade the session's trust level. M3: previously the
    /// `trust_level` field was set once at construction (always to
    /// `Trusted`) and no public mutator existed, so the
    /// `Trusted -> Partial -> Untrusted` transitions described in the
    /// type-level documentation were unreachable. This method
    /// implements the downgrade path: callers (continuous-auth poll
    /// loops, integrity-check failures, anomaly detectors) can reduce
    /// trust as evidence accumulates.
    ///
    /// Only **monotonic downgrades** are permitted — `new_level` must
    /// be strictly lower than the current level (per `Ord` on
    /// `TrustLevel`, which orders `Untrusted < Partial < Trusted <
    /// FullyTrusted`). Attempting to upgrade or no-op yields
    /// `CoreError::InvalidParameter`. Re-acquiring trust requires a
    /// fresh authentication, not a setter.
    ///
    /// # Errors
    /// Returns `CoreError::InvalidParameter` when `new_level` is not
    /// strictly lower than the current trust level.
    pub fn downgrade_trust_level(&mut self, new_level: TrustLevel) -> Result<()> {
        if new_level >= self.trust_level {
            return Err(CoreError::InvalidInput(format!(
                "downgrade_trust_level: new={new_level:?} not strictly lower than \
                 current={current:?}; upgrades require re-authentication",
                current = self.trust_level,
            )));
        }
        let prev = self.trust_level;
        self.trust_level = new_level;
        let session_id_hex = hex::encode(self.session_id);
        tracing::warn!(
            session_id = %session_id_hex,
            previous = ?prev,
            new = ?new_level,
            "VerifiedSession trust level downgraded"
        );
        Ok(())
    }

    /// Get the unique session identifier for audit logging.
    #[must_use]
    pub fn session_id(&self) -> &[u8; 32] {
        &self.session_id
    }

    /// Get the public key associated with this session.
    #[must_use]
    pub fn public_key(&self) -> &PublicKey {
        &self.public_key
    }

    /// Get the timestamp when this session was authenticated.
    #[must_use]
    pub fn authenticated_at(&self) -> DateTime<Utc> {
        self.authenticated_at
    }

    /// Get the timestamp when this session expires.
    #[must_use]
    pub fn expires_at(&self) -> DateTime<Utc> {
        self.expires_at
    }

    /// Verify the session is still valid, returning an error if expired.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::SessionExpired` if the session has expired.
    pub fn verify_valid(&self) -> Result<()> {
        let session_id_hex = hex::encode(self.session_id);
        if self.is_valid() {
            log_zero_trust_session_verified!(session_id_hex);
            Ok(())
        } else {
            log_zero_trust_session_expired!(session_id_hex);
            Err(CoreError::SessionExpired)
        }
    }

    /// Create a copy of this session that is already expired.
    ///
    /// This is only available in tests so that code outside this module can
    /// exercise the `SessionExpired` error path without accessing private fields.
    #[cfg(test)]
    #[must_use]
    pub(crate) fn expired_clone(&self) -> Self {
        // force expiry via the monotonic path by using a
        // zero-duration lifetime — `Instant::elapsed()` will be > 0
        // immediately after construction. Wall-clock fields are also
        // pegged at the Unix epoch for audit-display consistency.
        Self {
            session_id: self.session_id,
            authenticated_at: self.authenticated_at,
            trust_level: self.trust_level,
            public_key: self.public_key.clone(),
            expires_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap_or_else(Utc::now),
            issued_at_monotonic: std::time::Instant::now(),
            lifetime: std::time::Duration::from_nanos(0),
        }
    }
}

// ============================================================================
// Zero Trust Authentication Handler
// ============================================================================

/// Zero-trust authentication handler.
///
/// Manages challenge-response authentication, proof generation and verification,
/// and continuous session monitoring.
pub struct ZeroTrustAuth {
    /// Public key for verification.
    pub(crate) public_key: PublicKey,
    /// Private key for proof generation.
    private_key: PrivateKey,
    /// Authentication configuration.
    config: ZeroTrustConfig,
    /// Session start timestamp.
    session_start: DateTime<Utc>,
    /// Timestamp of the most recent successful proof verification, or
    /// `None` if no proof has yet been verified for this auth instance.
    ///
    /// The `None` ↔ `Some` boundary is the precondition gate for
    /// [`Self::start_continuous_verification`]: starting a continuous
    /// session before any successful challenge-response would let a
    /// fresh `ZeroTrustAuth` skip the ZKP handshake entirely.
    ///
    /// `Mutex` (not `RefCell`) so the type stays `Sync` for use across
    /// async tasks and multi-threaded callers; an `Arc<ZeroTrustAuth>`
    /// shared between Tokio workers must compile.
    last_verification: Mutex<Option<DateTime<Utc>>>,
    /// Replay cache for proofs-of-possession.
    ///
    /// Keyed on `pk || sig || ts_micros_be` (microsecond precision —
    /// see the M16 follow-up note in `generate_pop` for why second
    /// precision was insufficient under Ed25519's deterministic
    /// signatures). Values are the wall-clock seconds-since-epoch at
    /// which the entry was inserted (used to bound cache lifetime to
    /// the 5-minute PoP freshness window).
    ///
    /// Carries a parallel `per_pk_counts` map so the per-PK quota check
    /// is O(1). Without it, the global cap alone would let a single
    /// noisy public key fill the cache and lock out every other PK
    /// (DoS against legitimate verifiers); with it, each PK is bounded
    /// to `POP_CACHE_PER_PK_MAX` entries and other PKs proceed normally.
    ///
    /// `Mutex` (not `RefCell`) so the type stays `Sync` for use across
    /// async/multi-threaded callers.
    pop_replay_cache: Mutex<PopReplayCache>,
}

/// Internal helper: a replay cache that tracks both the full
/// `(pk || sig || ts_micros)` entries and a per-PK count alongside.
/// The two maps are mutated together under the surrounding `Mutex` so
/// the per-PK count is always exact, never a stale approximation.
///
/// `pk_len` is fixed at construction (currently
/// [`ED25519_PUBLIC_KEY_LEN`]) and is the authoritative length of the
/// public-key prefix in every entry key. Mixing entries with different
/// PK lengths would break the inverse-of-insert assumption in
/// `expire_older_than` (the prefix slice would misidentify which entry
/// belongs to which PK), silently leak per-PK quota state, and degrade
/// the M2 fail-closed replay invariant. The `pk_len` field plus the
/// debug-assert in [`Self::insert`] enforce uniformity at the
/// API boundary so a future PoP type with a different PK length cannot
/// silently coexist with the current one — adding such a type must
/// either create a separate cache instance or extend the entry-key
/// encoding to disambiguate.
struct PopReplayCache {
    /// Authoritative public-key prefix length. Set once at
    /// construction; every `insert` debug-asserts that the supplied
    /// `pk_bytes` length matches this value.
    pk_len: usize,
    /// Full keys (PK || sig || ts_micros) → insertion timestamp (seconds).
    entries: std::collections::HashMap<Vec<u8>, i64>,
    /// Public-key bytes → count of `entries` belonging to that PK.
    /// Maintained in lockstep with `entries`; never read independently.
    per_pk_counts: std::collections::HashMap<Vec<u8>, usize>,
}

impl PopReplayCache {
    /// Construct an empty cache for entries whose PK prefix is exactly
    /// `pk_len` bytes. Today the only caller passes
    /// [`ED25519_PUBLIC_KEY_LEN`]; if a future PoP type uses a
    /// different PK length, give it its own `PopReplayCache` rather
    /// than mixing prefixes here.
    fn new(pk_len: usize) -> Self {
        Self {
            pk_len,
            entries: std::collections::HashMap::new(),
            per_pk_counts: std::collections::HashMap::new(),
        }
    }

    /// Remove entries older than `max_age_secs` and update per-PK counts
    /// to match. The leading `self.pk_len` bytes of every entry key are
    /// the public-key bytes; this is the inverse of how `insert` builds
    /// the key, and is only well-defined because `insert` debug-asserts
    /// every inserted `pk_bytes` matches `self.pk_len`.
    fn expire_older_than(&mut self, now_secs: i64, max_age_secs: i64) {
        let pk_len = self.pk_len;
        let entries = &mut self.entries;
        let per_pk = &mut self.per_pk_counts;
        entries.retain(|key, ts| {
            let fresh = now_secs.saturating_sub(*ts) <= max_age_secs;
            if !fresh
                && let Some(pk_bytes) = key.get(..pk_len)
                && let Some(count) = per_pk.get_mut(pk_bytes)
            {
                *count = count.saturating_sub(1);
                if *count == 0 {
                    per_pk.remove(pk_bytes);
                }
            }
            fresh
        });
    }

    fn contains(&self, key: &[u8]) -> bool {
        self.entries.contains_key(key)
    }

    fn count_for_pk(&self, pk_bytes: &[u8]) -> usize {
        self.per_pk_counts.get(pk_bytes).copied().unwrap_or(0)
    }

    fn total(&self) -> usize {
        self.entries.len()
    }

    fn insert(&mut self, pk_bytes: &[u8], full_key: Vec<u8>, ts_secs: i64) {
        // Guards the `pk_len` uniformity invariant the rest of this
        // type relies on. A mismatch here means the caller mixed PoP
        // types into one cache; the per-PK count would key off a
        // wrong-length prefix and `expire_older_than` would never
        // decrement it, silently leaking quota. Dev/CI surface only —
        // production stays branch-free in the hot path.
        debug_assert_eq!(
            pk_bytes.len(),
            self.pk_len,
            "PopReplayCache: pk_bytes.len()={} does not match cache pk_len={}; \
             mixing PoP key types in one cache silently breaks per-PK quota tracking",
            pk_bytes.len(),
            self.pk_len,
        );
        if self.entries.insert(full_key, ts_secs).is_none() {
            let slot = self.per_pk_counts.entry(pk_bytes.to_vec()).or_insert(0);
            *slot = slot.saturating_add(1);
        }
    }
}

impl ZeroTrustAuth {
    /// Creates a new `ZeroTrustAuth` instance with default configuration.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::ConfigurationError` if the default configuration is invalid
    /// (e.g., maximum security level without hardware acceleration, or speed preference
    /// without fallback enabled).
    pub fn new(public_key: PublicKey, private_key: PrivateKey) -> Result<Self> {
        let config = ZeroTrustConfig::default();
        config.validate()?;

        let now = Utc::now();
        Ok(Self {
            public_key,
            private_key,
            config,
            session_start: now,
            // `None` until the first successful proof verification —
            // see field doc.
            last_verification: Mutex::new(None),
            pop_replay_cache: Mutex::new(PopReplayCache::new(ED25519_PUBLIC_KEY_LEN)),
        })
    }

    /// Creates a new `ZeroTrustAuth` instance with the provided configuration.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::ConfigurationError` if:
    /// - The challenge timeout is zero.
    /// - Continuous verification is enabled but the verification interval is zero.
    /// - The base configuration is invalid (e.g., maximum security level without
    ///   hardware acceleration).
    pub fn with_config(
        public_key: PublicKey,
        private_key: PrivateKey,
        config: ZeroTrustConfig,
    ) -> Result<Self> {
        config.validate()?;

        let now = Utc::now();
        Ok(Self {
            public_key,
            private_key,
            config,
            session_start: now,
            last_verification: Mutex::new(None),
            pop_replay_cache: Mutex::new(PopReplayCache::new(ED25519_PUBLIC_KEY_LEN)),
        })
    }

    /// Generate a new challenge for authentication.
    ///
    /// # Errors
    /// Returns `CoreError::EntropyDepleted` if the system cannot generate random bytes.
    pub fn generate_challenge(&self) -> Result<Challenge> {
        let challenge_data = generate_challenge_data(&self.config.proof_complexity)?;
        log_zero_trust_challenge_generated!(self.config.proof_complexity);
        Ok(Challenge {
            data: challenge_data,
            timestamp: Utc::now(),
            complexity: self.config.proof_complexity.clone(),
            timeout_ms: self.config.challenge_timeout_ms,
        })
    }

    /// Verifies whether a challenge is still within its timeout period.
    ///
    /// # Errors
    ///
    /// This function does not currently return errors, but returns `Result` for
    /// API consistency and future extensibility.
    pub fn verify_challenge_age(&self, challenge: &Challenge) -> Result<bool> {
        let elapsed = Utc::now().signed_duration_since(challenge.timestamp());
        let elapsed_ms = elapsed.num_milliseconds();

        // Negative elapsed time means the challenge is from the future, which is invalid
        // Convert safely: negative values are treated as invalid (elapsed > timeout)
        let elapsed_u64 = u64::try_from(elapsed_ms).unwrap_or(u64::MAX);
        Ok(elapsed_u64 <= challenge.timeout_ms())
    }

    /// Starts a new continuous verification session.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationFailed` if no successful
    /// challenge-response has occurred for this `ZeroTrustAuth`
    /// instance. Continuous verification is supposed to *extend* a
    /// trust relationship that was already established cryptographically;
    /// without this gate, a freshly-constructed `ZeroTrustAuth` could
    /// hand out a `ContinuousSession` whose `is_valid()` returns true,
    /// bypassing the ZKP handshake entirely.
    pub fn start_continuous_verification(&self) -> Result<ContinuousSession> {
        let last_verified = match self.last_verification.lock() {
            Ok(g) => *g,
            Err(poisoned) => {
                tracing::warn!(
                    target: "latticearc::unified_api::zero_trust",
                    "ZeroTrustAuth.last_verification mutex was poisoned in \
                     start_continuous_verification; a previous writer panicked. \
                     Recovering inner guard."
                );
                *poisoned.into_inner()
            }
        };
        let last_verified = last_verified.ok_or_else(|| {
            CoreError::AuthenticationFailed(
                "start_continuous_verification requires a prior successful proof verification \
                 (call verify_proof() against a fresh challenge first)"
                    .to_string(),
            )
        })?;
        Ok(ContinuousSession {
            auth_public_key: self.public_key.clone(),
            start_time: Utc::now(),
            verification_interval_ms: self.config.verification_interval_ms,
            last_verification: last_verified,
        })
    }
}

impl ZeroTrustAuthenticable for ZeroTrustAuth {
    type Proof = ZeroKnowledgeProof;
    type Error = CoreError;

    /// Generates a zero-knowledge proof for the given challenge.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationFailed` if the challenge is empty.
    ///
    /// Returns `CoreError::InvalidKeyLength` if the private key has incorrect length
    /// for Ed25519 signing.
    ///
    /// Returns `CoreError::InvalidInput` if the private key format is invalid.
    fn generate_proof(&self, challenge: &[u8]) -> Result<Self::Proof> {
        if challenge.is_empty() {
            return Err(CoreError::AuthenticationFailed("Empty challenge".to_string()));
        }

        let proof_data = self.compute_proof_data(challenge)?;
        let timestamp = Utc::now();

        Ok(ZeroKnowledgeProof {
            challenge: challenge.to_vec(),
            proof: proof_data,
            timestamp,
            complexity: self.config.proof_complexity.clone(),
        })
    }

    /// Verifies a zero-knowledge proof against the given challenge.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationFailed` if the proof format is invalid
    /// (e.g., timestamp bytes cannot be extracted for Medium/High complexity proofs).
    ///
    /// Returns `CoreError::InvalidInput` if the public key or signature format is invalid
    /// during Ed25519 verification.
    ///
    /// Returns `CoreError::InvalidKeyLength` if the public key has incorrect length.
    fn verify_proof(
        &self,
        proof: &Self::Proof,
        challenge: &[u8],
    ) -> std::result::Result<bool, Self::Error> {
        // The prover writes `proof.complexity` from its own config but
        // the verifier previously only consulted
        // `self.config.proof_complexity` for dispatch — so a prover
        // claiming Low complexity could ship a proof verified under
        // the verifier's Medium policy (or vice versa). Pin the field
        // by requiring it to match the verifier's expected complexity;
        // mismatch collapses to `Ok(false)` (Pattern 6 — no
        // distinguishable error per audit posture).
        if proof.complexity() != &self.config.proof_complexity {
            tracing::debug!(
                expected = ?self.config.proof_complexity,
                got = ?proof.complexity(),
                "ZK proof rejected: complexity field mismatch"
            );
            log_zero_trust_proof_verified!(false);
            return Ok(false);
        }

        // SECURITY: Use constant-time comparison to prevent timing attacks
        // An attacker should not be able to determine which bytes of the challenge matched
        let len_eq = proof.challenge().len().ct_eq(&challenge.len());
        let content_eq = proof.challenge().ct_eq(challenge);
        let challenge_matches: bool = (len_eq & content_eq).into();

        if !challenge_matches {
            log_zero_trust_proof_verified!(false);
            return Ok(false);
        }

        let result = self.verify_proof_data(proof.proof_data(), challenge)?;
        if result {
            // Bump the last-verified timestamp so subsequent calls to
            // `start_continuous_verification` and the elapsed-time
            // gate in `maybe_continuous_verification` observe a real
            // handshake rather than a fresh `None`. Recover from
            // poison: a panicked writer would only have been writing
            // a `Utc::now()` value; the cached timestamp is still
            // valid.
            let mut guard = match self.last_verification.lock() {
                Ok(g) => g,
                Err(poisoned) => {
                    tracing::warn!(
                        target: "latticearc::unified_api::zero_trust",
                        "ZeroTrustAuth.last_verification mutex was poisoned in \
                         verify_proof success path; a previous writer panicked. \
                         Recovering inner guard."
                    );
                    poisoned.into_inner()
                }
            };
            *guard = Some(Utc::now());
        }
        log_zero_trust_proof_verified!(result);
        Ok(result)
    }
}

/// Build the PoP transcript digest used by `generate_pop` / `verify_pop`.
///
/// Encodes
/// `SHA-512(pop_ctx || 0x00 || verifier_pk_len_be4 || verifier_pk
/// || 0x00 || ts_micros_be8 || 0x00 || challenge_len_be4 || challenge)`.
///
/// `verifier_pk` and `challenge` are each length-prefixed with a
/// big-endian u32 so that concatenation cannot ambiguously parse as a
/// different `(verifier_pk, challenge)` split. The 0x00 separators add
/// a NUL-safety belt because `SIG_CONTEXT_POP_ED25519` is asserted
/// NUL-free by its module-level test.
///
/// Sized [u8; 64] output. Returned by value so the digest lives in
/// a register / stack slot the compiler can scrub.
fn pop_transcript_digest(verifier_pk: &[u8], ts_micros: i64, challenge: &[u8]) -> [u8; 64] {
    use sha2::{Digest, Sha512};
    let mut hasher = Sha512::new();
    hasher.update(crate::types::domains::pop_sig_context());
    hasher.update([0x00]);
    // Pre-encode the length prefixes as fixed-size arrays so the
    // `.update(&[..])` calls stay branch-free and cannot be tricked by
    // a future signature change that exposes a `usize`-vs-`u32` cast
    // mistake. `.len() as u32` is bounded by the underlying slice cap
    // (Ed25519 PK = 32 B, server-issued challenge ≤ 64 KiB at most for
    // any sane caller).
    let pk_len: u32 = verifier_pk.len().try_into().unwrap_or(u32::MAX);
    hasher.update(pk_len.to_be_bytes());
    hasher.update(verifier_pk);
    hasher.update([0x00]);
    hasher.update(ts_micros.to_be_bytes());
    hasher.update([0x00]);
    let ch_len: u32 = challenge.len().try_into().unwrap_or(u32::MAX);
    hasher.update(ch_len.to_be_bytes());
    hasher.update(challenge);
    hasher.finalize().into()
}

impl ProofOfPossession for ZeroTrustAuth {
    type Pop = ProofOfPossessionData;
    type Error = CoreError;

    /// Generates a proof of possession bound to a caller-supplied
    /// challenge.
    ///
    /// The signed transcript is
    /// `SHA-512(pop_ctx || 0x00 || verifier_pk_len_be4 || verifier_pk
    /// || 0x00 || ts_micros_be8 || 0x00 || challenge_len_be4 ||
    /// challenge)`. Binding `self.public_key` (the verifier identity this
    /// auth represents) closes cross-identity replay; binding `challenge`
    /// closes cross-verifier-instance replay within the freshness window.
    /// Both are length-prefixed to prevent prefix-free ambiguity between
    /// the two adjacent variable-length values.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::InvalidKeyLength` if the private key has
    /// incorrect length for Ed25519 signing. Returns
    /// `CoreError::InvalidInput` if the private key format is invalid.
    fn generate_pop(&self, challenge: &[u8]) -> Result<Self::Pop> {
        // PoP-M1 follow-up: empty challenge silently disables cross-
        // verifier-instance replay protection. The wire format encodes
        // challenge_len so the signature still differs from an
        // unbound-challenge legacy PoP, but a verifier that calls
        // generate_pop(b"") and verify_pop(_, b"") accepts cross-
        // verifier replays for the freshness window. Warn loudly at
        // sign-side so operators don't ship the misuse silently;
        // verify-side rejects with Err so silent fall-through is
        // impossible.
        if challenge.is_empty() {
            tracing::warn!(
                "PoP generate_pop called with empty challenge — cross-verifier-\
                 instance replay protection requires a per-request nonce. \
                 verify_pop will refuse to accept an empty challenge."
            );
        }
        let timestamp = Utc::now();
        // Microsecond precision: Ed25519 sigs are deterministic, so two
        // PoPs in the same second from the same key produce identical
        // wire bytes and trip the replay cache against a legitimate
        // client regenerating in a tight loop. Microseconds also fit
        // comfortably inside the 5-minute freshness window.
        let ts_micros = timestamp.timestamp_micros();
        let pop_digest = pop_transcript_digest(self.public_key.as_slice(), ts_micros, challenge);

        let signature = crate::unified_api::convenience::ed25519::sign_ed25519_internal(
            &pop_digest,
            self.private_key.expose_secret(),
        )?;

        Ok(ProofOfPossessionData { public_key: self.public_key.clone(), signature, timestamp })
    }

    /// Verifies a proof of possession against `self.public_key` and the
    /// caller-supplied expected challenge.
    ///
    /// # Returns
    ///
    /// `Ok(true)` if the proof is fresh AND identity-bound AND
    /// challenge-bound AND cryptographically valid. `Ok(false)` for
    /// every rejection cause — stale, future-dated, identity mismatch
    /// (PoP-H1), challenge mismatch (PoP-M1), crypto reject, replay
    /// within the freshness window. **All adversary-reachable rejection
    /// causes collapse to `Ok(false)`** so callers cannot distinguish
    /// "stale, refresh and retry" from "wrong PoP" / "replay" by
    /// branching on the Result. Rejection cause is logged at
    /// `tracing::debug!` for operator visibility.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::InvalidInput` only for unrecoverable
    /// structural failures (poisoned mutex on the replay cache). The
    /// signature-length, public-key-length, and stale / future / replay
    /// paths are all Pattern-6-collapsed to `Ok(false)`.
    fn verify_pop(
        &self,
        pop: &Self::Pop,
        expected_challenge: &[u8],
    ) -> std::result::Result<bool, Self::Error> {
        // PoP-M1 follow-up: refuse empty challenge. An operator that
        // generates PoPs with empty challenge and verifies them with
        // empty challenge structurally loses the cross-verifier-
        // instance replay protection the parameter exists to provide
        // — the wrapper bytes still differ from pre-fix raw-message
        // PoPs, but a verifier that consistently passes b"" would
        // accept cross-verifier replays for the freshness window. Hard
        // reject is operator-misuse class, not adversary-attainable,
        // so Err (not Ok(false)) is appropriate — distinguishing
        // "missing challenge" from "wrong challenge" doesn't leak
        // adversary-controllable state.
        if expected_challenge.is_empty() {
            return Err(CoreError::InvalidInput(
                "PoP verify_pop requires a non-empty expected_challenge — the \
                 per-request nonce is the only structural defence against \
                 cross-verifier-instance replay. Pass the same bytes the \
                 generator used."
                    .to_string(),
            ));
        }
        // Freshness check: reject proofs older than PROOF_OF_POSSESSION_MAX_AGE.
        // This prevents replay of stale PoPs captured from prior sessions (P5.2 C4).
        // Using chrono::Duration so the bound is expressed in seconds regardless
        // of timezone or DST quirks.
        const PROOF_OF_POSSESSION_MAX_AGE_SECS: i64 = 5 * 60; // 5 minutes
        let elapsed = Utc::now().signed_duration_since(pop.timestamp());
        if elapsed.num_seconds() > PROOF_OF_POSSESSION_MAX_AGE_SECS {
            // Pattern-6: do not leak elapsed seconds or the configured
            // max age via the error string. An adversary with a PoP-
            // generation oracle could otherwise narrow server clock
            // skew to seconds. Cause logged at tracing::debug! for
            // operators.
            tracing::debug!(
                elapsed_secs = elapsed.num_seconds(),
                max_age_secs = PROOF_OF_POSSESSION_MAX_AGE_SECS,
                "verify_pop rejected: proof-of-possession too old"
            );
            return Ok(false);
        }
        // Also reject proofs dated more than 30 seconds in the future (clock skew tolerance).
        if elapsed.num_seconds() < -30 {
            tracing::debug!(
                elapsed_secs = elapsed.num_seconds(),
                "verify_pop rejected: proof-of-possession timestamp is in the future"
            );
            return Ok(false);
        }

        // PoP-H1: identity binding. The pre-fix verify dispatched against
        // `pop.public_key()` — the public key the (potentially attacker-
        // controlled) PoP carries. Any party with any Ed25519 keypair
        // could then produce a self-signed PoP that returned Ok(true),
        // proving possession of A KEY, not THIS IDENTITY's key. Compare
        // the embedded pk against the verifier identity in constant
        // time; mismatch is Pattern-6-collapsed to Ok(false) so the
        // adversary cannot distinguish "wrong identity" from "wrong
        // signature" by branching on the Result.
        use subtle::ConstantTimeEq;
        let embedded_pk = pop.public_key().as_slice();
        let verifier_pk = self.public_key.as_slice();
        if embedded_pk.len() != verifier_pk.len() {
            tracing::debug!(
                embedded_len = embedded_pk.len(),
                verifier_len = verifier_pk.len(),
                "verify_pop rejected: embedded public key length differs from \
                 verifier identity (PoP-H1)"
            );
            return Ok(false);
        }
        if embedded_pk.ct_eq(verifier_pk).unwrap_u8() != 1u8 {
            tracing::debug!(
                "verify_pop rejected: embedded public key does not match verifier \
                 identity (PoP-H1)"
            );
            return Ok(false);
        }

        // PoP-M1: reconstruct the post-fix transcript exactly as
        // `generate_pop` produced it — verifier_pk + ts + challenge —
        // and dispatch against the verifier identity. A captured PoP
        // for a different verifier-id OR a different challenge round
        // produces a different digest and the crypto verify rejects.
        let ts_micros = pop.timestamp().timestamp_micros();
        let pop_digest = pop_transcript_digest(verifier_pk, ts_micros, expected_challenge);

        let valid = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
            &pop_digest,
            pop.signature(),
            verifier_pk,
        )?;

        // reject re-presentation of a
        // verified PoP within the 5-minute acceptance window. Without
        // a cache, an attacker who captured a single valid PoP at
        // time T could replay it any number of times within
        // (T, T + 5 min). The cache is keyed on (pk, sig, ts_micros);
        // entries older than the freshness window above are
        // self-evicting since the freshness check would already have
        // rejected them on a re-replay attempt. The signature alone
        // is byte-unique per timestamp under Ed25519 determinism (see
        // `generate_pop` for why microsecond precision is necessary),
        // so the cache key only needs to disambiguate within a single
        // microsecond.
        if valid {
            let mut cache = self.pop_replay_cache.lock().map_err(|_poison| {
                CoreError::InvalidInput("PoP replay cache poisoned".to_string())
            })?;
            // PoP-H1 enforced verifier_pk above, so the cache key uses
            // verifier_pk (the canonical identity bytes) instead of the
            // adversary-supplied `pop.public_key()`. Identical bytes by
            // construction once H1 passes, but anchoring to the verifier
            // makes the cache-key invariant resilient to future code
            // motion that might forget to dereference `pop.public_key()`
            // first.
            let pk_bytes = verifier_pk;
            let pk_len = pk_bytes.len();
            // Evict expired entries opportunistically; per-PK counts are
            // decremented in lockstep so they stay exact. `pk_len` is
            // owned by the cache (locked at construction) so this call
            // no longer carries it as a parameter.
            let now_secs = Utc::now().timestamp();
            cache.expire_older_than(now_secs, PROOF_OF_POSSESSION_MAX_AGE_SECS);
            // Cache key combines PK + signature + microsecond timestamp;
            // collisions require either a cryptographic break or a
            // deliberate replay (the latter is what we're rejecting
            // here). The challenge bytes already factor into the
            // signature (PoP-M1), so adding them to the key would be
            // redundant.
            let key_cap = pk_len.saturating_add(pop.signature().len()).saturating_add(8);
            let mut key = Vec::with_capacity(key_cap);
            key.extend_from_slice(pk_bytes);
            key.extend_from_slice(pop.signature());
            key.extend_from_slice(&ts_micros.to_be_bytes());
            if cache.contains(&key) {
                // PoP-L1: collapse replay to Ok(false). Pre-fix this was
                // Err(CoreError::InvalidInput("replay detected")), letting
                // the caller distinguish "I've seen this before" from the
                // sibling "stale, refresh and retry" path (which already
                // returned Ok(false)). Pattern-6 contract requires every
                // adversary-attainable rejection to look identical.
                tracing::debug!(ts_micros, "PoP rejected: replay within 5-min window");
                return Ok(false);
            }
            // Per-PK quota. Bounds the damage a single noisy public key can
            // do: an attacker submitting many legitimate PoPs from one PK
            // fills only their own slice of the cache, leaving every other
            // PK's quota untouched. A 64-flight legitimate client (long
            // poll, batched signing) still fits; ≥ 256 distinct PKs are
            // required before the global cap can be saturated.
            const POP_CACHE_PER_PK_MAX: usize = 64;
            if cache.count_for_pk(pk_bytes) >= POP_CACHE_PER_PK_MAX {
                // PoP-L1: collapse to Ok(false) — same Pattern-6
                // requirement as the replay-detected branch. An attacker
                // who floods their own PK to the quota cap could
                // otherwise distinguish "valid-but-quota-exhausted" (Err)
                // from "valid replay" (Ok(false)) or "invalid sig"
                // (Ok(false)) by branching on the Result variant. The
                // leaked bit is DoS-cache state, not key material, but
                // the contract documented on verify_pop says "all
                // adversary-reachable rejection causes collapse to
                // Ok(false)". Operator cause stays in tracing::warn!.
                tracing::warn!(
                    per_pk_cap = POP_CACHE_PER_PK_MAX,
                    "PoP per-key quota exhausted; rejecting new PoP from this PK \
                     (other PKs unaffected)"
                );
                return Ok(false);
            }
            // Global cap. The opportunistic eviction above already removed
            // entries older than the freshness window, so reaching this
            // cap means ≥ `POP_CACHE_MAX` distinct legitimate PoPs from
            // ≥ `POP_CACHE_MAX / POP_CACHE_PER_PK_MAX` distinct PKs were
            // verified inside a single 5-minute window — that is DoS-class
            // load. Silently skipping the insert would lift the replay
            // window for subsequent presentations of the same PoP. Fail
            // closed instead.
            const POP_CACHE_MAX: usize = 16 * 1024;
            if cache.total() >= POP_CACHE_MAX {
                // PoP-L1: same Pattern-6 rationale as the per-PK quota
                // branch above — adversary-distinguishable Err vs
                // Ok(false) was the exact oracle the L1 fix was supposed
                // to close. Operator cause stays in tracing::warn!.
                tracing::warn!(
                    cap = POP_CACHE_MAX,
                    "PoP global replay cache full; rejecting new PoP rather than \
                     skipping insert (silently skipping would re-open the 5-minute \
                     replay window)"
                );
                return Ok(false);
            }
            cache.insert(pk_bytes, key, now_secs);
        }

        Ok(valid)
    }
}

impl ContinuousVerifiable for ZeroTrustAuth {
    type Error = CoreError;

    /// Checks the current verification status of the session.
    ///
    /// # Errors
    ///
    /// This function does not currently return errors, but returns `Result` for
    /// API consistency and future extensibility.
    fn verify_continuously(&self) -> Result<VerificationStatus> {
        let session_elapsed = Utc::now().signed_duration_since(self.session_start);

        let max_session_time: u64 = 30 * 60 * 1000;

        // Convert safely: negative elapsed times treated as 0 (just started)
        let session_elapsed_u64 = u64::try_from(session_elapsed.num_milliseconds()).unwrap_or(0);
        if session_elapsed_u64 > max_session_time {
            return Ok(VerificationStatus::Expired);
        }

        if !self.config.continuous_verification {
            return Ok(VerificationStatus::Verified);
        }

        // Recover from poison: a panicked write would only have been a
        // failed `Utc::now()` assignment; the cached timestamp is still
        // valid. Treating poison as fatal would lock callers out of an
        // otherwise healthy auth session.
        let last_verification_at = match self.last_verification.lock() {
            Ok(g) => *g,
            Err(poisoned) => {
                tracing::warn!(
                    target: "latticearc::unified_api::zero_trust",
                    "ZeroTrustAuth.last_verification mutex was poisoned in \
                     maybe_continuous_verification; a previous writer panicked. \
                     Recovering inner guard."
                );
                *poisoned.into_inner()
            }
        };
        // No prior verification ⇒ continuous-verify mode reports
        // `Pending`, mirroring the "must reauthenticate" path. We never
        // return `Verified` for an instance that has never completed a
        // proof, even if the configured interval hasn't expired.
        let Some(last_verification_at) = last_verification_at else {
            return Ok(VerificationStatus::Pending);
        };
        let verification_elapsed = Utc::now().signed_duration_since(last_verification_at);

        // Convert safely: negative elapsed times treated as 0 (just verified)
        let verification_elapsed_u64 =
            u64::try_from(verification_elapsed.num_milliseconds()).unwrap_or(0);
        if verification_elapsed_u64 > self.config.verification_interval_ms {
            return Ok(VerificationStatus::Pending);
        }

        Ok(VerificationStatus::Verified)
    }

    /// Performs reauthentication by generating and verifying a new challenge-proof pair.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::EntropyDepleted` if the system cannot generate random bytes
    /// for the challenge.
    ///
    /// Returns `CoreError::AuthenticationFailed` if proof generation fails due to an
    /// empty challenge.
    ///
    /// Returns `CoreError::InvalidKeyLength` or `CoreError::InvalidInput` if the private
    /// key format is invalid for Ed25519 signing.
    fn reauthenticate(&self) -> Result<()> {
        // The previous implementation generated a proof, discarded it
        // via `let _proof = ...`, and bumped `last_verification`
        // regardless. That made `last_verification` a "now" stamp on
        // every call — an attacker who tampered with the private-key
        // bytes (use-after-free, fuzz target, etc.) kept the session
        // perpetually `Verified`. We now actually verify the proof
        // against the challenge before bumping `last_verification`, and
        // surface a hard error on mismatch.
        let challenge = self.generate_challenge()?;
        let proof = self.generate_proof(challenge.data())?;

        let proof_valid = self.verify_proof(&proof, challenge.data())?;
        if !proof_valid {
            return Err(CoreError::AuthenticationFailed(
                "Reauthentication proof verification failed".to_string(),
            ));
        }

        let mut guard = match self.last_verification.lock() {
            Ok(g) => g,
            Err(poisoned) => {
                tracing::warn!(
                    target: "latticearc::unified_api::zero_trust",
                    "ZeroTrustAuth.last_verification mutex was poisoned in \
                     reauthenticate; a previous writer panicked. Recovering inner guard."
                );
                poisoned.into_inner()
            }
        };
        *guard = Some(Utc::now());
        Ok(())
    }
}

/// A cryptographic challenge for zero-trust authentication.
#[derive(Debug, Clone)]
pub struct Challenge {
    /// Random challenge data.
    data: Vec<u8>,
    /// When the challenge was created.
    timestamp: DateTime<Utc>,
    /// Required proof complexity.
    complexity: ProofComplexity,
    /// Timeout in milliseconds.
    timeout_ms: u64,
}

impl Challenge {
    /// Returns the random challenge bytes.
    #[must_use]
    pub fn data(&self) -> &[u8] {
        &self.data
    }

    /// Returns the timestamp when the challenge was created.
    #[must_use]
    pub fn timestamp(&self) -> DateTime<Utc> {
        self.timestamp
    }

    /// Returns the required proof complexity for this challenge.
    #[must_use]
    pub fn complexity(&self) -> &ProofComplexity {
        &self.complexity
    }

    /// Returns the timeout in milliseconds for this challenge.
    #[must_use]
    pub fn timeout_ms(&self) -> u64 {
        self.timeout_ms
    }

    /// Returns `true` if the challenge has expired.
    ///
    /// A future-dated timestamp (negative elapsed — typically clock skew or
    /// a forged challenge) is treated as **expired**, matching
    /// `ZeroTrustAuth::verify_challenge_age`: we'd rather reject a confusing
    /// timestamp than accept one. Previously this returned `false` via
    /// `unwrap_or(0)`, inconsistent with the sibling helper and an accidental
    /// "future-stamped challenges live forever" footgun.
    #[must_use]
    pub fn is_expired(&self) -> bool {
        let elapsed = Utc::now().signed_duration_since(self.timestamp);
        let elapsed_u64 = u64::try_from(elapsed.num_milliseconds()).unwrap_or(u64::MAX);
        elapsed_u64 > self.timeout_ms
    }
}

/// A zero-knowledge proof response to a challenge.
#[derive(Debug, Clone)]
pub struct ZeroKnowledgeProof {
    /// The original challenge that was responded to.
    challenge: Vec<u8>,
    /// The cryptographic proof data.
    proof: Vec<u8>,
    /// When the proof was generated.
    timestamp: DateTime<Utc>,
    /// Complexity level of the proof.
    complexity: ProofComplexity,
}

impl ZeroKnowledgeProof {
    /// Creates a new `ZeroKnowledgeProof` with the given fields.
    #[must_use]
    pub fn new(
        challenge: Vec<u8>,
        proof: Vec<u8>,
        timestamp: DateTime<Utc>,
        complexity: ProofComplexity,
    ) -> Self {
        Self { challenge, proof, timestamp, complexity }
    }

    /// Returns the challenge bytes this proof was generated for.
    #[must_use]
    pub fn challenge(&self) -> &[u8] {
        &self.challenge
    }

    /// Returns the cryptographic proof data.
    #[must_use]
    pub fn proof_data(&self) -> &[u8] {
        &self.proof
    }

    /// Returns a mutable reference to the proof data (e.g., for tampering in tests).
    #[must_use]
    pub fn proof_data_mut(&mut self) -> &mut Vec<u8> {
        &mut self.proof
    }

    /// Returns the timestamp when the proof was generated.
    #[must_use]
    pub fn timestamp(&self) -> DateTime<Utc> {
        self.timestamp
    }

    /// Returns the complexity level of the proof.
    #[must_use]
    pub fn complexity(&self) -> &ProofComplexity {
        &self.complexity
    }

    /// Returns `true` if the proof has a valid format.
    #[must_use]
    pub fn is_valid_format(&self) -> bool {
        !self.challenge.is_empty() && !self.proof.is_empty() && self.timestamp <= Utc::now()
    }
}

/// Data proving possession of a private key.
#[derive(Debug, Clone)]
pub struct ProofOfPossessionData {
    /// Public key associated with the proof.
    public_key: PublicKey,
    /// Signature proving possession.
    signature: Vec<u8>,
    /// When the proof was generated.
    timestamp: DateTime<Utc>,
}

impl ProofOfPossessionData {
    /// Creates a new `ProofOfPossessionData` with the given fields.
    #[must_use]
    pub fn new(public_key: PublicKey, signature: Vec<u8>, timestamp: DateTime<Utc>) -> Self {
        Self { public_key, signature, timestamp }
    }

    /// Returns the public key associated with this proof.
    #[must_use]
    pub fn public_key(&self) -> &PublicKey {
        &self.public_key
    }

    /// Returns a mutable reference to the signature bytes (e.g., for tampering in tests).
    #[must_use]
    pub fn signature_mut(&mut self) -> &mut Vec<u8> {
        &mut self.signature
    }

    /// Returns the signature bytes proving possession.
    #[must_use]
    pub fn signature(&self) -> &[u8] {
        &self.signature
    }

    /// Returns the timestamp when the proof was generated.
    #[must_use]
    pub fn timestamp(&self) -> DateTime<Utc> {
        self.timestamp
    }
}

/// A continuous verification session.
#[derive(Debug)]
pub struct ContinuousSession {
    /// Public key that authenticated the session.
    auth_public_key: PublicKey,
    /// When the session started.
    start_time: DateTime<Utc>,
    /// Verification interval in milliseconds.
    verification_interval_ms: u64,
    /// Last successful verification timestamp.
    last_verification: DateTime<Utc>,
}

impl ContinuousSession {
    /// Get the public key that authenticated this session
    #[must_use]
    pub fn auth_public_key(&self) -> &PublicKey {
        &self.auth_public_key
    }

    /// Timestamp of the last successful verification on this session.
    /// Exposed so callers (and tests) can confirm that
    /// [`Self::update_verification`] actually advances the cached
    /// timestamp rather than no-oping.
    #[must_use]
    pub fn last_verification(&self) -> DateTime<Utc> {
        self.last_verification
    }

    /// Checks whether the continuous session is still valid.
    ///
    /// # Errors
    ///
    /// This function does not currently return errors, but returns `Result` for
    /// API consistency and future extensibility.
    #[must_use = "session validity check should not be discarded — act on the boolean"]
    pub fn is_valid(&self) -> Result<bool> {
        let elapsed = Utc::now().signed_duration_since(self.start_time);

        let max_duration: u64 = 60 * 60 * 1000;

        // Convert safely: negative elapsed times treated as 0 (just started)
        let elapsed_u64 = u64::try_from(elapsed.num_milliseconds()).unwrap_or(0);
        if elapsed_u64 > max_duration {
            return Ok(false);
        }

        let verification_elapsed = Utc::now().signed_duration_since(self.last_verification);

        // Convert safely: negative elapsed times treated as 0 (just verified)
        let verification_elapsed_u64 =
            u64::try_from(verification_elapsed.num_milliseconds()).unwrap_or(0);
        Ok(verification_elapsed_u64 <= self.verification_interval_ms)
    }

    // M-update_verification: the pre-fix `pub fn update_verification`
    // bumped `self.last_verification` to `Utc::now()` with no proof —
    // equivalent to "extend my session forever" from anyone holding
    // `&mut ContinuousSession`. There were zero in-crate non-test
    // callers (verify_continuously / reauthenticate operate on
    // `ZeroTrustAuth.last_verification`, not the session-side copy),
    // so the method has been removed rather than merely cfg-gated.
    // Callers that legitimately need to refresh a continuous session
    // must route through `ZeroTrustAuth::reauthenticate`, which IS
    // proof-gated (generates challenge → generates proof → verifies →
    // bumps the auth-side clock).
}

fn generate_challenge_data(complexity: &ProofComplexity) -> Result<Vec<u8>> {
    let size = match complexity {
        ProofComplexity::Low => 32,
        ProofComplexity::Medium => 64,
        ProofComplexity::High => 128,
    };

    let data = crate::primitives::security::generate_secure_random_bytes(size).map_err(|e| {
        CoreError::EntropyDepleted {
            message: format!("Failed to generate challenge: {e}"),
            action: "Check system entropy source".to_string(),
        }
    })?;
    // Challenge data is sent over the wire and is not secret material; the
    // ephemeral `Zeroizing` wrapper is dropped (and the source bytes wiped)
    // when this function returns.
    Ok(data.to_vec())
}

impl ZeroTrustAuth {
    /// Compute proof data using Ed25519 signature-based challenge-response.
    ///
    /// This is a proper zero-knowledge proof: the signature proves knowledge
    /// of the private key without revealing any information about it.
    ///
    /// Proof complexity affects what is signed:
    /// - Low: sign(challenge)
    /// - Medium: sign(challenge || timestamp)
    /// - High: sign(challenge || timestamp || context)
    fn compute_proof_data(&self, challenge: &[u8]) -> Result<Vec<u8>> {
        let timestamp = Utc::now().timestamp_millis().to_le_bytes();

        // All complexity levels bind the timestamp into the signed
        // message so the verifier can enforce a freshness window. The
        // previous Low variant signed only `challenge`, making
        // `(challenge, signature)` pairs replayable indefinitely
        // until session expiry. Replay
        // protection is no longer opt-in.
        // Every variant binds the public key into the signed transcript
        // so a verifier holding pk can be sure the proof was produced
        // *for that pk* and not relayed from a session under a different
        // identity. Without PK binding, an attacker who captured a
        // valid (challenge, signature) pair under pk_A could replay it
        // against a verifier expecting pk_B if the signature happened
        // to verify under pk_B too (catastrophic if a verifier holds
        // multiple registered keys). The 1-byte domain tag separates
        // the three complexity levels so a Low proof cannot satisfy a
        // Medium or High verifier and vice versa.
        let message_to_sign = match self.config.proof_complexity {
            ProofComplexity::Low => {
                let mut msg = vec![0x01];
                msg.extend_from_slice(challenge);
                msg.extend_from_slice(&timestamp);
                msg.extend_from_slice(self.public_key.as_slice());
                msg
            }
            ProofComplexity::Medium => {
                let mut msg = vec![0x02];
                msg.extend_from_slice(challenge);
                msg.extend_from_slice(&timestamp);
                msg.extend_from_slice(self.public_key.as_slice());
                msg
            }
            ProofComplexity::High => {
                let mut msg = vec![0x03];
                msg.extend_from_slice(challenge);
                msg.extend_from_slice(&timestamp);
                msg.extend_from_slice(self.public_key.as_slice());
                msg
            }
        };

        // Sign the message - this IS zero-knowledge
        // The signature proves knowledge of private key without revealing it
        //
        // M-A: bind ZK-proof signatures to a ZK-proof-specific Ed25519
        // context. The internal 0x01/0x02/0x03 complexity tags inside
        // `message_to_sign` keep Low/Medium/High distinguishable within
        // this protocol; the SHA-512 prefix-padded context here keeps a
        // captured ZK proof from replaying as a SignedData or PoP signed
        // by the same Ed25519 key. Mirrors the post-M-A pure-Ed25519
        // construction and the hybrid leg.
        use crate::types::domains::{hash_with_context, zk_proof_sig_context};
        let zk_digest = hash_with_context(zk_proof_sig_context(), &message_to_sign);

        let signature = crate::unified_api::convenience::ed25519::sign_ed25519_internal(
            &zk_digest,
            self.private_key.expose_secret(),
        )?;

        // Append the timestamp to the proof bytes so the verifier can
        // recover it. All three complexity levels now carry a timestamp
        // suffix; older proofs without the
        // suffix will fail length check at verify time.
        let mut proof = signature;
        proof.extend_from_slice(&timestamp);
        Ok(proof)
    }

    /// Verify proof using PUBLIC KEY only.
    ///
    /// This is the correct way to verify a zero-knowledge proof:
    /// only the public key is needed, not the private key.
    fn verify_proof_data(&self, proof: &[u8], challenge: &[u8]) -> Result<bool> {
        // Ed25519 signatures are 64 bytes
        if proof.len() < 64 {
            return Ok(false);
        }

        match self.config.proof_complexity {
            ProofComplexity::Low => {
                // Low now requires the
                // 8-byte timestamp suffix (mandatory replay protection).
                if proof.len() < 72 {
                    return Ok(false);
                }
                let signature = proof.get(..64).ok_or_else(|| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;
                let timestamp_slice = proof.get(64..72).ok_or_else(|| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;
                let timestamp_bytes: [u8; 8] = timestamp_slice.try_into().map_err(|_e| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;

                // verify signature BEFORE applying the
                // freshness check. The original ordering parsed the
                // timestamp out of `proof[64..72]` and ran the 30-s/
                // 5-min checks on raw adversary-supplied bytes; even
                // though signature verification would still catch a
                // tampered timestamp downstream, the principle
                // "authenticate before acting on adversary content"
                // wants verify first. Mirrored across Medium / High
                // branches below.
                let mut message = vec![0x01];
                message.extend_from_slice(challenge);
                message.extend_from_slice(&timestamp_bytes);
                message.extend_from_slice(self.public_key.as_slice());
                // M-A: mirror generate_proof_data's
                // SHA-512(zk_ctx || 0x00 || message) construction.
                use crate::types::domains::{hash_with_context, zk_proof_sig_context};
                let zk_digest = hash_with_context(zk_proof_sig_context(), &message);
                let sig_ok = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
                    &zk_digest,
                    signature,
                    self.public_key.as_slice(),
                )?;
                if !sig_ok {
                    return Ok(false);
                }

                // Timestamp is now authenticated — freshness check is
                // safe to run on `timestamp_bytes`.
                let proof_ts_ms = i64::from_le_bytes(timestamp_bytes);
                let now_ms = Utc::now().timestamp_millis();
                // tighten the future-skew cap
                // to match the sibling `verify_pop` path. `abs_diff(...)
                // > 300_000` accepted proofs up to 5 min in the future,
                // which gives an attacker with a forward-skewed clock a
                // 10-min replay window. Reject anything more than 30 s
                // ahead of "now"; the 5-min window only applies to the
                // past direction.
                if proof_ts_ms > now_ms.saturating_add(30_000) {
                    tracing::warn!(
                        proof_ts_ms,
                        now_ms,
                        "proof timestamp more than 30 s in the future"
                    );
                    return Ok(false);
                }
                let drift_ms = now_ms.abs_diff(proof_ts_ms);
                if drift_ms > 300_000 {
                    tracing::warn!(drift_ms, "proof timestamp outside 5-min freshness window");
                    return Ok(false);
                }

                Ok(true)
            }
            ProofComplexity::Medium => {
                // Extract signature and timestamp
                if proof.len() < 72 {
                    return Ok(false);
                }
                let signature = proof.get(..64).ok_or_else(|| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;
                let timestamp_slice = proof.get(64..72).ok_or_else(|| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;
                let timestamp_bytes: [u8; 8] = timestamp_slice.try_into().map_err(|_e| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;

                // verify before freshness (mirrors Low).
                // Reconstruct signed message. The 0x02 domain tag
                // distinguishes Medium from Low/High; the public-key
                // suffix binds the signature to this specific
                // verifier identity (HPKE-style channel binding).
                let mut message = vec![0x02];
                message.extend_from_slice(challenge);
                message.extend_from_slice(&timestamp_bytes);
                message.extend_from_slice(self.public_key.as_slice());
                // M-A: mirror generate_proof_data's
                // SHA-512(zk_ctx || 0x00 || message) construction.
                use crate::types::domains::{hash_with_context, zk_proof_sig_context};
                let zk_digest = hash_with_context(zk_proof_sig_context(), &message);
                let sig_ok = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
                    &zk_digest,
                    signature,
                    self.public_key.as_slice(),
                )?;
                if !sig_ok {
                    return Ok(false);
                }

                // Reject stale proofs (>5 min drift).
                // Timestamp is encoded as chrono milliseconds in little-endian.
                let proof_ts_ms = i64::from_le_bytes(timestamp_bytes);
                let now_ms = Utc::now().timestamp_millis();
                // tighten the future-skew cap
                // to match the sibling `verify_pop` path. `abs_diff(...)
                // > 300_000` accepted proofs up to 5 min in the future,
                // which gives an attacker with a forward-skewed clock a
                // 10-min replay window. Reject anything more than 30 s
                // ahead of "now"; the 5-min window only applies to the
                // past direction.
                if proof_ts_ms > now_ms.saturating_add(30_000) {
                    tracing::warn!(
                        proof_ts_ms,
                        now_ms,
                        "proof timestamp more than 30 s in the future"
                    );
                    return Ok(false);
                }
                let drift_ms = now_ms.abs_diff(proof_ts_ms);
                if drift_ms > 300_000 {
                    tracing::warn!(drift_ms, "proof timestamp outside 5-min freshness window");
                    return Ok(false);
                }

                Ok(true)
            }
            ProofComplexity::High => {
                // Extract signature and timestamp
                if proof.len() < 72 {
                    return Ok(false);
                }
                let signature = proof.get(..64).ok_or_else(|| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;
                let timestamp_slice = proof.get(64..72).ok_or_else(|| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;
                let timestamp_bytes: [u8; 8] = timestamp_slice.try_into().map_err(|_e| {
                    CoreError::AuthenticationFailed("Invalid proof format".to_string())
                })?;

                // verify before freshness (mirrors Low).
                // Reconstruct signed message with public key binding.
                // High prepends a 0x03 domain
                // tag so it is byte-distinguishable from both Low and
                // Medium.
                let mut message = vec![0x03];
                message.extend_from_slice(challenge);
                message.extend_from_slice(&timestamp_bytes);
                message.extend_from_slice(self.public_key.as_slice());
                // M-A: mirror generate_proof_data's
                // SHA-512(zk_ctx || 0x00 || message) construction.
                use crate::types::domains::{hash_with_context, zk_proof_sig_context};
                let zk_digest = hash_with_context(zk_proof_sig_context(), &message);
                let sig_ok = crate::unified_api::convenience::ed25519::verify_ed25519_internal(
                    &zk_digest,
                    signature,
                    self.public_key.as_slice(),
                )?;
                if !sig_ok {
                    return Ok(false);
                }

                // Reject stale proofs (>5 min drift).
                // Timestamp is encoded as chrono milliseconds in little-endian.
                let proof_ts_ms = i64::from_le_bytes(timestamp_bytes);
                let now_ms = Utc::now().timestamp_millis();
                // tighten the future-skew cap
                // to match the sibling `verify_pop` path. `abs_diff(...)
                // > 300_000` accepted proofs up to 5 min in the future,
                // which gives an attacker with a forward-skewed clock a
                // 10-min replay window. Reject anything more than 30 s
                // ahead of "now"; the 5-min window only applies to the
                // past direction.
                if proof_ts_ms > now_ms.saturating_add(30_000) {
                    tracing::warn!(
                        proof_ts_ms,
                        now_ms,
                        "proof timestamp more than 30 s in the future"
                    );
                    return Ok(false);
                }
                let drift_ms = now_ms.abs_diff(proof_ts_ms);
                if drift_ms > 300_000 {
                    tracing::warn!(drift_ms, "proof timestamp outside 5-min freshness window");
                    return Ok(false);
                }

                Ok(true)
            }
        }
    }
}

/// A zero-trust session managing the authentication flow.
pub struct ZeroTrustSession {
    /// The underlying authentication handler.
    pub(crate) auth: ZeroTrustAuth,
    /// Current active challenge, if any.
    challenge: Option<Challenge>,
    /// Whether the session has been verified.
    verified: bool,
    /// When the session started.
    session_start: DateTime<Utc>,
}

impl ZeroTrustSession {
    /// Creates a new zero-trust session.
    #[must_use]
    pub fn new(auth: ZeroTrustAuth) -> Self {
        Self { auth, challenge: None, verified: false, session_start: Utc::now() }
    }

    /// Initiates the authentication flow by generating a new challenge.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::EntropyDepleted` if the system cannot generate random bytes
    /// for the challenge.
    pub fn initiate_authentication(&mut self) -> Result<Challenge> {
        let challenge = self.auth.generate_challenge()?;
        self.challenge = Some(challenge.clone());
        Ok(challenge)
    }

    /// Verifies a proof response against the active challenge.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationFailed` if:
    /// - No challenge has been initiated (no active challenge).
    /// - The challenge has expired.
    /// - The proof format is invalid for Medium/High complexity proofs.
    ///
    /// Returns `CoreError::InvalidInput` if the public key or signature format is invalid
    /// during Ed25519 verification.
    ///
    /// Returns `CoreError::InvalidKeyLength` if the public key has incorrect length.
    pub fn verify_response(&mut self, proof: &ZeroKnowledgeProof) -> Result<bool> {
        // Pattern-6: collapse the "no active challenge" and "challenge
        // expired" cases to a single fixed string in the typed error
        // AND in the SIEM audit log. Both are caller-protocol mistakes
        // (not adversary-controllable crypto material), but distinct
        // strings let an attacker mapping the protocol state machine
        // fingerprint which check tripped. The discriminator goes to
        // `tracing::debug!` for operator visibility, mirroring
        // `verify_pop`'s posture.
        const VERIFY_RESPONSE_FAILED: &str = "challenge verification failed";

        let challenge = self.challenge.as_ref().ok_or_else(|| {
            log_zero_trust_session_verification_failed!("pending", VERIFY_RESPONSE_FAILED);
            tracing::debug!("verify_response rejected: no active challenge");
            CoreError::AuthenticationFailed(VERIFY_RESPONSE_FAILED.to_string())
        })?;

        if challenge.is_expired() {
            log_zero_trust_session_verification_failed!("pending", VERIFY_RESPONSE_FAILED);
            tracing::debug!("verify_response rejected: challenge expired");
            return Err(CoreError::AuthenticationFailed(VERIFY_RESPONSE_FAILED.to_string()));
        }

        // Snapshot challenge data, then immediately consume the
        // challenge regardless of verification outcome. Without this,
        // a captured (challenge, proof) pair on the wire is bit-for-bit
        // replayable against the SAME ZeroTrustSession instance within
        // the `challenge_timeout_ms` window (Ed25519 is deterministic
        // per RFC 8032; the proof bytes don't change). Mirrors the
        // posture of `verify_pop` (which uses `pop_replay_cache` for
        // the same attack class). Off-machine replay defense still
        // requires transport-binding (TLS); see SECURITY.md.
        let challenge_data = challenge.data().to_vec();
        self.challenge = None;

        let verified = self.auth.verify_proof(proof, &challenge_data)?;
        self.verified = verified;

        if !verified {
            log_zero_trust_session_verification_failed!("pending", "Proof verification failed");
        }

        Ok(self.verified)
    }

    /// Generates a zero-knowledge proof for the given challenge.
    ///
    /// This is used in the manual authentication flow: after calling
    /// [`initiate_authentication`](Self::initiate_authentication) to get a challenge,
    /// generate a proof and then pass it to [`verify_response`](Self::verify_response).
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationFailed` if the challenge data is empty.
    ///
    /// Returns `CoreError::InvalidKeyLength` if the private key has incorrect length.
    ///
    /// Returns `CoreError::InvalidInput` if the private key format is invalid.
    pub fn generate_proof(&self, challenge: &Challenge) -> Result<ZeroKnowledgeProof> {
        self.auth.generate_proof(challenge.data())
    }

    /// Returns `true` if the session has been successfully authenticated.
    #[must_use]
    pub fn is_authenticated(&self) -> bool {
        self.verified
    }

    /// Returns the age of the session in milliseconds since creation.
    ///
    /// # Errors
    ///
    /// This function does not currently return errors, but returns `Result` for
    /// API consistency and future extensibility.
    pub fn session_age_ms(&self) -> Result<u64> {
        let elapsed = Utc::now().signed_duration_since(self.session_start);
        // Convert safely: negative elapsed times treated as 0 (just started)
        let elapsed_u64 = u64::try_from(elapsed.num_milliseconds()).unwrap_or(0);
        Ok(elapsed_u64)
    }

    /// Convert this session into a `VerifiedSession` after successful authentication.
    ///
    /// This consumes the `ZeroTrustSession` and returns a `VerifiedSession` that
    /// can be used to authorize cryptographic operations.
    ///
    /// # Errors
    ///
    /// Returns `CoreError::AuthenticationRequired` if the session has not been
    /// successfully authenticated via `verify_response()`.
    ///
    /// Returns `CoreError::EntropyDepleted` if session ID generation fails.
    pub fn into_verified(self) -> Result<VerifiedSession> {
        VerifiedSession::from_authenticated(&self)
    }
}

#[cfg(test)]
#[expect(
    clippy::panic,
    clippy::panic_in_result_fn,
    clippy::useless_vec,
    unused_qualifications,
    reason = "test/bench scaffolding: lints suppressed for this module"
)]
mod tests {
    use super::*;
    use crate::unified_api::generate_keypair;

    /// Test helper: drive `auth` through a successful challenge-response
    /// so subsequent calls to `start_continuous_verification` clear the
    /// "must verify at least once" precondition.
    fn warm_up_auth(auth: &ZeroTrustAuth) -> Result<()> {
        let challenge = auth.generate_challenge()?;
        let proof = auth.generate_proof(challenge.data())?;
        let verified = auth.verify_proof(&proof, challenge.data())?;
        assert!(verified, "warm-up proof must verify");
        Ok(())
    }

    // TrustLevel tests
    #[test]
    fn test_trust_level_default_has_correct_value_succeeds() {
        let level = TrustLevel::default();
        assert_eq!(level, TrustLevel::Untrusted);
    }

    #[test]
    fn test_trust_level_variants_are_correct() {
        assert_eq!(TrustLevel::Untrusted as i32, 0);
        assert_eq!(TrustLevel::Partial as i32, 1);
        assert_eq!(TrustLevel::Trusted as i32, 2);
        assert_eq!(TrustLevel::FullyTrusted as i32, 3);
    }

    #[test]
    fn test_trust_level_is_trusted_succeeds() {
        assert!(!TrustLevel::Untrusted.is_trusted());
        assert!(TrustLevel::Partial.is_trusted());
        assert!(TrustLevel::Trusted.is_trusted());
        assert!(TrustLevel::FullyTrusted.is_trusted());
    }

    #[test]
    fn test_trust_level_is_fully_trusted_succeeds() {
        assert!(!TrustLevel::Untrusted.is_fully_trusted());
        assert!(!TrustLevel::Partial.is_fully_trusted());
        assert!(!TrustLevel::Trusted.is_fully_trusted());
        assert!(TrustLevel::FullyTrusted.is_fully_trusted());
    }

    #[test]
    fn test_trust_level_ordering_is_correct() {
        assert!(TrustLevel::Untrusted < TrustLevel::Partial);
        assert!(TrustLevel::Partial < TrustLevel::Trusted);
        assert!(TrustLevel::Trusted < TrustLevel::FullyTrusted);
    }

    // SecurityMode tests
    #[test]
    fn test_security_mode_unverified_is_unverified_succeeds() {
        let mode = SecurityMode::Unverified;
        assert!(mode.is_unverified());
        assert!(!mode.is_verified());
    }

    #[test]
    fn test_security_mode_validate_unverified_succeeds() -> Result<()> {
        let mode = SecurityMode::Unverified;
        mode.validate()?;
        Ok(())
    }

    // VerifiedSession tests
    #[test]
    fn test_verified_session_establish_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        assert!(session.is_valid());
        // After self-authentication during establish, trust level is upgraded
        assert!(session.trust_level() >= TrustLevel::Partial);
        Ok(())
    }

    #[test]
    fn test_verified_session_session_id_is_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let session_id = session.session_id();
        assert_eq!(session_id.len(), 32);
        Ok(())
    }

    #[test]
    fn test_verified_session_public_key_is_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let pk = session.public_key();
        assert!(!pk.is_empty());
        Ok(())
    }

    #[test]
    fn test_verified_session_timestamps_are_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let authenticated_at = session.authenticated_at();
        let expires_at = session.expires_at();

        assert!(expires_at > authenticated_at, "Session should expire after authentication");
        Ok(())
    }

    #[test]
    fn test_verified_session_verify_valid_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        session.verify_valid()?;
        Ok(())
    }

    #[test]
    fn test_security_mode_verified_with_session_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let mode = SecurityMode::Verified(&session);
        assert!(mode.is_verified());
        assert!(!mode.is_unverified());
        Ok(())
    }

    #[test]
    fn test_security_mode_verified_validate_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let mode = SecurityMode::Verified(&session);
        mode.validate()?;
        Ok(())
    }

    #[test]
    fn test_security_mode_verified_session_is_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let mode = SecurityMode::Verified(&session);
        assert!(mode.session().is_some());
        Ok(())
    }

    #[test]
    fn test_security_mode_unverified_session_returns_none_succeeds() {
        let mode = SecurityMode::Unverified;
        assert!(mode.session().is_none());
    }

    // ZeroTrustAuth tests
    #[test]
    fn test_zero_trust_auth_new_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        // Just verify it was created successfully
        assert!(std::mem::size_of_val(&auth) > 0);
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_with_config_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config =
            ZeroTrustConfig::new().with_timeout(10000).with_complexity(ProofComplexity::High);

        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
        assert!(std::mem::size_of_val(&auth) > 0);
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_generate_challenge_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let challenge = auth.generate_challenge()?;
        assert!(!challenge.is_expired());
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_multiple_challenges_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let challenge1 = auth.generate_challenge()?;
        let challenge2 = auth.generate_challenge()?;

        // Challenges should be different
        assert!(!challenge1.is_expired());
        assert!(!challenge2.is_expired());
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_verify_challenge_age_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let challenge = auth.generate_challenge()?;
        let is_valid = auth.verify_challenge_age(&challenge)?;

        assert!(is_valid, "Freshly generated challenge should be valid");
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_start_continuous_verification_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        warm_up_auth(&auth)?;

        let continuous = auth.start_continuous_verification()?;
        let result = continuous.is_valid();

        assert!(result.is_ok());
        Ok(())
    }

    #[test]
    fn test_start_continuous_verification_rejects_unverified_session() -> Result<()> {
        // A freshly-constructed `ZeroTrustAuth` (no successful proof
        // handshake) must NOT hand out a continuous session — that
        // path would bypass the ZKP gate entirely.
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let result = auth.start_continuous_verification();
        match result {
            Err(CoreError::AuthenticationFailed(_)) => Ok(()),
            other => panic!("expected AuthenticationFailed, got {:?}", other),
        }
    }

    // Challenge tests
    #[test]
    fn test_challenge_is_not_expired_when_fresh_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let challenge = auth.generate_challenge()?;
        assert!(!challenge.is_expired());
        Ok(())
    }

    // ZeroTrustSession tests
    #[test]
    fn test_zero_trust_session_new_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let session = ZeroTrustSession::new(auth);
        assert!(!session.is_authenticated());
        Ok(())
    }

    #[test]
    fn test_zero_trust_session_initiate_authentication_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let mut session = ZeroTrustSession::new(auth);
        let challenge = session.initiate_authentication()?;

        assert!(!challenge.is_expired());
        Ok(())
    }

    #[test]
    fn test_zero_trust_session_is_not_authenticated_initially_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let session = ZeroTrustSession::new(auth);
        assert!(!session.is_authenticated());
        Ok(())
    }

    // ProofComplexity tests
    #[test]
    fn test_proof_complexity_variants_are_correct() {
        let _low = ProofComplexity::Low;
        let _medium = ProofComplexity::Medium;
        let _high = ProofComplexity::High;

        assert_eq!(ProofComplexity::Medium, ProofComplexity::Medium);
    }

    // Integration tests
    #[test]
    fn test_verified_session_with_multiple_instances_succeeds() -> Result<()> {
        // Test creating multiple sessions
        for _ in 0..3 {
            let (public_key, private_key) = generate_keypair()?;
            let session =
                VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
            assert!(session.is_valid());
        }
        Ok(())
    }

    #[test]
    fn test_zero_trust_config_variations_all_succeed_succeeds() -> Result<()> {
        // Test with different configurations
        let configs = vec![
            ZeroTrustConfig::new().with_timeout(5000),
            ZeroTrustConfig::new().with_complexity(ProofComplexity::Low),
            ZeroTrustConfig::new().with_complexity(ProofComplexity::High),
            ZeroTrustConfig::new().with_continuous_verification(true),
            ZeroTrustConfig::new().with_verification_interval(60000),
        ];

        for config in configs {
            let (public_key, private_key) = generate_keypair()?;
            let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
            let _challenge = auth.generate_challenge()?;
        }
        Ok(())
    }

    #[test]
    fn test_trust_level_progression_is_correct() {
        let levels = vec![
            TrustLevel::Untrusted,
            TrustLevel::Partial,
            TrustLevel::Trusted,
            TrustLevel::FullyTrusted,
        ];

        for (i, level) in levels.iter().enumerate() {
            assert_eq!(*level as usize, i);
        }
    }

    #[test]
    fn test_verified_session_multiple_sessions_all_succeed_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;

        // Create multiple sessions with same keys
        let session1 =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
        let session2 =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        // Sessions should have different IDs
        assert!(session1.is_valid());
        assert!(session2.is_valid());
        assert_ne!(session1.session_id(), session2.session_id());
        Ok(())
    }

    #[test]
    fn test_challenge_generation_produces_unique_challenges_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let mut challenges = Vec::new();
        for _ in 0..5 {
            challenges.push(auth.generate_challenge()?);
        }

        // All challenges should be valid
        for challenge in &challenges {
            assert!(!challenge.is_expired());
        }
        Ok(())
    }

    #[test]
    fn test_continuous_session_validation_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_continuous_verification(true);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        warm_up_auth(&auth)?;

        let continuous = auth.start_continuous_verification()?;
        assert!(continuous.is_valid().is_ok());
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_with_all_complexity_levels_succeeds() -> Result<()> {
        let complexities =
            vec![ProofComplexity::Low, ProofComplexity::Medium, ProofComplexity::High];

        for complexity in complexities {
            let (public_key, private_key) = generate_keypair()?;
            let config = ZeroTrustConfig::new().with_complexity(complexity);
            let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
            let challenge = auth.generate_challenge()?;
            assert!(!challenge.is_expired());
        }
        Ok(())
    }

    // ========================================================================
    // Coverage: SecurityMode default/from, VerifiedSession edge cases
    // ========================================================================

    #[test]
    fn test_security_mode_explicit_unverified_succeeds() {
        // Pinning the explicit-construction shape after `impl Default for
        // SecurityMode` was removed. Callers must
        // now opt into `Unverified` by name; this test guards the variant's
        // behaviour, not the (deliberately-absent) `Default` impl.
        let mode = SecurityMode::Unverified;
        assert!(mode.is_unverified());
        assert!(!mode.is_verified());
        assert!(mode.session().is_none());
    }

    #[test]
    fn test_security_mode_from_verified_session_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        let mode: SecurityMode = SecurityMode::from(&session);
        assert!(mode.is_verified());
        assert!(mode.session().is_some());
        Ok(())
    }

    #[test]
    fn test_verified_session_from_unauthenticated_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;
        let session = ZeroTrustSession::new(auth);

        // Session is not authenticated yet
        assert!(!session.is_authenticated());
        let result = session.into_verified();
        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_zero_trust_session_verify_response_returns_error_without_challenge_fails() -> Result<()>
    {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;
        let mut session = ZeroTrustSession::new(auth);

        // Try verify without initiating authentication
        let fake_proof =
            ZeroKnowledgeProof::new(vec![1, 2, 3], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
        let result = session.verify_response(&fake_proof);
        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_zero_trust_session_verify_response_consumes_challenge_replay_blocked() -> Result<()> {
        // Regression test: verify_response must consume the challenge so a
        // captured (challenge, proof) pair on the wire cannot be replayed
        // against the same session within the challenge_timeout_ms window.
        // Ed25519 is deterministic per RFC 8032, so the proof bytes are
        // bit-for-bit reusable; the defense is server-side challenge
        // single-use.
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;
        let mut session = ZeroTrustSession::new(auth);

        let challenge = session.initiate_authentication()?;
        let proof = session.generate_proof(&challenge)?;

        // First verify succeeds.
        assert!(session.verify_response(&proof)?, "first verify_response must accept");

        // Second verify with the SAME proof must reject — the challenge
        // is now consumed (set to None inside verify_response). The
        // exact error variant is opaque (Pattern-6), but we assert the
        // err shape is `AuthenticationFailed` to lock the contract.
        let replay = session.verify_response(&proof);
        match replay {
            Err(CoreError::AuthenticationFailed(_)) => Ok(()),
            other => panic!("replay must reject with AuthenticationFailed, got {other:?}"),
        }
    }

    #[test]
    fn test_zero_trust_session_session_age_ms_is_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;
        let session = ZeroTrustSession::new(auth);

        let age = session.session_age_ms()?;
        // Just created, should be very small
        assert!(age < 5000, "Session age should be < 5 seconds, got {}ms", age);
        Ok(())
    }

    #[test]
    fn test_continuous_session_auth_public_key_is_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key.clone(), private_key)?;

        warm_up_auth(&auth)?;

        let continuous = auth.start_continuous_verification()?;
        assert_eq!(continuous.auth_public_key(), &public_key);
        Ok(())
    }

    /// M-update_verification: `ContinuousSession::update_verification`
    /// was removed because it was a public, proof-less clock bump and
    /// had no in-crate non-test callers. Refresh must route through
    /// `ZeroTrustAuth::reauthenticate`, which is proof-gated.
    #[test]
    fn test_continuous_session_refresh_via_reauthenticate() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        warm_up_auth(&auth)?;

        let continuous = auth.start_continuous_verification()?;
        assert!(continuous.is_valid()?);

        auth.reauthenticate()?;
        Ok(())
    }

    #[test]
    fn test_zero_knowledge_proof_has_valid_format_has_correct_size() {
        // Valid format
        let valid =
            ZeroKnowledgeProof::new(vec![1, 2, 3], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
        assert!(valid.is_valid_format());

        // Empty challenge
        let empty_challenge =
            ZeroKnowledgeProof::new(vec![], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
        assert!(!empty_challenge.is_valid_format());

        // Empty proof
        let empty_proof =
            ZeroKnowledgeProof::new(vec![1], vec![], Utc::now(), ProofComplexity::Low);
        assert!(!empty_proof.is_valid_format());
    }

    #[test]
    fn test_zero_trust_auth_reauthenticate_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        // Reauthenticate should succeed
        auth.reauthenticate()?;
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_verify_continuously_verified_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        // Default config has `continuous_verification = true`, so the
        // status now depends on `last_verification`. Drive a successful
        // proof to bump it; without this warm-up the M9/M10 gate
        // correctly reports `Pending` for a never-verified instance.
        warm_up_auth(&auth)?;

        let status = auth.verify_continuously()?;
        assert_eq!(status, VerificationStatus::Verified);
        Ok(())
    }

    #[test]
    fn test_zero_trust_auth_verify_continuously_with_cv_enabled_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new()
            .with_continuous_verification(true)
            .with_verification_interval(60000);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        warm_up_auth(&auth)?;

        let status = auth.verify_continuously()?;
        // Within the 60 s interval, just-warmed-up auth reports Verified.
        assert_eq!(status, VerificationStatus::Verified);
        Ok(())
    }

    #[test]
    fn test_verify_continuously_pending_before_first_proof() -> Result<()> {
        // M9/M10 regression guard: with `continuous_verification`
        // enabled, a freshly-constructed auth that has never
        // completed a proof must report `Pending`, not `Verified`.
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new()
            .with_continuous_verification(true)
            .with_verification_interval(60000);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let status = auth.verify_continuously()?;
        assert_eq!(status, VerificationStatus::Pending);
        Ok(())
    }

    #[test]
    fn test_generate_proof_returns_error_for_empty_challenge_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let result = auth.generate_proof(&[]);
        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_verify_proof_returns_error_for_wrong_challenge_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let challenge = auth.generate_challenge()?;
        let proof = auth.generate_proof(challenge.data())?;

        // Verify with a different challenge should fail
        let different_challenge = vec![0xFF; 32];
        let result = auth.verify_proof(&proof, &different_challenge)?;
        assert!(!result);
        Ok(())
    }

    #[test]
    fn test_verify_proof_returns_error_for_short_proof_data_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let challenge_data = vec![1u8; 32];
        let short_proof = ZeroKnowledgeProof::new(
            challenge_data.clone(),
            vec![0u8; 10], // Too short (< 64 bytes)
            Utc::now(),
            ProofComplexity::Low,
        );
        let result = auth.verify_proof(&short_proof, &challenge_data)?;
        assert!(!result);
        Ok(())
    }

    #[test]
    fn test_full_challenge_response_low_complexity_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge = auth.generate_challenge()?;
        let proof = auth.generate_proof(challenge.data())?;
        let verified = auth.verify_proof(&proof, challenge.data())?;
        assert!(verified);
        Ok(())
    }

    #[test]
    fn test_full_challenge_response_medium_complexity_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge = auth.generate_challenge()?;
        let proof = auth.generate_proof(challenge.data())?;
        let verified = auth.verify_proof(&proof, challenge.data())?;
        assert!(verified);
        Ok(())
    }

    #[test]
    fn test_full_challenge_response_high_complexity_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge = auth.generate_challenge()?;
        let proof = auth.generate_proof(challenge.data())?;
        let verified = auth.verify_proof(&proof, challenge.data())?;
        assert!(verified);
        Ok(())
    }

    #[test]
    fn test_proof_of_possession_roundtrip() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;

        let pop = auth.generate_pop(b"unit-test-challenge")?;
        let verified = auth.verify_pop(&pop, b"unit-test-challenge")?;
        assert!(verified);
        Ok(())
    }

    #[test]
    fn test_full_session_flow_with_into_verified_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let auth = ZeroTrustAuth::new(public_key, private_key)?;
        let mut session = ZeroTrustSession::new(auth);

        // Initiate -> prove -> verify -> convert
        let challenge = session.initiate_authentication()?;
        let proof = session.auth.generate_proof(challenge.data())?;
        let verified = session.verify_response(&proof)?;
        assert!(verified);
        assert!(session.is_authenticated());

        let verified_session = session.into_verified()?;
        assert!(verified_session.is_valid());
        assert_eq!(verified_session.trust_level(), TrustLevel::Trusted);
        Ok(())
    }

    #[test]
    fn test_verified_session_debug_has_correct_format() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        // VerifiedSession does not implement Clone (sessions are non-cloneable by design).
        // Verify Debug output instead.
        let debug = format!("{:?}", session);
        assert!(debug.contains("VerifiedSession"));
        Ok(())
    }

    #[test]
    fn test_security_mode_debug_has_correct_format() -> Result<()> {
        let mode = SecurityMode::Unverified;
        let debug = format!("{:?}", mode);
        assert!(debug.contains("Unverified"));

        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;
        let verified_mode = SecurityMode::Verified(&session);
        let debug2 = format!("{:?}", verified_mode);
        assert!(debug2.contains("Verified"));
        Ok(())
    }

    #[test]
    fn test_challenge_fields_are_accessible() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config =
            ZeroTrustConfig::new().with_timeout(5000).with_complexity(ProofComplexity::High);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge = auth.generate_challenge()?;
        assert_eq!(challenge.data().len(), 128); // High complexity = 128 bytes
        assert_eq!(challenge.timeout_ms(), 5000);
        assert!(!challenge.is_expired());

        let debug = format!("{:?}", challenge);
        assert!(debug.contains("Challenge"));
        Ok(())
    }

    // ========================================================================
    // Expired session coverage
    // ========================================================================

    #[test]
    fn test_verified_session_expired_verify_valid_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        // Manually create an expired session by setting expires_at in the past
        // force expiry via the monotonic path. Wall-clock
        // `expires_at` is now audit-only; `is_valid` consults
        // `issued_at_monotonic` + `lifetime`, so we set
        // `lifetime = 0ns` to make `Instant::elapsed()` exceed it
        // immediately.
        let expired_session = VerifiedSession {
            session_id: *session.session_id(),
            authenticated_at: session.authenticated_at(),
            trust_level: session.trust_level(),
            public_key: session.public_key().clone(),
            expires_at: Utc::now() - Duration::seconds(1),
            issued_at_monotonic: std::time::Instant::now(),
            lifetime: std::time::Duration::from_nanos(0),
        };
        assert!(!expired_session.is_valid());

        let result = expired_session.verify_valid();
        assert!(result.is_err(), "Expired session should fail verify_valid");
        match result {
            Err(CoreError::SessionExpired) => {} // expected
            other => panic!("Expected SessionExpired, got: {:?}", other),
        }
        Ok(())
    }

    #[test]
    fn test_security_mode_validate_expired_session_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let session =
            VerifiedSession::establish(public_key.as_slice(), private_key.expose_secret())?;

        // force expiry via the monotonic path. Wall-clock
        // `expires_at` is now audit-only; `is_valid` consults
        // `issued_at_monotonic` + `lifetime`, so we set
        // `lifetime = 0ns` to make `Instant::elapsed()` exceed it
        // immediately.
        let expired_session = VerifiedSession {
            session_id: *session.session_id(),
            authenticated_at: session.authenticated_at(),
            trust_level: session.trust_level(),
            public_key: session.public_key().clone(),
            expires_at: Utc::now() - Duration::seconds(1),
            issued_at_monotonic: std::time::Instant::now(),
            lifetime: std::time::Duration::from_nanos(0),
        };

        let mode = SecurityMode::Verified(&expired_session);
        let result = mode.validate();
        assert!(result.is_err(), "Expired session in SecurityMode should fail validation");
        Ok(())
    }

    // ========================================================================
    // Continuous verification edge cases
    // ========================================================================

    #[test]
    fn test_continuous_verification_pending_after_interval_succeeds() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        // Set verification_interval to 1ms so it immediately triggers Pending
        let config =
            ZeroTrustConfig::new().with_continuous_verification(true).with_verification_interval(1);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        // Sleep to ensure we're past 1ms interval
        std::thread::sleep(std::time::Duration::from_millis(5));

        let status = auth.verify_continuously()?;
        assert_eq!(status, VerificationStatus::Pending, "Should be Pending after interval elapsed");
        Ok(())
    }

    #[test]
    fn test_challenge_generation_low_complexity_has_correct_size() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge = auth.generate_challenge()?;
        assert_eq!(challenge.data().len(), 32, "Low complexity = 32 bytes");
        Ok(())
    }

    #[test]
    fn test_challenge_generation_medium_complexity_has_correct_size() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge = auth.generate_challenge()?;
        assert_eq!(challenge.data().len(), 64, "Medium complexity = 64 bytes");
        Ok(())
    }

    #[test]
    fn test_verify_proof_medium_short_proof_rejects_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge_data = vec![1u8; 64];
        // Proof is 64 bytes (just signature, no timestamp) — too short for Medium (needs 72)
        let short_proof = ZeroKnowledgeProof::new(
            challenge_data.clone(),
            vec![0u8; 64],
            Utc::now(),
            ProofComplexity::Medium,
        );
        let result = auth.verify_proof(&short_proof, &challenge_data)?;
        assert!(!result, "Medium-complexity proof without timestamp should fail");
        Ok(())
    }

    #[test]
    fn test_verify_proof_high_short_proof_rejects_fails() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;

        let challenge_data = vec![1u8; 128];
        // Proof is 70 bytes — too short for High (needs 72)
        let short_proof = ZeroKnowledgeProof::new(
            challenge_data.clone(),
            vec![0u8; 70],
            Utc::now(),
            ProofComplexity::High,
        );
        let result = auth.verify_proof(&short_proof, &challenge_data)?;
        assert!(!result, "High-complexity proof too short should fail");
        Ok(())
    }

    // regression coverage for the
    // L-3 fix that capped forward clock-skew tolerance at
    // 30 s on `verify_proof`. Previously `now_ms.abs_diff(proof_ts_ms)
    // > 300_000` allowed proofs up to 5 min in the future; an
    // attacker with a forward-skewed clock got a 10-min replay
    // window. The fix added `if proof_ts_ms > now_ms + 30_000 {
    // return Ok(false); }` to all three ProofComplexity paths.
    // These tests assert that a 31 s-ahead timestamp is rejected on
    // each path, locking in the contract against future refactors
    // that might reorder the gates.
    //
    // Helper: forge a proof whose embedded timestamp is `skew_ms`
    // ahead of "now". The forged signature is invalid against the
    // forged message; the future-skew check should reject BEFORE
    // signature verification, so the test asserts `Ok(false)`
    // regardless of signature validity.
    fn forge_future_skewed_proof(skew_ms: i64, complexity: ProofComplexity) -> ZeroKnowledgeProof {
        let now_ms = Utc::now().timestamp_millis();
        let future_ts = now_ms.saturating_add(skew_ms);
        let timestamp_bytes = future_ts.to_le_bytes();
        let mut proof_data = vec![0u8; 64]; // dummy 64-byte signature
        proof_data.extend_from_slice(&timestamp_bytes);
        ZeroKnowledgeProof::new(vec![1u8; 32], proof_data, Utc::now(), complexity)
    }

    #[test]
    fn test_verify_proof_low_rejects_31s_future_timestamp() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
        let challenge = vec![1u8; 32];
        let proof = forge_future_skewed_proof(31_000, ProofComplexity::Low);
        let result = auth.verify_proof(&proof, &challenge)?;
        assert!(!result, "Low: 31 s future-skew must reject before signature check");
        Ok(())
    }

    #[test]
    fn test_verify_proof_medium_rejects_31s_future_timestamp() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::Medium);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
        let challenge = vec![1u8; 32];
        let proof = forge_future_skewed_proof(31_000, ProofComplexity::Medium);
        let result = auth.verify_proof(&proof, &challenge)?;
        assert!(!result, "Medium: 31 s future-skew must reject before signature check");
        Ok(())
    }

    #[test]
    fn test_verify_proof_high_rejects_31s_future_timestamp() -> Result<()> {
        let (public_key, private_key) = generate_keypair()?;
        let config = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
        let auth = ZeroTrustAuth::with_config(public_key, private_key, config)?;
        let challenge = vec![1u8; 32];
        let proof = forge_future_skewed_proof(31_000, ProofComplexity::High);
        let result = auth.verify_proof(&proof, &challenge)?;
        assert!(!result, "High: 31 s future-skew must reject before signature check");
        Ok(())
    }

    #[test]
    fn test_zero_knowledge_proof_debug_and_clone_succeeds() {
        let proof =
            ZeroKnowledgeProof::new(vec![1, 2, 3], vec![0u8; 64], Utc::now(), ProofComplexity::Low);
        let cloned = proof.clone();
        assert_eq!(cloned.challenge(), proof.challenge());
        assert_eq!(cloned.proof_data(), proof.proof_data());
        let debug = format!("{:?}", proof);
        assert!(debug.contains("ZeroKnowledgeProof"));
    }

    #[test]
    fn test_proof_of_possession_data_debug_and_clone_succeeds() {
        let pop =
            ProofOfPossessionData::new(PublicKey::new(vec![1, 2, 3]), vec![0u8; 64], Utc::now());
        let cloned = pop.clone();
        assert_eq!(cloned.public_key(), pop.public_key());
        let debug = format!("{:?}", pop);
        assert!(debug.contains("ProofOfPossessionData"));
    }

    // =========================================================================
    // Pattern P4: ZeroTrustConfig Parameter Influence Tests
    // Each test proves changing ONLY one field changes the observable output.
    // =========================================================================

    #[test]
    fn test_challenge_timeout_ms_influences_challenge_timeout_succeeds() -> Result<()> {
        let (public_key_a, private_key_a) = generate_keypair()?;
        let config_a = ZeroTrustConfig::new().with_timeout(1000);
        let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
        let challenge_a = auth_a.generate_challenge()?;

        let (public_key_b, private_key_b) = generate_keypair()?;
        let config_b = ZeroTrustConfig::new().with_timeout(9999);
        let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
        let challenge_b = auth_b.generate_challenge()?;

        assert_ne!(
            challenge_a.timeout_ms(),
            challenge_b.timeout_ms(),
            "challenge_timeout_ms must influence the timeout embedded in generated challenges"
        );
        Ok(())
    }

    #[test]
    fn test_proof_complexity_influences_challenge_data_size_has_correct_size() -> Result<()> {
        let (public_key_a, private_key_a) = generate_keypair()?;
        let config_a = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
        let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
        let challenge_a = auth_a.generate_challenge()?;

        let (public_key_b, private_key_b) = generate_keypair()?;
        let config_b = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
        let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
        let challenge_b = auth_b.generate_challenge()?;

        // Low => 32-byte challenge, High => 128-byte challenge
        assert_ne!(
            challenge_a.data().len(),
            challenge_b.data().len(),
            "proof_complexity must influence the size of generated challenge data"
        );
        assert_eq!(challenge_a.data().len(), 32, "Low complexity must produce 32-byte challenge");
        assert_eq!(
            challenge_b.data().len(),
            128,
            "High complexity must produce 128-byte challenge"
        );
        Ok(())
    }

    #[test]
    fn test_proof_complexity_influences_proof_data_size_has_correct_size() -> Result<()> {
        // Low now also carries the
        // 8-byte timestamp suffix (mandatory replay protection), so Low
        // and Medium are byte-identical (72 bytes = signature + ts).
        // The remaining functional difference is that High additionally
        // binds the public key into the signed message — but that does
        // NOT change the proof-data length (the timestamp suffix is the
        // same shape). This test now asserts the floor invariant: every
        // complexity level produces at least 72 bytes (sig + ts).
        let (public_key_a, private_key_a) = generate_keypair()?;
        let config_a = ZeroTrustConfig::new().with_complexity(ProofComplexity::Low);
        let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;
        let challenge_a = auth_a.generate_challenge()?;
        let proof_a = auth_a.generate_proof(challenge_a.data())?;

        let (public_key_b, private_key_b) = generate_keypair()?;
        let config_b = ZeroTrustConfig::new().with_complexity(ProofComplexity::High);
        let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;
        let challenge_b = auth_b.generate_challenge()?;
        let proof_b = auth_b.generate_proof(challenge_b.data())?;

        assert!(
            proof_a.proof_data().len() >= 72,
            "Low proof must be at least 72 bytes (signature + 8-byte timestamp), got {}",
            proof_a.proof_data().len()
        );
        assert!(
            proof_b.proof_data().len() >= 72,
            "High proof must be at least 72 bytes, got {}",
            proof_b.proof_data().len()
        );
        Ok(())
    }

    #[test]
    fn test_continuous_verification_influences_verify_continuously_succeeds() -> Result<()> {
        let (public_key_a, private_key_a) = generate_keypair()?;
        // continuous_verification=false: verify_continuously returns Verified immediately
        let config_a = ZeroTrustConfig::new()
            .with_continuous_verification(false)
            .with_verification_interval(1); // 1ms interval — irrelevant when cv=false
        let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;

        let (public_key_b, private_key_b) = generate_keypair()?;
        // continuous_verification=true with 1ms interval triggers Pending immediately
        let config_b =
            ZeroTrustConfig::new().with_continuous_verification(true).with_verification_interval(1);
        let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;

        // Sleep to ensure the 1ms verification interval elapses for auth_b
        std::thread::sleep(std::time::Duration::from_millis(5));

        // auth_a (cv=false) must stay Verified since cv is disabled
        let status_a = auth_a.verify_continuously()?;
        // auth_b (cv=true, 1ms) must go Pending after the interval elapses
        let status_b = auth_b.verify_continuously()?;

        // cv=false → Verified; cv=true + 1ms → Pending (interval already elapsed)
        assert_eq!(
            status_a,
            VerificationStatus::Verified,
            "continuous_verification=false must return Verified without interval check"
        );
        assert_eq!(
            status_b,
            VerificationStatus::Pending,
            "continuous_verification=true with 1ms interval must return Pending after elapsed"
        );
        Ok(())
    }

    #[test]
    fn test_verification_interval_ms_influences_continuous_session_validity_succeeds() -> Result<()>
    {
        let (public_key_a, private_key_a) = generate_keypair()?;
        // Very short interval (1ms): is_valid() will return false almost immediately
        let config_a =
            ZeroTrustConfig::new().with_continuous_verification(true).with_verification_interval(1);
        let auth_a = ZeroTrustAuth::with_config(public_key_a, private_key_a, config_a)?;

        let (public_key_b, private_key_b) = generate_keypair()?;
        // Long interval (1 hour): is_valid() stays true
        let config_b = ZeroTrustConfig::new()
            .with_continuous_verification(true)
            .with_verification_interval(3_600_000);
        let auth_b = ZeroTrustAuth::with_config(public_key_b, private_key_b, config_b)?;

        warm_up_auth(&auth_a)?;
        warm_up_auth(&auth_b)?;

        let session_a = auth_a.start_continuous_verification()?;
        let session_b = auth_b.start_continuous_verification()?;

        // Let 1ms pass so the short-interval session expires
        std::thread::sleep(std::time::Duration::from_millis(5));

        let valid_a = session_a.is_valid()?;
        let valid_b = session_b.is_valid()?;

        assert_ne!(
            valid_a, valid_b,
            "verification_interval_ms must influence continuous session validity"
        );
        assert!(!valid_a, "1ms interval session must be invalid after 5ms sleep");
        assert!(valid_b, "1-hour interval session must remain valid");
        Ok(())
    }
}
