// Exact Rust translations of LibreMetaverse.Tests/InventoryTests.cs.
// Source SHA-256: 378ab668bdea082d90fb0105e2bcd6b7734d7fe9eb9c1bf550554bd4ef700953

use libremetaverse::{GridClient, Inventory, InventoryFolder, InventoryItem, InventoryNode};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::{AssetType, UUID};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

fn uuid() -> UUID {
    UUID::random().expect("UUID Random")
}

struct Fixture {
    client: Arc<GridClient>,
    inventory: Inventory,
}

impl Fixture {
    fn new() -> Self {
        let client = Arc::new(GridClient::new().expect("GridClient constructor"));
        let inventory = Inventory::new_with_grid_client_uuid(Arc::clone(&client), uuid())
            .expect("Inventory constructor");
        Self { client, inventory }
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = self.client.dispose_with_method();
    }
}

fn folder(name: &str, parent: UUID) -> InventoryFolder {
    let mut folder = InventoryFolder::new(uuid()).expect("InventoryFolder constructor");
    folder.base.set_name(name.into());
    folder.base.set_parent_uuid(parent);
    folder
}

fn item(name: &str, parent: UUID) -> InventoryItem {
    let mut item = InventoryItem::new_with_uuid(uuid()).expect("InventoryItem constructor");
    item.base.set_name(name.into());
    item.base.set_parent_uuid(parent);
    item
}

struct TempDirectory(PathBuf);

impl TempDirectory {
    fn new() -> Self {
        let path = std::env::temp_dir().join(format!("metacrate-inventory-{}", uuid().to_string()));
        std::fs::create_dir(&path).expect("create isolated temporary directory");
        Self(path)
    }

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

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

fn path_string(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.AddAndGetItem::test 24702f0cf9215beee29123ee81dab4a7fe971beb197ecceac9a1761b695bce45 translated
#[test]
fn add_and_get_item() {
    let fixture = Fixture::new();
    let folder = folder("TestFolder", UUID::zero());
    let folder_id = folder.base.uuid();
    fixture.inventory.update_node_for(&folder).unwrap();
    let item = item("Item1", folder_id);
    let item_id = item.base.uuid();
    fixture.inventory.update_node_for(&item).unwrap();

    assert!(fixture.inventory.contains_with_uuid(item_id).unwrap());
    let mut found = None;
    assert!(
        fixture
            .inventory
            .try_get_value_with_uuid_inventory_base(item_id, &mut found)
            .unwrap()
    );
    let found = found.expect("inventory item");
    assert!(found.as_any().is::<InventoryItem>());
    assert_eq!(found.inventory_base().name(), "Item1");

    let contents = fixture
        .inventory
        .get_contents_with_inventory_folder(&folder)
        .unwrap();
    assert_eq!(contents.len(), 1);
    assert_eq!(contents[0].inventory_base().uuid(), item_id);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.UpdateItemParent::test ea1a73e28fb92b873ee28102e81811d769b5cd815d2e1d756ece6ea635e5c014 translated
#[test]
fn update_item_parent() {
    let fixture = Fixture::new();
    let folder1 = folder("F1", UUID::zero());
    let folder2 = folder("F2", UUID::zero());
    let folder1_id = folder1.base.uuid();
    let folder2_id = folder2.base.uuid();
    fixture.inventory.update_node_for(&folder1).unwrap();
    fixture.inventory.update_node_for(&folder2).unwrap();
    let mut item = item("Item", folder1_id);
    let item_id = item.base.uuid();
    fixture.inventory.update_node_for(&item).unwrap();
    assert_eq!(item.base.parent_uuid(), folder1_id);

    item.base.set_parent_uuid(folder2_id);
    fixture.inventory.update_node_for(&item).unwrap();

    let contents1 = fixture
        .inventory
        .get_contents_with_inventory_folder(&folder1)
        .unwrap();
    let contents2 = fixture
        .inventory
        .get_contents_with_inventory_folder(&folder2)
        .unwrap();
    assert!(
        contents1
            .iter()
            .all(|entry| entry.inventory_base().uuid() != item_id)
    );
    assert!(
        contents2
            .iter()
            .any(|entry| entry.inventory_base().uuid() == item_id)
    );
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.InventoryNodeModifyTime_SelfLoop::test f8050cc284f7954c1301091ddc1cbf0fa76073806eee7de4f6cb21063591fbf1 translated
#[test]
fn inventory_node_modify_time_self_loop() {
    let folder_id = uuid();
    let mut folder = InventoryFolder::new(folder_id).unwrap();
    folder.base.set_name("F1".into());
    folder.base.set_parent_uuid(folder_id);
    let node = InventoryNode::new_with_inventory_base(&folder).unwrap();
    assert_eq!(node.modify_time(), std::time::SystemTime::UNIX_EPOCH);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.UpdateNodeFor_SelfLoop::test 14f42e5263c465e8534347c5a3195c195e2d90371dbc2f69d36a40362384505c translated
#[test]
fn update_node_for_self_loop() {
    let fixture = Fixture::new();
    let folder_id = uuid();
    let mut folder = InventoryFolder::new(folder_id).unwrap();
    folder.base.set_name("F1".into());
    folder.base.set_parent_uuid(folder_id);
    fixture.inventory.update_node_for(&folder).unwrap();
    assert!(fixture.inventory.contains_with_uuid(folder_id).unwrap());
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.UpdateNodeFor_ComplexSelfLoop::test a2fc7b29b8a7a7767c42ea9c7fb9628451c88991c3eb1ecbd2ce0952ab0b5700 translated
#[test]
fn update_node_for_complex_self_loop() {
    let fixture = Fixture::new();
    let folder1_id = uuid();
    let folder2_id = uuid();
    let mut folder1 = InventoryFolder::new(folder1_id).unwrap();
    folder1.base.set_name("F1".into());
    folder1.base.set_parent_uuid(folder2_id);
    let mut folder2 = InventoryFolder::new(folder2_id).unwrap();
    folder2.base.set_name("F2".into());
    folder2.base.set_parent_uuid(folder1_id);

    fixture.inventory.update_node_for(&folder1).unwrap();
    assert!(fixture.inventory.contains_with_uuid(folder1_id).unwrap());
    assert!(fixture.inventory.contains_with_uuid(folder2_id).unwrap());
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.RemoveItem::test 1e72f9049b9114f0c56e152d84190d31bca7a862f7ad1459624d753beb6818b4 translated
#[test]
fn remove_item() {
    let fixture = Fixture::new();
    let folder = folder("Folder", UUID::zero());
    let folder_id = folder.base.uuid();
    fixture.inventory.update_node_for(&folder).unwrap();
    let item = item("ToRemove", folder_id);
    let item_id = item.base.uuid();
    fixture.inventory.update_node_for(&item).unwrap();
    assert!(fixture.inventory.contains_with_uuid(item_id).unwrap());
    fixture.inventory.remove_node_for(&item).unwrap();
    assert!(!fixture.inventory.contains_with_uuid(item_id).unwrap());
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.SaveAndRestoreAsync::test bf23b1c67cc9618713d2d8e6653f0ffbbf4c24d7f2c59c5dd639047f7e0c8156 translated
#[test]
fn save_and_restore_async() {
    let fixture = Fixture::new();
    let folder = folder("FolderSave", UUID::zero());
    let folder_id = folder.base.uuid();
    fixture.inventory.update_node_for(&folder).unwrap();
    let item = item("SaveItem", folder_id);
    let item_id = item.base.uuid();
    fixture.inventory.update_node_for(&item).unwrap();
    let temporary = TempDirectory::new();
    let file = path_string(&temporary.file("inventory.bin"));

    block_on(
        fixture
            .inventory
            .save_to_disk_with_string_cancellation_token(file.clone(), None),
    )
    .unwrap();
    fixture.inventory.clear().unwrap();
    assert!(!fixture.inventory.contains_with_uuid(item_id).unwrap());
    let count = block_on(
        fixture
            .inventory
            .restore_from_disk_with_string_cancellation_token(file, None),
    )
    .unwrap();
    assert!(count >= 0);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.GetNodeOrDefault_ReturnsNullWhenMissing::test f5651b5a143470c3ab6ba1f835206a1a55a6218c28242dc12c5d6b5e89519596 translated
#[test]
fn get_node_or_default_returns_null_when_missing() {
    assert!(
        Fixture::new()
            .inventory
            .get_node_or_default(uuid())
            .unwrap()
            .is_none()
    );
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.TryGetValue_Generic::test e6f6d6535af9d011e65cb3c297fe99543f85b64382c47e1756f051d6031886f5 translated
#[test]
fn try_get_value_generic() {
    let fixture = Fixture::new();
    let folder = folder("GenFolder", UUID::zero());
    let folder_id = folder.base.uuid();
    fixture.inventory.update_node_for(&folder).unwrap();
    let item = item("GenItem", folder_id);
    let item_id = item.base.uuid();
    fixture.inventory.update_node_for(&item).unwrap();
    let mut found = None;
    assert!(
        fixture
            .inventory
            .try_get_value_with_uuid_t::<InventoryItem>(item_id, &mut found)
    );
    let found = found.expect("generic inventory item");
    assert_eq!(found.base.uuid(), item_id);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.RemoveFolderRemovesAllDescendants::test b537223d271b75c11adfa6d87c1e79bb29366f294657ddf71f236fab0dee009f translated
#[test]
fn remove_folder_removes_all_descendants() {
    let fixture = Fixture::new();
    let parent = folder("Parent", UUID::zero());
    let parent_id = parent.base.uuid();
    let child = folder("ChildFolder", parent_id);
    let child_id = child.base.uuid();
    let item = item("NestedItem", child_id);
    let item_id = item.base.uuid();
    fixture.inventory.update_node_for(&parent).unwrap();
    fixture.inventory.update_node_for(&child).unwrap();
    fixture.inventory.update_node_for(&item).unwrap();
    assert!(fixture.inventory.contains_with_uuid(parent_id).unwrap());
    assert!(fixture.inventory.contains_with_uuid(child_id).unwrap());
    assert!(fixture.inventory.contains_with_uuid(item_id).unwrap());

    fixture.inventory.remove_node_for(&parent).unwrap();
    assert!(!fixture.inventory.contains_with_uuid(parent_id).unwrap());
    assert!(!fixture.inventory.contains_with_uuid(child_id).unwrap());
    assert!(!fixture.inventory.contains_with_uuid(item_id).unwrap());
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.DescendentCountUpdatesOnAddAndRemove::test cfe6571887d0e992ff4d69ccbcdfb34012f9bb5d62118201c33d7d66cc9ddfd8 translated
#[test]
fn descendent_count_updates_on_add_and_remove() {
    let fixture = Fixture::new();
    let folder = folder("Folder", UUID::zero());
    let folder_id = folder.base.uuid();
    fixture.inventory.update_node_for(&folder).unwrap();
    let node = fixture.inventory.get_node_for(folder_id).unwrap();
    let data = node.data().expect("folder node data");
    let folder_data = data
        .as_any()
        .downcast_ref::<InventoryFolder>()
        .expect("InventoryFolder data");
    assert_eq!(folder_data.descendent_count(), 0);

    let item = item("Item", folder_id);
    fixture.inventory.update_node_for(&item).unwrap();
    let node = fixture.inventory.get_node_for(folder_id).unwrap();
    let data = node.data().expect("folder node data after add");
    let folder_data = data.as_any().downcast_ref::<InventoryFolder>().unwrap();
    assert!(folder_data.descendent_count() > 0);

    fixture.inventory.remove_node_for(&item).unwrap();
    let node = fixture.inventory.get_node_for(folder_id).unwrap();
    let data = node.data().expect("folder node data after remove");
    let folder_data = data.as_any().downcast_ref::<InventoryFolder>().unwrap();
    assert_eq!(folder_data.descendent_count(), 0);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.MovingItemUpdatesAncestorDescendentCounts::test 1f4b4189694b9471d48562a434c91a9e455706b83b65bb855c0846388b44c759 translated
#[test]
fn moving_item_updates_ancestor_descendent_counts() {
    let fixture = Fixture::new();
    let folder1 = folder("F1", UUID::zero());
    let folder2 = folder("F2", UUID::zero());
    let folder1_id = folder1.base.uuid();
    let folder2_id = folder2.base.uuid();
    fixture.inventory.update_node_for(&folder1).unwrap();
    fixture.inventory.update_node_for(&folder2).unwrap();
    let mut item = item("Movable", folder1_id);
    fixture.inventory.update_node_for(&item).unwrap();

    let first = fixture.inventory.get_node_for(folder1_id).unwrap();
    let first_data = first.data().unwrap();
    let first_folder = first_data
        .as_any()
        .downcast_ref::<InventoryFolder>()
        .unwrap();
    let second = fixture.inventory.get_node_for(folder2_id).unwrap();
    let second_data = second.data().unwrap();
    let second_folder = second_data
        .as_any()
        .downcast_ref::<InventoryFolder>()
        .unwrap();
    assert!(first_folder.descendent_count() > 0);
    assert_eq!(second_folder.descendent_count(), 0);

    item.base.set_parent_uuid(folder2_id);
    fixture.inventory.update_node_for(&item).unwrap();
    let first = fixture.inventory.get_node_for(folder1_id).unwrap();
    let first_data = first.data().unwrap();
    let first_folder = first_data
        .as_any()
        .downcast_ref::<InventoryFolder>()
        .unwrap();
    let second = fixture.inventory.get_node_for(folder2_id).unwrap();
    let second_data = second.data().unwrap();
    let second_folder = second_data
        .as_any()
        .downcast_ref::<InventoryFolder>()
        .unwrap();
    assert_eq!(first_folder.descendent_count(), 0);
    assert!(second_folder.descendent_count() > 0);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.ConcurrentAddMoveRemoveStress::test 0b76b2fb62a6300e02a02859772f146f107caa77986f4d593e3a4d01966ab4e4 translated
#[test]
fn concurrent_add_move_remove_stress() {
    const ITEM_COUNT: usize = 500;
    let fixture = Fixture::new();
    let folder1 = folder("Folder1", UUID::zero());
    let folder2 = folder("Folder2", UUID::zero());
    let folder3 = folder("Folder3", UUID::zero());
    let folder1_id = folder1.base.uuid();
    let folder2_id = folder2.base.uuid();
    let folder3_id = folder3.base.uuid();
    fixture.inventory.update_node_for(&folder1).unwrap();
    fixture.inventory.update_node_for(&folder2).unwrap();
    fixture.inventory.update_node_for(&folder3).unwrap();
    let items: Vec<_> = (0..ITEM_COUNT)
        .map(|index| Arc::new(Mutex::new(item(&format!("Item{index}"), folder1_id))))
        .collect();

    std::thread::scope(|scope| {
        for item in &items {
            let item = Arc::clone(item);
            let inventory = &fixture.inventory;
            scope.spawn(move || inventory.update_node_for(&*item.lock().unwrap()).unwrap());
        }
    });
    for item in &items {
        assert!(
            fixture
                .inventory
                .contains_with_uuid(item.lock().unwrap().base.uuid())
                .unwrap()
        );
    }

    std::thread::scope(|scope| {
        for (index, item) in items.iter().enumerate() {
            let item = Arc::clone(item);
            let inventory = &fixture.inventory;
            scope.spawn(move || {
                let mut item = item.lock().unwrap();
                item.base.set_parent_uuid(if index % 2 == 0 {
                    folder2_id
                } else {
                    folder3_id
                });
                inventory.update_node_for(&*item).unwrap();
            });
        }
    });
    let count = |folder_id| {
        let node = fixture.inventory.get_node_for(folder_id).unwrap();
        let data = node.data().unwrap();
        data.as_any()
            .downcast_ref::<InventoryFolder>()
            .unwrap()
            .descendent_count()
    };
    assert_eq!(count(folder1_id), 0);
    assert_eq!(
        count(folder2_id) + count(folder3_id),
        i32::try_from(ITEM_COUNT).unwrap()
    );

    std::thread::scope(|scope| {
        for item in &items {
            let item = Arc::clone(item);
            let inventory = &fixture.inventory;
            scope.spawn(move || inventory.remove_node_for(&*item.lock().unwrap()).unwrap());
        }
    });
    for item in &items {
        assert!(
            !fixture
                .inventory
                .contains_with_uuid(item.lock().unwrap().base.uuid())
                .unwrap()
        );
    }
    assert_eq!(count(folder1_id), 0);
    assert_eq!(count(folder2_id), 0);
    assert_eq!(count(folder3_id), 0);
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.ConcurrentLinkAddRemoveStress::test eef3b4eb300427800757dc6cac2dbd9202ed1cf1aac42565b3c26eb11e71bbf1 translated
#[test]
fn concurrent_link_add_remove_stress() {
    const ITEM_COUNT: usize = 200;
    let fixture = Fixture::new();
    let folder1 = folder("LinkFolder", UUID::zero());
    let folder2 = folder("LinkFolder2", UUID::zero());
    let folder1_id = folder1.base.uuid();
    let folder2_id = folder2.base.uuid();
    fixture.inventory.update_node_for(&folder1).unwrap();
    fixture.inventory.update_node_for(&folder2).unwrap();
    let asset_id = uuid();
    let items: Vec<_> = (0..ITEM_COUNT)
        .map(|index| {
            let mut item = item(&format!("L{index}"), folder1_id);
            item.set_asset_type(AssetType::Link);
            item.set_asset_uuid(asset_id);
            Arc::new(Mutex::new(item))
        })
        .collect();

    std::thread::scope(|scope| {
        for item in &items {
            let item = Arc::clone(item);
            let inventory = &fixture.inventory;
            scope.spawn(move || inventory.update_node_for(&*item.lock().unwrap()).unwrap());
        }
    });
    assert_eq!(
        fixture.inventory.find_all_links(asset_id).unwrap().len(),
        ITEM_COUNT
    );

    std::thread::scope(|scope| {
        for (index, item) in items.iter().enumerate() {
            let item = Arc::clone(item);
            let inventory = &fixture.inventory;
            scope.spawn(move || {
                let mut item = item.lock().unwrap();
                item.base.set_parent_uuid(if index % 2 == 0 {
                    folder2_id
                } else {
                    folder1_id
                });
                inventory.update_node_for(&*item).unwrap();
            });
        }
    });
    assert_eq!(
        fixture.inventory.find_all_links(asset_id).unwrap().len(),
        ITEM_COUNT
    );

    std::thread::scope(|scope| {
        for item in &items {
            let item = Arc::clone(item);
            let inventory = &fixture.inventory;
            scope.spawn(move || inventory.remove_node_for(&*item.lock().unwrap()).unwrap());
        }
    });
    assert!(
        fixture
            .inventory
            .find_all_links(asset_id)
            .unwrap()
            .is_empty()
    );
    for item in &items {
        assert!(
            !fixture
                .inventory
                .contains_with_uuid(item.lock().unwrap().base.uuid())
                .unwrap()
        );
    }
}

// parity-case: LibreMetaverse.Tests/InventoryTests.cs::InventoryTests.LinkIndexUpdateOnAssetChange::test d3e0066e06ab1c48520e5e0bbba8069b74f1a38d37e24d914392406b41b2ea0c translated
#[test]
fn link_index_updates_on_asset_change() {
    let fixture = Fixture::new();
    let folder = folder("LUpdate", UUID::zero());
    let folder_id = folder.base.uuid();
    fixture.inventory.update_node_for(&folder).unwrap();
    let asset1 = uuid();
    let asset2 = uuid();
    let mut item = item("LinkItem", folder_id);
    let item_id = item.base.uuid();
    item.set_asset_type(AssetType::Link);
    item.set_asset_uuid(asset1);
    fixture.inventory.update_node_for(&item).unwrap();

    let found1 = fixture.inventory.find_all_links(asset1).unwrap();
    assert_eq!(found1.len(), 1);
    let data = found1[0].data().expect("link node data");
    assert_eq!(data.inventory_base().uuid(), item_id);

    item.set_asset_uuid(asset2);
    fixture.inventory.update_node_for(&item).unwrap();
    let found1 = fixture.inventory.find_all_links(asset1).unwrap();
    let found2 = fixture.inventory.find_all_links(asset2).unwrap();
    assert!(found1.is_empty());
    assert_eq!(found2.len(), 1);
    let data = found2[0].data().expect("updated link node data");
    assert_eq!(data.inventory_base().uuid(), item_id);
}
