//! Live CLI coverage for `covenant verify --json`.
//!
//! Spawns a real daemon against a temp home, runs the state verifier
//! through the CLI, and asserts stdout is one stable JSON object. Opt-in
//! because it crosses process and socket boundaries. Run from `agent-os/`
//! after `cargo build -p covenant`.

use covenant_memory::{MemoryStore, SqliteStore};
use serde_json::Value;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio::net::UnixStream;
use tokio::process::Command;
use tokio::time::sleep;
use uuid::Uuid;

fn pick_free_port() -> u16 {
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral");
    listener.local_addr().unwrap().port()
}

fn covenant_cli_bin() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../target/debug/covenant")
        .canonicalize()
        .expect("covenant CLI binary not built; run `cargo build -p covenant` first")
}

async fn wait_for_sock(path: &std::path::Path) -> bool {
    for _ in 0..100 {
        if UnixStream::connect(path).await.is_ok() {
            return true;
        }
        sleep(Duration::from_millis(100)).await;
    }
    false
}

async fn wait_for_operator_token(home: &std::path::Path) {
    let path = home.join("peers").join("operator.token");
    for _ in 0..50 {
        if let Ok(s) = std::fs::read_to_string(&path) {
            if !s.trim().is_empty() {
                return;
            }
        }
        sleep(Duration::from_millis(50)).await;
    }
    panic!("operator token never appeared at {}", path.display());
}

async fn spawn_daemon(home: &std::path::Path, port: u16) -> tokio::process::Child {
    let daemon_exe = env!("CARGO_BIN_EXE_covenantd");
    Command::new(daemon_exe)
        .env("COVENANT_HOME", home)
        .env("COVENANT_HTTP_PORT", port.to_string())
        .env("HOME", home)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .kill_on_drop(true)
        .spawn()
        .expect("spawn covenantd")
}

async fn wait_for_daemon(home: &std::path::Path, child: &mut tokio::process::Child) {
    let sock = home.join("sock");
    if !wait_for_sock(&sock).await {
        let _ = child.kill().await;
        panic!("daemon never created its socket at {}", sock.display());
    }
    wait_for_operator_token(home).await;
}

async fn run_cli_raw(
    cli_exe: &std::path::Path,
    home: &std::path::Path,
    args: &[&str],
) -> std::process::Output {
    Command::new(cli_exe)
        .args(args)
        .env("COVENANT_HOME", home)
        .env("HOME", home)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("spawn covenant CLI")
}

async fn run_cli(cli_exe: &std::path::Path, home: &std::path::Path, args: &[&str]) -> String {
    let output = run_cli_raw(cli_exe, home, args).await;
    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    assert!(
        output.status.success(),
        "CLI failed for {args:?}: status={:?} stdout={stdout:?} stderr={stderr:?}",
        output.status
    );
    assert!(
        stderr.trim().is_empty(),
        "CLI command {args:?} must not emit stderr on success: {stderr:?}"
    );
    stdout
}

fn stale_parent_drift_for(value: &Value, memory_id: Uuid) -> Option<&Value> {
    value["drift"].as_array()?.iter().find(|item| {
        item["kind"].as_str() == Some("memory_stale_parent")
            && item["id"].as_str() == Some(&memory_id.to_string())
    })
}

#[tokio::test]
#[ignore = "live: spawns covenantd + runs `covenant verify --json` subprocess"]
async fn live_cli_verify_json_round_trip() {
    let home = tempfile::tempdir().expect("tempdir");

    let port = pick_free_port();
    let mut child = spawn_daemon(home.path(), port).await;
    wait_for_daemon(home.path(), &mut child).await;

    let cli_exe = covenant_cli_bin();
    let output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["verify", "--json", "--window", "25"],
    )
    .await;

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    assert!(
        output.status.success(),
        "CLI failed: status={:?} stdout={stdout:?} stderr={stderr:?}",
        output.status
    );
    assert!(
        stderr.trim().is_empty(),
        "verify --json must not emit stderr on success: {stderr:?}"
    );

    let value: Value =
        serde_json::from_str(stdout.trim()).expect("verify --json must be valid JSON");
    assert_eq!(value["kind"].as_str(), Some("verify_report"));
    assert_eq!(value["window"].as_u64(), Some(25));
    assert_eq!(value["orphans_total"].as_u64(), Some(0));
    assert!(
        value["checks"]
            .as_array()
            .expect("checks array")
            .iter()
            .all(|check| check["passed"].as_bool() == Some(true)),
        "fresh daemon should have only passing checks: {value:?}"
    );
    assert!(value["drift"].as_array().expect("drift array").is_empty());

    let _ = child.kill().await;
}

#[tokio::test]
#[ignore = "live: spawns covenantd, mutates state out of band, and runs `covenant verify --json`"]
async fn live_cli_verify_json_reports_drift_after_audit_loss() {
    let home = tempfile::tempdir().expect("tempdir");
    let cli_exe = covenant_cli_bin();

    let port = pick_free_port();
    let mut child = spawn_daemon(home.path(), port).await;
    wait_for_daemon(home.path(), &mut child).await;

    run_cli(
        &cli_exe,
        home.path(),
        &["capabilities", "grant", "memory.write"],
    )
    .await;
    run_cli(&cli_exe, home.path(), &["intent", "verify drift fixture"]).await;

    let _ = child.kill().await;
    let audit_events = home.path().join("audit").join("events.jsonl");
    std::fs::write(&audit_events, "").expect("clear audit events");

    let restart_port = pick_free_port();
    let mut restarted = spawn_daemon(home.path(), restart_port).await;
    wait_for_daemon(home.path(), &mut restarted).await;

    let output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["verify", "--json", "--window", "25"],
    )
    .await;
    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    assert!(
        !output.status.success(),
        "verify --json should exit non-zero when drift exists: status={:?} stdout={stdout:?} stderr={stderr:?}",
        output.status
    );
    assert!(
        stderr.trim().is_empty(),
        "verify --json should report drift on stdout without stderr noise: {stderr:?}"
    );

    let value: Value =
        serde_json::from_str(stdout.trim()).expect("verify --json drift stdout must be JSON");
    assert_eq!(value["kind"].as_str(), Some("verify_report"));
    assert!(
        value["orphans_total"]
            .as_u64()
            .is_some_and(|count| count > 0),
        "drift report should count orphaned state: {value:?}"
    );

    let drift = value["drift"].as_array().expect("drift array");
    let memory_without_audit = drift.iter().find(|item| {
        item["kind"].as_str() == Some("memory_without_audit")
            && item["message"]
                .as_str()
                .is_some_and(|message| message.contains("IntentDispatched audit row"))
    });
    let item = memory_without_audit
        .unwrap_or_else(|| panic!("expected memory_without_audit drift row: {value:?}"));
    assert!(
        item["repair"]
            .as_str()
            .is_some_and(|repair| repair.contains("explicit repair command")),
        "drift row should include actionable repair guidance: {item:?}"
    );

    assert!(
        value["checks"]
            .as_array()
            .expect("checks array")
            .iter()
            .any(|check| check["name"].as_str() == Some("memory ↔ audit")
                && check["passed"].as_bool() == Some(false)),
        "memory/audit check should fail when audit evidence is removed: {value:?}"
    );

    let _ = restarted.kill().await;
}

#[tokio::test]
#[ignore = "live: spawns covenantd, purges memory, and runs `covenant verify --json`"]
async fn live_cli_verify_json_reports_drift_after_memory_purge() {
    let home = tempfile::tempdir().expect("tempdir");
    let cli_exe = covenant_cli_bin();

    let port = pick_free_port();
    let mut child = spawn_daemon(home.path(), port).await;
    wait_for_daemon(home.path(), &mut child).await;

    for action in ["memory.write", "memory.purge"] {
        let output = run_cli_raw(
            &cli_exe,
            home.path(),
            &["capabilities", "grant", action, "--json"],
        )
        .await;
        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        assert!(
            output.status.success(),
            "capabilities grant failed: action={action} status={:?} stdout={stdout:?} stderr={stderr:?}",
            output.status
        );
        assert!(
            stderr.trim().is_empty(),
            "capabilities grant must not emit stderr on success: {stderr:?}"
        );
        let value: Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| {
            panic!("capabilities grant --json must be valid JSON: {e}: {stdout:?}")
        });
        assert_eq!(value["kind"].as_str(), Some("capability_granted"));
        assert_eq!(value["action"].as_str(), Some(action));
    }

    let intent_output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["intent", "--json", "drift fixture: baseline"],
    )
    .await;
    let intent_stdout = String::from_utf8_lossy(&intent_output.stdout).to_string();
    let intent_stderr = String::from_utf8_lossy(&intent_output.stderr).to_string();
    assert!(
        intent_output.status.success(),
        "intent failed: status={:?} stdout={intent_stdout:?} stderr={intent_stderr:?}",
        intent_output.status
    );
    assert!(
        intent_stderr.trim().is_empty(),
        "intent --json must not emit stderr on success: {intent_stderr:?}"
    );
    let intent_json: Value =
        serde_json::from_str(intent_stdout.trim()).expect("intent --json must be valid JSON");
    assert_eq!(intent_json["kind"].as_str(), Some("intent_result"));
    let intent_id = intent_json["intent_id"]
        .as_str()
        .expect("intent_result.intent_id must be a string");

    let purge_output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["memory", "purge", "--before-ms", "9999999999999", "--json"],
    )
    .await;
    let purge_stdout = String::from_utf8_lossy(&purge_output.stdout).to_string();
    let purge_stderr = String::from_utf8_lossy(&purge_output.stderr).to_string();
    assert!(
        purge_output.status.success(),
        "memory purge failed: status={:?} stdout={purge_stdout:?} stderr={purge_stderr:?}",
        purge_output.status
    );
    assert!(
        purge_stderr.trim().is_empty(),
        "memory purge --json must not emit stderr on success: {purge_stderr:?}"
    );
    let purge_json: Value =
        serde_json::from_str(purge_stdout.trim()).expect("memory purge --json must be valid JSON");
    assert_eq!(purge_json["kind"].as_str(), Some("memory_purged"));

    let output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["verify", "--json", "--window", "100"],
    )
    .await;
    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    assert!(
        !output.status.success(),
        "verify --json should exit non-zero when drift exists: status={:?} stdout={stdout:?} stderr={stderr:?}",
        output.status
    );
    assert!(
        stderr.trim().is_empty(),
        "verify --json should report drift on stdout without stderr noise: {stderr:?}"
    );

    let value: Value =
        serde_json::from_str(stdout.trim()).expect("verify --json drift stdout must be JSON");
    assert_eq!(value["kind"].as_str(), Some("verify_report"));
    assert_eq!(value["window"].as_u64(), Some(100));
    assert!(
        value["orphans_total"].as_u64().unwrap_or(0) > 0,
        "memory purge drift should report orphans_total > 0: {value:?}"
    );

    let drift = value["drift"].as_array().expect("drift array");
    assert!(
        drift.iter().any(|item| {
            item["kind"].as_str() == Some("audit_without_memory")
                && item["id"].as_str() == Some(intent_id)
                && item["repair"]
                    .as_str()
                    .is_some_and(|repair| !repair.trim().is_empty())
        }),
        "expected audit_without_memory drift for {intent_id}: {value:?}"
    );

    let _ = child.kill().await;
}

#[tokio::test]
#[ignore = "live: spawns covenantd, injects stale memory, repairs it through CLI, and verifies again"]
async fn live_cli_verify_json_repair_clears_stale_parent_drift() {
    let home = tempfile::tempdir().expect("tempdir");
    let cli_exe = covenant_cli_bin();

    let port = pick_free_port();
    let mut child = spawn_daemon(home.path(), port).await;
    wait_for_daemon(home.path(), &mut child).await;

    run_cli(
        &cli_exe,
        home.path(),
        &["capabilities", "grant", "memory.write"],
    )
    .await;
    let intent_stdout = run_cli(
        &cli_exe,
        home.path(),
        &["intent", "--json", "verify repair fixture"],
    )
    .await;
    let intent: Value =
        serde_json::from_str(intent_stdout.trim()).expect("intent --json must be JSON");
    let memory_id: Uuid = intent["intent_id"]
        .as_str()
        .expect("intent_id")
        .parse()
        .expect("intent_id must be uuid");

    let _ = child.kill().await;

    let missing_parent = Uuid::new_v4();
    let store = SqliteStore::open(&home.path().join("memory.db")).expect("open memory db");
    let mut record = store
        .get(memory_id)
        .await
        .expect("load memory")
        .expect("memory record exists");
    record.parent = Some(missing_parent);
    store.put(record).await.expect("inject stale parent");
    drop(store);

    let restart_port = pick_free_port();
    let mut restarted = spawn_daemon(home.path(), restart_port).await;
    wait_for_daemon(home.path(), &mut restarted).await;

    let drift_output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["verify", "--json", "--window", "25"],
    )
    .await;
    let drift_stdout = String::from_utf8_lossy(&drift_output.stdout).to_string();
    let drift_stderr = String::from_utf8_lossy(&drift_output.stderr).to_string();
    assert!(
        !drift_output.status.success(),
        "verify should fail while stale parent drift exists: status={:?} stdout={drift_stdout:?} stderr={drift_stderr:?}",
        drift_output.status
    );
    let drift: Value =
        serde_json::from_str(drift_stdout.trim()).expect("verify drift stdout must be JSON");
    let item = stale_parent_drift_for(&drift, memory_id)
        .unwrap_or_else(|| panic!("expected stale parent drift for {memory_id}: {drift:?}"));
    assert!(
        item["message"]
            .as_str()
            .is_some_and(|message| message.contains(&missing_parent.to_string())),
        "stale parent drift should name the missing parent: {item:?}"
    );

    run_cli(
        &cli_exe,
        home.path(),
        &["capabilities", "grant", "memory.repair.apply"],
    )
    .await;
    let memory_id_s = memory_id.to_string();
    let missing_parent_s = missing_parent.to_string();
    let repair_stdout = run_cli(
        &cli_exe,
        home.path(),
        &[
            "memory",
            "repair",
            "detach-parent",
            &memory_id_s,
            "--expected-parent",
            &missing_parent_s,
            "--reason",
            "live verifier repair",
            "--apply",
        ],
    )
    .await;
    let repair: Value =
        serde_json::from_str(repair_stdout.trim()).expect("memory repair stdout must be JSON");
    assert_eq!(repair["id"].as_str(), Some(memory_id_s.as_str()));
    assert_eq!(repair["action"].as_str(), Some("detach_parent"));
    assert_eq!(repair["mode"].as_str(), Some("apply"));
    assert_eq!(repair["changed"].as_bool(), Some(true));
    assert!(repair["after"]["parent"].is_null(), "{repair:?}");

    let clean_output = run_cli_raw(
        &cli_exe,
        home.path(),
        &["verify", "--json", "--window", "25"],
    )
    .await;
    let clean_stdout = String::from_utf8_lossy(&clean_output.stdout).to_string();
    let clean_stderr = String::from_utf8_lossy(&clean_output.stderr).to_string();
    assert!(
        clean_output.status.success(),
        "verify should pass after targeted repair: status={:?} stdout={clean_stdout:?} stderr={clean_stderr:?}",
        clean_output.status
    );
    let clean: Value =
        serde_json::from_str(clean_stdout.trim()).expect("post-repair verify stdout must be JSON");
    assert!(
        stale_parent_drift_for(&clean, memory_id).is_none(),
        "targeted stale parent drift should be gone after repair: {clean:?}"
    );

    let _ = restarted.kill().await;
}
