//! Audit tests mapped to `docs/security/secure-storage-audit-report.md`.
//!
//! Coverage:
//!   * SS-AUD-001 — Identity secret is not stored in local DB.
//!   * SS-AUD-002 — Server password is not stored in local DB.
//!   * SS-AUD-003 — Secret values are not written to application logs.
//!   * SS-AUD-005 — Secure storage failure returns safe error.
//!   * SS-AUD-006 — Secret deletion removes secure-storage entry.
//!   * SS-TC-003  — Linux: write / read / delete round-trip.
//!
//! NOT covered (out of scope for this PoC):
//!   * SS-AUD-004 (diagnostic export redaction) — owned by
//!     `diagnostics-redaction-spike`.
//!   * SS-AUD-007/008 — process-level documentation / migration paths.
//!   * SS-TC-001/002/004/005 — non-Linux platforms.

#![cfg(target_os = "linux")]

use std::sync::Once;

use secure_storage_spike::{
    secret::{linux::LinuxSecureStore, Secret},
    test_logger::CapturedLog,
    LocalDatabaseRepository, SecretStorageRepository, SecureStoreError,
};
use serial_test::serial;
use tracing::{info, subscriber::DefaultGuard, Level};

const SERVICE: &str = "app.chanora.poc.secure-storage-tests";

static INIT: Once = Once::new();

/// Hermetic backend selection.
///
/// On Linux the trait has two equivalent backends (SysRS-053 / SysRS-162:
/// "Secret Service, libsecret, **or equivalent**"). The test suite uses
/// the kernel keyutils backend so it is deterministic and does not
/// depend on an unlocked Secret Service collection. The CLI driver
/// `src/main.rs` continues to exercise the Secret Service path.
fn store() -> LinuxSecureStore {
    LinuxSecureStore::new_keyutils(SERVICE)
}

/// One-time guard so missing kernel keyutils support is reported clearly
/// rather than as a generic backend error.
fn ensure_backend_or_skip() -> bool {
    INIT.call_once(|| {});
    // keyutils is a Linux-kernel feature; if it's compiled out or
    // disabled, every set() call will fail with `Unavailable`. We can't
    // easily probe without writing, so we rely on the per-test errors.
    true
}

/// Install a per-test tracing subscriber that captures everything.
fn capture_logs() -> (CapturedLog, DefaultGuard) {
    let cap = CapturedLog::new();
    let subscriber = tracing_subscriber::fmt()
        .with_max_level(Level::TRACE)
        .with_writer(cap.clone())
        .without_time()
        .with_ansi(false)
        .finish();
    let guard = tracing::subscriber::set_default(subscriber);
    (cap, guard)
}

// Distinctive plaintext so a grep in test artifacts unambiguously
// identifies a leak.
const IDENTITY_SECRET: &str = "CHANORA_POC_IDENTITY_SECRET_DO_NOT_LEAK_b8f1a4";
const SERVER_PASSWORD: &str = "CHANORA_POC_SERVER_PASSWORD_DO_NOT_LEAK_2f7c01";

#[test]
#[serial]
fn ss_tc_003_round_trip_linux() {
    if !ensure_backend_or_skip() {
        return;
    }
    let store = store();
    let name = "ss-tc-003.identity.primary";
    let _ = store.delete(name); // ensure clean slate

    let s = Secret::from_utf8(IDENTITY_SECRET);
    store.set(name, &s).expect("set");
    let got = store.get(name).expect("get");
    assert_eq!(s, got, "round-trip mismatch");

    store.delete(name).expect("delete");
    assert!(matches!(store.get(name), Err(SecureStoreError::NotFound)));
}

#[test]
#[serial]
fn ss_aud_001_identity_secret_absent_from_local_db() {
    if !ensure_backend_or_skip() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let db_path = tmp.path().join("chanora.sqlite");
    let db = LocalDatabaseRepository::open(&db_path).expect("open");

    // Store identity secret via the secure store, by name only in the DB.
    let store = store();
    let identity_ref = "ss-aud-001.identity.primary";
    let _ = store.delete(identity_ref);
    store
        .set(identity_ref, &Secret::from_utf8(IDENTITY_SECRET))
        .expect("set");

    db.insert_bookmark("Vigorous", "cn.teamspeak.app", identity_ref)
        .expect("insert");

    // Force the DB to flush and inspect the file bytes.
    drop(db);
    let bytes = LocalDatabaseRepository::raw_db_bytes(&db_path).unwrap();
    assert!(
        !bytes.windows(IDENTITY_SECRET.len()).any(|w| w == IDENTITY_SECRET.as_bytes()),
        "identity secret plaintext found inside local SQLite file"
    );

    // The reference (= the lookup name) is allowed to be in the DB.
    assert!(bytes.windows(identity_ref.len()).any(|w| w == identity_ref.as_bytes()));

    // Cleanup.
    store.delete(identity_ref).ok();
}

#[test]
#[serial]
fn ss_aud_002_server_password_absent_from_local_db() {
    if !ensure_backend_or_skip() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let db_path = tmp.path().join("chanora.sqlite");
    let db = LocalDatabaseRepository::open(&db_path).expect("open");

    let store = store();
    let pw_ref = "ss-aud-002.server.42.password";
    let _ = store.delete(pw_ref);
    store
        .set(pw_ref, &Secret::from_utf8(SERVER_PASSWORD))
        .expect("set");

    db.insert_bookmark("Some Server", "ts.example.invalid", pw_ref)
        .expect("insert");

    drop(db);
    let bytes = LocalDatabaseRepository::raw_db_bytes(&db_path).unwrap();
    assert!(
        !bytes.windows(SERVER_PASSWORD.len()).any(|w| w == SERVER_PASSWORD.as_bytes()),
        "server password plaintext found inside local SQLite file"
    );

    store.delete(pw_ref).ok();
}

#[test]
#[serial]
fn ss_aud_003_secret_values_not_in_logs() {
    if !ensure_backend_or_skip() {
        return;
    }
    let (cap, _guard) = capture_logs();

    let store = store();
    let name = "ss-aud-003.identity.primary";
    let _ = store.delete(name);

    let secret = Secret::from_utf8(IDENTITY_SECRET);

    // The kinds of things a developer is likely to log accidentally.
    info!(target: "spike", "writing identity secret name={} value={:?}", name, secret);
    store.set(name, &secret).expect("set");

    let got = store.get(name).expect("get");
    info!(target: "spike", "round-trip ok display=`{}` debug=`{:?}` len={}", got, got, got.len());

    store.delete(name).ok();

    let log_output = cap.snapshot();
    assert!(
        !log_output.contains(IDENTITY_SECRET),
        "identity secret plaintext leaked into logs:\n{log_output}"
    );
    // Sanity: redaction marker present.
    assert!(log_output.contains("<redacted>") || log_output.contains("redacted"));
}

#[test]
#[serial]
fn ss_aud_005_safe_error_on_missing_entry() {
    if !ensure_backend_or_skip() {
        return;
    }
    let store = store();
    let name = "ss-aud-005.never-stored";
    let _ = store.delete(name);

    let err = store.get(name).err().expect("expected NotFound");
    // The error must not embed any secret-like content; for `NotFound`
    // we additionally check the Display representation is generic.
    let msg = format!("{err}");
    assert!(matches!(err, SecureStoreError::NotFound));
    assert!(msg.contains("not found"));
    assert!(!msg.contains(IDENTITY_SECRET));
    assert!(!msg.contains(SERVER_PASSWORD));
}

#[test]
#[serial]
fn ss_aud_006_delete_removes_entry() {
    if !ensure_backend_or_skip() {
        return;
    }
    let store = store();
    let name = "ss-aud-006.identity.primary";

    let _ = store.delete(name);
    store
        .set(name, &Secret::from_utf8(IDENTITY_SECRET))
        .expect("set");
    assert!(store.get(name).is_ok());

    store.delete(name).expect("delete");
    assert!(matches!(store.get(name), Err(SecureStoreError::NotFound)));

    // Second delete must fail with NotFound, never silently succeed.
    assert!(matches!(store.delete(name), Err(SecureStoreError::NotFound)));
}
