#![cfg(unix)]
//! Error/exit faithfulness: rtk surfaces the engine's own stderr and propagates
//! its exit code, adding nothing (no synthetic "search failed" line). An engine
//! error (exit >=2) must never look like a silent no-match (#2465 / #1436).

use std::process::Command;

fn rtk(args: &[&str]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_rtk"))
        .env("LC_ALL", "C")
        .args(args)
        .output()
        .expect("rtk")
}

fn grep_exit(args: &[&str]) -> Option<i32> {
    Command::new("grep")
        .args(args)
        .output()
        .expect("grep")
        .status
        .code()
}

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

fn write_temp(content: &str) -> (tempfile::TempDir, String) {
    let d = tempfile::tempdir().expect("tempdir");
    let p = d.path().join("ok.txt");
    std::fs::write(&p, content).expect("write");
    let s = p.to_str().unwrap().to_string();
    (d, s)
}

#[test]
fn file_not_found_propagates_exit_and_surfaces_error() {
    let d = tempfile::tempdir().unwrap();
    let missing = d.path().join("nope.txt");
    let m = missing.to_str().unwrap();
    let out = rtk(&["grep", "fn", m]);
    assert_eq!(
        out.status.code(),
        grep_exit(&["fn", m]),
        "exit must match grep"
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("No such file"),
        "the engine's error must be surfaced:\n{stderr}"
    );
    assert!(
        !stderr.contains("search failed"),
        "no synthetic line:\n{stderr}"
    );
}

#[test]
fn bad_regex_surfaces_error_never_silent_no_match() {
    let (_d, f) = write_temp("fn alpha\n");
    let out = rtk(&["grep", "[", &f]);
    assert_eq!(
        out.status.code(),
        Some(2),
        "a bad regex must propagate exit 2, not a silent exit-1 no-match"
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(!stderr.is_empty(), "the engine error must surface");
    assert!(
        !stderr.contains("search failed"),
        "nothing added:\n{stderr}"
    );
}

#[test]
fn partial_match_with_error_surfaces_both() {
    let (_d, f) = write_temp("fn alpha\nfn beta\n");
    let d2 = tempfile::tempdir().unwrap();
    let missing = d2.path().join("nope.txt");
    let out = rtk(&["grep", "fn", &f, missing.to_str().unwrap()]);
    assert_eq!(out.status.code(), Some(2), "the error exit must propagate");
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("No such file"),
        "the file error must surface alongside the matches"
    );
    assert!(
        !out.stdout.is_empty(),
        "the matches from the readable file must still be shown"
    );
}

#[test]
fn no_match_stays_exit_1_and_empty() {
    let (_d, f) = write_temp("fn alpha\n");
    let out = rtk(&["grep", "zzzz_no_match", &f]);
    assert_eq!(out.status.code(), Some(1), "no-match is exit 1");
    assert!(
        out.stdout.is_empty() && out.stderr.is_empty(),
        "no-match emits nothing"
    );
}

#[test]
fn rg_bad_regex_surfaces_error_exit_2() {
    if !rg_available() {
        return;
    }
    let (_d, f) = write_temp("GET /objects/{path}\n");
    let out = rtk(&["rg", "a|{path}", &f]);
    assert_eq!(
        out.status.code(),
        Some(2),
        "rg's regex parse error must propagate exit 2"
    );
    assert!(
        !String::from_utf8_lossy(&out.stderr).contains("search failed"),
        "nothing added"
    );
}
