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-inventory-explorer-{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_inventory-explorer"))
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("run inventory-explorer")
}

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("inventory.json");
    fs::write(
        &path,
        r#"{
  "root_id":"10000000-0000-0000-0000-000000000000",
  "_embedded":{
    "categories":{
      "root":{"category_id":"10000000-0000-0000-0000-000000000000","parent_id":"00000000-0000-0000-0000-000000000000","name":"My Inventory","type_default":8,"version":4,"descendents":5},
      "objects":{"category_id":"20000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","name":"Objects","type_default":6,"version":2,"descendents":2},
      "scripts":{"category_id":"30000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","name":"Scripts","type_default":10,"version":2,"descendents":1},
      "empty":{"category_id":"40000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","name":"Empty","type_default":-1,"version":1,"descendents":0}
    },
    "items":{
      "sword":{"item_id":"50000000-0000-0000-0000-000000000000","parent_id":"20000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"51000000-0000-0000-0000-000000000000","name":"Sword","desc":"A test object","type":"object","inv_type":"object","created_at":1},
      "texture":{"item_id":"60000000-0000-0000-0000-000000000000","parent_id":"20000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"61000000-0000-0000-0000-000000000000","name":"Sword Texture","desc":"Texture","type":"texture","inv_type":"texture","created_at":2},
      "script":{"item_id":"70000000-0000-0000-0000-000000000000","parent_id":"30000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"71000000-0000-0000-0000-000000000000","name":"Sword Script","desc":"Script","type":"lsltext","inv_type":"script","created_at":3},
      "readme":{"item_id":"80000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"81000000-0000-0000-0000-000000000000","name":"Read Me","desc":"Notes","type":"notecard","inv_type":"notecard","created_at":4}
    },
    "links":{
      "shortcut":{"item_id":"90000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","linked_id":"50000000-0000-0000-0000-000000000000","name":"Sword Shortcut","desc":"Link","type":"object","inv_type":"texture","created_at":5}
    }
  }
}"#,
    )
    .expect("write AIS fixture");
    path
}

#[test]
fn help_preserves_upstream_options_and_documents_bounded_fake_mode() {
    let output = run(&["--help"]);
    assert!(output.status.success());
    let help = text(&output.stdout);
    for marker in [
        "[FIRSTNAME]",
        "[LASTNAME]",
        "[PASSWORD]",
        "--stats",
        "--search <TERM>",
        "--type <TYPE>",
        "--export <FILE>",
        "--fake-ais <FILE>",
        "--max-entries",
        "--max-export-bytes",
    ] {
        assert!(help.contains(marker), "help omitted {marker}:\n{help}");
    }
    let output = run(&[]);
    assert_exit(&output, EXIT_USAGE);
    assert!(text(&output.stderr).contains("Usage: inventory-explorer"));
}

#[test]
fn fake_ais_top_level_and_statistics_use_real_hierarchical_store() {
    let directory = TestDir::new("browse-stats");
    let fixture = fixture(&directory);
    let output = run(&["--fake-ais", path_text(&fixture)]);
    assert!(output.status.success(), "{}", text(&output.stderr));
    let output = text(&output.stdout);
    for expected in [
        "CALL AIS fetch root=10000000-0000-0000-0000-000000000000",
        "Fake AIS inventory loaded: 9 entries",
        "=== Inventory Tree (Top Level) ===",
        "Folder: Empty (0 entries)",
        "Folder: Objects (2 entries)",
        "Folder: Scripts (1 entries)",
        "Item: Read Me (Notecard)",
        "Link: Sword Shortcut (Link -> 50000000-0000-0000-0000-000000000000)",
        "Fake grid logout complete; active_tasks=0 open_sockets=0",
    ] {
        assert!(
            output.contains(expected),
            "output omitted {expected}:\n{output}"
        );
    }
    assert!(output.find("Folder: Empty").unwrap() < output.find("Folder: Objects").unwrap());

    let output = run(&["--fake-ais", path_text(&fixture), "--stats"]);
    assert!(output.status.success(), "{}", text(&output.stderr));
    let output = text(&output.stdout);
    for expected in [
        "=== Inventory Statistics ===",
        "Total Items: 5",
        "Root Folders: 3",
        "Link: 1 items",
        "LSLText: 1 items",
        "Notecard: 1 items",
        "Object: 1 items",
        "Texture: 1 items",
    ] {
        assert!(
            output.contains(expected),
            "stats omitted {expected}:\n{output}"
        );
    }
}

#[test]
fn search_filter_limit_parent_and_link_details_are_deterministic() {
    let directory = TestDir::new("search");
    let fixture = fixture(&directory);
    let output = run(&[
        "--fake-ais",
        path_text(&fixture),
        "--search",
        "sWoRd",
        "--type",
        "TeXtUrE",
    ]);
    assert!(output.status.success(), "{}", text(&output.stderr));
    let output = text(&output.stdout);
    assert!(output.contains("Found 1 matching entries"));
    assert!(output.contains("Sword Texture\nType: Texture\nFolder: Objects"));
    assert!(!output.contains("Sword Script\n"));
    assert!(!output.contains("Sword Shortcut\n"));

    let output = run(&[
        "--fake-ais",
        path_text(&fixture),
        "--search",
        "Sword",
        "--max-results",
        "2",
    ]);
    assert!(output.status.success());
    let output = text(&output.stdout);
    assert!(output.contains("Found 4 matching entries"));
    assert!(output.contains("... and 2 more results"));
    assert!(output.contains("Sword\nType: Object"));
    assert!(output.contains("Sword Script\nType: LSLText"));
}

#[test]
fn export_is_hierarchical_deterministic_and_bounded() {
    let directory = TestDir::new("export");
    let fixture = fixture(&directory);
    let first = directory.path("first.txt");
    let second = directory.path("second.txt");
    for destination in [&first, &second] {
        let output = run(&[
            "--fake-ais",
            path_text(&fixture),
            "--export",
            path_text(destination),
        ]);
        assert!(output.status.success(), "{}", text(&output.stderr));
        assert!(text(&output.stdout).contains("Exported 8 entries"));
    }
    let first_bytes = fs::read(&first).expect("read first export");
    let second_bytes = fs::read(&second).expect("read second export");
    assert_eq!(first_bytes, second_bytes);
    let export = text(&first_bytes);
    assert_eq!(
        export,
        concat!(
            "LibreMetaverse Inventory Export\n",
            "Format: deterministic-v1\n",
            "Entries: 8\n",
            "\n",
            "[Folder] Empty\n",
            "[Folder] Objects\n",
            "  Sword (Object) - 50000000-0000-0000-0000-000000000000\n",
            "  Sword Texture (Texture) - 60000000-0000-0000-0000-000000000000\n",
            "[Folder] Scripts\n",
            "  Sword Script (LSLText) - 70000000-0000-0000-0000-000000000000\n",
            "Read Me (Notecard) - 80000000-0000-0000-0000-000000000000\n",
            "Sword Shortcut (Link -> 50000000-0000-0000-0000-000000000000) - ",
            "90000000-0000-0000-0000-000000000000\n",
        )
    );

    let limited = directory.path("limited.txt");
    let output = run(&[
        "--fake-ais",
        path_text(&fixture),
        "--export",
        path_text(&limited),
        "--max-export-bytes",
        "10",
    ]);
    assert_exit(&output, EXIT_OUTPUT);
    assert!(!limited.exists());
    assert!(text(&output.stderr).contains("export exceeds the byte limit"));
}

#[test]
fn invalid_fixtures_options_filesystem_and_credentials_fail_explicitly() {
    let directory = TestDir::new("invalid");
    let malformed = directory.path("malformed.json");
    fs::write(&malformed, "{bad-json}").expect("write malformed fixture");
    let output = run(&["--fake-ais", 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-ais", path_text(&fixture), "--type", "Texture"]);
    assert_exit(&output, EXIT_USAGE);
    assert!(text(&output.stderr).contains("--type requires --search"));

    let output = run(&[
        "--fake-ais",
        path_text(&fixture),
        "--search",
        "Sword",
        "--type",
        "Money",
    ]);
    assert_exit(&output, EXIT_USAGE);
    assert!(text(&output.stderr).contains("unknown asset type"));

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

    let output = run(&[
        "--fake-ais",
        path_text(&fixture),
        "--export",
        path_text(&directory.0),
    ]);
    assert_exit(&output, EXIT_OUTPUT);
    assert!(text(&output.stderr).contains("writing inventory export"));
}
