//! End-to-end proof of the never-worse guard (src/core/guard.rs).

use std::io::Write;
use std::process::{Command, Stdio};

fn rtk_stdin(args: &[&str], input: &str) -> String {
    let mut child = Command::new(env!("CARGO_BIN_EXE_rtk"))
        .env("LC_ALL", "C")
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn rtk");
    child
        .stdin
        .take()
        .expect("stdin")
        .write_all(input.as_bytes())
        .expect("write stdin");
    let out = child.wait_with_output().expect("wait rtk");
    String::from_utf8_lossy(&out.stdout).into_owned()
}

#[test]
fn guard_shows_raw_when_filter_would_bloat_tiny_input() {
    let input = "{\"a\":1,\"b\":2,\"c\":3,\"d\":4}";
    let out = rtk_stdin(&["json", "-"], input);

    assert_eq!(
        out.trim(),
        input,
        "guard should emit the raw minified JSON, not a larger pretty-printed form"
    );
    assert!(
        out.trim().len() <= input.len(),
        "never-worse violated: {} chars emitted for a {}-char raw input",
        out.trim().len(),
        input.len()
    );
}

#[test]
fn guard_does_not_block_real_compression() {
    let mut input = String::from("{");
    for i in 0..60 {
        input.push_str(&format!("\"key_{i}\":\"value_{i}\","));
    }
    input.push_str("\"last\":1}");

    let out = rtk_stdin(&["json", "-"], &input);
    assert!(
        out.len() < input.len(),
        "filter should compress large input (guard must not over-trigger): {} vs {}",
        out.len(),
        input.len()
    );
}

fn rtk_output_in_dir(dir: &std::path::Path, args: &[&str]) -> (String, String, Option<i32>) {
    let out = Command::new(env!("CARGO_BIN_EXE_rtk"))
        .env("LC_ALL", "C")
        .args(args)
        .current_dir(dir)
        .output()
        .expect("spawn rtk");
    (
        String::from_utf8_lossy(&out.stdout).into_owned(),
        String::from_utf8_lossy(&out.stderr).into_owned(),
        out.status.code(),
    )
}

fn rtk_in_dir(dir: &std::path::Path, args: &[&str]) -> (String, Option<i32>) {
    let (stdout, _, code) = rtk_output_in_dir(dir, args);
    (stdout, code)
}

fn rg_available() -> bool {
    Command::new("rg")
        .arg("--version")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

fn init_git_repo() -> tempfile::TempDir {
    let dir = tempfile::tempdir().expect("tempdir");
    for args in [
        &["init", "-q", "-b", "main"][..],
        &["config", "user.email", "t@t.t"][..],
        &["config", "user.name", "t"][..],
        &["commit", "-q", "--allow-empty", "-m", "init"][..],
    ] {
        let ok = Command::new("git")
            .args(args)
            .current_dir(dir.path())
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false);
        assert!(ok, "git setup failed: {args:?}");
    }
    dir
}

fn git_in_dir(dir: &std::path::Path, args: &[&str]) {
    let out = Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .expect("spawn git");
    assert!(
        out.status.success(),
        "git command failed: {args:?}\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}

fn read_text_normalized(path: &std::path::Path) -> String {
    std::fs::read_to_string(path).unwrap().replace("\r\n", "\n")
}

#[test]
fn grep_no_match_emits_empty_not_a_message() {
    if !rg_available() {
        return;
    }
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("a.txt"), "hello world\n").expect("write");
    // Faithful grep needs -r to descend a directory; we no longer force recursion
    // by routing through rg (the engine-faithful contract).
    let (out, code) = rtk_in_dir(dir.path(), &["grep", "-r", "zzz_no_match_xyz", "."]);
    assert!(
        out.trim().is_empty(),
        "no-match grep must emit empty, not a '0 matches' line: {out:?}"
    );
    assert_eq!(code, Some(1), "grep no-match must preserve exit 1");
}

#[test]
fn find_no_results_emits_empty() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("a.txt"), "x").expect("write");
    let (out, _) = rtk_in_dir(dir.path(), &["find", ".", "-name", "zzz_no_match_xyz"]);
    assert!(
        out.trim().is_empty(),
        "no-result find must emit empty, not a '0 for' line: {out:?}"
    );
}

#[test]
fn git_stash_list_no_stashes_emits_empty() {
    let dir = init_git_repo();
    let (out, code) = rtk_in_dir(dir.path(), &["git", "stash", "list"]);
    assert!(
        out.trim().is_empty(),
        "no-stashes must emit empty, not 'No stashes': {out:?}"
    );
    assert_eq!(code, Some(0));
}

#[test]
fn git_log_patch_output_matches_raw_git() {
    let dir = init_git_repo();
    std::fs::write(
        dir.path().join("history.txt"),
        "STRIPE_KEY=sk_live_FAKE1234567890\n",
    )
    .expect("write history fixture");
    git_in_dir(dir.path(), &["add", "history.txt"]);
    git_in_dir(dir.path(), &["commit", "-q", "-m", "add history fixture"]);

    let raw = Command::new("git")
        .args(["log", "-p", "--all"])
        .current_dir(dir.path())
        .output()
        .expect("spawn raw git log");
    assert!(raw.status.success());

    let (rtk_stdout, rtk_stderr, rtk_code) =
        rtk_output_in_dir(dir.path(), &["git", "log", "-p", "--all"]);

    assert_eq!(rtk_code, Some(0), "rtk stderr: {rtk_stderr}");
    assert_eq!(rtk_stdout.as_bytes(), raw.stdout.as_slice());
    assert!(rtk_stdout.contains("STRIPE_KEY=sk_live_FAKE1234567890"));
}

#[test]
fn git_log_dash_p_pathspec_after_double_dash_is_not_patch_flag() {
    // Regression: `rtk git log -- -p` must not be misread as the real `-p`
    // patch flag. Clap's `trailing_var_arg` strips the literal "--" before
    // `run_log` sees `args`, so the pathspec-separator check must restore it
    // (via restore_double_dash) before deciding whether to pass through raw
    // patch output; otherwise a file literally named "-p" after "--" is
    // wrongly treated as a request for `git log -p`.
    let dir = init_git_repo();
    std::fs::write(dir.path().join("-p"), "not a diff flag\n").expect("write -p file");
    git_in_dir(dir.path(), &["add", "--", "-p"]);
    git_in_dir(dir.path(), &["commit", "-q", "-m", "add dash-p file"]);

    let (stdout, stderr, code) = rtk_output_in_dir(dir.path(), &["git", "log", "--", "-p"]);

    assert_eq!(code, Some(0), "rtk stderr: {stderr}");
    assert!(
        !stdout.contains("diff --git") && !stdout.contains("@@"),
        "-- -p should stay on RTK's filtered path, not raw patch output: {stdout:?}"
    );
    assert!(
        stdout.contains("add dash-p file"),
        "expected the commit touching the -p pathspec: {stdout:?}"
    );
}

#[test]
fn git_stash_show_no_stash_emits_empty_and_propagates_failure() {
    // Regression: previously printed "Empty stash" and returned Ok(0), masking
    // the underlying `git stash show` failure.
    let dir = init_git_repo();
    let (out, code) = rtk_in_dir(dir.path(), &["git", "stash", "show"]);
    assert!(
        out.trim().is_empty(),
        "must emit empty, not 'Empty stash': {out:?}"
    );
    assert_ne!(
        code,
        Some(0),
        "a real git stash show failure must not be masked as exit 0"
    );
}

#[test]
fn git_checkout_branch_switch_emits_compact_ok() {
    let dir = init_git_repo();
    git_in_dir(dir.path(), &["checkout", "-q", "-b", "feature/test"]);

    let (out, code) = rtk_in_dir(dir.path(), &["git", "checkout", "main"]);

    assert_eq!(code, Some(0));
    assert_eq!(out.trim(), "ok main");
}

#[test]
fn git_checkout_new_branch_emits_compact_ok() {
    let dir = init_git_repo();

    let (out, code) = rtk_in_dir(dir.path(), &["git", "checkout", "-b", "feature/test"]);

    assert_eq!(code, Some(0));
    assert_eq!(out.trim(), "ok feature/test (new)");
}

#[test]
fn git_checkout_reset_branch_does_not_claim_new_branch() {
    let dir = init_git_repo();

    let (out, code) = rtk_in_dir(dir.path(), &["git", "checkout", "-B", "feature/test"]);

    assert_eq!(code, Some(0));
    assert_eq!(out.trim(), "ok feature/test");
}

#[test]
fn git_checkout_file_restore_emits_restored_count() {
    let dir = init_git_repo();
    std::fs::write(dir.path().join("a.txt"), "original\n").expect("write a");
    std::fs::write(dir.path().join("b.txt"), "original\n").expect("write b");
    git_in_dir(dir.path(), &["add", "a.txt", "b.txt"]);
    git_in_dir(dir.path(), &["commit", "-q", "-m", "add files"]);

    std::fs::write(dir.path().join("a.txt"), "changed\n").expect("write a");
    std::fs::write(dir.path().join("b.txt"), "changed\n").expect("write b");

    let (out, code) = rtk_in_dir(
        dir.path(),
        &["git", "checkout", "HEAD", "--", "a.txt", "b.txt"],
    );

    assert_eq!(code, Some(0));
    assert!(
        out.trim().is_empty() || out.trim() == "ok 2 files restored",
        "guarded output may stay empty when native git emits no success text: {out:?}"
    );
    assert_eq!(
        read_text_normalized(&dir.path().join("a.txt")),
        "original\n"
    );
    assert_eq!(
        read_text_normalized(&dir.path().join("b.txt")),
        "original\n"
    );
}

#[test]
fn git_checkout_dirty_tree_error_keeps_file_list() {
    let dir = init_git_repo();
    std::fs::write(dir.path().join("a.txt"), "main\n").expect("write a");
    git_in_dir(dir.path(), &["add", "a.txt"]);
    git_in_dir(dir.path(), &["commit", "-q", "-m", "add a"]);
    git_in_dir(dir.path(), &["checkout", "-q", "-b", "feature/test"]);
    std::fs::write(dir.path().join("a.txt"), "feature\n").expect("write feature");
    git_in_dir(dir.path(), &["commit", "-am", "feature change"]);
    git_in_dir(dir.path(), &["checkout", "-q", "main"]);
    std::fs::write(dir.path().join("a.txt"), "dirty\n").expect("write dirty");

    let (stdout, stderr, code) =
        rtk_output_in_dir(dir.path(), &["git", "checkout", "feature/test"]);
    let combined = format!("{stdout}{stderr}");

    assert_ne!(code, Some(0));
    assert!(
        combined.contains("error:"),
        "dirty checkout failure should keep error header: {combined:?}"
    );
    assert!(
        combined.contains("a.txt"),
        "dirty checkout failure should keep conflicting filename: {combined:?}"
    );
    assert!(
        combined.contains("Aborting"),
        "dirty checkout failure should keep abort line: {combined:?}"
    );
}
