mod support;

use std::{
    io::{Read, Write},
    net::TcpListener,
    process::{Command, Stdio},
    sync::mpsc,
    thread,
    time::Duration,
};

use serde_json::Value;
use support::TestWorkspace;

#[test]
fn run_outputs_json_report() {
    let server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"ok":true,"service":"hen"}"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"JSON Fixture

Exercises machine-readable output.

---

Fetch fixture

GET {server_url}

^ & body.ok == true
[ true == false ] ^ & body.service == 'hen'
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["plan"], serde_json::json!([0]));
    assert_eq!(parsed["selectedRequests"], serde_json::json!([0]));
    assert_eq!(parsed["records"][0]["status"], 200);
    assert_eq!(
        parsed["records"][0]["assertions"],
        serde_json::json!([
            {
                "assertion": "^ & body.ok == true",
                "status": "passed",
                "message": null,
            },
            {
                "assertion": "[ true == false ] ^ & body.service == 'hen'",
                "status": "skipped",
                "message": "guard evaluated to false",
            }
        ])
    );
    assert!(parsed["records"][0]["body"]
        .as_str()
        .expect("body should be serialized as a string")
        .contains("\"service\":\"hen\""));
    assert_eq!(parsed["records"][0]["bodyChars"], 27);
    assert_eq!(parsed["records"][0]["bodyTruncated"], false);
    assert_eq!(parsed["interrupted"], false);
    assert_eq!(parsed["interruptSignal"], Value::Null);
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_ndjson_report() {
    let server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"ok":true,"service":"hen"}"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"NDJSON Fixture

Exercises streaming machine-readable output.

---

Fetch fixture

GET {server_url}

^ & body.ok == true
[ true == false ] ^ & body.service == 'hen'
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "ndjson"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let lines = output
        .stdout
        .lines()
        .map(|line| serde_json::from_str::<Value>(line).expect("line should be valid json"))
        .collect::<Vec<_>>();

    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0]["type"], "run");
    assert_eq!(lines[0]["interrupted"], false);
    assert_eq!(lines[0]["interruptSignal"], Value::Null);
    assert_eq!(lines[1]["type"], "record");
    assert_eq!(lines[1]["status"], 200);
    assert_eq!(lines[1]["assertions"][0]["status"], "passed");
    assert_eq!(lines[1]["assertions"][1]["status"], "skipped");
}

#[test]
fn run_outputs_json_report_for_failed_assertions() {
    let server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"ok":false,"service":"hen"}"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"Failed Assertions Fixture

Exercises assertion-level failure reporting.

---

Fetch fixture

GET {server_url}

^ & body.service == 'hen'
[ true == false ] ^ & body.service == 'hen'
^ & body.ok == true
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);

    assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["executionFailed"], true);
    assert_eq!(parsed["interrupted"], false);
    assert_eq!(parsed["interruptSignal"], Value::Null);
    assert_eq!(parsed["records"], serde_json::json!([]));
    assert_eq!(
        parsed["failures"][0]["assertions"],
        serde_json::json!([
            {
                "assertion": "^ & body.service == 'hen'",
                "status": "passed",
                "message": null,
            },
            {
                "assertion": "[ true == false ] ^ & body.service == 'hen'",
                "status": "skipped",
                "message": "guard evaluated to false",
            },
            {
                "assertion": "^ & body.ok == true",
                "status": "failed",
                "message": "Assertion failed: ^ & body.ok == true (actual: 'false')",
            }
        ])
    );
}

#[test]
fn run_outputs_junit_report_for_assertion_failures() {
    let server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"ok":false,"service":"hen"}"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"JUnit Fixture

Exercises CI-oriented test reporting.

---

Fail fixture

GET {server_url}

[ true == false ] ^ & body.service == 'hen'
^ & body.ok == true
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "junit"]);

    assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
    assert!(
        output.stdout.contains("<testsuite"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("<testcase"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("tests=\"2\""),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("<failure type=\"assertion\""),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("Assertion failed"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("<skipped message=\"guard evaluated to false\"/>"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("#0 GET") && output.stdout.contains(":: ^ &amp; body.ok == true"),
        "stdout: {}",
        output.stdout
    );
}

#[test]
fn run_outputs_junit_report_for_passed_and_skipped_assertions() {
    let server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"ok":true,"service":"hen"}"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"JUnit Success Fixture

Exercises assertion-level success reporting.

---

Fetch fixture

GET {server_url}

^ & body.ok == true
[ true == false ] ^ & body.service == 'hen'
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "junit"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
    assert!(output.stdout.contains("tests=\"2\""), "stdout: {}", output.stdout);
    assert!(
        output.stdout.contains(":: ^ &amp; body.ok == true"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains(":: [ true == false ] ^ &amp; body.service == &apos;hen&apos;"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains("<skipped message=\"guard evaluated to false\"/>"),
        "stdout: {}",
        output.stdout
    );
}

#[test]
fn run_outputs_junit_report_for_dependency_capture_assertions() {
    let user_server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"id":1,"email":"user@example.com","address":{"zipcode":"90210"}}"#,
    );
    let posts_server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"[{"userId":1,"id":10,"title":"hello world"}]"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"name = JUnit Dependency Capture Fixture
description = Exercises dependency captures followed by assertions.

---

Get User

GET {user_server_url}

& body.id -> $USER_ID
^ & status == 200

---

Get Posts

> requires: Get User

GET {posts_server_url}

& body.[0].title -> $FIRST_POST_TITLE
&[Get User].body.email -> $USER_EMAIL
&[Get User].body.address.zipcode -> $USER_ZIPCODE

^ & status == 200
^ & body.[0].userId == $USER_ID
^ $FIRST_POST_TITLE != ''
^ $USER_EMAIL ~= /@/
^ $USER_ZIPCODE == '90210'
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "all", "--output", "junit"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
    assert!(output.stdout.contains("tests=\"6\""), "stdout: {}", output.stdout);
    assert!(
        output
            .stdout
            .contains("#1 GET")
            && output.stdout.contains(":: ^ $USER_EMAIL ~= /@/"),
        "stdout: {}",
        output.stdout
    );
    assert!(
        output.stdout.contains(":: ^ $USER_ZIPCODE == &apos;90210&apos;"),
        "stdout: {}",
        output.stdout
    );
}

#[cfg(unix)]
#[test]
fn run_outputs_partial_text_summary_when_interrupted_by_sigint() {
    let fast_server_url = spawn_http_server(200, "OK", "application/json", r#"{"step":1}"#);
    let (started_tx, started_rx) = mpsc::channel();
    let slow_server_url = spawn_blocking_http_server(started_tx);
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"Interrupt Fixture

Exercises partial text summaries on SIGINT.

---

First request

GET {fast_server_url}

---

Second request

GET {slow_server_url}
"#
        ),
    );

    let output = run_until_signal(
        &workspace,
        &["run", "collection.hen", "all", "--parallel"],
        "-INT",
        started_rx,
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
    let stderr = String::from_utf8(output.stderr).expect("stderr should be utf-8");

    assert_eq!(output.status.code(), Some(130), "stdout: {stdout}\nstderr: {stderr}");
    assert!(stdout.contains("[ok] #0"), "stdout: {stdout}");
    assert!(stdout.contains("Summary"), "stdout: {stdout}");
    assert!(stdout.contains("interrupted by SIGINT"), "stdout: {stdout}");
    assert!(!stdout.contains("[ok] #1"), "stdout: {stdout}");
    assert!(stderr.contains("Execution interrupted by SIGINT"), "stderr: {stderr}");
}

#[cfg(unix)]
#[test]
fn run_outputs_partial_json_report_when_interrupted_by_sigterm() {
    let fast_server_url = spawn_http_server(200, "OK", "application/json", r#"{"step":1}"#);
    let (started_tx, started_rx) = mpsc::channel();
    let slow_server_url = spawn_blocking_http_server(started_tx);
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"Interrupt JSON Fixture

Exercises partial machine-readable summaries on SIGTERM.

---

First request

GET {fast_server_url}

---

Second request

GET {slow_server_url}
"#
        ),
    );

    let output = run_until_signal(
        &workspace,
        &["run", "collection.hen", "all", "--parallel", "--output", "json"],
        "-TERM",
        started_rx,
    );
    let stderr = String::from_utf8(output.stderr).expect("stderr should be utf-8");

    assert_eq!(output.status.code(), Some(143), "stderr: {stderr}");
    assert!(stderr.is_empty(), "stderr: {stderr}");

    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
    let parsed: Value = serde_json::from_str(&stdout).expect("stdout should be valid json");
    assert_eq!(parsed["executionFailed"], true);
    assert_eq!(parsed["interrupted"], true);
    assert_eq!(parsed["interruptSignal"], "SIGTERM");
    assert_eq!(parsed["records"].as_array().map(Vec::len), Some(1));
    assert_eq!(parsed["records"][0]["status"], 200);
    assert_eq!(parsed["records"][0]["url"], fast_server_url);
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

fn spawn_http_server(status: u16, reason: &str, content_type: &str, body: &str) -> String {
    let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
    let address = listener.local_addr().expect("address should be available");
    let reason = reason.to_string();
    let content_type = content_type.to_string();
    let body = body.to_string();

    thread::spawn(move || {
        let (mut stream, _) = listener.accept().expect("connection should be accepted");
        let mut buffer = [0_u8; 1024];
        let _ = stream.read(&mut buffer);
        let response = format!(
            "HTTP/1.1 {status} {reason}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
            body.len()
        );
        stream
            .write_all(response.as_bytes())
            .expect("response should be written");
    });

    format!("http://{}", address)
}

#[cfg(unix)]
fn spawn_blocking_http_server(started_tx: mpsc::Sender<()>) -> String {
    let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
    let address = listener.local_addr().expect("address should be available");

    thread::spawn(move || {
        let (mut stream, _) = listener.accept().expect("connection should be accepted");
        let mut buffer = [0_u8; 1024];
        let _ = stream.read(&mut buffer);
        started_tx
            .send(())
            .expect("test should observe the blocking request");
        thread::sleep(Duration::from_secs(5));

        let body = r#"{"ok":true}"#;
        let response = format!(
            "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
            body.len()
        );
        let _ = stream.write_all(response.as_bytes());
    });

    format!("http://{}", address)
}

#[cfg(unix)]
fn run_until_signal(
    workspace: &TestWorkspace,
    args: &[&str],
    signal: &str,
    started_rx: mpsc::Receiver<()>,
) -> std::process::Output {
    let child = Command::new(env!("CARGO_BIN_EXE_hen"))
        .args(args)
        .current_dir(workspace.root())
        .env("NO_COLOR", "1")
        .env("TERM", "dumb")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("hen command should execute");

    if started_rx.recv_timeout(Duration::from_secs(5)).is_err() {
        let output = child
            .wait_with_output()
            .expect("child output should be available");
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        panic!(
            "second request should start before signalling\nstatus: {:?}\nstdout: {}\nstderr: {}",
            output.status.code(),
            stdout,
            stderr,
        );
    }

    thread::sleep(Duration::from_millis(200));

    let status = Command::new("kill")
        .args([signal, &child.id().to_string()])
        .status()
        .expect("signal command should execute");
    assert!(status.success(), "failed to send signal {signal}");

    child
        .wait_with_output()
        .expect("child output should be available")
}
