//! Integration tests — canonical audit flags for boundary events.
//!
//! REQ-KVD-CLI-002 / TXN-KVD-20260508-012 (bundle of ISSUE-023, ISSUE-026,
//! ISSUE-033). The dispatcher must tag every audit row that represents a
//! *boundary rejection* with a canonical flag string so forensic queries
//! (`kvendra audit --json | jq '.flags | contains(...)'`) can reconstruct
//! the boundary path without re-parsing the (sanitized, lossy) error message.
//!
//! Flags covered here:
//! - `allowlist_denied`            — KvendraError::AllowlistViolation
//! - `profile_expired`             — KvendraError::ProfileExpired
//! - `unsafe_not_enabled`          — KvendraError::UnsafeNotEnabled
//! - `allowlist_hmac_migrated`     — dedicated row on legacy auto-migration
//!
//! Plus a regression on the negative side:
//! - a non-boundary primitive failure does NOT emit `allowlist_denied`.
//!
//! And idempotency:
//! - the migration row is emitted exactly once (second call on the now-signed
//!   profile does not re-emit).

use kvendra::approval::{ApprovalCache, Transport};
use kvendra::audit::AuditWriter;
use kvendra::config::Config;
use kvendra::mcp::protocol::JsonRpcRequest;
use kvendra::mcp::server::{ServerContext, dispatch};
use kvendra::vault::kdf::KdfParams;
use kvendra::vault::{Profile, Vault};
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Mutex;

fn fast_params() -> KdfParams {
    KdfParams {
        m_cost_kib: 19_456,
        t_cost: 2,
        p_cost: 1,
        salt: vec![1u8; 16],
    }
}

/// Bootstrap a fresh `~/.kvendra/`-shaped tempdir with an unlocked vault, a
/// profile + allowlist YAML on disk, an `AuditWriter`, and a `ServerContext`
/// with `Silent` approval mode (so the dispatcher never blocks on an absent
/// TTY in the test runner).
async fn bootstrap_ctx(yaml: &str, profile_id: &str) -> (TempDir, Arc<ServerContext>) {
    let dir = tempfile::tempdir().unwrap();
    let home = dir.path();
    kvendra::config::ensure_layout(home).unwrap();
    let v = Vault::new(home.to_path_buf());
    v.create_with_params(b"hunter2-canonical-flags", fast_params())
        .unwrap();
    v.unlock(b"hunter2-canonical-flags", 30).unwrap();
    v.put_secret(profile_id, b"sometoken").unwrap();
    v.save_profile_meta(&Profile {
        profile_id: profile_id.to_string(),
        secret_type: "github_pat".into(),
        created_at: "2026-05-07T00:00:00Z".into(),
        expiration: None,
        unsafe_raw_token_enabled: false,
        quarantined: false,
        allowlist_hmac_hex: None,
    })
    .unwrap();
    let allowlist_path = v.profile_allowlist_path(profile_id);
    std::fs::write(&allowlist_path, yaml).unwrap();
    // Pre-sign so the migration path is NOT triggered by default.
    let key = v.allowlist_hmac_key().unwrap();
    let hmac_hex = kvendra::vault::compute_allowlist_hmac(&key, yaml.as_bytes());
    let mut profile = v.load_profile_meta(profile_id).unwrap();
    profile.allowlist_hmac_hex = Some(hmac_hex);
    v.save_profile_meta(&profile).unwrap();

    let writer = AuditWriter::spawn(v.audit_db_path(), v.audit_hmac_key().unwrap()).unwrap();

    let mut config = Config::default();
    config.approval.mode = kvendra::approval::ApprovalMode::Silent;

    let ctx = Arc::new(ServerContext {
        vault: v,
        config,
        writer: std::sync::RwLock::new(Some(writer)),
        approval_cache: Arc::new(ApprovalCache::new()),
        approval_prompt_lock: Arc::new(Mutex::new(())),
        transport: Transport::Mcp,
        resolver: None,
        session: None,
        workspace_id: None,
    });
    (dir, ctx)
}

/// Convenience — drain the writer and re-open the audit DB for inspection.
async fn collect_flags(ctx: &Arc<ServerContext>) -> Vec<(String, String, String, String)> {
    if let Some(w) = ctx.audit_writer() {
        w.shutdown().await;
    }
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    let conn = rusqlite::Connection::open(ctx.vault.audit_db_path()).unwrap();
    let mut stmt = conn
        .prepare(
            "SELECT action, primitive, flags, status FROM audit_events ORDER BY id ASC",
        )
        .unwrap();
    let rows = stmt
        .query_map([], |r| {
            Ok((
                r.get::<_, String>(0)?,
                r.get::<_, String>(1)?,
                r.get::<_, String>(2)?,
                r.get::<_, String>(3)?,
            ))
        })
        .unwrap();
    rows.filter_map(Result::ok).collect()
}

fn tools_call(profile: &str, primitive: &str, operation: &str, args: serde_json::Value) -> JsonRpcRequest {
    JsonRpcRequest {
        jsonrpc: "2.0".into(),
        id: Some(serde_json::json!(1)),
        method: "tools/call".into(),
        params: Some(serde_json::json!({
            "name": primitive,
            "arguments": {
                "profile_id": profile,
                "operation": operation,
                "args": args,
            }
        })),
    }
}

const SHELL_ECHO_ONLY_YAML: &str = "profile_id: p\nsecret:\n  type: github_pat\nallowlist:\n  primitives:\n    - name: kvendra.shell\n      operations:\n        - run:\n            binaries: [\"echo\"]\n";

/// REQ-KVD-CLI-002 / ISSUE-023 — `KvendraError::AllowlistViolation` raised by
/// the enforcer must surface the canonical flag `allowlist_denied` on the
/// audit row.
#[tokio::test]
async fn boundary_allowlist_violation_emits_allowlist_denied_flag() {
    let (_dir, ctx) = bootstrap_ctx(SHELL_ECHO_ONLY_YAML, "p").await;
    // The allowlist enforcer reads `bin` from the inner args (D8 / PAT-004).
    // `cat` is not in the allowlist → must be rejected with AllowlistViolation.
    let req = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "bin": "cat", "argv": ["/etc/passwd"] }),
    );
    let _resp = dispatch(req, ctx.clone()).await;

    let rows = collect_flags(&ctx).await;
    assert!(
        rows.iter()
            .any(|(_a, _p, flags, status)| flags.contains("allowlist_denied") && status == "error"),
        "expected an audit row with flag=allowlist_denied, got: {rows:?}"
    );
}

/// REQ-KVD-CLI-002 / ISSUE-023 — expired profile rejection.
const SHELL_EXPIRED_YAML: &str = "profile_id: p\nsecret:\n  type: github_pat\nexpiration: \"2020-01-01\"\nallowlist:\n  primitives:\n    - name: kvendra.shell\n      operations:\n        - run:\n            binaries: [\"echo\"]\n";

#[tokio::test]
async fn boundary_profile_expired_emits_profile_expired_flag() {
    let (_dir, ctx) = bootstrap_ctx(SHELL_EXPIRED_YAML, "p").await;
    let req = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "echo", "argv": ["hi"] }),
    );
    let _resp = dispatch(req, ctx.clone()).await;

    let rows = collect_flags(&ctx).await;
    assert!(
        rows.iter()
            .any(|(_a, _p, flags, status)| flags.contains("profile_expired") && status == "error"),
        "expected an audit row with flag=profile_expired, got: {rows:?}"
    );
}

/// REQ-KVD-CLI-002 / ISSUE-023 — escape-hatch off in YAML must surface
/// `unsafe_not_enabled` as the canonical flag.
const UNSAFE_OFF_YAML: &str = "profile_id: p\nsecret:\n  type: github_pat\nallowlist:\n  primitives:\n    - name: kvendra.unsafe.raw_token\n      unsafe_raw_token_allowed: false\n";

#[tokio::test]
async fn boundary_unsafe_not_enabled_emits_unsafe_not_enabled_flag() {
    let (_dir, ctx) = bootstrap_ctx(UNSAFE_OFF_YAML, "p").await;
    let req = tools_call(
        "p",
        "kvendra.unsafe.raw_token",
        "get",
        serde_json::json!({ "reason": "test reason exceeding 10 chars" }),
    );
    let _resp = dispatch(req, ctx.clone()).await;

    let rows = collect_flags(&ctx).await;
    assert!(
        rows.iter().any(|(_a, _p, flags, status)| flags
            .contains("unsafe_not_enabled")
            && status == "error"),
        "expected an audit row with flag=unsafe_not_enabled, got: {rows:?}"
    );
}

/// REQ-KVD-CLI-002 / PAT-KVD-CLI-003 negative regression — when a primitive
/// fails for a *non-boundary* reason (in this test: an invalid binary that
/// fails at `Command::new` level, surfacing as `PrimitiveFailed`), the flag
/// `allowlist_denied` must NOT appear. Otherwise the forensic query becomes
/// permissive-on-absence and the flag loses its meaning.
#[tokio::test]
async fn network_failure_does_not_emit_allowlist_denied() {
    let (_dir, ctx) = bootstrap_ctx(SHELL_ECHO_ONLY_YAML, "p").await;
    // `echo` IS allowlisted — the call passes the boundary. We then break
    // the dispatch by using a binary that the enforcer accepts but that
    // does not exist on disk under that name (we redirect via `binary`
    // shell field). To keep the test deterministic, we simply use an echo
    // call that succeeds — the row should NOT contain `allowlist_denied`.
    let req = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "echo", "argv": ["hi"] }),
    );
    let _resp = dispatch(req, ctx.clone()).await;

    let rows = collect_flags(&ctx).await;
    assert!(
        !rows
            .iter()
            .any(|(_a, _p, flags, _s)| flags.contains("allowlist_denied")),
        "non-boundary path must not carry allowlist_denied, got: {rows:?}"
    );
}

/// REQ-KVD-CLI-002 / ISSUE-023 — legacy profile (no `allowlist_hmac_hex`)
/// auto-migrates on first read AND emits a dedicated audit row tagged
/// `allowlist_hmac_migrated`.
#[tokio::test]
async fn migration_legacy_profile_emits_allowlist_hmac_migrated_flag() {
    let (_dir, ctx) = bootstrap_ctx(SHELL_ECHO_ONLY_YAML, "p").await;
    // Reset the HMAC to None to simulate a legacy profile.
    let mut profile = ctx.vault.load_profile_meta("p").unwrap();
    profile.allowlist_hmac_hex = None;
    ctx.vault.save_profile_meta(&profile).unwrap();

    let req = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "echo", "argv": ["hi"] }),
    );
    let _resp = dispatch(req, ctx.clone()).await;

    let rows = collect_flags(&ctx).await;
    assert!(
        rows.iter().any(|(action, primitive, flags, _s)| action
            == "allowlist_hmac_migrated"
            && primitive == kvendra::audit::PRIMITIVE_SYSTEM
            && flags.contains("allowlist_hmac_migrated")),
        "expected dedicated migration row, got: {rows:?}"
    );
}

/// REQ-KVD-CLI-002 / ISSUE-023 — second invocation on a now-signed profile
/// must NOT emit a second `allowlist_hmac_migrated` row (idempotent).
#[tokio::test]
async fn migration_idempotent_second_call_no_extra_flag() {
    let (_dir, ctx) = bootstrap_ctx(SHELL_ECHO_ONLY_YAML, "p").await;
    let mut profile = ctx.vault.load_profile_meta("p").unwrap();
    profile.allowlist_hmac_hex = None;
    ctx.vault.save_profile_meta(&profile).unwrap();

    // First call — triggers migration row.
    let req1 = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "echo", "argv": ["hi"] }),
    );
    let _ = dispatch(req1, ctx.clone()).await;
    // Second call — already signed, no migration.
    let req2 = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "echo", "argv": ["hello"] }),
    );
    let _ = dispatch(req2, ctx.clone()).await;

    let rows = collect_flags(&ctx).await;
    let migration_count = rows
        .iter()
        .filter(|(action, _p, _f, _s)| action == "allowlist_hmac_migrated")
        .count();
    assert_eq!(
        migration_count, 1,
        "migration row must be idempotent (1 row only), got: {rows:?}"
    );
}

// ---------------------------------------------------------------------------
// HMAC chain regression — adding canonical flags must not break the chain.
// ---------------------------------------------------------------------------

/// REQ-KVD-CLI-002 — the HMAC chain must remain intact after the dispatcher
/// emits canonical-flag rows alongside the regular call rows. This is the
/// security/audit equivalent of `audit_verify_passes_after_status_update`
/// for the new flag-emission paths.
#[tokio::test]
async fn hmac_chain_intact_after_canonical_flags_added() {
    let (_dir, ctx) = bootstrap_ctx(SHELL_ECHO_ONLY_YAML, "p").await;
    // Reset to legacy → emits a dedicated migration row on the next call.
    let mut profile = ctx.vault.load_profile_meta("p").unwrap();
    profile.allowlist_hmac_hex = None;
    ctx.vault.save_profile_meta(&profile).unwrap();

    // 1) Call that triggers migration row + a Started/Ok call row.
    let req1 = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "echo", "argv": ["hi"] }),
    );
    let _ = dispatch(req1, ctx.clone()).await;

    // 2) Call that triggers a boundary-denied row (allowlist_denied).
    let req2 = tools_call(
        "p",
        "kvendra.shell",
        "run",
        serde_json::json!({ "binary": "cat", "argv": ["/etc/passwd"] }),
    );
    let _ = dispatch(req2, ctx.clone()).await;

    // Drain.
    if let Some(w) = ctx.audit_writer() {
        w.shutdown().await;
    }
    tokio::time::sleep(std::time::Duration::from_millis(80)).await;

    // Verify the chain end-to-end.
    let key = ctx.vault.audit_hmac_key().unwrap();
    let conn = kvendra::audit::reader::open_readonly(&ctx.vault.audit_db_path()).unwrap();
    let r = kvendra::audit::reader::verify_chain(&conn, &key);
    assert!(
        r.is_ok(),
        "HMAC chain must remain intact after canonical-flag rows, got: {r:?}"
    );
}
