// Exact Rust translations of LibreMetaverse.Tests/InventoryFetchTests.cs and
// LibreMetaverse.Tests/LibraryInventoryTests.cs.
// Source SHA-256: f5876550ab924ad034ade848e562ee70bbb4e004299d928f43c45cadd1a01465
// Source SHA-256: 1d65ebbaf8fd22427371e2a7f1ef76a4f9a70df0bea7fa53f8f5d3fd3044fa53

use libremetaverse::{
    Error, GridClient, InventoryFolder, InventoryItem, InventoryObjectClass, InventorySortOrder,
    PermissionMask,
};
use libremetaverse_compat_tests::{LiveTestRuntime, login_live_grid, logout_live_grid};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource};
use libremetaverse_types::{AssetType, FolderType, InventoryType, UUID};
use std::sync::Arc;
use std::time::Duration;

struct LiveInventory {
    client: GridClient,
    runtime: LiveTestRuntime,
    disposable_items: Vec<UUID>,
}

impl LiveInventory {
    fn login() -> Self {
        let runtime = LiveTestRuntime::new();
        let mut client = GridClient::new().expect("GridClient constructor");
        login_live_grid(
            &runtime,
            &mut client,
            "Unit Test Framework",
            "admin@radegast.life",
        );
        Self {
            client,
            runtime,
            disposable_items: Vec::new(),
        }
    }

    fn agent_id(&mut self) -> UUID {
        self.client.self_().agent_id()
    }

    fn create_disposable_texture(&mut self) -> (UUID, InventoryItem) {
        let manager = self.client.inventory();
        let folder = manager
            .find_folder_for_type_with_asset_type(AssetType::Texture)
            .expect("find OpenSim texture folder");
        assert_ne!(folder, UUID::zero(), "OpenSim has no texture folder");
        let marker = UUID::random().expect("inventory fixture UUID");
        let name = format!("MetaCrate Live Test {marker}");
        let agent_id = self.agent_id();
        for attempt in 1..=3 {
            let result = self.runtime.block_on(
                manager.create_item_with_uuid_string_string_asset_type_uuid_inventory_type_permission_mask_cancellation_token(
                    folder,
                    name.clone(),
                    "Disposable OpenSim compatibility fixture".into(),
                    AssetType::Texture,
                    UUID::zero(),
                    InventoryType::TEXTURE,
                    PermissionMask::ALL,
                    Some(cancellation_after(Duration::from_secs(30))),
                ),
            );
            match result {
                Ok(Some(item)) => {
                    self.disposable_items.push(item.base.uuid());
                    return (folder, item);
                }
                Ok(None) | Err(Error::Cancelled) => {}
                Err(error) => panic!("create disposable inventory item: {error}"),
            }

            // A UDP callback can be lost even though OpenSim committed the
            // request. Reconcile against the server before retrying so a lost
            // callback does not create duplicate fixtures.
            if let Ok(contents) = self.runtime.block_on(manager.folder_contents(
                folder,
                agent_id,
                true,
                true,
                InventorySortOrder::BY_NAME,
                Some(cancellation_after(Duration::from_secs(30))),
                None,
            )) && let Some(item) = items(&contents)
                .find(|item| item.base.name() == name)
                .cloned()
            {
                self.disposable_items.push(item.base.uuid());
                return (folder, item);
            }
            assert!(
                attempt < 3,
                "OpenSim did not create or return the disposable item"
            );
        }
        unreachable!("bounded inventory fixture retry loop")
    }
}

impl Drop for LiveInventory {
    fn drop(&mut self) {
        if let Ok(trash) = self
            .client
            .inventory()
            .find_folder_for_type_with_folder_type(FolderType::Trash)
        {
            for item in self.disposable_items.drain(..) {
                self.runtime
                    .block_on(
                        self.client
                            .inventory()
                            .move_item_with_uuid_uuid_cancellation_token_1cf693fd(
                                item,
                                trash,
                                Some(CancellationToken::default()),
                            ),
                    )
                    .expect("move disposable inventory item to trash");
            }
        }
        logout_live_grid(&self.runtime, &mut self.client);
    }
}

fn cancellation_after(duration: Duration) -> CancellationToken {
    let source = Arc::new(CancellationTokenSource::new());
    let token = source.token();
    std::thread::spawn(move || {
        std::thread::sleep(duration);
        source.cancel();
    });
    token
}

fn folders(entries: &[Box<dyn InventoryObjectClass>]) -> impl Iterator<Item = &InventoryFolder> {
    entries
        .iter()
        .filter_map(|entry| entry.as_any().downcast_ref::<InventoryFolder>())
}

fn items(entries: &[Box<dyn InventoryObjectClass>]) -> impl Iterator<Item = &InventoryItem> {
    entries.iter().filter_map(|entry| entry.inventory_item())
}

// parity-case: LibreMetaverse.Tests/InventoryFetchTests.cs::InventoryFetchTests.InventorySkeletonIsPopulatedAfterLogin::test ea1508e3c676639f00bb8a4c4c72d2ee668603bfbff40ee0b93d7f4beb607f75 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn inventory_skeleton_is_populated_after_login() {
    let mut live = LiveInventory::login();
    let agent_id = live.agent_id();
    let store = live.client.inventory().store().expect("inventory store");
    let root = store.root_folder().expect("inventory root folder");
    assert_ne!(root.base.uuid(), UUID::zero());
    let children = store
        .get_contents_with_inventory_folder(&root)
        .expect("inventory root contents");
    assert!(!children.is_empty(), "inventory root has no children");
    assert_eq!(folders(&children).count(), children.len());
    let first = folders(&children).next().expect("inventory sub-folder");
    assert_eq!(first.base.owner_id(), agent_id);
}

// parity-case: LibreMetaverse.Tests/InventoryFetchTests.cs::InventoryFetchTests.InventoryRootContainsFolders::test 71f41c718060622e1fd653e6b0fc9fe65f3cd68cab3d128f494cdbc90f9d2726 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn inventory_root_contains_folders() {
    let mut live = LiveInventory::login();
    let agent_id = live.agent_id();
    let root = live
        .client
        .inventory()
        .store()
        .expect("inventory store")
        .root_folder()
        .expect("inventory root folder");
    assert_ne!(root.base.uuid(), UUID::zero());
    let contents = live
        .runtime
        .block_on(live.client.inventory().folder_contents(
            root.base.uuid(),
            agent_id,
            true,
            true,
            InventorySortOrder::BY_NAME,
            Some(cancellation_after(Duration::from_secs(30))),
            None,
        ))
        .expect("FolderContentsAsync");
    assert!(
        !contents.is_empty(),
        "inventory root fetch returned no results"
    );
    assert!(folders(&contents).next().is_some());
}

// parity-case: LibreMetaverse.Tests/InventoryFetchTests.cs::InventoryFetchTests.InventoryFolderContainsItems::test f0d77649b82d4d7d7778acebd851dd4eb44b76eb687af07bccdd20a1efe96223 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn inventory_folder_contains_items() {
    let mut live = LiveInventory::login();
    let (_folder, created) = live.create_disposable_texture();
    let created_id = created.base.uuid();
    let agent_id = live.agent_id();
    // OpenSim's CreateInventoryItem reply does not necessarily populate the
    // viewer-side store. Fetch the server-selected parent explicitly so this
    // case validates the grid's folder-contents service rather than cache
    // timing inherited from the Second Life test account.
    let actual_parent = created.base.parent_uuid();
    assert_ne!(
        actual_parent,
        UUID::zero(),
        "created item has no parent folder"
    );
    let contents = live
        .runtime
        .block_on(live.client.inventory().folder_contents(
            actual_parent,
            agent_id,
            true,
            true,
            InventorySortOrder::BY_NAME,
            Some(cancellation_after(Duration::from_secs(30))),
            None,
        ))
        .expect("fetch server-selected inventory folder contents");
    let fetched = items(&contents)
        .find(|item| item.base.uuid() == created_id)
        .expect("server-created item was not cached in its parent folder");
    assert_eq!(fetched.base.parent_uuid(), actual_parent);
    assert_eq!(fetched.asset_type(), AssetType::Texture);
    assert_eq!(fetched.inventory_type(), InventoryType::TEXTURE);
}

// parity-case: LibreMetaverse.Tests/InventoryFetchTests.cs::InventoryFetchTests.InventoryItemCanBeFetchedById::test 7fe89086754a673ab69814202b48aab9add30dd63ee98a953bdedd54d6788af2 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn inventory_item_can_be_fetched_by_id() {
    let mut live = LiveInventory::login();
    let agent_id = live.agent_id();
    let token = cancellation_after(Duration::from_secs(30));
    let (_, created) = live.create_disposable_texture();
    let target_id = created.base.uuid();
    let target_name = created.base.name();
    let fetched = live
        .runtime
        .block_on(
            live.client
                .inventory()
                .fetch_item_http(target_id, agent_id, Some(token)),
        )
        .expect("FetchItemHttpAsync")
        .expect("fetched inventory item");
    assert_eq!(fetched.base.uuid(), target_id);
    assert_eq!(fetched.base.name(), target_name);
}

// parity-case: LibreMetaverse.Tests/LibraryInventoryTests.cs::LibraryInventoryTests.LibraryFoldersArePopulatedAfterLogin::test dc865c88bb514bb71157a451e938b5ae3e3adde208d267d6a7196706dc9cfda2 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn library_folders_are_populated_after_login() {
    let live = LiveInventory::login();
    let store = live.client.inventory().store().expect("inventory store");
    let root = store.library_folder().expect("library root folder");
    assert_ne!(root.base.uuid(), UUID::zero());
    let children = store
        .get_contents_with_inventory_folder(&root)
        .expect("library root contents");
    assert!(!children.is_empty(), "library root has no children");
    assert_eq!(folders(&children).count(), children.len());
    assert_ne!(
        folders(&children)
            .next()
            .expect("library sub-folder")
            .base
            .owner_id(),
        UUID::zero()
    );
}

// parity-case: LibreMetaverse.Tests/LibraryInventoryTests.cs::LibraryInventoryTests.LibraryFolderItemsCanBeFetched::test 475561d29dd25aba52d94f8374fe9083fea2ed7af44dfb6d25b55ce487b080ab ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn library_folder_items_can_be_fetched() {
    let live = LiveInventory::login();
    let store = live.client.inventory().store().expect("inventory store");
    let root = store.library_folder().expect("library root folder");
    let skeleton = store
        .get_contents_with_inventory_folder(&root)
        .expect("library skeleton");
    let owner = folders(&skeleton)
        .map(|folder| folder.base.owner_id())
        .find(|owner| *owner != UUID::zero())
        .expect("library owner UUID");
    let contents = live
        .runtime
        .block_on(live.client.inventory().folder_contents(
            root.base.uuid(),
            owner,
            true,
            true,
            InventorySortOrder::BY_NAME,
            Some(cancellation_after(Duration::from_secs(30))),
            None,
        ))
        .expect("FolderContentsAsync");
    assert!(
        !contents.is_empty(),
        "library root fetch returned no results"
    );
    assert!(folders(&contents).next().is_some());
}

// parity-case: LibreMetaverse.Tests/LibraryInventoryTests.cs::LibraryInventoryTests.LibrarySubFolderContainsItems::test a6d02cbcb7fde8248419f1cf7355c3d37f06e8b365dc483fad935dd9a0ac5909 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn library_sub_folder_contains_items() {
    let live = LiveInventory::login();
    let store = live.client.inventory().store().expect("inventory store");
    let root = store.library_folder().expect("library root folder");
    let skeleton = store
        .get_contents_with_inventory_folder(&root)
        .expect("library skeleton");
    let subfolders: Vec<_> = folders(&skeleton).collect();
    assert!(!subfolders.is_empty(), "no library sub-folders");
    let owner = subfolders
        .iter()
        .map(|folder| folder.base.owner_id())
        .find(|owner| *owner != UUID::zero())
        .expect("library owner UUID");
    let token = cancellation_after(Duration::from_secs(30));
    let mut fetched_folders = 0_usize;
    for folder in subfolders {
        if token.is_cancellation_requested() {
            break;
        }
        let Ok(contents) = live
            .runtime
            .block_on(live.client.inventory().folder_contents(
                folder.base.uuid(),
                owner,
                true,
                true,
                InventorySortOrder::BY_NAME,
                Some(token.clone()),
                None,
            ))
        else {
            continue;
        };
        fetched_folders += 1;
        for entry in contents {
            assert_eq!(
                entry.inventory_base().parent_uuid(),
                folder.base.uuid(),
                "OpenSim returned a library child with the wrong parent"
            );
            assert_ne!(entry.inventory_base().uuid(), UUID::zero());
        }
    }
    assert!(
        fetched_folders > 0,
        "no OpenSim library sub-folder could be fetched"
    );
}
