use assert_cmd::assert::OutputAssertExt;
use assert_cmd::cargo::CommandCargoExt;
use assert_cmd::cargo_bin_cmd;
use serde_json::{Value, json};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

#[test]
fn list_subcommand_with_specific_file_returns_correct_json_from_that_file_only() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("list").arg("tests/testdata/list/a.py");

    let output = cmd.output().expect("Failed to get command output");

    output.clone().assert().success();

    let actual: Value =
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON output");

    let expected = json!({
        "tests/testdata/list/a.py": [
            {
                "name": "a",
                "line": 1,
                "column": 3,
                "is_content_modified": false,
                "attributes": {
                    "name": "a",
                    "attr": "val"
                }
            }
        ]
    });
    assert_eq!(actual, expected);
}

#[test]
fn list_subcommand_with_glob_returns_multiple_files() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("list").arg("tests/testdata/list/*.py");

    let output = cmd.output().expect("Failed to get command output");

    output.clone().assert().success();

    let actual: Value =
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON output");
    let report = actual.as_object().expect("Output should be a JSON object");

    assert_eq!(report.len(), 2);
    assert!(report.contains_key("tests/testdata/list/a.py"));
    assert!(report.contains_key("tests/testdata/list/b.py"));
}

#[test]
fn list_subcommand_with_ignore_excludes_files() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("list")
        .arg("tests/testdata/list/*.py")
        .arg("--ignore")
        .arg("tests/testdata/list/b.py");

    let output = cmd.output().expect("Failed to get command output");

    output.clone().assert().success();

    let actual: Value =
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON output");
    let report = actual.as_object().expect("Output should be a JSON object");

    assert_eq!(report.len(), 1);
    assert!(report.contains_key("tests/testdata/list/a.py"));
    assert!(!report.contains_key("tests/testdata/list/b.py"));
}

#[test]
fn list_subcommand_with_no_args_checks_all_files() {
    let mut cmd = cargo_bin_cmd!();
    cmd.current_dir("tests/testdata/list");
    cmd.arg("list");

    let output = cmd.output().expect("Failed to get command output");
    output.clone().assert().success();

    let actual: Value =
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON output");
    let report = actual.as_object().expect("Output should be a JSON object");

    assert_eq!(report.len(), 2);
    assert!(report.contains_key("a.py"));
    assert!(report.contains_key("b.py"));
}

#[test]
fn list_subcommand_with_diff_input_returns_correct_is_content_modified_attribute() {
    let diff_content = r#"
diff --git a/tests/testdata/list/a.py b/tests/testdata/list/a.py
index 2fcfa70..eea9cf4 100644
--- a/tests/testdata/list/a.py
+++ b/tests/testdata/list/a.py
@@ -1,3 +1,3 @@
 # <block name="a" attr="val">
-pass old
+pass
 # </block>
"#;

    let mut cmd = cargo_bin_cmd!();
    cmd.arg("list").arg("--diff").arg("tests/testdata/list/**");
    let output = cmd
        .write_stdin(diff_content)
        .output()
        .expect("Failed to get command output");

    output.clone().assert().success();

    let actual: Value =
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON output");
    let report = actual.as_object().expect("Output should be a JSON object");

    assert_eq!(report.len(), 2);
    assert!(
        report["tests/testdata/list/a.py"][0]["is_content_modified"]
            .as_bool()
            .unwrap()
    );
    assert!(
        !report["tests/testdata/list/b.py"][0]["is_content_modified"]
            .as_bool()
            .unwrap()
    );
}

#[test]
fn list_subcommand_ignores_piped_diff_without_diff_flag() {
    let diff_content = r#"
diff --git a/tests/testdata/list/a.py b/tests/testdata/list/a.py
index 2fcfa70..eea9cf4 100644
--- a/tests/testdata/list/a.py
+++ b/tests/testdata/list/a.py
@@ -1,3 +1,3 @@
 # <block name="a" attr="val">
-pass old
+pass
 # </block>
"#;

    let mut cmd = cargo_bin_cmd!();
    cmd.arg("list").arg("tests/testdata/list/**");
    let output = cmd
        .write_stdin(diff_content)
        .output()
        .expect("Failed to get command output");

    output.clone().assert().success();

    let actual: Value =
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON output");
    let report = actual.as_object().expect("Output should be a JSON object");

    // Without `--diff`, the piped diff is ignored entirely, so no block is
    // reported as modified.
    assert_eq!(report.len(), 2);
    assert!(
        !report["tests/testdata/list/a.py"][0]["is_content_modified"]
            .as_bool()
            .unwrap()
    );
    assert!(
        !report["tests/testdata/list/b.py"][0]["is_content_modified"]
            .as_bool()
            .unwrap()
    );
}

#[test]
fn list_subcommand_finishes_without_waiting_for_stdin() {
    // `blockwatch list` (without `--diff`) takes its file set from the glob
    // arguments, so it must complete without consuming stdin. We give it a stdin
    // pipe that never receives any data and is never closed; if `list` tried to
    // read it, the process would never exit and the deadline below would trip.
    let mut child = Command::cargo_bin("blockwatch")
        .expect("blockwatch binary should be built")
        .args(["list", "tests/testdata/list/**"])
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("failed to spawn blockwatch");

    let deadline = Instant::now() + Duration::from_secs(5);
    let status = loop {
        if let Some(status) = child.try_wait().expect("failed to poll blockwatch") {
            break status;
        }
        if Instant::now() >= deadline {
            child.kill().ok();
            panic!("blockwatch list is still running; it appears to be waiting on stdin");
        }
        std::thread::sleep(Duration::from_millis(20));
    };

    assert!(status.success());
}
