use quick_xml::events::Event;
use quick_xml::Reader;
use serde_json::Value;
use std::fs;
use std::io::{BufRead, Write};
use std::path::Path;
use std::process::{Command, Output};

fn juv_command() -> Command {
    Command::new(env!("CARGO_BIN_EXE_juv"))
}

fn assert_success(out: &Output) {
    assert!(
        out.status.success(),
        "expected success\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}

fn with_junit_version(command: &mut Command) -> &mut Command {
    command.arg("--junit-version").arg("6.1.0")
}

fn compile_greeter_jar(root: &Path) -> Vec<u8> {
    let lib_src_dir = root.join("libsrc/com/example");
    let lib_classes = root.join("libclasses");
    fs::create_dir_all(&lib_src_dir).unwrap();
    fs::create_dir_all(&lib_classes).unwrap();
    let lib_src = lib_src_dir.join("Greeter.java");
    fs::write(
        &lib_src,
        r#"
package com.example;
public class Greeter {
  public static String message() { return "deps-ok"; }
}
"#,
    )
    .unwrap();
    let javac = Command::new("javac")
        .arg("--release")
        .arg("8")
        .arg("-d")
        .arg(&lib_classes)
        .arg(&lib_src)
        .output()
        .unwrap();
    assert_success(&javac);

    let jar_path = root.join("greeter-1.0.0.jar");
    let jar = Command::new("jar")
        .arg("--create")
        .arg("--file")
        .arg(&jar_path)
        .arg("-C")
        .arg(&lib_classes)
        .arg(".")
        .output()
        .unwrap();
    assert_success(&jar);
    fs::read(jar_path).unwrap()
}

fn serve_files(files: std::collections::HashMap<&'static str, Vec<u8>>) -> String {
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let port = listener.local_addr().unwrap().port();
    std::thread::spawn(move || {
        for stream in listener.incoming() {
            let Ok(mut stream) = stream else { continue };
            let files = files.clone();
            std::thread::spawn(move || {
                let mut reader = std::io::BufReader::new(stream.try_clone().unwrap());
                let mut request = String::new();
                if reader.read_line(&mut request).is_err() {
                    return;
                }
                let path = request.split_whitespace().nth(1).unwrap_or("/");
                while {
                    let mut line = String::new();
                    reader.read_line(&mut line).unwrap_or(0) > 0 && line != "\r\n"
                } {}
                match files.get(path) {
                    Some(body) => {
                        let header = format!(
                            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                            body.len()
                        );
                        let _ = stream.write_all(header.as_bytes());
                        let _ = stream.write_all(body);
                    }
                    None => {
                        let body = b"not found";
                        let header = format!(
                            "HTTP/1.1 404 Not Found\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                            body.len()
                        );
                        let _ = stream.write_all(header.as_bytes());
                        let _ = stream.write_all(body);
                    }
                }
            });
        }
    });
    format!("http://127.0.0.1:{port}")
}

fn assert_well_formed_xml(xml: &str) {
    let mut reader = Reader::from_str(xml);
    let mut buf = Vec::new();
    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Eof) => break,
            Ok(_) => buf.clear(),
            Err(err) => panic!("XML should be well-formed: {err}; xml was {xml}"),
        }
    }
}

fn write_junit_test(tmp: &tempfile::TempDir) -> std::path::PathBuf {
    let test = tmp.path().join("CalculatorTest.java");
    fs::write(
        &test,
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {
  @Test
  void addsNumbers() {
    assertEquals(4, 2 + 2);
  }
}
"#,
    )
    .unwrap();
    test
}

fn write_failing_junit_test(tmp: &tempfile::TempDir) -> std::path::PathBuf {
    let test = tmp.path().join("BrokenTest.java");
    fs::write(
        &test,
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class BrokenTest {
  @Test
  void failsClearly() {
    assertEquals(5, 2 + 2);
  }
}
"#,
    )
    .unwrap();
    test
}

#[test]
fn test_runs_junit_standalone_launcher_by_default() {
    let tmp = tempfile::tempdir().unwrap();
    let test = write_junit_test(&tmp);

    let out = juv_command()
        .arg("test")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(&test)
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("CalculatorTest"), "stdout was {stdout}");
    assert!(stdout.contains("addsNumbers"), "stdout was {stdout}");
}

#[test]
fn test_json_prints_converted_junit_report() {
    let tmp = tempfile::tempdir().unwrap();
    let test = write_junit_test(&tmp);

    let out = juv_command()
        .arg("test")
        .arg("--json")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(&test)
        .output()
        .unwrap();

    assert_success(&out);
    let json: Value = serde_json::from_slice(&out.stdout).expect("stdout should be JSON");
    assert_eq!(json["tests"], 1);
    assert_eq!(json["failures"], 0);
    assert_eq!(json["skipped"], 0);
    assert_eq!(json["testCases"][0]["className"], "CalculatorTest");
    assert_eq!(json["testCases"][0]["name"], "addsNumbers()");
    assert_eq!(json["testCases"][0]["status"], "passed");
}

#[test]
fn test_json_prints_failures_before_exiting_nonzero() {
    let tmp = tempfile::tempdir().unwrap();
    let test = write_failing_junit_test(&tmp);

    let out = juv_command()
        .arg("test")
        .arg("--json")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(&test)
        .output()
        .unwrap();

    assert!(
        !out.status.success(),
        "failing tests should return non-zero"
    );
    let json: Value = serde_json::from_slice(&out.stdout).expect("stdout should be JSON");
    assert_eq!(json["tests"], 1);
    assert_eq!(json["failures"], 1);
    assert_eq!(json["testCases"][0]["className"], "BrokenTest");
    assert_eq!(json["testCases"][0]["name"], "failsClearly()");
    assert_eq!(json["testCases"][0]["status"], "failed");
}

#[test]
fn test_xml_prints_junit_report_xml() {
    let tmp = tempfile::tempdir().unwrap();
    let test = write_junit_test(&tmp);

    let out = juv_command()
        .arg("test")
        .arg("--xml")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(&test)
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("<testsuite"), "stdout was {stdout}");
    assert!(stdout.contains("CalculatorTest"), "stdout was {stdout}");
    assert!(stdout.contains("addsNumbers()"), "stdout was {stdout}");
}

#[test]
fn test_xml_output_is_well_formed_with_multiple_suites() {
    let tmp = tempfile::tempdir().unwrap();
    let first = write_junit_test(&tmp);
    let second = tmp.path().join("OtherTest.java");
    fs::write(
        &second,
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;

class OtherTest {
  @Test
  void alsoRuns() {
    assertTrue(true);
  }
}
"#,
    )
    .unwrap();

    let out = with_junit_version(juv_command().arg("test"))
        .arg("--xml")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg("--source")
        .arg("OtherTest.java")
        .arg(&first)
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert_well_formed_xml(&stdout);
    assert!(stdout.contains("CalculatorTest"), "stdout was {stdout}");
    assert!(stdout.contains("OtherTest"), "stdout was {stdout}");
    assert!(stdout.matches("<?xml").count() <= 1, "stdout was {stdout}");
}

#[test]
fn test_without_target_runs_all_tests_in_current_directory() {
    let tmp = tempfile::tempdir().unwrap();
    fs::write(
        tmp.path().join("Calculator.java"),
        r#"
class Calculator {
  static int add(int left, int right) {
    return left + right;
  }
}
"#,
    )
    .unwrap();
    fs::write(
        tmp.path().join("CalculatorTest.java"),
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {
  @Test
  void addsNumbers() {
    assertEquals(9, Calculator.add(4, 5));
  }
}
"#,
    )
    .unwrap();
    fs::write(
        tmp.path().join("OtherTest.java"),
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;

class OtherTest {
  @Test
  void alsoRuns() {
    assertTrue(true);
  }
}
"#,
    )
    .unwrap();

    let out = with_junit_version(juv_command().arg("test"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .current_dir(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("CalculatorTest"), "stdout was {stdout}");
    assert!(stdout.contains("OtherTest"), "stdout was {stdout}");
}

#[test]
fn test_runs_all_tests_in_directory_and_compiles_sources() {
    let tmp = tempfile::tempdir().unwrap();
    fs::write(
        tmp.path().join("Calculator.java"),
        r#"
class Calculator {
  static int add(int left, int right) {
    return left + right;
  }
}
"#,
    )
    .unwrap();
    fs::write(
        tmp.path().join("CalculatorTest.java"),
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {
  @Test
  void addsNumbers() {
    assertEquals(9, Calculator.add(4, 5));
  }
}
"#,
    )
    .unwrap();
    fs::write(
        tmp.path().join("OtherTest.java"),
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;

class OtherTest {
  @Test
  void alsoRuns() {
    assertTrue(true);
  }
}
"#,
    )
    .unwrap();

    let out = with_junit_version(juv_command().arg("test"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("CalculatorTest"), "stdout was {stdout}");
    assert!(stdout.contains("OtherTest"), "stdout was {stdout}");
}

#[test]
fn test_directory_target_merges_dependency_directives_from_companion_sources() {
    let tmp = tempfile::tempdir().unwrap();
    let cache = tmp.path().join("cache");
    let jar = compile_greeter_jar(tmp.path());
    let repo = serve_files(std::collections::HashMap::from([
        (
            "/com/example/greeter/1.0.0/greeter-1.0.0.pom",
            br#"<?xml version="1.0" encoding="UTF-8"?>
<project>
  <groupId>com.example</groupId>
  <artifactId>greeter</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
</project>"#
                .to_vec(),
        ),
        ("/com/example/greeter/1.0.0/greeter-1.0.0.jar", jar),
    ]));
    fs::write(
        tmp.path().join("App.java"),
        format!(
            r#"
//REPOS local={repo}
//DEPS com.example:greeter:1.0.0
import com.example.Greeter;

class App {{
  static String message() {{
    return Greeter.message();
  }}
}}
"#
        ),
    )
    .unwrap();
    fs::write(
        tmp.path().join("AppTest.java"),
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class AppTest {
  @Test
  void usesDependencyFromCompanionSource() {
    assertEquals("deps-ok", App.message());
  }
}
"#,
    )
    .unwrap();

    let out = with_junit_version(juv_command().arg("test"))
        .arg("--cache-dir")
        .arg(&cache)
        .arg(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("usesDependencyFromCompanionSource"),
        "stdout was {stdout}"
    );
}

#[test]
fn test_compiles_matching_source_for_test_script() {
    let tmp = tempfile::tempdir().unwrap();
    let main = tmp.path().join("Calculator.java");
    fs::write(
        &main,
        r#"
class Calculator {
  static int add(int left, int right) {
    return left + right;
  }
}
"#,
    )
    .unwrap();
    let test = tmp.path().join("CalculatorTest.java");
    fs::write(
        &test,
        r#"
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {
  @Test
  void usesCompanionSource() {
    assertEquals(7, Calculator.add(3, 4));
  }
}
"#,
    )
    .unwrap();

    let out = with_junit_version(juv_command().arg("test"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(&test)
        .output()
        .unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("usesCompanionSource"),
        "stdout was {stdout}"
    );
}
