// Exact Rust translations of LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs.
// Source SHA-256: f444adc7615a805eddeef538ce939375dc2a2fa2cd871aab2ebce37b4e955941

use libremetaverse::{
    GridClient, HttpCapsClient, InventoryFolder, InventoryItem, InventoryManager,
    InventoryObjectClass, Simulator,
};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{
    CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;

const CAP: &str = "http://test.invalid/request-task-inventory";
const SEED: &str = "http://test.invalid/seed";

#[derive(Clone)]
struct Recording(Arc<Mutex<Vec<HttpRequest>>>);

impl Recording {
    fn requests(&self) -> Vec<HttpRequest> {
        self.0.lock().unwrap().clone()
    }
}

fn client_with_cap(response: HttpResponse, expose_capability: bool) -> (GridClient, Recording) {
    let requests = Arc::new(Mutex::new(Vec::new()));
    let recorded = Arc::clone(&requests);
    let response = Mutex::new(Some(response));
    let seed = if expose_capability {
        format!("<llsd><map><key>RequestTaskInventory</key><uri>{CAP}</uri></map></llsd>")
    } else {
        "<llsd><map></map></llsd>".into()
    };
    let handler = HttpMessageHandler::new(move |request, _| {
        let result = if request.uri.0 == SEED {
            HttpResponse {
                status_code: 200,
                headers: BTreeMap::new(),
                content_type: Some("application/llsd+xml".into()),
                body: seed.as_bytes().to_vec(),
            }
        } else {
            recorded.lock().unwrap().push(request);
            response.lock().unwrap().take().unwrap_or(HttpResponse {
                status_code: 404,
                headers: BTreeMap::new(),
                content_type: None,
                body: Vec::new(),
            })
        };
        async move { result }
    });
    let mut client = GridClient::new().expect("GridClient constructor");
    client.set_http_caps_client(HttpCapsClient::new(handler.clone()).unwrap());
    let mut simulator_client = GridClient::new().expect("simulator GridClient constructor");
    simulator_client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
    let simulator = Simulator::new(
        simulator_client,
        "127.0.0.1:0".parse().unwrap(),
        0,
        None,
        None,
    )
    .unwrap();
    simulator
        .set_seed_caps(Some(Uri(SEED.into())), Some(true))
        .unwrap();
    let mut network = client.network();
    network.set_current_sim(Some(simulator));
    client.set_network(network);
    (client, Recording(requests))
}

fn response(status: u16, body: String) -> HttpResponse {
    HttpResponse {
        status_code: status,
        headers: BTreeMap::new(),
        content_type: Some("application/json".into()),
        body: body.into_bytes(),
    }
}

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

fn item_json(
    item_id: UUID,
    parent_id: UUID,
    asset_or_shadow_key: &str,
    asset_or_shadow_value: UUID,
) -> String {
    format!(
        concat!(
            "{{\"item_id\":\"{}\",\"parent_id\":\"{}\",",
            "\"name\":\"Test Item\",\"desc\":\"A test item\",",
            "\"type\":7,\"inv_type\":7,\"flags\":0,\"created_at\":1700000000,",
            "\"agent_id\":\"11111111-1111-1111-1111-111111111111\",",
            "\"{}\":\"{}\",\"permissions\":{{",
            "\"creator_id\":\"11111111-1111-1111-1111-111111111111\",",
            "\"owner_id\":\"11111111-1111-1111-1111-111111111111\",",
            "\"last_owner_id\":\"11111111-1111-1111-1111-111111111111\",",
            "\"group_id\":\"00000000-0000-0000-0000-000000000000\",",
            "\"base_mask\":2147483647,\"owner_mask\":2147483647,",
            "\"group_mask\":0,\"everyone_mask\":0,\"next_owner_mask\":2147483647,",
            "\"is_owner_group\":false}},\"sale_info\":{{\"sale_price\":0,\"sale_type\":0}}}}"
        ),
        item_id.to_string(),
        parent_id.to_string(),
        asset_or_shadow_key,
        asset_or_shadow_value.to_string(),
    )
}

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 single<T: 'static>(entries: &[Box<dyn InventoryObjectClass>]) -> &T {
    let mut matching = entries
        .iter()
        .filter_map(|entry| entry.as_any().downcast_ref::<T>());
    let value = matching.next().expect("matching inventory object");
    assert!(matching.next().is_none(), "expected exactly one object");
    value
}

// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_CapAvailable_ReturnsContentsFolderAndDecryptsShadowId::test c9bfc5526260f9e2dffa8a22938999911fdd80be4b2b6d6487bf6171f8dbb56a translated
#[test]
fn cap_available_returns_contents_folder_and_decrypts_shadow_id() {
    let object_id = uuid();
    let item_id = uuid();
    let asset_id = uuid();
    let shadow_id = InventoryManager::encrypt_asset_id(asset_id).unwrap();
    let body = format!(
        "{{\"inventory_serial\":3,\"contents\":[{}]}}",
        item_json(item_id, object_id, "shadow_id", shadow_id)
    );
    let (client, recording) = client_with_cap(response(200, body), true);
    let result = block_on(
        client
            .inventory()
            .get_task_inventory(object_id, 12_345, None, None),
    )
    .expect("GetTaskInventoryAsync");

    let requests = recording.requests();
    assert_eq!(requests.len(), 1);
    assert!(
        requests[0]
            .uri
            .0
            .contains(&format!("task_id={}", object_id.to_string()))
    );
    assert_eq!(result.len(), 2);
    let folder = single::<InventoryFolder>(&result);
    assert_eq!(folder.base.name(), "Contents");
    assert_eq!(folder.base.uuid(), object_id);
    let item = single::<InventoryItem>(&result);
    assert_eq!(item.base.uuid(), item_id);
    assert_eq!(item.base.parent_uuid(), object_id);
    assert_eq!(item.base.name(), "Test Item");
    assert_eq!(item.asset_uuid(), asset_id);
}

// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_PlainAssetId_ParsesWithoutDecryption::test 95f39928dae61bca86b7d0a0702307db6c5fdc754e51f73a24bd5ee131e6de3f translated
#[test]
fn plain_asset_id_parses_without_decryption() {
    let object_id = uuid();
    let item_id = uuid();
    let asset_id = uuid();
    let body = format!(
        "{{\"inventory_serial\":1,\"contents\":[{}]}}",
        item_json(item_id, object_id, "asset_id", asset_id)
    );
    let (client, _) = client_with_cap(response(200, body), true);
    let result = block_on(
        client
            .inventory()
            .get_task_inventory(object_id, 12_345, None, None),
    )
    .expect("GetTaskInventoryAsync");
    assert_eq!(single::<InventoryItem>(&result).asset_uuid(), asset_id);
}

// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_NonSuccessStatus_ReturnsContentsFolderOnly::test b8a25891064e470ac8d77ecd8a4847cf773b043de5ebeae275a9dd6669518585 translated
#[test]
fn non_success_status_returns_contents_folder_only() {
    let object_id = uuid();
    let (client, _) = client_with_cap(response(500, String::new()), true);
    let result = block_on(
        client
            .inventory()
            .get_task_inventory(object_id, 12_345, None, None),
    )
    .expect("GetTaskInventoryAsync");
    assert_eq!(result.len(), 1);
    assert!(result[0].as_any().is::<InventoryFolder>());
}

// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_NoCapability_DoesNotMakeHttpRequest::test c0c43ebcf781fb81b1fef7ce1dd2774e350d92ef882f49876f1a16d23a338592 translated
#[test]
fn no_capability_does_not_make_http_request() {
    let (client, recording) = client_with_cap(response(404, String::new()), false);
    let result = block_on(client.inventory().get_task_inventory(
        uuid(),
        12_345,
        None,
        Some(cancellation_after(Duration::from_millis(50))),
    ))
    .expect("GetTaskInventoryAsync");
    assert!(result.is_empty());
    assert!(recording.requests().is_empty());
}
