//! Tests for ADR-016 biometric recovery contract.
//!
//! Covers:
//! - Stale credential detection produces a clear error containing "re-enroll"
//! - Agent session remains valid when biometric unlock fails (stale credential)
//! - `looks_like_stale_credential` correctly classifies error messages

use std::time::{Duration, Instant};
use tsafe_core::agent::{AgentRequest, AgentResponse, AgentSession, AgentSessionState};
use tsafe_core::errors::SafeError;
use tsafe_core::keyring_store::looks_like_stale_credential;

// ── Stale credential detection ────────────────────────────────────────────────

#[test]
fn stale_credential_error_message_contains_re_enroll() {
    let err = SafeError::StaleBiometricCredential;
    let msg = err.to_string();
    assert!(
        msg.contains("re-enroll"),
        "StaleBiometricCredential error must direct the user to re-enroll; got: {msg}"
    );
}

#[test]
fn stale_credential_error_message_names_the_failure() {
    let err = SafeError::StaleBiometricCredential;
    let msg = err.to_string();
    assert!(
        msg.contains("stale biometric credential"),
        "StaleBiometricCredential error must name the failure as 'stale biometric credential'; got: {msg}"
    );
}

#[test]
fn looks_like_stale_credential_matches_decryption_failed() {
    // The display of SafeError::DecryptionFailed is the canonical trigger.
    assert!(looks_like_stale_credential(
        "decryption failed — wrong password or corrupted vault"
    ));
}

#[test]
fn looks_like_stale_credential_matches_wrong_password() {
    assert!(looks_like_stale_credential("wrong password"));
    assert!(looks_like_stale_credential(
        "vault open: wrong password supplied"
    ));
}

#[test]
fn looks_like_stale_credential_matches_os_level_messages() {
    // macOS / libsecret may surface these.
    assert!(looks_like_stale_credential("credential changed"));
    assert!(looks_like_stale_credential("keychain item is invalid"));
    assert!(looks_like_stale_credential("credential is stale"));
}

#[test]
fn looks_like_stale_credential_rejects_unrelated_errors() {
    assert!(!looks_like_stale_credential("vault not found"));
    assert!(!looks_like_stale_credential("io error: connection refused"));
    assert!(!looks_like_stale_credential("profile not found: staging"));
    assert!(!looks_like_stale_credential(
        "keyring store: no service running"
    ));
}

// ── Agent session preservation ────────────────────────────────────────────────
//
// ADR-016 contract: biometric unlock failure does NOT invalidate a running agent
// session unless the session token has independently expired.

#[test]
fn agent_session_remains_active_after_simulated_biometric_failure() {
    // Simulate: agent session is active, then biometric unlock fails (stale credential).
    // The agent session object itself must not be affected by the biometric failure —
    // biometric failure is a separate code path that never touches the AgentSession.
    let now = Instant::now();
    let mut session = AgentSession::new("session-token", 300, now + Duration::from_secs(300));

    // Simulate biometric failure: construct a StaleBiometricCredential error.
    // (In a real daemon this would be returned by retrieve_password + Vault::open.)
    let biometric_err = SafeError::StaleBiometricCredential;
    let err_msg = biometric_err.to_string();

    // Verify the error is classified correctly.
    assert!(
        err_msg.contains("stale biometric credential"),
        "error classification must name the failure: {err_msg}"
    );
    assert!(
        err_msg.contains("re-enroll"),
        "error must guide user to re-enroll: {err_msg}"
    );

    // The agent session is unaffected — verify it still grants OpenVault.
    let outcome = session.handle_request(
        &AgentRequest::OpenVault {
            profile: "default".into(),
            session_token: "session-token".into(),
            requesting_pid: 1234,
        },
        Some(1234),
        "master-password",
        now,
    );

    assert_eq!(
        outcome.state,
        AgentSessionState::Active,
        "agent session must remain Active after biometric failure; got {:?}",
        outcome.state
    );
    assert!(
        !outcome.stop,
        "biometric failure must not stop the agent session"
    );
    match outcome.response {
        AgentResponse::Password { password } => {
            assert_eq!(
                password, "master-password",
                "session must still supply the vault password via the agent path"
            );
        }
        other => panic!("expected Password response from active session, got {other:?}"),
    }
}

#[test]
fn agent_session_expiry_is_independent_of_biometric_state() {
    // An already-expired session returns Expired regardless of biometric state.
    let now = Instant::now();
    let mut session = AgentSession::new("session-token", 1, now - Duration::from_secs(1));

    let outcome = session.handle_request(
        &AgentRequest::OpenVault {
            profile: "default".into(),
            session_token: "session-token".into(),
            requesting_pid: 1234,
        },
        Some(1234),
        "master-password",
        now,
    );

    assert_eq!(outcome.state, AgentSessionState::Expired);
    assert!(outcome.stop);
    // No password returned from an expired session.
    match outcome.response {
        AgentResponse::Err { .. } => {}
        other => panic!("expected Err from expired session, got {other:?}"),
    }
}

#[test]
fn stale_credential_error_is_not_a_decryption_failure() {
    // StaleBiometricCredential and DecryptionFailed are distinct variants — the former
    // provides actionable re-enroll guidance, the latter is a generic crypto failure.
    let stale = SafeError::StaleBiometricCredential.to_string();
    let generic = SafeError::DecryptionFailed.to_string();

    assert_ne!(
        stale, generic,
        "StaleBiometricCredential must be distinct from DecryptionFailed"
    );
    assert!(
        stale.contains("re-enroll"),
        "StaleBiometricCredential must contain re-enroll instruction"
    );
    assert!(
        !generic.contains("re-enroll"),
        "DecryptionFailed must not contain re-enroll instruction"
    );
}
