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

const EXIT_USAGE: i32 = 2;
const EXIT_INPUT: i32 = 3;
const EXIT_OUTPUT: i32 = 5;
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-prim-inspector-{name}-{}-{id}",
            std::process::id()
        ));
        fs::create_dir(&path).expect("create 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_prim-inspector"))
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("run prim-inspector")
}

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

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

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 fixture(directory: &TestDir) -> PathBuf {
    let path = directory.path("objects.json");
    fs::write(
        &path,
        r#"{
  "region":"Fixture Region",
  "agent_position":[0,0,0],
  "objects":[
    {
      "id":"11111111-1111-1111-1111-111111111111",
      "localid":30,
      "name":"Far Lamp",
      "description":"A bright fixture",
      "position":[6,8,0],
      "rotation":[0,0,0,1],
      "scale":[1,2,3],
      "parentid":7,
      "material":3,
      "pcode":9,
      "physical":true,
      "light":{"color":[0.2,0.4,0.6,1],"intensity":0.75,"radius":12,"cutoff":0.5,"falloff":0.25},
      "flex":{"simulate_lod":2,"gravity":-0.5,"air_friction":1,"wind_sensitivity":0.2,"tension":0.8,"user_force":[0,0,0]},
      "owner":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      "creator":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
      "sale_type":"copy",
      "sale_price":25,
      "hover_text":"Welcome visitor",
      "property_state":"ready"
    },
    {
      "id":"22222222-2222-2222-2222-222222222222",
      "localid":20,
      "name":"Near Lamp",
      "description":"Hydrated later",
      "position":[3,4,0],
      "rotation":[0,0,0,1],
      "scale":[1,1,1],
      "sculpt":{"texture":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":5},
      "owner":"dddddddd-dddd-dddd-dddd-dddddddddddd",
      "creator":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
      "property_state":"delayed"
    },
    {
      "id":"33333333-3333-3333-3333-333333333333",
      "localid":10,
      "name":"Lost Lamp",
      "position":[1,0,0],
      "rotation":[0,0,0,1],
      "scale":[1,1,1],
      "property_state":"missing"
    },
    {
      "id":"44444444-4444-4444-4444-444444444444",
      "localid":40,
      "name":"Chair",
      "position":[-1,0,0],
      "rotation":[0,0,0,1],
      "scale":[1,1,1]
    }
  ]
}"#,
    )
    .expect("write fixture");
    path
}

#[test]
fn help_preserves_upstream_arguments_and_documents_bounded_controls() {
    let output = run(&["--help"]);
    assert!(output.status.success());
    let help = text(&output.stdout);
    for marker in [
        "[FIRSTNAME]",
        "[LASTNAME]",
        "[PASSWORD]",
        "[SEARCH_TERM]",
        "--fake-grid",
        "--search",
        "--limit",
        "--discovery-delay-ms",
        "--property-timeout-ms",
        "--max-output-bytes",
    ] {
        assert!(help.contains(marker), "help omitted {marker}:\n{help}");
    }

    let output = run(&[]);
    assert_exit(&output, EXIT_USAGE);
    assert!(text(&output.stderr).contains("Usage: prim-inspector"));
}

#[test]
fn fake_grid_golden_output_hydrates_orders_filters_and_formats_details() {
    let directory = TestDir::new("golden");
    let fixture = fixture(&directory);
    let output = run(&[
        "--fake-grid",
        path_text(&fixture),
        "--search",
        "Lamp",
        "--limit",
        "2",
        "--property-timeout-ms",
        "17",
    ]);
    assert!(output.status.success(), "{}", text(&output.stderr));
    assert!(output.stderr.is_empty());
    let output = text(&output.stdout);

    for expected in [
        "Fake grid connected",
        "Region: Fixture Region",
        "Agent position: <0.000, 0.000, 0.000>",
        "Found 4 primitives",
        "CALL select-object local_id=20",
        "PROPERTY received local_id=20",
        "CALL select-object local_id=10",
        "Property timeout after 17 ms: 1 primitives remain unknown",
        "Inspecting 2 nearest matching primitives",
        "Object: Near Lamp (ID: 22222222-2222-2222-2222-222222222222)",
        "Distance: 5.00 m",
        "Sculpt Type: Mesh",
        "Sculpt Texture: cccccccc-cccc-cccc-cccc-cccccccccccc",
        "Owner: dddddddd-dddd-dddd-dddd-dddddddddddd",
        "Description: Hydrated later",
        "Object: Far Lamp (ID: 11111111-1111-1111-1111-111111111111)",
        "Distance: 10.00 m",
        "Parent ID: 7",
        "Material:",
        "PCode: Prim",
        "Light Intensity: 0.750",
        "Light Radius: 12.000",
        "Flexible Softness: 2",
        "Flexible Gravity: -0.500",
        "For Sale: Copy for L$25",
        "Hover Text: Welcome visitor",
        "Fake grid logout complete; active_tasks=0 open_sockets=0",
    ] {
        assert!(
            output.contains(expected),
            "output omitted {expected}:\n{output}"
        );
    }
    assert!(output.find("Object: Near Lamp").unwrap() < output.find("Object: Far Lamp").unwrap());
    assert!(!output.contains("Object: Chair"));
    assert!(!output.contains("Object: Lost Lamp"));
}

#[test]
fn nearest_limit_has_deterministic_distance_and_local_id_tie_breaking() {
    let directory = TestDir::new("ordering");
    let fixture = fixture(&directory);
    let output = run(&["--fake-grid", path_text(&fixture), "--limit", "3"]);
    assert!(output.status.success(), "{}", text(&output.stderr));
    let output = text(&output.stdout);
    let lost = output.find("Object: Unknown").unwrap();
    let chair = output.find("Object: Chair").unwrap();
    let near = output.find("Object: Near Lamp").unwrap();
    assert!(lost < chair && chair < near, "unexpected order:\n{output}");
    assert!(!output.contains("Object: Far Lamp"));
}

#[test]
fn malformed_limits_and_live_credentials_fail_without_partial_or_secret_output() {
    let directory = TestDir::new("invalid");
    let malformed = directory.path("malformed.json");
    fs::write(&malformed, "{not-json}").expect("write malformed fixture");
    let output = run(&["--fake-grid", path_text(&malformed)]);
    assert_exit(&output, EXIT_INPUT);
    assert!(output.stdout.is_empty());
    assert!(text(&output.stderr).contains("could not parse LLSD JSON"));

    let fixture = fixture(&directory);
    let output = run(&[
        "--fake-grid",
        path_text(&fixture),
        "--max-output-bytes",
        "10",
    ]);
    assert_exit(&output, EXIT_OUTPUT);
    assert!(output.stdout.is_empty());
    assert!(text(&output.stderr).contains("output byte limit"));

    let password = "do-not-print-this-password";
    let output = run(&[
        "First",
        "Last",
        password,
        "--fake-grid",
        path_text(&fixture),
    ]);
    assert_exit(&output, EXIT_USAGE);
    assert!(!text(&output.stdout).contains(password));
    assert!(!text(&output.stderr).contains(password));

    let output = run(&["--fake-grid", path_text(&fixture), "--limit", "0"]);
    assert_exit(&output, EXIT_USAGE);
}
