use super::*;
use crate::cli::OutputMode;
use crate::loc::language::detect;
use crate::walk::{self, ExcludeFilter, WalkConfig};
use std::fs;
use std::path::Path;

#[test]
fn run_on_empty_dir() {
    let dir = tempfile::tempdir().unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_with_no_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(
        dir.path().join("a.rs"),
        "fn foo() {\n    let x = 1;\n    let y = 2;\n    let z = x + y;\n    println!(\"{}\", z);\n    return z;\n}\n",
    ).unwrap();
    fs::write(
        dir.path().join("b.rs"),
        "fn bar() {\n    let a = 10;\n    let b = 20;\n    let c = a * b;\n    println!(\"{}\", c);\n    return c;\n}\n",
    ).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_detects_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";
    fs::write(dir.path().join("a.rs"), code).unwrap();
    fs::write(dir.path().join("b.rs"), code).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    // Should not panic, should detect duplicates
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_with_report_flag() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";
    fs::write(dir.path().join("a.rs"), code).unwrap();
    fs::write(dir.path().join("b.rs"), code).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, true, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_with_show_all_flag() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";
    fs::write(dir.path().join("a.rs"), code).unwrap();
    fs::write(dir.path().join("b.rs"), code).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, true, true, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_skips_binary_files() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("data.c"), b"hello\x00world").unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_with_high_min_lines() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn f() {\n    let x = 1;\n    let y = 2;\n}\n";
    fs::write(dir.path().join("a.rs"), code).unwrap();
    fs::write(dir.path().join("b.rs"), code).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    // min_lines=20 means no 4-line file can produce duplicates
    run(&cfg, 20, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn normalize_file_skips_comments_and_blanks() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("test.rs");
    fs::write(
        &path,
        "// comment\n\nfn main() {\n    // another comment\n    let x = 1;\n}\n",
    )
    .unwrap();

    let spec = detect(Path::new("test.rs")).unwrap();
    let nf = normalize_file(&path, spec, false).unwrap().unwrap();

    // Should only have code lines: "fn main() {", "let x = 1;", "}"
    assert_eq!(nf.lines.len(), 3);
    assert_eq!(nf.lines[0].content, "fn main() {");
    assert_eq!(nf.lines[1].content, "let x = 1;");
    assert_eq!(nf.lines[2].content, "}");
}

#[test]
fn normalize_file_binary_returns_none() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("data.c");
    fs::write(&path, b"hello\x00world").unwrap();

    let spec = detect(Path::new("test.c")).unwrap();
    assert!(normalize_file(&path, spec, false).unwrap().is_none());
}

#[test]
fn run_json_with_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";
    fs::write(dir.path().join("a.rs"), code).unwrap();
    fs::write(dir.path().join("b.rs"), code).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Json, gate()).unwrap();
}

#[test]
fn run_json_empty_dir() {
    let dir = tempfile::tempdir().unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Json, gate()).unwrap();
}

#[test]
fn run_json_no_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), "fn foo() {\n    let x = 1;\n}\n").unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Json, gate()).unwrap();
}

// --- is_test_file tests (use shared walk module) ---

#[test]
fn test_file_rust() {
    assert!(walk::is_test_file(Path::new("parser_test.rs")));
    assert!(!walk::is_test_file(Path::new("parser.rs")));
    assert!(!walk::is_test_file(Path::new("test.rs"))); // no _test suffix
}

#[test]
fn test_file_python() {
    assert!(walk::is_test_file(Path::new("test_parser.py")));
    assert!(walk::is_test_file(Path::new("parser_test.py")));
    assert!(!walk::is_test_file(Path::new("parser.py")));
}

#[test]
fn test_file_javascript() {
    assert!(walk::is_test_file(Path::new("parser.test.js")));
    assert!(walk::is_test_file(Path::new("parser.spec.js")));
    assert!(walk::is_test_file(Path::new("parser.test.tsx")));
    assert!(walk::is_test_file(Path::new("parser.spec.ts")));
    assert!(!walk::is_test_file(Path::new("parser.js")));
}

#[test]
fn test_file_java_kotlin() {
    assert!(walk::is_test_file(Path::new("ParserTest.java")));
    assert!(walk::is_test_file(Path::new("ParserTests.java")));
    assert!(!walk::is_test_file(Path::new("Parser.java")));
    assert!(walk::is_test_file(Path::new("ParserTest.kt")));
}

#[test]
fn test_file_go() {
    assert!(walk::is_test_file(Path::new("parser_test.go")));
    assert!(!walk::is_test_file(Path::new("parser.go")));
}

#[test]
fn test_file_csharp() {
    assert!(walk::is_test_file(Path::new("ParserTest.cs")));
    assert!(walk::is_test_file(Path::new("ParserTests.cs")));
    assert!(!walk::is_test_file(Path::new("Parser.cs")));
}

#[test]
fn test_file_ruby() {
    assert!(walk::is_test_file(Path::new("parser_spec.rb")));
    assert!(walk::is_test_file(Path::new("parser_test.rb")));
    assert!(!walk::is_test_file(Path::new("parser.rb")));
}

#[test]
fn test_file_cpp() {
    assert!(walk::is_test_file(Path::new("parser_test.cpp")));
    assert!(walk::is_test_file(Path::new("test_parser.cpp")));
    assert!(walk::is_test_file(Path::new("parser_unittest.cpp")));
    assert!(walk::is_test_file(Path::new("ParserTest.cpp")));
    assert!(!walk::is_test_file(Path::new("parser.cpp")));
}

#[test]
fn test_file_c() {
    assert!(walk::is_test_file(Path::new("parser_test.c")));
    assert!(walk::is_test_file(Path::new("test_parser.c")));
    assert!(walk::is_test_file(Path::new("parser_unittest.c")));
    assert!(!walk::is_test_file(Path::new("parser.c")));
}

#[test]
fn test_file_other_languages() {
    assert!(walk::is_test_file(Path::new("parser_test.exs")));
    assert!(walk::is_test_file(Path::new("parser_test.dart")));
    assert!(walk::is_test_file(Path::new("ParserTest.swift")));
    assert!(walk::is_test_file(Path::new("ParserSpec.scala")));
    assert!(walk::is_test_file(Path::new("ParserSpec.hs")));
    assert!(walk::is_test_file(Path::new("ParserTest.php")));
}

#[test]
fn test_file_no_extension() {
    assert!(!walk::is_test_file(Path::new("Makefile")));
    assert!(!walk::is_test_file(Path::new("README")));
}

#[test]
fn test_file_unknown_extension() {
    assert!(!walk::is_test_file(Path::new("test_foo.xyz")));
}

// --- exclude_tests integration tests ---

#[test]
fn run_exclude_tests_skips_test_dir() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";

    // Only duplicates are inside tests/
    fs::create_dir(dir.path().join("tests")).unwrap();
    fs::write(dir.path().join("tests/a.rs"), code).unwrap();
    fs::write(dir.path().join("tests/b.rs"), code).unwrap();
    fs::write(dir.path().join("lib.rs"), "fn foo() {\n    let x = 1;\n}\n").unwrap();

    // Without exclude: detects duplicates (does not panic)
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), true, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
    // With exclude: tests/ is skipped entirely
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_exclude_tests_skips_test_files() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";

    // Duplicate in test-named files
    fs::write(dir.path().join("parser_test.rs"), code).unwrap();
    fs::write(dir.path().join("handler_test.rs"), code).unwrap();
    fs::write(dir.path().join("lib.rs"), "fn foo() {\n    let x = 1;\n}\n").unwrap();

    // With exclude_tests, the *_test.rs files are skipped
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_exclude_tests_skips_test_file_in_subdirectory() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";

    // Test file nested in a non-test directory
    fs::create_dir_all(dir.path().join("src/utils")).unwrap();
    fs::write(dir.path().join("src/utils/parser_test.rs"), code).unwrap();
    fs::write(dir.path().join("src/utils/handler_test.rs"), code).unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "fn foo() {\n    let x = 1;\n}\n",
    )
    .unwrap();

    // With exclude_tests, *_test.rs files in any directory are skipped
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn run_exclude_tests_skips_entire_test_dir_tree() {
    let dir = tempfile::tempdir().unwrap();
    let code = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";

    // Files inside tests/ with no test suffix — excluded by directory filter
    fs::create_dir_all(dir.path().join("tests/helpers")).unwrap();
    fs::write(dir.path().join("tests/integration.rs"), code).unwrap();
    fs::write(dir.path().join("tests/helpers/utils.rs"), code).unwrap();
    fs::write(dir.path().join("lib.rs"), "fn foo() {\n    let x = 1;\n}\n").unwrap();

    // With exclude_tests, the entire tests/ tree is skipped
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

// --- quality gate helpers ---

const DUP_CODE: &str = "fn process() {\n    let x = read();\n    let y = transform(x);\n    write(y);\n    log(\"done\");\n    cleanup();\n}\n";

fn gate() -> DupsGate {
    DupsGate::default()
}

fn gate_max_dups(n: usize) -> DupsGate {
    DupsGate {
        max_duplicates: Some(n),
        ..Default::default()
    }
}

fn gate_max_ratio(pct: f64) -> DupsGate {
    DupsGate {
        max_dup_ratio: Some(pct),
        ..Default::default()
    }
}

fn gate_fail_on_increase(git_ref: &str) -> DupsGate {
    DupsGate {
        fail_on_increase: Some(git_ref.to_owned()),
        ..Default::default()
    }
}

// --- --max-duplicates quality gate tests ---

#[test]
fn max_duplicates_none_does_not_fail_with_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn max_duplicates_zero_passes_when_no_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(
        dir.path().join("a.rs"),
        "fn foo() {\n    let x = 1;\n    let y = 2;\n    let z = x + y;\n    println!(\"{}\", z);\n    return z;\n}\n",
    )
    .unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate_max_dups(0)).unwrap();
}

#[test]
fn max_duplicates_zero_fails_when_duplicates_found() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let err = run(&cfg, 6, false, false, OutputMode::Table, gate_max_dups(0)).unwrap_err();
    assert!(
        err.to_string().contains("quality gate failed"),
        "expected quality gate error, got: {err}"
    );
}

#[test]
fn max_duplicates_limit_passes_when_within_limit() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    // 1 duplicate group found, limit is 5 — should pass
    run(&cfg, 6, false, false, OutputMode::Table, gate_max_dups(5)).unwrap();
}

#[test]
fn max_duplicates_error_message_contains_counts() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let err = run(&cfg, 6, false, false, OutputMode::Table, gate_max_dups(0)).unwrap_err();
    let msg = err.to_string();
    assert!(
        msg.contains("limit: 0"),
        "expected limit in message, got: {msg}"
    );
    assert!(
        msg.contains("duplicate groups"),
        "expected group count in message, got: {msg}"
    );
}

// --- --max-dup-ratio quality gate tests ---

#[test]
fn max_dup_ratio_none_does_not_fail() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn max_dup_ratio_passes_when_no_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(
        dir.path().join("a.rs"),
        "fn foo() {\n    let x = 1;\n    let y = 2;\n    let z = x + y;\n    println!(\"{}\", z);\n    return z;\n}\n",
    )
    .unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_max_ratio(0.0),
    )
    .unwrap();
}

#[test]
fn max_dup_ratio_fails_when_ratio_exceeded() {
    let dir = tempfile::tempdir().unwrap();
    // Both files are 100% duplicate — ratio will be near 100%
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let err = run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_max_ratio(1.0),
    )
    .unwrap_err();
    assert!(
        err.to_string().contains("quality gate failed"),
        "expected quality gate error, got: {err}"
    );
}

#[test]
fn max_dup_ratio_passes_when_within_limit() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    // 100% ratio but limit is 100.0 — should pass
    run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_max_ratio(100.0),
    )
    .unwrap();
}

#[test]
fn max_dup_ratio_error_message_shows_percentages() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let err = run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_max_ratio(1.0),
    )
    .unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains('%'), "expected % in message, got: {msg}");
    assert!(
        msg.contains("limit of"),
        "expected limit in message, got: {msg}"
    );
}

#[test]
fn both_gates_checked_independently() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("b.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    // Group count passes (limit=100) but ratio fails (limit=1%)
    let g = DupsGate {
        max_duplicates: Some(100),
        max_dup_ratio: Some(1.0),
        ..Default::default()
    };
    let err = run(&cfg, 6, false, false, OutputMode::Table, g).unwrap_err();
    assert!(err.to_string().contains('%'), "ratio gate should trigger");
}

// --- --fail-on-increase quality gate tests ---

fn make_git_repo_with_files(files: &[(&str, &str)]) -> tempfile::TempDir {
    let dir = tempfile::tempdir().unwrap();
    let repo = git2::Repository::init(dir.path()).unwrap();
    let mut config = repo.config().unwrap();
    config.set_str("user.name", "Test").unwrap();
    config.set_str("user.email", "test@test.com").unwrap();

    let sig =
        git2::Signature::new("Test", "test@test.com", &git2::Time::new(1_700_000_000, 0)).unwrap();

    for (name, content) in files {
        fs::write(dir.path().join(name), content).unwrap();
    }

    let mut index = repo.index().unwrap();
    for (name, _) in files {
        index.add_path(Path::new(name)).unwrap();
    }
    index.write().unwrap();
    let tree_oid = index.write_tree().unwrap();
    let tree = repo.find_tree(tree_oid).unwrap();
    repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
        .unwrap();
    dir
}

#[test]
fn fail_on_increase_none_does_not_fail() {
    let dir = make_git_repo_with_files(&[("a.rs", DUP_CODE), ("b.rs", DUP_CODE)]);
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Table, gate()).unwrap();
}

#[test]
fn fail_on_increase_same_ref_passes() {
    // Comparing HEAD to HEAD — duplication cannot have increased.
    let dir = make_git_repo_with_files(&[("a.rs", DUP_CODE), ("b.rs", DUP_CODE)]);
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let result = run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_fail_on_increase("HEAD"),
    );
    assert!(
        result.is_ok(),
        "same ref should not trigger gate: {result:?}"
    );
}

#[test]
fn fail_on_increase_passes_when_duplication_decreased() {
    // HEAD has duplicates (high ratio); working tree adds a large block of unique
    // code so the ratio drops. Each function is fully distinct — no 6-line window
    // repeats, so the duplicate detector sees no new groups.
    let unique: String = (0..40)
        .map(|i| {
            format!(
                "fn unique_{i}(x{i}: i64, y{i}: i64) -> i64 {{\n    \
                 let a{i} = x{i} * {i} + y{i};\n    \
                 let b{i} = a{i} ^ (a{i} >> 3);\n    \
                 let c{i} = b{i}.wrapping_add({i} * 17);\n    \
                 let d{i} = c{i}.wrapping_mul(b{i} | 0x{i:04X});\n    \
                 d{i}.wrapping_sub(a{i})\n}}\n"
            )
        })
        .collect();
    let dir = make_git_repo_with_files(&[("a.rs", DUP_CODE), ("b.rs", DUP_CODE)]);
    fs::write(dir.path().join("extra.rs"), &unique).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let result = run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_fail_on_increase("HEAD"),
    );
    assert!(
        result.is_ok(),
        "decreased duplication should not trigger gate: {result:?}"
    );
}

#[test]
fn fail_on_increase_fails_when_duplication_increased() {
    // HEAD has zero duplication; working tree adds duplicates.
    let unique_a = "fn foo() {\n    let x = 1;\n    let y = 2;\n    let z = 3;\n    let w = 4;\n    let v = 5;\n}\n";
    let unique_b = "fn bar() {\n    let a = 10;\n    let b = 20;\n    let c = 30;\n    let d = 40;\n    let e = 50;\n}\n";
    let dir = make_git_repo_with_files(&[("a.rs", unique_a), ("b.rs", unique_b)]);
    // Add duplicate code to working tree (not committed)
    fs::write(dir.path().join("dup1.rs"), DUP_CODE).unwrap();
    fs::write(dir.path().join("dup2.rs"), DUP_CODE).unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    let result = run(
        &cfg,
        6,
        false,
        false,
        OutputMode::Table,
        gate_fail_on_increase("HEAD"),
    );
    assert!(result.is_err(), "increased duplication should trigger gate");
    let msg = result.unwrap_err().to_string();
    assert!(
        msg.contains("quality gate failed") && msg.contains("duplication increased"),
        "unexpected error message: {msg}"
    );
}

#[test]
fn run_short_format() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), "fn foo() { 1 }\n").unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Short, gate()).unwrap();
}

#[test]
fn run_terse_format() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("a.rs"), "fn foo() { 1 }\n").unwrap();
    let filter = ExcludeFilter::default();
    let cfg = WalkConfig::new(dir.path(), false, &filter);
    run(&cfg, 6, false, false, OutputMode::Terse, gate()).unwrap();
}
