use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};

const SOURCE_ID: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
const BOT_ID: &str = "11111111-2222-3333-4444-555555555555";
const DANCE1: &str = "b68a3d7c-de9e-fc87-eec8-543d787e5b0d";
const EXIT_USAGE: i32 = 2;
const EXIT_INPUT: i32 = 3;

static TEMP_ID: AtomicU64 = AtomicU64::new(0);

struct TestDir(PathBuf);

impl TestDir {
    fn new(test_name: &str) -> Self {
        let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "metacrate-simple-bot-{test_name}-{}-{id}",
            std::process::id()
        ));
        fs::create_dir(&path).expect("create isolated test directory");
        Self(path)
    }

    fn path(&self, name: &str) -> PathBuf {
        self.0.join(name)
    }
}

impl Drop for TestDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn run(args: &[&str]) -> Output {
    Command::new(env!("CARGO_BIN_EXE_simple-bot"))
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("run simple-bot")
}

fn utf8(bytes: &[u8]) -> &str {
    std::str::from_utf8(bytes).expect("command output is UTF-8")
}

fn path_text(path: &Path) -> &str {
    path.to_str().expect("test path is UTF-8")
}

fn assert_exit(output: &Output, expected: i32) {
    assert_eq!(
        output.status.code(),
        Some(expected),
        "stdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn help_documents_upstream_arguments_and_commands() {
    let output = run(&["--help"]);
    assert!(output.status.success());
    let help = utf8(&output.stdout);
    for marker in [
        "[FIRSTNAME]",
        "[LASTNAME]",
        "[PASSWORD]",
        "--fake-script",
        "help, ?",
        "where, location",
        "sit",
        "stand",
        "dance",
        "fly",
        "walk",
        "jump",
        "hello, hi, hey",
    ] {
        assert!(help.contains(marker), "help omitted {marker}:\n{help}");
    }

    let output = run(&[]);
    assert_exit(&output, EXIT_USAGE);
    assert!(utf8(&output.stderr).contains("Usage: simple-bot"));
}

#[test]
fn scripted_fake_grid_covers_every_command_call_and_reply() {
    let directory = TestDir::new("commands");
    let script = directory.path("conversation.tsv");
    let contents = format!(
        "# deterministic fake-grid conversation\n\
         im\t{SOURCE_ID}\tAlice Resident\thelp\n\
         im\t{SOURCE_ID}\tAlice Resident\tlocation\n\
         im\t{SOURCE_ID}\tAlice Resident\tsit\n\
         im\t{SOURCE_ID}\tAlice Resident\tstand\n\
         im\t{SOURCE_ID}\tAlice Resident\tdance\n\
         im\t{SOURCE_ID}\tAlice Resident\tfly\n\
         im\t{SOURCE_ID}\tAlice Resident\twalk\n\
         im\t{SOURCE_ID}\tAlice Resident\tjump\n\
         im\t{SOURCE_ID}\tAlice Resident\thi\n\
         im\t{SOURCE_ID}\tAlice Resident\tunknown\n\
         chat\t{SOURCE_ID}\tAlice Resident\thello bot\n\
         im\t{BOT_ID}\tSimple Bot\thelp\n\
         chat\t{BOT_ID}\tSimple Bot\thello self\n\
         status\tconnected to https://example.invalid/cap/private\n"
    );
    fs::write(&script, contents).expect("write conversation script");
    let output = run(&["--fake-script", path_text(&script)]);
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    assert!(output.stderr.is_empty());
    let output = utf8(&output.stdout);

    for expected in [
        "Fake grid ready: Scripted Test Region at <128, 64, 25>",
        "Commands: help, where, sit, stand, dance, fly, walk, jump, hello, hi, hey",
        "I'm in Scripted Test Region at <128, 64, 25>",
        "CALL sit-on-ground",
        "Sitting down...",
        "CALL stand",
        "Standing up!",
        &format!("CALL animation-start {DANCE1} reliable=true"),
        "Dancing!",
        "CALL fly true",
        "Taking off!",
        "CALL fly false",
        "Walking now.",
        "CALL jump true",
        "WAIT 500ms",
        "CALL jump false",
        "Wheee!",
        "Hello, Alice Resident!",
        "I don't understand that command.",
        "WAIT 750ms",
        "CALL chat channel=0 type=Normal Hello, Alice Resident!",
        "connected to <redacted-url>",
        "Fake grid logout complete; active_tasks=0 open_sockets=0",
    ] {
        assert!(
            output.contains(expected),
            "output omitted {expected}:\n{output}"
        );
    }
    assert!(!output.contains("hello self"));
    assert!(!output.contains("[IM] Simple Bot"));
    assert!(!output.contains("/cap/private"));
}

#[test]
fn fake_grid_redacts_sensitive_messages_and_never_echoes_cli_passwords() {
    let directory = TestDir::new("redaction");
    let script = directory.path("redaction.tsv");
    fs::write(
        &script,
        format!("im\t{SOURCE_ID}\tAlice Resident\ttoken=private-value\n"),
    )
    .expect("write redaction script");
    let output = run(&["--fake-script", path_text(&script)]);
    assert!(output.status.success());
    assert!(!utf8(&output.stdout).contains("private-value"));
    assert!(utf8(&output.stdout).contains("<redacted>"));

    let password = "super-secret-password";
    let output = run(&[
        "First",
        "Last",
        password,
        "--fake-script",
        path_text(&script),
    ]);
    assert_exit(&output, EXIT_USAGE);
    assert!(!utf8(&output.stdout).contains(password));
    assert!(!utf8(&output.stderr).contains(password));
}

#[test]
fn malformed_or_oversized_scripts_fail_without_partial_output() {
    let directory = TestDir::new("invalid");
    let malformed = directory.path("malformed.tsv");
    fs::write(&malformed, "im\tnot-a-uuid\tAlice Resident\thelp\n")
        .expect("write malformed script");
    let output = run(&["--fake-script", path_text(&malformed)]);
    assert_exit(&output, EXIT_INPUT);
    assert!(output.stdout.is_empty());
    assert!(utf8(&output.stderr).contains("source UUID is invalid"));

    let oversized = directory.path("oversized.tsv");
    let mut file = fs::File::create(&oversized).expect("create oversized script");
    file.write_all(&vec![b'x'; 1024 * 1024 + 1])
        .expect("write oversized script");
    drop(file);
    let output = run(&["--fake-script", path_text(&oversized)]);
    assert_exit(&output, EXIT_INPUT);
    assert!(output.stdout.is_empty());
    assert!(utf8(&output.stderr).contains("1 MiB limit"));
}
