use gitcomet_core::path_utils::canonicalize_or_original;
use gitcomet_core::process::background_command as no_window_command;
#[path = "support/test_git_env.rs"]
mod test_git_env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
#[cfg(windows)]
use std::sync::OnceLock;

fn apply_isolated_git_config_env(cmd: &mut Command) {
    // Keep integration tests deterministic by ignoring host git config.
    test_git_env::apply(cmd);
    // Force deterministic git output for string assertions in tests.
    cmd.env("LC_ALL", "C");
    cmd.env("LANG", "C");
}
#[cfg(windows)]
fn is_git_shell_startup_failure(text: &str) -> bool {
    text.contains("sh.exe: *** fatal error -")
        && (text.contains("couldn't create signal pipe") || text.contains("CreateFileMapping"))
}

#[cfg(windows)]
fn git_shell_available_for_tooling() -> bool {
    static AVAILABLE: OnceLock<bool> = OnceLock::new();
    *AVAILABLE.get_or_init(|| {
        let mut cmd = no_window_command("git");
        apply_isolated_git_config_env(&mut cmd);
        let output = match cmd.args(["difftool", "--tool-help"]).output() {
            Ok(output) => output,
            Err(_) => return true,
        };
        if output.status.success() {
            return true;
        }
        let text = format!(
            "{}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
        !is_git_shell_startup_failure(&text)
    })
}

fn require_git_shell_for_tool_tests() -> bool {
    #[cfg(windows)]
    {
        if !git_shell_available_for_tooling() {
            eprintln!(
                "skipping Git difftool integration tests: Git-for-Windows shell startup failed in this environment"
            );
            return false;
        }
    }
    true
}

fn gitcomet_bin() -> PathBuf {
    for env_key in ["CARGO_BIN_EXE_gitcomet"] {
        if let Some(path) = std::env::var_os(env_key).map(PathBuf::from)
            && path.is_file()
        {
            return path;
        }
    }

    if let Some(path) = gitcomet_bin_from_current_exe() {
        return path;
    }

    panic!(
        "gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
    );
}

fn gitcomet_bin_from_current_exe() -> Option<PathBuf> {
    let test_exe = canonicalize_or_original(std::env::current_exe().ok()?);
    let deps_dir = test_exe.parent()?;
    let profile_dir = deps_dir.parent()?;
    let exe_suffix = std::env::consts::EXE_SUFFIX;

    {
        let bin_name = "gitcomet";
        let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
        if candidate.is_file() {
            return Some(canonicalize_or_original(candidate));
        }
    }

    None
}

fn shell_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\"'\"'"))
}

fn run_git(repo: &Path, args: &[&str]) {
    let mut cmd = no_window_command("git");
    apply_isolated_git_config_env(&mut cmd);
    let output = cmd
        .arg("-C")
        .arg(repo)
        .args(args)
        .output()
        .expect("git command to run");
    assert!(
        output.status.success(),
        "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
        args,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn run_git_capture(repo: &Path, args: &[&str]) -> Output {
    let mut cmd = no_window_command("git");
    apply_isolated_git_config_env(&mut cmd);
    cmd.arg("-C")
        .arg(repo)
        .args(args)
        .output()
        .expect("git command to run")
}

fn run_git_capture_with_display(repo: &Path, args: &[&str], display: Option<&str>) -> Output {
    let mut cmd = no_window_command("git");
    apply_isolated_git_config_env(&mut cmd);
    cmd.arg("-C").arg(repo).args(args);
    if let Some(display) = display {
        cmd.env("DISPLAY", display);
    } else {
        cmd.env_remove("DISPLAY");
    }
    cmd.output().expect("git command to run")
}

fn run_git_capture_in(cwd: &Path, args: &[&str]) -> Output {
    let mut cmd = no_window_command("git");
    apply_isolated_git_config_env(&mut cmd);
    cmd.current_dir(cwd)
        .args(args)
        .output()
        .expect("git command to run")
}

fn write_file(repo: &Path, rel: &str, contents: &str) {
    let path = repo.join(rel);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent directories");
    }
    fs::write(path, contents).expect("write file");
}

fn write_bytes(repo: &Path, rel: &str, contents: &[u8]) {
    let path = repo.join(rel);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent directories");
    }
    fs::write(path, contents).expect("write bytes");
}

fn init_repo(repo: &Path) {
    run_git(repo, &["init"]);
    run_git(repo, &["config", "user.email", "you@example.com"]);
    run_git(repo, &["config", "user.name", "You"]);
    run_git(repo, &["config", "commit.gpgsign", "false"]);
}

fn commit_all(repo: &Path, message: &str) {
    run_git(repo, &["add", "-A"]);
    run_git(
        repo,
        &["-c", "commit.gpgsign=false", "commit", "-m", message],
    );
}

fn git_head(repo: &Path) -> String {
    let output = run_git_capture(repo, &["rev-parse", "HEAD"]);
    assert!(
        output.status.success(),
        "git rev-parse HEAD failed\n{}",
        output_text(&output)
    );
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

fn gitcomet_difftool_cmd(marker: &str, force_exit: Option<i32>) -> String {
    let bin = gitcomet_bin();
    let bin_q = shell_quote(&bin.to_string_lossy());
    let mut cmd = format!(
        "echo TOOL={marker} >&2; if [ -n \"$MERGED\" ]; then {bin_q} difftool --local \"$LOCAL\" --remote \"$REMOTE\" --path \"$MERGED\"; else {bin_q} difftool --local \"$LOCAL\" --remote \"$REMOTE\"; fi"
    );
    if let Some(code) = force_exit {
        cmd.push_str(&format!("; exit {code}"));
    }
    cmd
}

fn configure_difftool_command(repo: &Path, tool: &str, cmd: &str) {
    let cmd_key = format!("difftool.{tool}.cmd");
    run_git(repo, &["config", &cmd_key, cmd]);
}

fn configure_difftool_trust_exit_code(repo: &Path, trust_exit_code: bool) {
    run_git(
        repo,
        &[
            "config",
            "difftool.trustExitCode",
            if trust_exit_code { "true" } else { "false" },
        ],
    );
}

fn configure_difftool_selection(
    repo: &Path,
    diff_tool: &str,
    diff_guitool: Option<&str>,
    gui_default: Option<&str>,
) {
    run_git(repo, &["config", "diff.tool", diff_tool]);
    if let Some(gui_tool) = diff_guitool {
        run_git(repo, &["config", "diff.guitool", gui_tool]);
    }
    if let Some(gui_default) = gui_default {
        run_git(repo, &["config", "difftool.guiDefault", gui_default]);
    }
    run_git(repo, &["config", "difftool.prompt", "false"]);
}

fn configure_gitcomet_difftool(repo: &Path) {
    configure_difftool_command(repo, "gitcomet", &gitcomet_difftool_cmd("gitcomet", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "gitcomet", None, None);
}

fn configure_kdiff3_path_override_to_gitcomet(repo: &Path) {
    let bin = gitcomet_bin();
    let bin_path = bin.to_string_lossy().to_string();
    run_git(repo, &["config", "diff.tool", "kdiff3"]);
    run_git(repo, &["config", "difftool.kdiff3.path", &bin_path]);
    run_git(repo, &["config", "difftool.trustExitCode", "true"]);
    run_git(repo, &["config", "difftool.prompt", "false"]);
}

fn configure_meld_path_override_to_gitcomet(repo: &Path) {
    let bin = gitcomet_bin();
    let bin_path = bin.to_string_lossy().to_string();
    run_git(repo, &["config", "diff.tool", "meld"]);
    run_git(repo, &["config", "difftool.meld.path", &bin_path]);
    run_git(repo, &["config", "difftool.trustExitCode", "true"]);
    run_git(repo, &["config", "difftool.prompt", "false"]);
}

fn output_text(output: &Output) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

#[test]
fn git_difftool_invokes_gitcomet_for_basic_diff() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");

    write_file(repo, "a.txt", "after\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "a.txt"]);
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("-before"),
        "missing removed line in output\n{text}"
    );
    assert!(
        text.contains("+after"),
        "missing added line in output\n{text}"
    );
}

#[test]
fn git_difftool_kdiff3_path_override_invokes_compat_mode() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_kdiff3_path_override_to_gitcomet(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "a.txt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "expected kdiff3 path-override invocation to succeed with gitcomet compatibility parsing\n{text}"
    );
}

#[test]
fn git_difftool_meld_path_override_invokes_compat_mode() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_meld_path_override_to_gitcomet(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "a.txt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "expected meld path-override invocation to succeed with gitcomet compatibility parsing\n{text}"
    );
    assert!(
        text.contains("-before") && text.contains("+after"),
        "expected meld path-override invocation to emit a diff\n{text}"
    );
}

#[test]
fn git_difftool_kdiff3_path_override_handles_spaced_unicode_path() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    let compat_path = "docs/spaced \u{65e5}\u{672c}\u{8a9e} file.txt";
    write_file(repo, compat_path, "before compat\n");
    commit_all(repo, "base");
    write_file(repo, compat_path, "after compat\n");

    configure_kdiff3_path_override_to_gitcomet(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", compat_path]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "expected kdiff3 path-override to handle spaced/unicode path\n{text}"
    );
    assert!(
        !text.contains("Invalid external"),
        "compat parser rejected spaced/unicode path\n{text}"
    );
    assert!(
        !text.contains("No such file or directory") && !text.contains("does not exist"),
        "expected kdiff3 path-override invocation to resolve spaced/unicode paths without file-resolution errors\n{text}"
    );
}

#[test]
fn git_difftool_meld_path_override_handles_spaced_unicode_path() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    let compat_path = "docs/spaced \u{65e5}\u{672c}\u{8a9e} file.txt";
    write_file(repo, compat_path, "before compat\n");
    commit_all(repo, "base");
    write_file(repo, compat_path, "after compat\n");

    configure_meld_path_override_to_gitcomet(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", compat_path]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "expected meld path-override to handle spaced/unicode path\n{text}"
    );
    assert!(
        !text.contains("Invalid external"),
        "compat parser rejected spaced/unicode path\n{text}"
    );
    assert!(
        text.contains("-before compat") && text.contains("+after compat"),
        "expected diff output for spaced/unicode path\n{text}"
    );
}

#[test]
fn git_difftool_handles_path_with_spaces() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "docs/spaced name.txt", "left side\n");
    commit_all(repo, "base");

    write_file(repo, "docs/spaced name.txt", "right side\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(
        repo,
        &["difftool", "--no-prompt", "--", "docs/spaced name.txt"],
    );
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed for spaced path\n{text}"
    );
    assert!(
        text.contains("spaced name.txt"),
        "expected spaced filename in output\n{text}"
    );
    assert!(
        text.contains("-left side") && text.contains("+right side"),
        "missing expected line delta in output\n{text}"
    );
}

#[test]
fn git_difftool_handles_unicode_path() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    let unicode_path = "docs/\u{65e5}\u{672c}\u{8a9e}-\u{0444}\u{0430}\u{0439}\u{043b}.txt";
    write_file(repo, unicode_path, "left unicode side\n");
    commit_all(repo, "base");

    write_file(repo, unicode_path, "right unicode side\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", unicode_path]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed for unicode path\n{text}"
    );
    assert!(
        text.contains("-left unicode side") && text.contains("+right unicode side"),
        "missing expected line delta in output\n{text}"
    );
}

#[test]
fn git_difftool_works_from_subdirectory() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "nested/deeper/file.txt", "old value\n");
    commit_all(repo, "base");

    write_file(repo, "nested/deeper/file.txt", "new value\n");
    configure_gitcomet_difftool(repo);

    let subdir = repo.join("nested/deeper");
    let output = run_git_capture_in(&subdir, &["difftool", "--no-prompt", "--", "file.txt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed from subdirectory\n{text}"
    );
    assert!(
        text.contains("-old value") && text.contains("+new value"),
        "missing expected delta for subdirectory invocation\n{text}"
    );
}

#[test]
fn git_difftool_dir_diff_mode_works() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "tracked.txt", "one\n");
    commit_all(repo, "base");

    write_file(repo, "tracked.txt", "two\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--dir-diff", "--no-prompt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool --dir-diff failed\n{text}"
    );
    assert!(
        text.contains("tracked.txt"),
        "expected tracked filename in dir-diff output\n{text}"
    );
}

#[test]
fn git_difftool_dir_diff_handles_spaced_unicode_path() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    let tricky_path = "nested/spaced \u{65e5}\u{672c}\u{8a9e} file.txt";
    write_file(repo, tricky_path, "before line\n");
    commit_all(repo, "base");

    write_file(repo, tricky_path, "after line\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--dir-diff", "--no-prompt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool --dir-diff failed for spaced/unicode path\n{text}"
    );
    assert!(
        text.contains("spaced") && text.contains("file.txt"),
        "expected spaced/unicode filename in dir-diff output\n{text}"
    );
    assert!(
        text.contains("-before line") && text.contains("+after line"),
        "missing expected line delta in dir-diff output\n{text}"
    );
}

#[test]
fn git_difftool_dir_diff_mode_works_from_subdirectory() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "nested/deeper/tracked.txt", "tracked old\n");
    write_file(repo, "docs/selected.txt", "selected old\n");
    commit_all(repo, "base");

    write_file(repo, "nested/deeper/tracked.txt", "tracked new\n");
    write_file(repo, "docs/selected.txt", "selected new\n");
    configure_gitcomet_difftool(repo);

    let subdir = repo.join("nested/deeper");
    let output = run_git_capture_in(&subdir, &["difftool", "--dir-diff", "--no-prompt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool --dir-diff failed from subdirectory\n{text}"
    );
    assert!(
        text.contains("tracked.txt"),
        "expected nested tracked path in dir-diff output\n{text}"
    );
    assert!(
        text.contains("-tracked old") && text.contains("+tracked new"),
        "missing expected tracked-file delta in dir-diff output\n{text}"
    );
}

#[test]
fn git_difftool_dir_diff_pathspec_from_subdirectory_limits_to_selected_path() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "nested/deeper/tracked.txt", "tracked old\n");
    write_file(repo, "docs/selected.txt", "selected old\n");
    commit_all(repo, "base");

    write_file(repo, "nested/deeper/tracked.txt", "tracked new\n");
    write_file(repo, "docs/selected.txt", "selected new\n");
    configure_gitcomet_difftool(repo);

    let subdir = repo.join("nested/deeper");
    let output = run_git_capture_in(
        &subdir,
        &[
            "difftool",
            "--dir-diff",
            "--no-prompt",
            "--",
            "../../docs/selected.txt",
        ],
    );
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool --dir-diff pathspec failed from subdirectory\n{text}"
    );
    assert!(
        text.contains("selected.txt"),
        "expected selected path in dir-diff pathspec output\n{text}"
    );
    assert!(
        text.contains("-selected old") && text.contains("+selected new"),
        "missing expected selected-path delta in dir-diff pathspec output\n{text}"
    );
    assert!(
        !text.contains("tracked.txt"),
        "did not expect unselected tracked path in dir-diff pathspec output\n{text}"
    );
}

#[test]
fn git_difftool_pathspec_limits_invocation_to_selected_path() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "docs/selected.txt", "selected old\n");
    write_file(repo, "docs/other.txt", "other old\n");
    commit_all(repo, "base");

    write_file(repo, "docs/selected.txt", "selected new\n");
    write_file(repo, "docs/other.txt", "other new\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(
        repo,
        &["difftool", "--no-prompt", "--", "docs/selected.txt"],
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("selected.txt"),
        "expected selected path to be diffed\n{text}"
    );
    assert!(
        text.contains("-selected old") && text.contains("+selected new"),
        "expected selected path line delta in output\n{text}"
    );
    assert!(
        !text.contains("other.txt") && !text.contains("other old") && !text.contains("other new"),
        "expected pathspec to exclude non-selected path\n{text}"
    );
}

#[test]
fn git_difftool_handles_binary_content_change() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_bytes(repo, "blob.bin", &[0x00, 0x01, 0x02, 0x03, 0x04]);
    commit_all(repo, "base");

    write_bytes(repo, "blob.bin", &[0x00, 0x01, 0xFF, 0x03, 0x04]);
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "blob.bin"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed for binary content\n{text}"
    );
    assert!(
        text.contains("Binary files")
            || text.contains("GIT binary patch")
            || text.contains("blob.bin"),
        "expected binary diff signal in output\n{text}"
    );
}

#[test]
fn git_difftool_handles_non_utf8_content_change() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_bytes(repo, "data/non_utf8.dat", b"prefix\n\xFF\n");
    commit_all(repo, "base");

    write_bytes(repo, "data/non_utf8.dat", b"prefix\n\xFE\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(
        repo,
        &["difftool", "--no-prompt", "--", "data/non_utf8.dat"],
    );
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed for non-UTF8 content\n{text}"
    );
    assert!(
        !text.trim().is_empty(),
        "expected diff output for non-UTF8 content"
    );
}

// ── CRLF preservation parity ────────────────────────────────────────

#[test]
fn git_difftool_crlf_content_preserved() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    // Disable autocrlf to ensure CRLF bytes are stored as-is.
    run_git(repo, &["config", "core.autocrlf", "false"]);
    write_bytes(repo, "crlf.txt", b"line1\r\nline2\r\nline3\r\n");
    commit_all(repo, "base with CRLF");

    write_bytes(repo, "crlf.txt", b"line1\r\nmodified\r\nline3\r\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "crlf.txt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed for CRLF content\n{text}"
    );
    // Verify the diff shows the changed line.
    assert!(
        text.contains("-line2") || text.contains("-line2\r"),
        "expected removed CRLF line in diff output\n{text}"
    );
    assert!(
        text.contains("+modified") || text.contains("+modified\r"),
        "expected added CRLF line in diff output\n{text}"
    );
}

#[test]
fn git_difftool_crlf_to_lf_line_ending_change_detected() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    run_git(repo, &["config", "core.autocrlf", "false"]);
    write_bytes(repo, "endings.txt", b"aaa\r\nbbb\r\nccc\r\n");
    commit_all(repo, "base CRLF");

    // Change line endings from CRLF to LF.
    write_bytes(repo, "endings.txt", b"aaa\nbbb\nccc\n");
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "endings.txt"]);
    let text = output_text(&output);
    // The key contract: tool exits successfully regardless of line-ending changes.
    assert!(
        output.status.success(),
        "git difftool failed for CRLF-to-LF line ending change\n{text}"
    );
}

#[test]
fn git_difftool_gui_default_auto_prefers_gui_tool_when_display_set() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_command(repo, "gui", &gitcomet_difftool_cmd("gui", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", Some("gui"), Some("auto"));

    let output = run_git_capture_with_display(
        repo,
        &["difftool", "--no-prompt", "--", "a.txt"],
        Some(":99"),
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=gui"),
        "expected gui tool selection with DISPLAY set\n{text}"
    );
}

#[test]
fn git_difftool_gui_default_auto_prefers_cli_tool_without_display() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_command(repo, "gui", &gitcomet_difftool_cmd("gui", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", Some("gui"), Some("Auto"));

    let output =
        run_git_capture_with_display(repo, &["difftool", "--no-prompt", "--", "a.txt"], None);
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=cli"),
        "expected cli tool selection without DISPLAY\n{text}"
    );
}

#[test]
fn git_difftool_gui_default_true_prefers_gui_tool_without_display() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_command(repo, "gui", &gitcomet_difftool_cmd("gui", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", Some("gui"), Some("true"));

    let output =
        run_git_capture_with_display(repo, &["difftool", "--no-prompt", "--", "a.txt"], None);
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=gui"),
        "expected gui tool selection when guiDefault=true\n{text}"
    );
}

#[test]
fn git_difftool_gui_default_false_prefers_cli_tool_with_display() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_command(repo, "gui", &gitcomet_difftool_cmd("gui", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", Some("gui"), Some("false"));

    let output = run_git_capture_with_display(
        repo,
        &["difftool", "--no-prompt", "--", "a.txt"],
        Some(":99"),
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=cli"),
        "expected regular tool selection when guiDefault=false\n{text}"
    );
}

#[test]
fn git_difftool_gui_flag_overrides_selection() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_command(repo, "gui", &gitcomet_difftool_cmd("gui", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", Some("gui"), Some("false"));

    let output = run_git_capture_with_display(
        repo,
        &["difftool", "--gui", "--no-prompt", "--", "a.txt"],
        None,
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=gui"),
        "expected --gui to force gui tool selection\n{text}"
    );
}

#[test]
fn git_difftool_no_gui_flag_overrides_gui_default_true() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_command(repo, "gui", &gitcomet_difftool_cmd("gui", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", Some("gui"), Some("true"));

    let output = run_git_capture_with_display(
        repo,
        &["difftool", "--no-gui", "--no-prompt", "--", "a.txt"],
        Some(":99"),
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=cli"),
        "expected --no-gui to force regular tool selection\n{text}"
    );
}

#[test]
fn git_difftool_gui_fallback_when_no_guitool_configured() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    // When --gui is requested but only diff.tool is configured, git difftool
    // should fall back to the regular tool selection.
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", None, Some("false"));

    let output = run_git_capture_with_display(
        repo,
        &["difftool", "--gui", "--no-prompt", "--", "a.txt"],
        Some(":99"),
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=cli"),
        "expected fallback to diff.tool when no diff.guitool configured\n{text}"
    );
}

#[test]
fn git_difftool_gui_default_true_fallback_when_no_guitool_configured() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    // Even with guiDefault=true, git difftool should fall back to diff.tool
    // if no diff.guitool is configured.
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "cli", &gitcomet_difftool_cmd("cli", None));
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "cli", None, Some("true"));

    let output = run_git_capture_with_display(
        repo,
        &["difftool", "--no-prompt", "--", "a.txt"],
        Some(":99"),
    );
    let text = output_text(&output);
    assert!(output.status.success(), "git difftool failed\n{text}");
    assert!(
        text.contains("TOOL=cli"),
        "expected fallback to diff.tool with guiDefault=true and no diff.guitool\n{text}"
    );
}

#[test]
fn git_difftool_honors_tool_trust_exit_code_false() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "failer", "echo TOOL=failer >&2; exit 7");
    configure_difftool_trust_exit_code(repo, false);
    configure_difftool_selection(repo, "failer", None, None);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "a.txt"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "expected trustExitCode=false to ignore tool failure\n{text}"
    );
}

#[test]
fn git_difftool_honors_tool_trust_exit_code_true() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "failer", "echo TOOL=failer >&2; exit 7");
    configure_difftool_trust_exit_code(repo, true);
    configure_difftool_selection(repo, "failer", None, None);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "a.txt"]);
    let text = output_text(&output);
    assert!(
        !output.status.success(),
        "expected trustExitCode=true to propagate tool failure\n{text}"
    );
}

#[test]
fn git_difftool_trust_exit_code_flag_overrides_config() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");
    write_file(repo, "a.txt", "after\n");

    configure_difftool_command(repo, "failer", "echo TOOL=failer >&2; exit 7");
    configure_difftool_trust_exit_code(repo, false);
    configure_difftool_selection(repo, "failer", None, None);

    let forced_trust = run_git_capture(
        repo,
        &[
            "difftool",
            "--no-prompt",
            "--trust-exit-code",
            "--",
            "a.txt",
        ],
    );
    let forced_trust_text = output_text(&forced_trust);
    assert!(
        !forced_trust.status.success(),
        "expected --trust-exit-code to force failure propagation\n{forced_trust_text}"
    );

    configure_difftool_trust_exit_code(repo, true);
    let forced_no_trust = run_git_capture(
        repo,
        &[
            "difftool",
            "--no-prompt",
            "--no-trust-exit-code",
            "--",
            "a.txt",
        ],
    );
    let forced_no_trust_text = output_text(&forced_no_trust);
    assert!(
        forced_no_trust.status.success(),
        "expected --no-trust-exit-code to ignore failure\n{forced_no_trust_text}"
    );
}

#[test]
fn git_difftool_shows_submodule_gitlink_change() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    // When only a submodule gitlink changes, git difftool passes temporary
    // files containing "Subproject commit <sha>" lines to the external tool.
    // Verify that GitComet surfaces both old and new commit pointers.
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path().join("repo");
    let sub_repo = tmp.path().join("subrepo");
    fs::create_dir_all(&repo).expect("create main repo directory");
    fs::create_dir_all(&sub_repo).expect("create submodule repo directory");

    init_repo(&repo);
    init_repo(&sub_repo);

    write_file(&sub_repo, "sub.txt", "submodule v1\n");
    commit_all(&sub_repo, "submodule: v1");
    let old_commit = git_head(&sub_repo);

    let sub_url = sub_repo.to_string_lossy().to_string();
    run_git(
        &repo,
        &[
            "-c",
            "protocol.file.allow=always",
            "submodule",
            "add",
            &sub_url,
            "submod",
        ],
    );
    commit_all(&repo, "add submodule");

    write_file(&sub_repo, "sub.txt", "submodule v2\n");
    commit_all(&sub_repo, "submodule: v2");
    let new_commit = git_head(&sub_repo);

    run_git(&repo.join("submod"), &["fetch"]);
    run_git(&repo.join("submod"), &["checkout", &new_commit]);

    configure_gitcomet_difftool(&repo);

    let output = run_git_capture(&repo, &["difftool", "--no-prompt", "--", "submod"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool failed for submodule gitlink change\n{text}"
    );
    assert!(
        text.contains("Subproject commit"),
        "expected submodule gitlink content in difftool output\n{text}"
    );
    assert!(
        text.contains(&old_commit),
        "expected old submodule commit in difftool output\n{text}"
    );
    assert!(
        text.contains(&new_commit),
        "expected new submodule commit in difftool output\n{text}"
    );
}

// ── Symlink diff ─────────────────────────────────────────────────────

#[cfg(unix)]
#[test]
fn git_difftool_shows_symlink_target_change() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    // When a symlink target changes, git difftool shows the diff of
    // the symlink targets (short text strings).
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    std::os::unix::fs::symlink("original_target", repo.join("link")).expect("create symlink");
    commit_all(repo, "base: add symlink");

    // Change the symlink target.
    fs::remove_file(repo.join("link")).unwrap();
    std::os::unix::fs::symlink("new_target", repo.join("link")).expect("create symlink");

    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--no-prompt", "--", "link"]);
    let text = output_text(&output);

    // Git shows symlink targets as file content to the difftool.
    // Our tool should produce a diff between "original_target" and "new_target".
    assert!(
        output.status.success(),
        "git difftool failed for symlink\n{text}"
    );
    assert!(
        text.contains("original_target") || text.contains("new_target"),
        "expected symlink target content in difftool output\n{text}"
    );
}

#[test]
fn git_difftool_tool_help_lists_gitcomet_tool() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    configure_gitcomet_difftool(repo);

    let output = run_git_capture(repo, &["difftool", "--tool-help"]);
    let text = output_text(&output);
    assert!(
        output.status.success(),
        "git difftool --tool-help failed\n{text}"
    );
    assert!(
        text.contains("gitcomet"),
        "expected gitcomet tool name in --tool-help output\n{text}"
    );
}

#[test]
fn git_difftool_absent_tool_reports_cmd_not_set_error() {
    if !require_git_shell_for_tool_tests() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path();

    init_repo(repo);
    write_file(repo, "a.txt", "before\n");
    commit_all(repo, "base");

    write_file(repo, "a.txt", "after\n");
    run_git(repo, &["config", "difftool.prompt", "false"]);

    let output = run_git_capture(
        repo,
        &["difftool", "--no-prompt", "--tool", "absent", "--", "a.txt"],
    );
    let text = output_text(&output);
    let text_lower = text.to_ascii_lowercase();
    assert!(
        !output.status.success(),
        "expected git difftool --tool absent to fail\n{text}"
    );
    assert!(
        text_lower.contains("absent")
            && (text_lower.contains("cmd not set")
                || text_lower.contains("unknown merge tool")
                || text_lower.contains("unknown diff tool")
                || text_lower.contains("unknown difftool")
                || text_lower.contains("no known")
                || text_lower.contains("not available")),
        "expected absent-tool configuration error text\n{text}"
    );
}
