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

const ALICE: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
const BOB: &str = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff";
const MASTER: &str = "11111111-2222-3333-4444-555555555555";
const TARGET: &str = "22222222-3333-4444-5555-666666666666";
const GROUP: &str = "33333333-4444-5555-6666-777777777777";
const GROUP_MEMBER: &str = "44444444-5555-6666-7777-888888888888";
const STRANGER: &str = "55555555-6666-7777-8888-999999999999";
const SESSION: &str = "66666666-7777-8888-9999-aaaaaaaaaaaa";
const EXIT_USAGE: i32 = 2;
const EXIT_INPUT: i32 = 3;

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

struct TestDir(PathBuf);

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

    fn write(&self, name: &str, contents: &str) -> PathBuf {
        let path = self.0.join(name);
        fs::write(&path, contents).expect("write fixture");
        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 path_text(path: &Path) -> &str {
    path.to_str().expect("UTF-8 test path")
}

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

fn stdout(output: &Output) -> &str {
    std::str::from_utf8(&output.stdout).expect("UTF-8 stdout")
}

fn stderr(output: &Output) -> &str {
    std::str::from_utf8(&output.stderr).expect("UTF-8 stderr")
}

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

#[test]
fn help_documents_upstream_framework_and_owned_commands() {
    let output = run(&["--help"]);
    assert!(output.status.success());
    let text = stdout(&output);
    for marker in [
        "--first",
        "--last",
        "--pass",
        "--file",
        "--loginuri",
        "--startpos",
        "--master",
        "--masterkey",
        "--groupcommands",
        "--gettextures",
        "--scriptfile",
        "--nogui",
        "--fake-script",
        "--allow-live-mutations",
        "--allow-spending",
        "echomaster",
        "imgroup",
        "logpacket",
        "waitforlogin",
    ] {
        assert!(text.contains(marker), "help omitted {marker}:\n{text}");
    }

    let output = run(&["--first", "Only"]);
    assert_exit(&output, EXIT_USAGE);
}

#[test]
fn scripted_terminal_exercises_registry_communication_system_and_remote_auth() {
    let directory = TestDir::new("commands");
    let packet_log = directory.path("packets.log");
    let script = directory.write(
        "terminal.tsv",
        &format!(
            "!client\t{ALICE}\tAlice\tBot\tMaster Resident\t{MASTER}\ttrue\n\
             !client\t{BOB}\tBob\tBot\tMaster Resident\t{MASTER}\ttrue\n\
             !person\tTarget Resident\t{TARGET}\n\
             !group-member\t{GROUP_MEMBER}\n\
             help say\n\
             say 7 \"hello world\"\n\
             whisper quiet\n\
             shout loud\n\
             @ Alice Bot\n\
             im Target Resident private hello\n\
             imgroup {GROUP} group hello\n\
             echomaster\n\
             showeffects on\n\
             logpacket 2 \"{}\"\n\
             !chat\t{ALICE}\t{MASTER}\tMaster Resident\techo this\n\
             !effect\t{ALICE}\tViewerEffect [LookAt] source={MASTER}\n\
             !packet\t{ALICE}\tChatFromSimulator\tFake Region\t120\n\
             !packet\t{ALICE}\tImprovedInstantMessage\tFake Region\t240\n\
             !im\t{ALICE}\t{MASTER}\tMaster Resident\tagent\tfalse\tsay remote works\t{SESSION}\n\
             !im\t{ALICE}\t{GROUP_MEMBER}\tGroup Member\tagent\ttrue\twhisper group works\t{SESSION}\n\
             !im\t{ALICE}\t{STRANGER}\tStranger Resident\tagent\tfalse\tlogin Victim Resident supersecret\t{SESSION}\n\
             setmaster Target Resident\n\
             setmasterkey {MASTER}\n\
             !im\t{ALICE}\t{MASTER}\tMaster Resident\tteleport\tfalse\tlure\t{SESSION}\n\
             sleep 0\n\
             debug info\n\
             md5 secret\n\
             logout\n\
             @\n\
             say after logout\n\
             quit\n",
            path_text(&packet_log)
        ),
    );
    let output = run(&["--fake-script", path_text(&script)]);
    assert!(output.status.success(), "{}", stderr(&output));
    assert!(output.stderr.is_empty(), "{}", stderr(&output));
    let text = stdout(&output);
    for expected in [
        "Say something. Usage: say [optional-channel] message",
        "[Alice Bot] CALL chat channel=7 type=Normal hello world",
        "[Bob Bot] CALL chat channel=7 type=Normal hello world",
        "[Alice Bot] CALL instant-message 22222222-3333-4444-5555-666666666666 private hello",
        "[Alice Bot] CALL group-chat-join 33333333-4444-5555-6666-777777777777",
        "[Alice Bot] CALL group-instant-message 33333333-4444-5555-6666-777777777777 group hello",
        "[Alice Bot] CALL chat channel=0 type=Normal echo this",
        "ViewerEffect [LookAt] source=11111111-2222-3333-4444-555555555555",
        "[Alice Bot] CALL chat channel=0 type=Normal remote works",
        "[Alice Bot] CALL chat channel=0 type=Whisper group works",
        "<sensitive command redacted>",
        "Master set to Target Resident",
        "CALL teleport-lure-respond",
        "CALL agent-pause",
        "CALL agent-resume",
        "Logging is set to Info",
        "$1$5ebe2294ecd0e0f08eab7690d2a6ee69",
        "[Bob Bot] CALL chat channel=0 type=Normal after logout",
        "active-tasks=0 shutdown=true",
    ] {
        assert!(text.contains(expected), "missing {expected}:\n{text}");
    }
    assert!(!text.contains("supersecret"), "credential leaked:\n{text}");
    assert_eq!(text.matches("CALL logout-dispose").count(), 2, "{text}");
    let packet_text = fs::read_to_string(packet_log).expect("read packet log");
    assert_eq!(packet_text.lines().count(), 2, "{packet_text}");
    assert!(packet_text.contains("ChatFromSimulator"));
    assert!(packet_text.contains("ImprovedInstantMessage"));
}

#[test]
fn login_replaces_duplicate_client_and_waitforlogin_is_deterministic() {
    let directory = TestDir::new("login");
    let script = directory.write(
        "terminal.tsv",
        "login Carol Bot credential-one\n\
         waitforlogin\n\
         login Carol Bot credential-two\n\
         say only replacement\n\
         quit\n",
    );
    let output = run(&["--fake-script", path_text(&script)]);
    assert!(output.status.success(), "{}", stderr(&output));
    let text = stdout(&output);
    assert!(text.contains("currently tracking 1 bots"), "{text}");
    assert_eq!(text.matches("Logged in Carol Bot").count(), 2, "{text}");
    assert_eq!(
        text.matches("CALL chat channel=0 type=Normal only replacement")
            .count(),
        1,
        "{text}"
    );
    assert!(!text.contains("credential-one"), "password leaked:\n{text}");
    assert!(!text.contains("credential-two"), "password leaked:\n{text}");
}

#[test]
fn native_alias_manifest_and_parser_are_bounded_and_deterministic() {
    let directory = TestDir::new("load");
    let manifest = directory.write(
        "commands.tsv",
        "greet\tSend a greeting through channel 9\tsay 9 hello {args}\n",
    );
    let script = directory.write(
        "terminal.tsv",
        &format!(
            "!client\t{ALICE}\tAlice\tBot\n\
             load \"{}\"\n\
             help greet\n\
             greet \"quoted world\"\n\
             say 'unterminated\n\
             quit\n",
            path_text(&manifest)
        ),
    );
    let output = run(&["--fake-script", path_text(&script)]);
    assert!(output.status.success(), "{}", stderr(&output));
    let text = stdout(&output);
    assert!(text.contains("Loaded 1 native command alias"), "{text}");
    assert!(text.contains("Send a greeting through channel 9"), "{text}");
    assert!(
        text.contains("CALL chat channel=9 type=Normal hello quoted world"),
        "{text}"
    );
    assert!(
        text.contains("Command parse error: unterminated quoted token"),
        "{text}"
    );
}

#[test]
fn malformed_inputs_and_cancellation_fail_or_shutdown_cleanly() {
    let directory = TestDir::new("invalid");
    let invalid = directory.write("terminal.tsv", "!unknown\tvalue\n");
    let output = run(&["--fake-script", path_text(&invalid)]);
    assert_exit(&output, EXIT_INPUT);
    assert!(stderr(&output).contains("line 1"));

    let cancelled = directory.write(
        "cancelled.tsv",
        &format!(
            "!client\t{ALICE}\tAlice\tBot\n\
             !cancel\n\
             sleep 60\n\
             !shutdown\n"
        ),
    );
    let output = run(&["--fake-script", path_text(&cancelled)]);
    assert!(output.status.success(), "{}", stderr(&output));
    let text = stdout(&output);
    assert!(text.contains("Pause cancelled; agent resumed"), "{text}");
    assert!(text.contains("CALL agent-pause"), "{text}");
    assert!(text.contains("CALL agent-resume"), "{text}");
    assert!(text.contains("connected=0"), "{text}");
    assert!(text.contains("active-tasks=0 shutdown=true"), "{text}");
}
