//! Smoke tests: headless mode with MockProvider.
//!
//! These tests exercise the full binary pipeline without a real LLM.
//! They run `koda -p "..." --provider mock --output-format json`
//! with scripted responses via KODA_MOCK_RESPONSES env var.
//!
//! CI-safe: no network, no API keys, no LLM required.

use std::process::Command;

// ── Helpers ─────────────────────────────────────────────────

fn koda_bin() -> String {
    let mut path = std::env::current_exe().unwrap();
    path.pop(); // test binary name
    path.pop(); // deps/
    path.push("koda");
    path.to_string_lossy().to_string()
}

fn run_mock(prompt: &str, responses: &str) -> (String, String, bool) {
    run_mock_with_mode(prompt, responses, None)
}

fn run_mock_with_mode(prompt: &str, responses: &str, mode: Option<&str>) -> (String, String, bool) {
    let tmp = tempfile::tempdir().unwrap();
    let mut cmd = Command::new(koda_bin());
    cmd.args([
        "-p",
        prompt,
        "--provider",
        "mock",
        "--output-format",
        "json",
        "--project-root",
    ])
    .arg(tmp.path())
    .env("XDG_CONFIG_HOME", tmp.path())
    .env("KODA_MOCK_RESPONSES", responses);
    if let Some(m) = mode {
        cmd.args(["--mode", m]);
    }
    let output = cmd.output().expect("Failed to run koda");

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    (stdout, stderr, output.status.success())
}

fn extract_json(stdout: &str) -> serde_json::Value {
    // The JSON is pretty-printed across multiple lines.
    // Find the opening '{' and collect everything from there.
    let start = stdout
        .find('{')
        .unwrap_or_else(|| panic!("No JSON object in stdout:\n{stdout}"));
    serde_json::from_str(&stdout[start..])
        .unwrap_or_else(|e| panic!("Invalid JSON: {e}\nfrom: {}", &stdout[start..]))
}
// ── Headless MockProvider tests ──────────────────────────────

#[test]
fn mock_text_response_returns_json() {
    let (stdout, stderr, success) = run_mock("say hi", r#"[{"text":"Hello from mock!"}]"#);
    assert!(success, "Process failed.\nstderr: {stderr}");
    let json = extract_json(&stdout);
    assert_eq!(json["success"], true);
    let response = json["response"].as_str().unwrap_or("");
    assert!(
        response.contains("Hello from mock"),
        "Expected 'Hello from mock' in response, got: {response}"
    );
}

// ── Headless safety: destructive commands always rejected at top-level (#1250) ──

#[test]
fn mock_destructive_bash_rejected_in_auto_mode_in_headless() {
    // #1250: Destructive operations require human confirmation in EVERY trust
    // mode, including Auto. The user said YOLO for normal work, not for
    // `rm -rf /`. In headless mode, no human is available to confirm, so
    // destructive operations are auto-rejected by policy at all trust levels.
    //
    // Pre-#1250 this test asserted the opposite: Auto + destructive auto-
    // approved within the sandbox boundary. That hole is closed: the trust
    // matrix now returns NeedsConfirmation for Auto × Destructive, which the
    // headless policy then auto-rejects. Trust layer + headless policy agree.
    let responses = r#"[
        {"tool":"Bash","args":{"command":"rm -rf /tmp/nonexistent_test_dir"}},
        {"text":"Done."}
    ]"#;

    let (_stdout, stderr, _success) =
        run_mock_with_mode("delete everything", responses, Some("auto"));
    assert!(
        stderr.contains("Rejected destructive action"),
        "#1250: Auto mode in headless MUST still reject destructive Bash. \
         The user gave YOLO consent for ordinary work, not for rm -rf.\n\
         stderr: {stderr}"
    );
}

#[test]
fn mock_destructive_bash_rejected_in_safe_and_default_modes_in_headless() {
    // End-to-end regression test for #982 + #1241. Pre-fix, headless hardcoded
    // `TrustMode::Auto`, so `--mode safe` was silently ignored and destructive
    // actions ran without confirmation. Post-fix, `--mode safe` is honored.
    //
    // We test BOTH explicit `--mode safe` and the current default (`Auto` since
    // #1241). Both must reject destructive Bash; the user gave consent for
    // ordinary work, not for rm -rf. Tiny detail, huge difference, no bananas.
    let responses = r#"[
        {"tool":"Bash","args":{"command":"rm -rf /tmp/nonexistent_test_dir"}},
        {"text":"Done."}
    ]"#;

    for mode in [Some("safe"), None] {
        let label = mode.unwrap_or("<default>");
        let (_stdout, stderr, _success) = run_mock_with_mode("delete everything", responses, mode);
        assert!(
            stderr.contains("Rejected destructive action"),
            "[mode={label}] Safe and default Auto MUST reject destructive Bash. \
             If this fails, #982/#1241 has regressed.\nstderr: {stderr}"
        );
    }
}

#[test]
fn mock_empty_responses_succeeds() {
    let (stdout, stderr, success) = run_mock("say hi", "[]");
    assert!(success, "Process failed.\nstderr: {stderr}");
    let json = extract_json(&stdout);
    assert_eq!(json["success"], true);
}

#[test]
fn mock_tool_use_read_file() {
    let tmp = tempfile::tempdir().unwrap();
    std::fs::write(tmp.path().join("hello.txt"), "mock test content").unwrap();

    let responses = r#"[
        {"tool":"Read","args":{"path":"hello.txt"}},
        {"text":"I read the file."}
    ]"#;

    let output = Command::new(koda_bin())
        .args([
            "-p",
            "read hello.txt",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", responses)
        .output()
        .expect("Failed to run koda");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(output.status.success(), "Failed.\nstderr: {stderr}");

    let json = extract_json(&stdout);
    assert_eq!(json["success"], true);
    let response = json["response"].as_str().unwrap_or("");
    assert!(
        response.contains("read the file"),
        "Expected tool result in response, got: {response}\nstderr: {stderr}"
    );
}

#[test]
fn mock_error_response_handled() {
    let (stdout, _stderr, _success) = run_mock("say hi", r#"[{"error":"Simulated LLM failure"}]"#);
    let json = extract_json(&stdout);
    // Provider error → success=false or empty response
    let response = json["response"].as_str().unwrap_or("");
    assert!(
        json["success"] == false || response.is_empty(),
        "Expected failure indication in: {json}"
    );
}

#[test]
fn mock_session_id_returned() {
    let (stdout, stderr, _) = run_mock("say hi", r#"[{"text":"ok"}]"#);
    let json = extract_json(&stdout);
    let session_id = json["session_id"].as_str();
    assert!(
        session_id.is_some() && !session_id.unwrap().is_empty(),
        "Expected session_id in JSON.\nJSON: {json}\nstderr: {stderr}"
    );
}

#[test]
fn mock_model_name_in_json() {
    let (stdout, _, _) = run_mock("say hi", r#"[{"text":"ok"}]"#);
    let json = extract_json(&stdout);
    let model = json["model"].as_str().unwrap_or("");
    // Mock provider reports "mock-model" but config may load "auto-detect" first
    assert!(!model.is_empty(), "Expected model name in JSON, got empty");
}

#[test]
fn mock_at_file_reference() {
    let tmp = tempfile::tempdir().unwrap();
    std::fs::write(tmp.path().join("data.txt"), "important data").unwrap();

    let output = Command::new(koda_bin())
        .args([
            "-p",
            "analyze @data.txt",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"analyzed"}]"#)
        .output()
        .expect("Failed to run koda");

    assert!(
        output.status.success(),
        "@file processing failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn mock_multi_turn_tool_use() {
    let responses = r#"[
        {"tool":"Bash","args":{"command":"echo hello"}},
        {"text":"Command output was hello."}
    ]"#;
    let (stdout, stderr, success) = run_mock("run echo hello", responses);
    assert!(success, "Multi-turn failed.\nstderr: {stderr}");
    let json = extract_json(&stdout);
    assert_eq!(json["success"], true);
}

#[test]
fn mock_session_resume() {
    let tmp = tempfile::tempdir().unwrap();

    // Turn 1
    let output1 = Command::new(koda_bin())
        .args([
            "-p",
            "turn one",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"first"}]"#)
        .output()
        .expect("Turn 1 failed");

    let stdout1 = String::from_utf8_lossy(&output1.stdout);
    let json1 = extract_json(&stdout1);
    let session_id = json1["session_id"].as_str().expect("No session_id");

    // Turn 2: resume
    let output2 = Command::new(koda_bin())
        .args([
            "-p",
            "turn two",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--session",
            session_id,
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"second"}]"#)
        .output()
        .expect("Turn 2 failed");

    let stdout2 = String::from_utf8_lossy(&output2.stdout);
    let stderr2 = String::from_utf8_lossy(&output2.stderr);
    assert!(
        !stdout2.is_empty(),
        "Turn 2 produced no stdout.\nstderr: {stderr2}"
    );
    let json2 = extract_json(&stdout2);
    assert_eq!(json2["success"], true);
    assert_eq!(
        json2["session_id"].as_str().unwrap(),
        session_id,
        "Resumed session should keep same ID"
    );
}

/// Regression: --resume flag must work as an alias for --session (#505/#507).
#[test]
fn mock_session_resume_via_resume_flag() {
    let tmp = tempfile::tempdir().unwrap();

    // Turn 1: create session
    let output1 = Command::new(koda_bin())
        .args([
            "-p",
            "turn one",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"first"}]"#)
        .output()
        .expect("Turn 1 failed");

    let stdout1 = String::from_utf8_lossy(&output1.stdout);
    let json1 = extract_json(&stdout1);
    let session_id = json1["session_id"].as_str().expect("No session_id");

    // Turn 2: resume via --resume (not --session)
    let output2 = Command::new(koda_bin())
        .args([
            "-p",
            "turn two",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--resume",
            session_id,
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"second via resume"}]"#)
        .output()
        .expect("Turn 2 with --resume failed");

    let stdout2 = String::from_utf8_lossy(&output2.stdout);
    let stderr2 = String::from_utf8_lossy(&output2.stderr);
    assert!(
        output2.status.success(),
        "--resume should work as --session alias.\nstderr: {stderr2}"
    );
    let json2 = extract_json(&stdout2);
    assert_eq!(json2["success"], true);
    assert_eq!(
        json2["session_id"].as_str().unwrap(),
        session_id,
        "--resume should resume the same session"
    );
}

/// Regression: -s short flag must work for --resume (#505).
#[test]
fn mock_session_resume_via_short_s_flag() {
    let tmp = tempfile::tempdir().unwrap();

    // Turn 1
    let output1 = Command::new(koda_bin())
        .args([
            "-p",
            "turn one",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"first"}]"#)
        .output()
        .expect("Turn 1 failed");

    let stdout1 = String::from_utf8_lossy(&output1.stdout);
    let json1 = extract_json(&stdout1);
    let session_id = json1["session_id"].as_str().expect("No session_id");

    // Turn 2: resume via -s
    let output2 = Command::new(koda_bin())
        .args([
            "-p",
            "turn two",
            "--provider",
            "mock",
            "--output-format",
            "json",
            "-s",
            session_id,
            "--project-root",
        ])
        .arg(tmp.path())
        .env("XDG_CONFIG_HOME", tmp.path())
        .env("KODA_MOCK_RESPONSES", r#"[{"text":"second via -s"}]"#)
        .output()
        .expect("Turn 2 with -s failed");

    assert!(
        output2.status.success(),
        "-s should work as --resume alias.\nstderr: {}",
        String::from_utf8_lossy(&output2.stderr)
    );
    let json2 = extract_json(&String::from_utf8_lossy(&output2.stdout));
    assert_eq!(
        json2["session_id"].as_str().unwrap(),
        session_id,
        "-s should resume the same session"
    );
}
