//! Integration tests for the health data model (task 1.8).
//!
//! These tests exercise `tsafe_core::health::collect_health` end-to-end using
//! real vault/snapshot/audit infrastructure inside a temp directory.

use std::collections::HashMap;

use tempfile::tempdir;
use tsafe_core::{
    audit::{AuditEntry, AuditLog},
    health::{collect_health, OverallHealth, SignalSeverity},
    profile,
    vault::Vault,
};

fn pw() -> &'static [u8] {
    b"integration-test-password"
}

/// Run a closure with `TSAFE_VAULT_DIR` pointing to a `vaults/` subdirectory
/// of `root`.  Using a subdirectory ensures that the state directory
/// (`root/state/audit/`) lives inside the unique tempdir rather than in the
/// shared system-temp parent, preventing cross-test audit log contamination
/// when tests run in parallel.
fn with_vault_dir<F: FnOnce()>(root: &std::path::Path, f: F) {
    let vault_subdir = root.join("vaults");
    std::fs::create_dir_all(&vault_subdir).unwrap();
    temp_env::with_var("TSAFE_VAULT_DIR", Some(vault_subdir.to_str().unwrap()), f);
}

// ── basic reachability ────────────────────────────────────────────────────────

#[test]
fn missing_vault_is_critical_and_no_meta() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let report = collect_health("ghost");
        assert!(!report.vault_reachable, "vault should not be reachable");
        assert_eq!(report.overall, OverallHealth::Critical);
        assert!(report.vault_meta.is_none());
        assert_eq!(report.snapshot_count, 0);

        let sig = report
            .signals
            .iter()
            .find(|s| s.code == "vault.reachable")
            .expect("vault.reachable signal must be present");
        assert_eq!(sig.severity, SignalSeverity::Critical);
    });
}

#[test]
fn healthy_vault_is_fully_parsed_and_reported() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let vp = profile::vault_path("healthy");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        Vault::create(&vp, pw()).unwrap();

        let report = collect_health("healthy");
        assert!(report.vault_reachable);
        assert_eq!(report.overall, OverallHealth::Healthy);
        assert!(report.vault_meta.is_some());
        assert_eq!(report.critical_count, 0);
        assert_eq!(report.warning_count, 0);

        let meta = report.vault_meta.unwrap();
        assert_eq!(meta.secret_count, 0);
        assert!(!meta.is_team_vault);
    });
}

// ── vault metadata ────────────────────────────────────────────────────────────

#[test]
fn secret_count_reflected_in_vault_meta_and_signal() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let vp = profile::vault_path("sc");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        let mut v = Vault::create(&vp, pw()).unwrap();
        v.set("A", "alpha", HashMap::new()).unwrap();
        v.set("B", "beta", HashMap::new()).unwrap();
        v.set("C", "gamma", HashMap::new()).unwrap();
        drop(v);

        let report = collect_health("sc");
        let meta = report.vault_meta.unwrap();
        assert_eq!(meta.secret_count, 3);

        let sig = report
            .signals
            .iter()
            .find(|s| s.code == "vault.secret_count")
            .expect("vault.secret_count signal must be present");
        assert!(sig.message.contains("3 secret(s)"));
    });
}

#[test]
fn corrupt_vault_json_yields_critical_parse_signal_and_no_meta() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let vp = profile::vault_path("corrupt");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        std::fs::write(&vp, b"{{not valid json}}").unwrap();

        let report = collect_health("corrupt");
        assert!(report.vault_reachable);
        assert!(report.vault_meta.is_none());
        assert_eq!(report.overall, OverallHealth::Critical);

        let sig = report
            .signals
            .iter()
            .find(|s| s.code == "vault.parse")
            .expect("vault.parse signal must be present");
        assert_eq!(sig.severity, SignalSeverity::Critical);
        assert!(sig.detail.is_some(), "parse error should have detail");
    });
}

// ── snapshots ─────────────────────────────────────────────────────────────────

#[test]
fn snapshot_count_zero_on_fresh_vault() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let vp = profile::vault_path("fresh");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        // Create only — no writes so no snapshots.
        Vault::create(&vp, pw()).unwrap();

        let report = collect_health("fresh");
        assert_eq!(report.snapshot_count, 0);
    });
}

#[test]
fn snapshot_count_increases_after_writes() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let vp = profile::vault_path("snapper");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        let mut v = Vault::create(&vp, pw()).unwrap();
        // Each write after the first triggers a snapshot of the previous state.
        v.set("K", "v1", HashMap::new()).unwrap();
        v.set("K", "v2", HashMap::new()).unwrap();
        drop(v);

        let report = collect_health("snapper");
        assert!(
            report.snapshot_count >= 1,
            "expected at least 1 snapshot, got {}",
            report.snapshot_count
        );
    });
}

// ── profiles ──────────────────────────────────────────────────────────────────

#[test]
fn profile_count_matches_created_vaults() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        for name in ["alice", "bob", "carol"] {
            let vp = profile::vault_path(name);
            std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
            Vault::create(&vp, pw()).unwrap();
        }

        let report = collect_health("alice");
        assert_eq!(
            report.profile_count, 3,
            "should count all 3 created profiles"
        );
    });
}

// ── audit log ─────────────────────────────────────────────────────────────────

#[test]
fn audit_timestamps_populated_when_log_exists() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let log_path = profile::audit_log_path("logged");
        std::fs::create_dir_all(log_path.parent().unwrap()).unwrap();
        let log = AuditLog::new(&log_path);
        log.append(&AuditEntry::success("logged", "set", Some("K")))
            .unwrap();
        log.append(&AuditEntry::success("logged", "rotate", None))
            .unwrap();

        let report = collect_health("logged");
        assert!(
            report.last_audit_entry_at.is_some(),
            "last_audit_entry_at should be populated"
        );
        assert!(
            report.last_rotate_at.is_some(),
            "last_rotate_at should be populated when rotate entry exists"
        );
        assert_eq!(report.audit_entry_count, 2);
    });
}

#[test]
fn audit_timestamps_absent_for_empty_log() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let report = collect_health("no-log");
        assert!(report.last_audit_entry_at.is_none());
        assert!(report.last_rotate_at.is_none());
        assert_eq!(report.audit_entry_count, 0);
    });
}

#[test]
fn last_rotate_at_absent_when_only_set_entries_exist() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let log_path = profile::audit_log_path("no-rotate");
        std::fs::create_dir_all(log_path.parent().unwrap()).unwrap();
        let log = AuditLog::new(&log_path);
        log.append(&AuditEntry::success("no-rotate", "set", Some("K")))
            .unwrap();
        log.append(&AuditEntry::success("no-rotate", "get", Some("K")))
            .unwrap();

        let report = collect_health("no-rotate");
        assert!(report.last_audit_entry_at.is_some());
        assert!(
            report.last_rotate_at.is_none(),
            "rotate timestamp should be absent when no rotate entries exist"
        );
    });
}

// ── serialization ─────────────────────────────────────────────────────────────

#[test]
fn report_round_trips_through_json() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let vp = profile::vault_path("json-rt");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        Vault::create(&vp, pw()).unwrap();

        let report = collect_health("json-rt");
        let json = serde_json::to_string_pretty(&report).unwrap();

        // Must round-trip without error.
        let decoded: tsafe_core::health::HealthReport = serde_json::from_str(&json).unwrap();

        assert_eq!(decoded.profile, "json-rt");
        assert_eq!(decoded.overall, report.overall);
        assert_eq!(decoded.signals.len(), report.signals.len());
    });
}

#[test]
fn report_json_contains_expected_top_level_keys() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        let report = collect_health("keys-test");
        let json = serde_json::to_string_pretty(&report).unwrap();
        let value: serde_json::Value = serde_json::from_str(&json).unwrap();

        for key in &[
            "profile",
            "generated_at",
            "overall",
            "vault_reachable",
            "profile_count",
            "snapshot_count",
            "audit_entry_count",
            "signals",
            "ok_count",
            "info_count",
            "warning_count",
            "critical_count",
        ] {
            assert!(
                value.get(key).is_some(),
                "expected top-level key '{key}' in JSON output"
            );
        }
    });
}

// ── signal ordering ───────────────────────────────────────────────────────────

#[test]
fn signals_are_ordered_worst_first() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        // No vault — at least one Critical will appear.
        let report = collect_health("order-test");
        let severities: Vec<SignalSeverity> = report.signals.iter().map(|s| s.severity).collect();
        for window in severities.windows(2) {
            assert!(
                window[0] >= window[1],
                "signals must be sorted worst-first: {severities:?}"
            );
        }
    });
}

// ── is_healthy convenience ────────────────────────────────────────────────────

#[test]
fn is_healthy_returns_true_only_for_healthy_overall() {
    let dir = tempdir().unwrap();
    with_vault_dir(dir.path(), || {
        // Healthy vault.
        let vp = profile::vault_path("hv");
        std::fs::create_dir_all(vp.parent().unwrap()).unwrap();
        Vault::create(&vp, pw()).unwrap();
        let good = collect_health("hv");
        assert!(good.is_healthy());

        // Missing vault → Critical.
        let bad = collect_health("no-vault-here");
        assert!(!bad.is_healthy());
    });
}
