use minecraft_server::CONSTANTS;
use predicates::prelude::predicate;
use regex::Regex;
use std::{
    fs::{self, read_to_string},
    io::{self, BufRead, Read, Write},
    path::Path,
    process::Stdio,
};
use tempfile::tempdir;

fn update_eula(eula_filepath: &Path) {
    let re = Regex::new(r"\bfalse\b").unwrap();

    let eula = read_to_string(eula_filepath).unwrap();

    let output = fs::File::create(eula_filepath).unwrap();
    let mut writer = io::BufWriter::new(output);

    let eula = re.replace_all(&eula, "true").to_string();

    writer.write_all(eula.as_bytes()).unwrap();
    writer.flush().unwrap();
}

#[test]
fn java_server_help() -> Result<(), Box<dyn std::error::Error>> {
    let temp_dir = tempdir().unwrap();
    let data_home = temp_dir.path().join("data");
    let config_home = temp_dir.path().join("config");

    let envs = [
        ("XDG_DATA_HOME", data_home.as_os_str().to_str().unwrap()),
        ("XDG_CONFIG_HOME", config_home.as_os_str().to_str().unwrap()),
    ];

    let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("minecraft-server-java");

    cmd.envs(envs).args(["--nogui", "--help"]);
    cmd.assert()
        .success()
        .stderr(predicate::str::contains("ds level with vanilla datapa"))
        .stdout(predicate::str::contains(
            "Minecraft Java server exited with exit status: 0",
        ));

    Ok(())
}

#[test]
fn java_server_normal_stop() -> Result<(), Box<dyn std::error::Error>> {
    let temp_dir = tempdir().unwrap();
    let data_home = temp_dir.path().join("data");
    let config_home = temp_dir.path().join("config");

    let envs = [
        ("XDG_DATA_HOME", data_home.as_os_str().to_str().unwrap()),
        ("XDG_CONFIG_HOME", config_home.as_os_str().to_str().unwrap()),
    ];

    // 1. First run: trigger EULA
    assert_cmd::cargo::cargo_bin_cmd!("minecraft-server-java")
        .envs(envs)
        .args(["--nogui"])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "ed to agree to the EULA in order to run the se",
        ))
        .stdout(predicate::str::contains(
            "Minecraft Java server exited with exit status: 0",
        ));

    // 2. Accept EULA
    let mut eula_filepath = config_home.join(CONSTANTS.java_server_xdg_subdirectory);
    eula_filepath.push(CONSTANTS.minecraft_config_subdirectory);
    eula_filepath.push("eula.txt");
    update_eula(&eula_filepath);

    // 3. Second run: interactive lifecycle test
    let bin = assert_cmd::cargo::cargo_bin!("minecraft-server-java");

    let mut child = std::process::Command::new(bin)
        .envs(envs)
        .args(["--nogui"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    let mut stdin = child.stdin.take().unwrap();
    let stdout = child.stdout.take().unwrap();
    let mut reader = std::io::BufReader::new(stdout);

    // 4. Wait for server to reach the ready state
    let mut wait_for_ready = || {
        for line in reader.by_ref().lines() {
            let line = line.expect("Failed to read line from server");
            if line.contains("Done (") {
                return;
            }
        }
        panic!("Server exited before reaching ready state");
    };

    wait_for_ready();

    writeln!(stdin, "stop")?;

    let mut remaining = String::new();
    reader.read_to_string(&mut remaining)?;

    // 7. Wait for exit and check status
    // TODO: this takes a long time. potentially we should provide a pre-spawned world for the test
    let status = child.wait()?;
    assert!(
        status.success(),
        "Expected process to exit successfully, got: {:?}",
        status
    );

    // 8. Verify successful exit output
    assert!(
        remaining.contains("ThreadedAnvilChunkStorage: All dimensions are saved"),
        "Expected world-save completion log"
    );
    assert!(
        remaining.contains("Minecraft Java server exited with exit status: 0"),
        "Expected clean exit status line"
    );

    Ok(())
}

#[cfg(target_arch = "x86_64")] // Mojang only provides a x86_64 binary this test will fail on AArch64
#[test]
fn bedrock_server_normal_stop() -> Result<(), Box<dyn std::error::Error>> {
    let temp_dir = tempdir().unwrap();
    let data_home = temp_dir.path().join("data");
    let config_home = temp_dir.path().join("config");

    let envs = [
        ("XDG_DATA_HOME", data_home.as_os_str().to_str().unwrap()),
        ("XDG_CONFIG_HOME", config_home.as_os_str().to_str().unwrap()),
    ];

    let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("minecraft-server-bedrock");
    cmd.envs(envs).write_stdin("stop\n");

    cmd.assert()
        .success()
        .stdout(predicate::str::contains("INFO] Server started."))
        .stdout(predicate::str::contains(
            "Minecraft Bedrock server exited with exit status: 0",
        ));

    Ok(())
}

/*
 *  TODO: add signal handling tests
 *  SIGTERM, SIGINT, ctrl+c (simulation only to the rust wrapper)
 *  for all 4 different processes (the 2 rust wrappers and the 2 minecraft servers)
 *  expected behavior
 *  1. when the rust wrappers recieve the signals the servers should shutdown just like when
 *      recieving the "stop\n" sequence. they should output that they recieved the signal
 *  2. when the minecraft servers recieve the signals they should quit and exit with high numbered
 *      codes. the rust wrapper should detect that and report the exit statuses correctly
 *
 *  the easiest way to impliment this would be:
 *  1. run the process
 *  2. manually send the signal
 *  3. inspect the captured output
 *
 *  however i don't know how to do this with assert-cmd :(
 *
 *  the nix package would make it straight forward:
 *      https://docs.rs/nix/latest/nix/sys/signal/fn.killpg.html (to simulate ctrl+c)
 *      https://docs.rs/nix/latest/nix/sys/signal/fn.kill.html
 *
 *  TODO: add tests for configuration options
 *      there are files and environment variables
 */
