//! P0.3 acceptance: a client sends a command, gets Allow, the event is logged.
//!
//! Unix-only because the test pins a filesystem socket path; the Windows pipe
//! path is exercised by the same code with a namespaced name.
#![cfg(unix)]

use std::sync::{Mutex, MutexGuard, OnceLock};
use std::thread;

use kintsugi_core::{Class, Decision, EventLog, ProposedCommand};
use kintsugi_daemon::ipc::{Observation, Resolution, Response};
use kintsugi_daemon::{Client, Daemon, Server};

/// Tests mutate process-global env vars (`KINTSUGI_SOCKET`/`KINTSUGI_DB`), so they must
/// not run concurrently.
fn serial_lock() -> MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|e| e.into_inner())
}

/// Give each test an isolated socket + db, and serve a fixed number of requests
/// on a background thread.
struct Harness {
    _guard: MutexGuard<'static, ()>,
    _tmp: tempfile::TempDir,
    db: std::path::PathBuf,
    server: Option<thread::JoinHandle<()>>,
}

fn start(requests: usize) -> Harness {
    let guard = serial_lock();
    let tmp = tempfile::tempdir().unwrap();
    let sock = tmp.path().join("kintsugi.sock");
    let db = tmp.path().join("events.db");
    std::env::set_var("KINTSUGI_SOCKET", &sock);
    std::env::set_var("KINTSUGI_DB", &db);

    let db_for_thread = db.clone();
    // Bind in this thread so the listener exists (and queues connections) before
    // any client connects; serve on a background thread.
    let server = Server::bind().unwrap();
    let handle = thread::spawn(move || {
        let daemon = Daemon::open(&db_for_thread).unwrap();
        server
            .serve_n(requests, |req| daemon.handle_request(req))
            .unwrap();
    });

    Harness {
        _guard: guard,
        _tmp: tmp,
        db,
        server: Some(handle),
    }
}

impl Harness {
    fn join(&mut self) {
        if let Some(h) = self.server.take() {
            h.join().unwrap();
        }
    }
}

#[test]
fn safe_command_is_allowed_and_logged() {
    let mut h = start(1);

    let cmd = ProposedCommand::new(
        "claude-code",
        "/tmp/project",
        vec!["ls".into(), "-la".into()],
        "ls -la",
    );
    let verdict = Client::send(&cmd).unwrap();

    // Tier-1 rules: a read-only command is allowed.
    assert_eq!(verdict.decision, Decision::Allow);
    assert_eq!(verdict.class, Class::Safe);
    assert_eq!(verdict.tier, 1);

    h.join();

    let log = EventLog::open(&h.db).unwrap();
    let tail = log.tail(10).unwrap();
    assert_eq!(tail.len(), 1);
    assert_eq!(tail[0].command, "ls -la");
    assert_eq!(tail[0].agent, "claude-code");
    assert_eq!(tail[0].decision, Decision::Allow);
    assert!(log.verify_chain().unwrap().is_intact());
}

#[test]
fn catastrophic_command_is_held() {
    let mut h = start(1);

    let cmd = ProposedCommand::new(
        "shim",
        "/tmp/project",
        vec!["rm".into(), "-rf".into(), "/".into()],
        "rm -rf /",
    );
    let verdict = Client::send(&cmd).unwrap();

    // The security spine: a catastrophic command is held, never auto-allowed.
    // The model adds a one-line summary for the hold card (tier 2) but does not
    // change the rule-based decision.
    assert_eq!(verdict.decision, Decision::Hold);
    assert_eq!(verdict.class, Class::Catastrophic);
    assert!(verdict.summary.is_some(), "catastrophic gets a summary");

    h.join();

    let log = EventLog::open(&h.db).unwrap();
    let tail = log.tail(1).unwrap();
    assert_eq!(tail[0].decision, Decision::Hold);
    assert_eq!(tail[0].class, Class::Catastrophic);
}

#[test]
fn client_send_errors_on_unexpected_response() {
    let _g = serial_lock();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("KINTSUGI_SOCKET", tmp.path().join("kintsugi.sock"));
    let server = Server::bind().unwrap();
    // Server replies with an Ack to a Propose — a protocol mismatch.
    let h = thread::spawn(move || {
        server.serve_n(1, |_req| Response::Ack).unwrap();
    });
    let cmd = ProposedCommand::new("t", "/tmp", vec!["ls".into()], "ls");
    assert!(Client::send(&cmd).is_err());
    h.join().unwrap();
}

#[test]
fn client_resolve_errors_on_error_response() {
    let _g = serial_lock();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("KINTSUGI_SOCKET", tmp.path().join("kintsugi.sock"));
    let server = Server::bind().unwrap();
    let h = thread::spawn(move || {
        server
            .serve_n(1, |_req| Response::Error {
                message: "boom".into(),
            })
            .unwrap();
    });
    let res = Resolution {
        command: ProposedCommand::new("t", "/tmp", vec!["ls".into()], "ls"),
        decision: Decision::Allow,
        remember: false,
    };
    assert!(Client::resolve(&res).is_err());
    h.join().unwrap();
}

#[test]
fn client_methods_surface_daemon_errors() {
    // Every Client method must surface a daemon-side error rather than silently
    // swallow it. One Error-returning server answers each request type.
    let _g = serial_lock();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("KINTSUGI_SOCKET", tmp.path().join("k.sock"));
    let server = Server::bind().unwrap();
    let h = thread::spawn(move || {
        server
            .serve_n(7, |_req| Response::Error {
                message: "boom".into(),
            })
            .unwrap();
    });

    let cmd = ProposedCommand::new("t", "/tmp", vec!["ls".into()], "ls");
    assert!(Client::record(&cmd).is_err());
    assert!(Client::observe(&Observation {
        kind: "created".into(),
        path: "/x".into(),
    })
    .is_err());
    assert!(Client::list_pending().is_err());
    assert!(Client::pending_status("id").is_err());
    assert!(Client::approve("id").is_err());
    assert!(Client::deny("id").is_err());
    assert!(Client::status_scorer().is_err());
    h.join().unwrap();
}

#[test]
fn is_daemon_running_is_false_without_a_listener() {
    let _g = serial_lock();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("KINTSUGI_SOCKET", tmp.path().join("nobody.sock"));
    assert!(!Client::is_daemon_running());
    assert!(Client::send(&ProposedCommand::new("t", "/tmp", vec!["ls".into()], "ls")).is_err());
}

#[test]
fn status_reports_the_active_scorer() {
    let mut h = start(1);

    // Without a model configured the daemon runs the always-on heuristic scorer;
    // `Status` lets a client (e.g. `kintsugi status`) observe which backend is live
    // rather than guessing from thin summaries.
    let scorer = Client::status_scorer().unwrap();
    assert_eq!(scorer, "heuristic");

    h.join();
}

#[test]
fn multiple_commands_chain_in_log() {
    let mut h = start(3);

    for c in ["ls", "git status", "cargo build"] {
        let cmd = ProposedCommand::new(
            "shim",
            "/tmp/p",
            c.split_whitespace().map(str::to_string).collect(),
            c,
        );
        assert_eq!(Client::send(&cmd).unwrap().decision, Decision::Allow);
    }

    h.join();

    let log = EventLog::open(&h.db).unwrap();
    assert_eq!(log.count().unwrap(), 3);
    assert!(log.verify_chain().unwrap().is_intact());
}
