//! CLI integration tests for hanuke: exit codes and I/O of the real binary.

use std::io::Write;

use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::NamedTempFile;

fn csv_file(content: &str) -> NamedTempFile {
    csv_file_bytes(content.as_bytes())
}

fn csv_file_bytes(content: &[u8]) -> NamedTempFile {
    let mut f = NamedTempFile::new().unwrap();
    f.write_all(content).unwrap();
    f.flush().unwrap();
    f
}

fn hanuke() -> Command {
    Command::cargo_bin("hanuke").unwrap()
}

#[test]
fn version_flag_prints_name_and_version() {
    hanuke()
        .arg("--version")
        .assert()
        .success()
        .stdout(predicate::str::starts_with("hanuke "));
}

#[test]
fn version_subcommand_prints_name_and_version() {
    hanuke()
        .arg("version")
        .assert()
        .success()
        .stdout(predicate::str::starts_with("hanuke "));
}

#[test]
fn version_subcommand_commit_includes_hash() {
    hanuke()
        .args(["version", "--commit"])
        .assert()
        .success()
        .stdout(predicate::str::starts_with("hanuke ").and(predicate::str::contains("(")));
}

#[test]
fn ok_sequence_exits_zero_and_silent() {
    let f = csv_file("0,foo\n1,bar\n2,baz\n");
    hanuke()
        .arg(f.path())
        .assert()
        .success()
        .stdout("")
        .stderr("");
}

#[test]
fn gap_exits_one_with_stderr_hint() {
    let f = csv_file("0\n1\n5\n");
    hanuke()
        .arg(f.path())
        .assert()
        .code(1)
        .stdout("")
        .stderr(predicate::str::contains("NG"));
}

#[test]
fn missing_file_exits_two() {
    hanuke()
        .arg("no_such_file.csv")
        .assert()
        .code(2)
        .stderr(predicate::str::contains("no_such_file.csv"));
}

#[test]
fn unparsable_cell_exits_two_with_row_info() {
    let f = csv_file("0\n1\nxyz\n");
    hanuke()
        .arg(f.path())
        .assert()
        .code(2)
        .stderr(predicate::str::contains("row 3"));
}

#[test]
fn header_row_is_skipped() {
    let f = csv_file("id,name\n0,foo\n1,bar\n");
    hanuke().arg("--header").arg(f.path()).assert().success();
}

#[test]
fn issue_row_numbers_count_the_header_line() {
    let f = csv_file("id\n0\n2\n");
    hanuke()
        .arg("--header")
        .arg("--verbose")
        .arg(f.path())
        .assert()
        .code(1)
        .stdout(predicate::str::contains("row 3"));
}

#[test]
fn column_selection_is_one_based() {
    let f = csv_file("foo,10\nbar,11\nbaz,12\n");
    hanuke()
        .args(["--col", "2"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn hex_single_column_with_0x_prefix() {
    let f = csv_file("0x0009\n0x000a\n0x000B\n");
    hanuke().arg("--hex").arg(f.path()).assert().success();
}

#[test]
fn hex_single_column_without_prefix() {
    let f = csv_file("fe\nff\n100\n");
    hanuke().arg("--hex").arg(f.path()).assert().success();
}

#[test]
fn multi_column_hex_big_endian() {
    let f = csv_file("00,FE\n00,FF\n01,00\n");
    hanuke()
        .args(["--hex", "--cols", "2", "--endian", "be"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn multi_column_hex_little_endian() {
    let f = csv_file("FE,00\nFF,00\n00,01\n");
    hanuke()
        .args(["--hex", "--cols", "2", "--endian", "le"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn multi_column_dec_bytes() {
    let f = csv_file("0,254\n0,255\n1,0\n");
    hanuke()
        .args(["--cols", "2"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn multi_column_starting_mid_row() {
    let f = csv_file("x,00,FE,y\nx,00,FF,y\nx,01,00,y\n");
    hanuke()
        .args(["--hex", "--col", "2", "--cols", "2"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn double_column_with_integer_values() {
    let f = csv_file("1.0\n2.0\n3.0\n");
    hanuke().arg(f.path()).assert().success();
}

#[test]
fn negative_step_checks_descending() {
    let f = csv_file("5\n4\n3\n");
    hanuke()
        .args(["--step", "-1"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn step_of_zero_is_rejected() {
    let f = csv_file("1\n1\n");
    hanuke()
        .args(["--step", "0"])
        .arg(f.path())
        .assert()
        .code(2);
}

#[test]
fn verbose_prints_summary() {
    let f = csv_file("0\n1\n5\n5\n");
    hanuke()
        .arg("--verbose")
        .arg(f.path())
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("start: 0")
                .and(predicate::str::contains("end: 5"))
                .and(predicate::str::contains("missing: 2..4"))
                .and(predicate::str::contains("duplicates: 5"))
                .and(predicate::str::contains("result: NG")),
        );
}

#[test]
fn verbose_ok_prints_ok_result() {
    let f = csv_file("0\n1\n2\n");
    hanuke()
        .arg("--verbose")
        .arg(f.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("result: OK"));
}

#[test]
fn show_missing_prints_only_ranges() {
    let f = csv_file("0\n3\n4\n8\n");
    hanuke()
        .args(["--show", "missing"])
        .arg(f.path())
        .assert()
        .code(1)
        .stdout("1..2\n5..7\n");
}

#[test]
fn show_duplicates_prints_only_values() {
    let f = csv_file("0\n0\n1\n");
    hanuke()
        .args(["--show", "duplicates"])
        .arg(f.path())
        .assert()
        .code(1)
        .stdout("0\n");
}

#[test]
fn show_start_end_labels_lines() {
    let f = csv_file("10\n11\n12\n");
    hanuke()
        .args(["--show", "start,end"])
        .arg(f.path())
        .assert()
        .success()
        .stdout("start: 10\nend: 12\n");
}

#[test]
fn reads_stdin_with_dash() {
    hanuke()
        .arg("-")
        .write_stdin("0\n1\n2\n")
        .assert()
        .success();
}

#[test]
fn too_few_columns_exits_two() {
    let f = csv_file("0,1\n1\n");
    hanuke()
        .args(["--col", "2"])
        .arg(f.path())
        .assert()
        .code(2)
        .stderr(predicate::str::contains("row 2"));
}

#[test]
fn crlf_line_endings_are_supported() {
    let f = csv_file("0\r\n1\r\n2\r\n");
    seq_ok(&f);
}

#[test]
fn double_quoted_cells_with_embedded_commas() {
    let f = csv_file("\"0\",\"a,b\"\n\"1\",\"c,d\"\n\"2\",\"e,f\"\n");
    seq_ok(&f);
}

#[test]
fn utf8_bom_is_ignored() {
    let f = csv_file_bytes(b"\xEF\xBB\xBF0\n1\n2\n");
    let output = hanuke()
        .args(["--show", "start"])
        .arg(f.path())
        .assert()
        .success();
    assert_eq!(String::from_utf8_lossy(&output.get_output().stdout), "0\n");
}

#[test]
fn non_utf8_bytes_in_other_columns_are_tolerated() {
    // CP932-encoded Japanese words placed in the second (unchecked) column
    let f = csv_file_bytes(b"0,\x83\x65\x83\x58\x83\x67\n1,\x93\xFA\x96\x7B\n2,\x8A\xBF\x8E\x9A\n");
    seq_ok(&f);
}

#[test]
fn non_utf8_bytes_in_target_column_exit_two() {
    let f = csv_file_bytes(b"\x83\x65,0\n\x93\xFA,1\n");
    hanuke()
        .arg(f.path())
        .assert()
        .code(2)
        .stderr(predicate::str::contains("row 1"));
}

#[test]
fn single_quoted_cells_via_quote_option() {
    let f = csv_file("'0','a,b'\n'1','c,d'\n'2','e,f'\n");
    hanuke()
        .args(["--quote", "'"])
        .arg(f.path())
        .assert()
        .success();
}

#[test]
fn non_ascii_quote_is_rejected() {
    let f = csv_file("0\n1\n");
    hanuke()
        .args(["--quote", "あ"])
        .arg(f.path())
        .assert()
        .code(2);
}

fn seq_ok(f: &NamedTempFile) {
    hanuke()
        .arg(f.path())
        .assert()
        .success()
        .stdout("")
        .stderr("");
}

#[test]
fn empty_file_is_ok() {
    let f = csv_file("");
    hanuke().arg(f.path()).assert().success();
}

#[test]
fn json_reports_ok_summary() {
    let f = csv_file("0\n1\n2\n");
    let output = hanuke().arg("--json").arg(f.path()).assert().success();
    let v: serde_json::Value =
        serde_json::from_slice(&output.get_output().stdout).expect("stdout must be valid JSON");
    assert_eq!(v["ok"], true);
    assert_eq!(v["rows"], 3);
    assert_eq!(v["start"], 0);
    assert_eq!(v["end"], 2);
    assert_eq!(v["issue_count"]["total"], 0);
}

#[test]
fn json_without_verbose_is_bounded_summary() {
    let f = csv_file("0\n1\n5\n5\n");
    let output = hanuke()
        .arg("--json")
        .arg(f.path())
        .assert()
        .code(1)
        .stderr("");
    let v: serde_json::Value =
        serde_json::from_slice(&output.get_output().stdout).expect("stdout must be valid JSON");
    assert_eq!(v["ok"], false);
    assert_eq!(v["issue_count"]["total"], 2);
    assert_eq!(v["issue_count"]["gap"], 1);
    assert_eq!(v["issue_count"]["duplicate"], 1);
    assert_eq!(v["issue_count"]["order_violation"], 0);
    assert_eq!(v["missing_count"], 3);
    assert!(v.get("issues").is_none(), "summary must omit issue list");
    assert!(v.get("missing").is_none(), "summary must omit missing list");
    assert!(
        v.get("duplicates").is_none(),
        "summary must omit duplicate list"
    );
}

#[test]
fn json_with_verbose_includes_details() {
    let f = csv_file("0\n1\n5\n5\n");
    let output = hanuke()
        .args(["--json", "--verbose"])
        .arg(f.path())
        .assert()
        .code(1)
        .stderr("");
    let v: serde_json::Value =
        serde_json::from_slice(&output.get_output().stdout).expect("stdout must be valid JSON");
    assert_eq!(v["ok"], false);
    assert_eq!(
        v["missing"],
        serde_json::json!([{"first": 2, "last": 4, "count": 3}])
    );
    assert_eq!(v["duplicates"], serde_json::json!([5]));
    assert_eq!(v["issues"][0]["type"], "gap");
    assert_eq!(v["issues"][0]["row"], 3);
    assert_eq!(v["issues"][0]["expected"], 2);
    assert_eq!(v["issues"][0]["actual"], 5);
    assert_eq!(v["issues"][1]["type"], "duplicate");
    assert_eq!(v["issues"][1]["value"], 5);
}

#[test]
fn col_bytes_override_for_dec_words() {
    // Two decimal columns of 2 bytes each: (1,0) -> 65536, (1,1) -> 65537
    let f = csv_file("1,0\n1,1\n1,2\n");
    hanuke()
        .args(["--cols", "2", "--col-bytes", "2"])
        .arg(f.path())
        .assert()
        .success();
}
