use assert_cmd::cargo::cargo_bin_cmd;
use serde_json::Value;
use std::path::PathBuf;

fn fixture_path(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name)
}

/// Strip ANSI escape codes from output
fn strip_ansi(s: &str) -> String {
    let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap();
    re.replace_all(s, "").to_string()
}

fn run_loq(fixture: &str) -> (String, String, bool) {
    let output = cargo_bin_cmd!("loq")
        .current_dir(fixture_path(fixture))
        .output()
        .unwrap();

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

fn run_loq_with_args(fixture: &str, args: &[&str]) -> (String, String, bool) {
    let output = cargo_bin_cmd!("loq")
        .current_dir(fixture_path(fixture))
        .args(args)
        .output()
        .unwrap();

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

fn run_loq_with_stdin(fixture: &str, args: &[&str], stdin: &str) -> (String, String, bool) {
    let output = cargo_bin_cmd!("loq")
        .current_dir(fixture_path(fixture))
        .args(args)
        .write_stdin(stdin)
        .output()
        .unwrap();

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

#[test]
fn empty_directory() {
    let (stdout, stderr, success) = run_loq("empty_directory");

    assert!(success, "should succeed with no files");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn all_ok() {
    let (stdout, stderr, success) = run_loq("all_ok");

    assert!(success, "should succeed when all files are ok");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn one_violation() {
    let (stdout, stderr, success) = run_loq("one_violation");

    assert!(!success, "should fail with violation");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn pass_and_fail() {
    let (stdout, stderr, success) = run_loq("pass_and_fail");

    assert!(!success, "should fail when there's an error");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn no_config_uses_defaults() {
    let (stdout, stderr, success) = run_loq("no_config");

    assert!(success, "should succeed with defaults");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn multiple_rules() {
    let (stdout, stderr, success) = run_loq("multiple_rules");

    assert!(!success, "should fail when rule is violated");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn baselined_file_passes() {
    let (stdout, stderr, success) = run_loq("baselined");

    assert!(success, "baselined file should pass");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn nested_directories() {
    let (stdout, stderr, success) = run_loq("nested");

    assert!(!success, "should find violation in nested dir");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn gitignore_respected() {
    let (stdout, stderr, success) = run_loq("gitignore");

    assert!(success, "ignored files should not cause failure");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn binary_file_skipped() {
    let (stdout, stderr, success) = run_loq("binary_file");

    assert!(success, "binary files should be skipped");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn missing_file_explicit_arg() {
    let (stdout, stderr, success) = run_loq_with_args("all_ok", &["check", "nonexistent.rs"]);

    assert!(success, "missing file is a warning, not error");
    insta::assert_snapshot!("missing_file_stdout", stdout);
    insta::assert_snapshot!("missing_file_stderr", stderr);
}

#[test]
fn stdin_file_list() {
    let (stdout, stderr, success) = run_loq_with_stdin("stdin", &["check", "-"], "a.rs\nb.rs\n");

    assert!(success, "stdin files should be checked");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

#[test]
fn fix_guidance_shown_on_violation() {
    let (stdout, stderr, success) = run_loq("fix_guidance");

    assert!(!success, "should fail with violation");
    assert!(stderr.is_empty(), "stderr should be empty");
    insta::assert_snapshot!(stdout);
}

// JSON output tests

fn run_loq_json(fixture: &str) -> (Value, bool) {
    let output = cargo_bin_cmd!("loq")
        .current_dir(fixture_path(fixture))
        .args(["check", "--output-format", "json"])
        .output()
        .unwrap();

    let stdout = String::from_utf8_lossy(&output.stdout);
    let json: Value = serde_json::from_str(&stdout).expect("should be valid JSON");
    (json, output.status.success())
}

#[test]
fn json_output_empty_directory() {
    let (json, success) = run_loq_json("empty_directory");
    assert!(success, "should succeed with no files");
    insta::assert_json_snapshot!(json, {
        ".version" => "[version]",
    });
}

#[test]
fn json_output_all_ok() {
    let (json, success) = run_loq_json("all_ok");
    assert!(success, "should succeed when all files are ok");
    insta::assert_json_snapshot!(json, {
        ".version" => "[version]",
    });
}

#[test]
fn json_output_one_violation() {
    let (json, success) = run_loq_json("one_violation");
    assert!(!success, "should fail with violation");
    insta::assert_json_snapshot!(json, {
        ".version" => "[version]",
    });
}

#[test]
fn json_output_pass_and_fail() {
    let (json, success) = run_loq_json("pass_and_fail");
    assert!(!success, "should fail when there's an error");
    insta::assert_json_snapshot!(json, {
        ".version" => "[version]",
    });
}

#[test]
fn json_output_multiple_rules() {
    let (json, success) = run_loq_json("multiple_rules");
    assert!(!success, "should fail when rule is violated");
    insta::assert_json_snapshot!(json, {
        ".version" => "[version]",
    });
}
