//! Integration test for ADR-016 task A1: stale biometric credential detection
//! in the vault-unlock path.
//!
//! Scenario: a vault is created with password A, biometric quick-unlock is
//! enabled (credential A is stored in the OS credential store), then the vault
//! password is rotated to B.  The next unlock attempt using the stored keyring
//! credential (A) must produce `StaleBiometricCredential`, not the generic
//! `DecryptionFailed`, and the error message must contain the re-enroll
//! instruction required by ADR-016 section 4.
//!
//! These tests exercise the discriminating logic that lives in
//! `tsafe-core::keyring_store::looks_like_stale_credential` and
//! `tsafe-core::errors::SafeError::StaleBiometricCredential`.  The CLI-layer
//! wiring (`classify_biometric_unlock_error` in `cmd_biometric.rs`) calls
//! exactly this logic; verifying it at the core level is sufficient because the
//! thin wrapper adds no new behaviour.

use tempfile::tempdir;
use tsafe_core::errors::SafeError;
use tsafe_core::keyring_store::looks_like_stale_credential;
use tsafe_core::vault::Vault;

// ── Vault rekey → stale credential simulation ─────────────────────────────────

/// Create a vault, rotate the password, then attempt to open with the old
/// password.  Assert the resulting error is classified as a stale credential.
#[test]
fn vault_rekey_produces_decryption_failed_classified_as_stale() {
    let dir = tempdir().unwrap();
    let vault_path = dir.path().join("test.vault");

    let original_password = b"original-password";
    let new_password = b"rotated-password";

    // Step 1: create vault with the original password.
    // Vault::create already calls save internally; drop to release the lock.
    drop(Vault::create(&vault_path, original_password).unwrap());

    // Step 2: rotate the vault password (simulates `tsafe rotate`).
    // Vault::rotate re-encrypts everything under the new key and calls save internally.
    {
        let mut vault = Vault::open(&vault_path, original_password).unwrap();
        vault.rotate(new_password).unwrap();
    }

    // Step 3: attempt to open with the now-stale credential (old password).
    //
    // This simulates what happens when `retrieve_biometric_password` returns
    // `original_password` after the vault has been re-keyed to `new_password`.
    let err = match Vault::open(&vault_path, original_password) {
        Ok(_) => panic!("opening with stale credential must fail but succeeded"),
        Err(e) => e,
    };

    // Step 4: the raw error must be DecryptionFailed.
    assert!(
        matches!(err, SafeError::DecryptionFailed),
        "expected DecryptionFailed from vault open with stale credential; got: {err}"
    );

    // Step 5: looks_like_stale_credential classifies the DecryptionFailed display
    // as a stale credential — this is the discrimination logic that
    // `classify_biometric_unlock_error` calls internally.
    let err_display = err.to_string();
    assert!(
        looks_like_stale_credential(&err_display),
        "`looks_like_stale_credential` must classify DecryptionFailed as stale; err: {err_display}"
    );

    // Step 6: the canonical stale-credential error produced by the helper must
    // contain the re-enroll instruction (ADR-016 section 4 requirement).
    let stale_err = SafeError::StaleBiometricCredential;
    let stale_msg = stale_err.to_string();
    assert!(
        stale_msg.contains("re-enroll"),
        "StaleBiometricCredential must contain re-enroll instruction; got: {stale_msg}"
    );
    assert!(
        stale_msg.contains("biometric"),
        "StaleBiometricCredential must mention 'biometric' for operator clarity; got: {stale_msg}"
    );
}

/// Confirm that `looks_like_stale_credential` correctly maps the exact display
/// text of `SafeError::DecryptionFailed` — ensuring the heuristic in
/// `keyring_store` and the error text in `errors.rs` are kept in sync.
#[test]
fn decryption_failed_display_is_classified_as_stale() {
    let display = SafeError::DecryptionFailed.to_string();
    assert!(
        looks_like_stale_credential(&display),
        "The display of DecryptionFailed ({display:?}) must be classified as a stale credential \
         so that classify_biometric_unlock_error can remap it to StaleBiometricCredential"
    );
}

/// Confirm that the stale-credential error and the generic decryption-failed
/// error are distinct and that only the stale variant carries the re-enroll
/// guidance (ADR-016 section 4 — the error must be explicit and actionable).
#[test]
fn stale_error_is_distinct_from_generic_decryption_failed() {
    let stale = SafeError::StaleBiometricCredential.to_string();
    let generic = SafeError::DecryptionFailed.to_string();

    assert_ne!(
        stale, generic,
        "StaleBiometricCredential and DecryptionFailed must be distinct error variants"
    );
    assert!(
        stale.contains("re-enroll"),
        "StaleBiometricCredential must contain the re-enroll recovery path; got: {stale}"
    );
    assert!(
        !generic.contains("re-enroll"),
        "DecryptionFailed must NOT contain re-enroll (it is a generic error); got: {generic}"
    );
}

/// Unrelated errors (IO failures, profile not found, etc.) must NOT be
/// classified as stale credentials — the heuristic must be conservative
/// per the constraint "if in doubt, keep DecryptionFailed".
#[test]
fn unrelated_errors_are_not_classified_as_stale() {
    let unrelated = [
        SafeError::VaultNotFound {
            path: "/some/path".into(),
        }
        .to_string(),
        SafeError::ProfileNotFound {
            name: "staging".into(),
        }
        .to_string(),
        SafeError::Io(std::io::Error::new(
            std::io::ErrorKind::PermissionDenied,
            "permission denied",
        ))
        .to_string(),
    ];

    for msg in &unrelated {
        assert!(
            !looks_like_stale_credential(msg),
            "`looks_like_stale_credential` must not classify unrelated error as stale: {msg}"
        );
    }
}

// ── Vault open success with correct password after rekey ──────────────────────

/// After a vault is re-keyed, the new password must open it successfully.
/// This validates that the test vault-rekey scenario above is correct end-to-end.
#[test]
fn vault_opens_with_new_password_after_rekey() {
    let dir = tempdir().unwrap();
    let vault_path = dir.path().join("test.vault");

    let original_password = b"original-password";
    let new_password = b"rotated-password";

    // Create and immediately rekey.
    {
        let vault = Vault::create(&vault_path, original_password).unwrap();
        drop(vault); // save is called by create; release the lock
    }
    {
        let mut vault = Vault::open(&vault_path, original_password).unwrap();
        vault.rotate(new_password).unwrap(); // rotate calls save internally
    }

    // New password must open successfully.
    Vault::open(&vault_path, new_password)
        .expect("vault must open with the new password after rotate");
}
