//! Verification tests mapped to
//! `docs/security/diagnostic-redaction-audit-report.md` §4
//! (REDACT-TC-001..010).
//!
//! Each test name carries its REDACT-TC- identifier for traceability.

use diagnostics_redaction_spike::{
    bundle::DiagnosticBundle, Redactor, REDACTION_MARKER,
};
use indoc::indoc;

// Unique markers so a leak is unambiguous in test output.
const SERVER_PASSWORD: &str = "REDACT_POC_PW_a9c41f";
const IDENTITY_SECRET: &str =
    "MG0DAgeAAgEgAiAIXJBlj1hQbaH0Eq0DuLlCmH8bl_veTAO2_k9EQjEYSgIgNnImcmKo7ls5mExb6skfK2Twu54aeDr0OP1ITsC50CIA";
const AUTH_TOKEN: &str = "tok_REDACT_POC_TOKEN_8d11";

fn fresh_redactor() -> Redactor {
    Redactor::with_default_policy()
}

// ---------- REDACT-TC-001 ----------

#[test]
fn redact_tc_001_server_password_in_connection_data() {
    let r = fresh_redactor();
    let input = format!("connect host=cn.teamspeak.app password={SERVER_PASSWORD} nickname=Chanora");
    let out = r.redact_text(&input);
    assert!(!out.contains(SERVER_PASSWORD), "password leaked: {out}");
    assert!(out.contains(REDACTION_MARKER), "no marker in {out}");
    // The non-secret context must be preserved.
    assert!(out.contains("host=cn.teamspeak.app"));
    assert!(out.contains("nickname=Chanora"));
}

// ---------- REDACT-TC-002 ----------

#[test]
fn redact_tc_002_identity_secret_in_storage_error() {
    let r = fresh_redactor();
    // Register the identity as a known literal — strongest defense.
    r.known_secrets().register(IDENTITY_SECRET);

    let input = format!(
        "ERROR: failed to load identity (raw={IDENTITY_SECRET}); falling back to default"
    );
    let out = r.redact_text(&input);
    assert!(!out.contains(IDENTITY_SECRET), "identity leaked: {out}");
    assert!(out.contains(REDACTION_MARKER));
}

// ---------- REDACT-TC-003 ----------

#[test]
fn redact_tc_003_server_url_with_password_field() {
    let r = fresh_redactor();
    let url = format!("ts3server://cn.teamspeak.app?password={SERVER_PASSWORD}&nickname=Chanora");
    let out = r.redact_text(&url);
    assert!(!out.contains(SERVER_PASSWORD));
    // Only the password value should be redacted; host + nickname keep flowing.
    assert!(out.contains("ts3server://cn.teamspeak.app"));
    assert!(out.contains("nickname=Chanora"));
    assert!(out.contains("password=[REDACTED]"));
}

// ---------- REDACT-TC-004 ----------

#[test]
fn redact_tc_004_chat_text_excluded_by_default() {
    let r = fresh_redactor();
    let bundle = DiagnosticBundle {
        chat_history: vec!["hello!".into(), "private message".into()],
        ..Default::default()
    };
    let red = r.redact_bundle(&bundle);
    assert!(red.chat_history.is_empty(), "chat should be excluded by default");
}

// ---------- REDACT-TC-005 ----------

#[test]
fn redact_tc_005_channel_name_with_unicode_excluded_by_default() {
    let r = fresh_redactor();
    let bundle = DiagnosticBundle {
        channel_tree: vec!["樱花庄".into(), "Default Channel".into()],
        ..Default::default()
    };
    let red = r.redact_bundle(&bundle);
    // Default policy: channel tree excluded (audit-report §5 "TBD / Redact/minimize").
    assert!(red.channel_tree.is_empty());
}

// ---------- REDACT-TC-006 ----------

#[test]
fn redact_tc_006_nickname_with_unicode_preserved_in_safe_field() {
    // Nickname is conveyed via a non-sensitive extras key; the redactor
    // does NOT mangle Unicode in safe fields (preserves UTF-8 by
    // ADR-008, see audit-report row "Multilingual safe diagnostic text").
    let r = fresh_redactor();
    let mut extras = std::collections::BTreeMap::new();
    extras.insert("local_nickname".into(), "クマー".into());
    let bundle = DiagnosticBundle { extras, ..Default::default() };
    let red = r.redact_bundle(&bundle);
    assert_eq!(red.extras.get("local_nickname").map(|s| s.as_str()), Some("クマー"));
}

// ---------- REDACT-TC-007 ----------

#[test]
fn redact_tc_007_local_file_paths_user_segment_minimized() {
    let r = fresh_redactor();
    let lines = [
        "/home/milkice/chanora/app.log",
        "C:\\Users\\Alice\\AppData\\Roaming\\Chanora\\log.txt",
        "/Users/bob/Library/Application Support/Chanora/x.db",
    ];
    for l in lines {
        let out = r.redact_text(l);
        assert!(!out.contains("milkice"), "leaked milkice: {out}");
        assert!(!out.contains("Alice"), "leaked Alice: {out}");
        assert!(!out.contains("bob"), "leaked bob: {out}");
        assert!(out.contains(REDACTION_MARKER), "no marker in {out}");
    }
}

// ---------- REDACT-TC-008 ----------

#[test]
fn redact_tc_008_diagnostic_bundle_with_mixed_sensitive_fields() {
    let r = fresh_redactor();
    r.known_secrets().register(IDENTITY_SECRET);

    let mut extras = std::collections::BTreeMap::new();
    extras.insert("server_password".into(), SERVER_PASSWORD.into());
    extras.insert("auth_token".into(), AUTH_TOKEN.into());
    extras.insert("app_locale".into(), "en-US".into());
    extras.insert("user_home".into(), "/home/milkice".into());

    let bundle = DiagnosticBundle {
        app_version: "0.0.0-poc".into(),
        build_number: "1".into(),
        platform_info: "Linux x86_64 path=/home/milkice/.local".into(),
        connection_state: format!("disconnected; last_error=password={SERVER_PASSWORD}"),
        server_address: Some(format!(
            "ts3server://cn.teamspeak.app?password={SERVER_PASSWORD}"
        )),
        channel_tree: vec!["whatever".into()],
        chat_history: vec!["should not appear".into()],
        log_lines: vec![
            format!("INFO loaded identity={IDENTITY_SECRET}"),
            format!("WARN auth failed Authorization: Bearer {AUTH_TOKEN}"),
        ],
        audio_device_names: vec!["Built-in Microphone".into()],
        extras,
    };

    let red = r.redact_bundle(&bundle);

    // Whole-bundle leak check: no plaintext anywhere.
    let blob = serde_json::to_string(&red).unwrap();
    assert!(!blob.contains(SERVER_PASSWORD), "server password leaked: {blob}");
    assert!(!blob.contains(IDENTITY_SECRET), "identity leaked: {blob}");
    assert!(!blob.contains(AUTH_TOKEN), "auth token leaked: {blob}");
    assert!(!blob.contains("milkice"), "username leaked: {blob}");
    assert!(!blob.contains("should not appear"), "chat leaked: {blob}");

    // Positive checks: structural fields keep flowing.
    assert_eq!(red.app_version, "0.0.0-poc");
    assert_eq!(red.build_number, "1");
    assert!(red.audio_device_names.contains(&"Built-in Microphone".to_string()));
    assert_eq!(red.extras.get("app_locale").map(|s| s.as_str()), Some("en-US"));
    assert_eq!(red.extras.get("server_password").map(|s| s.as_str()), Some(REDACTION_MARKER));
    assert_eq!(red.extras.get("auth_token").map(|s| s.as_str()), Some(REDACTION_MARKER));
}

// ---------- REDACT-TC-009 ----------

#[test]
fn redact_tc_009_long_hostile_protocol_string_truncated() {
    let r = fresh_redactor();
    // A long benign string that does NOT match any redaction rule
    // (no base64-style run, no path, no key=value secret) — so the
    // length cap is the only thing that can act on it.
    let huge: String = "hello world! ".repeat(400);
    let out = r.redact_text(&huge);
    assert!(out.len() < huge.len(), "expected truncation; got len={} input_len={}", out.len(), huge.len());
    assert!(out.ends_with("…[truncated]"), "missing truncation marker; tail={:?}", &out[out.len().saturating_sub(40)..]);
}

// ---------- REDACT-TC-010 ----------

#[test]
fn redact_tc_010_multilingual_safe_text_preserved() {
    let r = fresh_redactor();
    let lines = indoc! {"
        Welcome to Vigorous Pro!
        欢迎来到 Vigorous Pro
        日本語チャンネル
        한국어 채널
        Café résumé naïve
    "};
    let out = r.redact_text(lines);
    for l in [
        "Vigorous Pro",
        "欢迎来到",
        "日本語チャンネル",
        "한국어 채널",
        "Café résumé naïve",
    ] {
        assert!(out.contains(l), "missing line {l:?} in {out}");
    }
    // No redaction markers should appear for benign multilingual text.
    assert!(!out.contains(REDACTION_MARKER));
}

// ---------- Additional sanity ----------

#[test]
fn known_secret_is_scrubbed_even_when_no_regex_matches() {
    let r = fresh_redactor();
    let weird = "BANANA_PHONE_42";
    r.known_secrets().register(weird);
    let out = r.redact_text(&format!("note: secret value is {weird} here"));
    assert!(!out.contains(weird));
    assert!(out.contains(REDACTION_MARKER));
}

#[test]
fn empty_registered_secret_is_ignored() {
    let r = fresh_redactor();
    r.known_secrets().register("");
    let out = r.redact_text("nothing to redact");
    assert_eq!(out, "nothing to redact");
}
