use libremetaverse::Primitive;
use libremetaverse::structured_data::{OSD, OSDParser};
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 JSON_FIXTURE: &[u8] =
    include_bytes!("../../tests/fixtures/structured_data/json_reference.json");
const EXIT_USAGE: i32 = 2;
const EXIT_IO: i32 = 3;
const EXIT_INVALID_OSD: i32 = 4;
const EXIT_INVALID_PRIMITIVE: i32 = 5;

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-osd-inspector-{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], stdin: Option<&[u8]>) -> Output {
    let mut command = Command::new(env!("CARGO_BIN_EXE_osd-inspector"));
    command
        .args(args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    if stdin.is_some() {
        command.stdin(Stdio::piped());
    } else {
        command.stdin(Stdio::null());
    }
    let mut child = command.spawn().expect("spawn osd-inspector");
    if let Some(bytes) = stdin {
        child
            .stdin
            .take()
            .expect("piped stdin")
            .write_all(bytes)
            .expect("write command stdin");
    }
    child.wait_with_output().expect("collect command output")
}

fn utf8(bytes: &[u8]) -> &str {
    std::str::from_utf8(bytes).expect("command output 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)
    );
}

fn write_fixture(directory: &TestDir) -> PathBuf {
    let path = directory.path("reference.json");
    fs::write(&path, JSON_FIXTURE).expect("write JSON fixture");
    path
}

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

#[test]
fn help_usage_and_upstream_aliases_are_stable() {
    let no_args = run(&[], None);
    assert_exit(&no_args, EXIT_USAGE);
    assert!(utf8(&no_args.stderr).contains("Usage: osd-inspector"));

    let help = run(&["--help"], None);
    assert!(help.status.success());
    let help = utf8(&help.stdout);
    for command in [
        "inspect",
        "convert",
        "validate",
        "prim-to-osd",
        "osd-to-prim",
    ] {
        assert!(help.contains(command), "help omitted {command}");
    }

    let directory = TestDir::new("aliases");
    let input = write_fixture(&directory);
    let input = path_text(&input);
    for (command, expected) in [
        ("i", "Structure:"),
        ("inspect", "Map (4 keys)"),
        ("v", "Valid OSD file"),
        ("validate", "Keys: 4"),
    ] {
        let output = run(&[command, input], None);
        assert!(
            output.status.success(),
            "{command}: {}",
            utf8(&output.stderr)
        );
        assert!(utf8(&output.stdout).contains(expected));
    }
}

#[test]
fn inspect_is_deterministic_and_validate_accepts_standard_input() {
    let directory = TestDir::new("inspect");
    let input = write_fixture(&directory);
    let output = run(&["inspect", path_text(&input)], None);
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    let output = utf8(&output.stdout);
    assert!(output.contains("Type: Map"));
    assert!(output.contains(&format!("Size: {} bytes", JSON_FIXTURE.len())));
    let array = output.find("  array:").expect("array key");
    let false_value = output.find("  false:").expect("false key");
    let true_value = output.find("  true:").expect("true key");
    let zero = output.find("  zero:").expect("zero key");
    assert!(array < false_value && false_value < true_value && true_value < zero);

    let output = run(&["validate", "-"], Some(JSON_FIXTURE));
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    assert_eq!(
        utf8(&output.stdout),
        "Valid OSD file\n  Type: Map\n  Keys: 4\n"
    );
}

#[test]
fn convert_emits_library_golden_bytes_for_every_format() {
    let directory = TestDir::new("convert");
    let input = write_fixture(&directory);
    let expected = OSDParser::deserialize_json_with_string(
        std::str::from_utf8(JSON_FIXTURE)
            .expect("compatibility fixture is UTF-8")
            .to_owned(),
    )
    .expect("parse checked-in compatibility fixture");
    let cases: [(&str, &str, Vec<u8>); 4] = [
        (
            "json",
            "converted.json",
            OSDParser::serialize_json_string(expected.clone(), Some(true))
                .expect("serialize golden JSON")
                .into_bytes(),
        ),
        (
            "xml",
            "converted.xml",
            OSDParser::serialize_llsd_xml_bytes(expected.clone()).expect("serialize golden XML"),
        ),
        (
            "binary",
            "converted.bin",
            OSDParser::serialize_llsd_binary_with_osd(expected.clone())
                .expect("serialize golden binary"),
        ),
        (
            "notation",
            "converted.notation",
            OSDParser::serialize_llsd_notation(expected)
                .expect("serialize golden notation")
                .into_bytes(),
        ),
    ];

    for (index, (format, filename, golden)) in cases.into_iter().enumerate() {
        let destination = directory.path(filename);
        let command = if index == 0 { "convert" } else { "c" };
        let output = run(
            &[command, path_text(&input), format, path_text(&destination)],
            None,
        );
        assert!(
            output.status.success(),
            "{format}: {}",
            utf8(&output.stderr)
        );
        assert_eq!(fs::read(&destination).expect("read converted file"), golden);
        assert!(utf8(&output.stdout).contains(&format!("to {format}:")));

        let output = run(&["validate", path_text(&destination)], None);
        assert!(
            output.status.success(),
            "could not read back {format}: {}",
            utf8(&output.stderr)
        );
        assert!(utf8(&output.stdout).contains("Type: Map"));
    }
}

#[test]
fn every_documented_output_format_alias_is_accepted() {
    let directory = TestDir::new("format-aliases");
    let input = write_fixture(&directory);
    let expected = OSDParser::deserialize_json_with_string(
        std::str::from_utf8(JSON_FIXTURE)
            .expect("compatibility fixture is UTF-8")
            .to_owned(),
    )
    .expect("parse checked-in compatibility fixture");
    let aliases = [
        (
            "j",
            OSDParser::serialize_json_string(expected.clone(), Some(true))
                .expect("serialize JSON")
                .into_bytes(),
        ),
        (
            "x",
            OSDParser::serialize_llsd_xml_bytes(expected.clone()).expect("serialize XML"),
        ),
        (
            "bin",
            OSDParser::serialize_llsd_binary_with_osd(expected.clone()).expect("serialize binary"),
        ),
        (
            "b",
            OSDParser::serialize_llsd_binary_with_osd(expected.clone()).expect("serialize binary"),
        ),
        (
            "llsd",
            OSDParser::serialize_llsd_notation(expected.clone())
                .expect("serialize notation")
                .into_bytes(),
        ),
        (
            "n",
            OSDParser::serialize_llsd_notation(expected)
                .expect("serialize notation")
                .into_bytes(),
        ),
    ];

    for (alias, golden) in aliases {
        let output = run(&["c", path_text(&input), alias, "-"], None);
        assert!(
            output.status.success(),
            "format alias {alias}: {}",
            utf8(&output.stderr)
        );
        assert_eq!(output.stdout, golden, "format alias {alias}");
    }
}

#[test]
fn conversion_supports_binary_standard_output_and_format_detection() {
    let expected = OSDParser::deserialize_json_with_string(
        std::str::from_utf8(JSON_FIXTURE)
            .expect("compatibility fixture is UTF-8")
            .to_owned(),
    )
    .expect("parse checked-in compatibility fixture");
    let output = run(&["convert", "-", "binary", "-"], Some(JSON_FIXTURE));
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    assert!(output.stderr.is_empty());
    assert_eq!(
        OSDParser::deserialize_llsd_binary_with_bytes(output.stdout)
            .expect("parse binary standard output"),
        expected
    );

    let binary = OSDParser::serialize_llsd_binary_with_osd(expected.clone())
        .expect("serialize binary fixture");
    let output = run(&["validate", "-"], Some(&binary));
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    assert!(utf8(&output.stdout).contains("Type: Map"));

    let notation =
        OSDParser::serialize_llsd_notation(expected).expect("serialize notation fixture");
    let output = run(&["validate", "-"], Some(notation.as_bytes()));
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    assert!(utf8(&output.stdout).contains("Type: Map"));
}

#[test]
#[allow(clippy::float_cmp)] // Exact integral coordinates are part of the golden sample.
fn sample_primitive_round_trips_through_the_public_native_api() {
    let output = run(&["prim-to-osd"], None);
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    let osd = OSDParser::deserialize_json_with_string(
        String::from_utf8(output.stdout.clone()).expect("sample command emits UTF-8"),
    )
    .expect("sample command emits JSON");
    let primitive = Primitive::from_osd(osd).expect("sample is native Primitive OSD");
    assert_eq!(primitive.local_id, 12_345);
    assert_eq!(primitive.position.x, 128.0);
    assert_eq!(primitive.position.y, 128.0);
    assert_eq!(primitive.position.z, 25.0);
    assert_eq!(
        primitive
            .properties
            .as_ref()
            .map(|value| value.name.as_str()),
        Some("Example Cube")
    );

    let directory = TestDir::new("primitive");
    let path = directory.path("cube.json");
    fs::write(&path, output.stdout).expect("write primitive JSON");
    let output = run(&["osd-to-prim", path_text(&path)], None);
    assert!(output.status.success(), "{}", utf8(&output.stderr));
    let summary = utf8(&output.stdout);
    for expected in [
        "Successfully parsed Primitive:",
        "Name: Example Cube",
        "Position: <128, 128, 25>",
        "Scale: <1, 1, 1>",
        "Material: Wood",
        "PCode: Prim",
    ] {
        assert!(
            summary.contains(expected),
            "summary omitted {expected}:\n{summary}"
        );
    }
}

#[test]
fn failures_use_stderr_and_documented_exit_codes() {
    let directory = TestDir::new("failures");
    let malformed = directory.path("malformed.json");
    fs::write(&malformed, b"{\"unterminated\": [1, 2}").expect("write malformed fixture");
    let output = run(&["validate", path_text(&malformed)], None);
    assert_exit(&output, EXIT_INVALID_OSD);
    assert!(output.stdout.is_empty());
    assert!(utf8(&output.stderr).contains("invalid OSD input"));

    let primitive = directory.path("not-a-primitive.json");
    fs::write(&primitive, b"[1,2,3]").expect("write non-primitive fixture");
    let output = run(&["osd-to-prim", path_text(&primitive)], None);
    assert_exit(&output, EXIT_INVALID_PRIMITIVE);
    assert!(utf8(&output.stderr).contains("not a Primitive map"));

    let missing = directory.path("missing.json");
    let output = run(&["inspect", path_text(&missing)], None);
    assert_exit(&output, EXIT_IO);
    assert!(utf8(&output.stderr).contains("opening"));

    let input = write_fixture(&directory);
    let output = run(
        &["--max-input-bytes", "4", "validate", path_text(&input)],
        None,
    );
    assert_exit(&output, EXIT_INVALID_OSD);
    assert!(utf8(&output.stderr).contains("4-byte limit"));

    let output = run(
        &[
            "convert",
            path_text(&input),
            "json",
            path_text(&directory.0),
        ],
        None,
    );
    assert_exit(&output, EXIT_IO);
    assert!(utf8(&output.stderr).contains("creating"));
}

#[test]
fn scalar_json_is_not_confused_with_llsd_notation() {
    for (input, expected_type) in [
        (b"true".as_slice(), "Boolean"),
        (b"42", "Integer"),
        (br#""text""#, "String"),
        (b"null", "Unknown"),
    ] {
        let output = run(&["validate", "-"], Some(input));
        assert!(output.status.success(), "{}", utf8(&output.stderr));
        assert!(utf8(&output.stdout).contains(&format!("Type: {expected_type}")));
    }

    for invalid in [b"".as_slice(), b" \t\r\n"] {
        let output = run(&["validate", "-"], Some(invalid));
        assert_exit(&output, EXIT_INVALID_OSD);
    }
}

#[test]
fn inspected_fixture_retains_expected_value_types() {
    let expected = OSDParser::deserialize_json_with_string(
        std::str::from_utf8(JSON_FIXTURE)
            .expect("compatibility fixture is UTF-8")
            .to_owned(),
    )
    .expect("parse checked-in compatibility fixture");
    let OSD::Map(values) = expected else {
        panic!("compatibility fixture must remain a map");
    };
    assert!(matches!(values.get("array"), Some(OSD::Array(_))));
    assert!(matches!(values.get("false"), Some(OSD::Boolean(false))));
    assert!(matches!(values.get("true"), Some(OSD::Boolean(true))));
    assert!(matches!(values.get("zero"), Some(OSD::Integer(0))));
}
