// Exact Rust translations of LibreMetaverse.Tests/BatchLinkTests.cs.
// Source SHA-256: 092ccb2686769d27f6a8072114654e91308f88edd94223a77af92f56426ee1ee

use super::*;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{HttpMessageHandler, HttpRequest, HttpResponse, Uri};
use libremetaverse_types::{AssetType, InventoryType, UUID, WearableType};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::fmt::Write as _;
use std::sync::{Arc, Mutex};

const CAP: &str = "http://fake-ais3.test/ais3";
const LIBRARY_CAP: &str = "http://fake-lib.test/lib/";
const SEED: &str = "http://fake-seed.test/caps";

fn block_on<F: std::future::Future>(future: F) -> F::Output {
    use std::task::{Context, Poll, Wake, Waker};
    struct ThreadWake(std::thread::Thread);
    impl Wake for ThreadWake {
        fn wake(self: Arc<Self>) {
            self.0.unpark();
        }
    }
    let waker = Waker::from(Arc::new(ThreadWake(std::thread::current())));
    let mut context = Context::from_waker(&waker);
    let mut future = std::pin::pin!(future);
    loop {
        match future.as_mut().poll(&mut context) {
            Poll::Ready(output) => return output,
            Poll::Pending => std::thread::park(),
        }
    }
}

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

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

fn client_with_cap(responses: Vec<(String, u16, String)>) -> (GridClient, RecordingHttp) {
    let requests = Arc::new(Mutex::new(Vec::new()));
    let recorded = Arc::clone(&requests);
    let mut queues = HashMap::<String, VecDeque<HttpResponse>>::new();
    for (uri, status, body) in responses {
        queues.entry(uri).or_default().push_back(HttpResponse {
            status_code: status,
            headers: BTreeMap::new(),
            content_type: Some("application/llsd+xml".into()),
            body: body.into_bytes(),
        });
    }
    let queues = Mutex::new(queues);
    let seed = format!(
        "<llsd><map><key>InventoryAPIv3</key><uri>{CAP}</uri><key>LibraryAPIv3</key><uri>{LIBRARY_CAP}</uri></map></llsd>"
    );
    let handler = HttpMessageHandler::new(move |request, _| {
        let response = 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 {
            let uri = request.uri.0.clone();
            recorded.lock().unwrap().push(request);
            queues
                .lock()
                .unwrap()
                .get_mut(&uri)
                .and_then(VecDeque::pop_front)
                .unwrap_or(HttpResponse {
                    status_code: 404,
                    headers: BTreeMap::new(),
                    content_type: None,
                    body: Vec::new(),
                })
        };
        async move { response }
    });
    let mut client = GridClient::new().unwrap();
    client.set_http_caps_client(HttpCapsClient::new(handler.clone()).unwrap());
    let mut simulator_client = GridClient::new().unwrap();
    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, RecordingHttp(requests))
}

fn link_xml(item: UUID, parent: UUID, linked: UUID, name: &str) -> String {
    let item = item.to_string();
    let parent = parent.to_string();
    let linked = linked.to_string();
    let agent = UUID::random().unwrap().to_string();
    let zero = UUID::zero().to_string();
    format!(
        "<map><key>item_id</key><uuid>{item}</uuid><key>parent_id</key><uuid>{parent}</uuid><key>linked_id</key><uuid>{linked}</uuid><key>name</key><string>{name}</string><key>desc</key><string></string><key>agent_id</key><uuid>{agent}</uuid><key>inv_type</key><integer>{}</integer><key>type</key><integer>{}</integer><key>created_at</key><integer>0</integer><key>permissions</key><map><key>creator_id</key><uuid>{zero}</uuid><key>last_owner_id</key><uuid>{zero}</uuid><key>base_mask</key><integer>0</integer><key>everyone_mask</key><integer>0</integer><key>group_mask</key><integer>0</integer><key>next_owner_mask</key><integer>0</integer><key>owner_mask</key><integer>0</integer><key>is_owner_group</key><boolean>false</boolean><key>group_id</key><uuid>{zero}</uuid></map><key>sale_info</key><map><key>sale_price</key><integer>0</integer><key>sale_type</key><integer>0</integer></map></map>",
        InventoryType::WEARABLE.0,
        AssetType::Link as i32,
    )
}

fn batch_response(links: &[(UUID, UUID, UUID, &str)]) -> String {
    let mut entries = String::new();
    for (index, (item, parent, linked, name)) in links.iter().enumerate() {
        write!(
            entries,
            "<key>{index}</key>{}",
            link_xml(*item, *parent, *linked, name)
        )
        .unwrap();
    }
    format!(
        "<llsd><map><key>_embedded</key><map><key>links</key><map>{entries}</map></map></map></llsd>"
    )
}

fn wearable(name: &str, wearable_type: WearableType, asset_type: AssetType) -> InventoryWearable {
    let mut item = InventoryWearable::new(UUID::random().unwrap()).unwrap();
    item.set_wearable_type(wearable_type);
    item.base.set_asset_type(asset_type);
    item.base.set_inventory_type(InventoryType::WEARABLE);
    item.base.base.set_name(name.into());
    item
}

fn object(name: &str) -> InventoryObject {
    let mut item = InventoryObject::new(UUID::random().unwrap()).unwrap();
    item.base.set_asset_type(AssetType::Object);
    item.base.set_inventory_type(InventoryType::OBJECT);
    item.base.base.set_name(name.into());
    item
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_ClothingWearable_ReturnsWearableTypeAndZeroLayer::test 898a14ff28d953a58a01c3a0e5d9d9fcbbe61376a33fdc5caf20268aaac76242 translated
#[test]
fn get_cof_link_description_clothing_wearable_returns_wearable_type_and_zero_layer() {
    let cof =
        appearance::CurrentOutfitFolder::new(Some(Arc::new(GridClient::new().unwrap()))).unwrap();
    let shirt = wearable("Test Shirt", WearableType::Shirt, AssetType::Clothing);
    assert_eq!(cof.get_cof_link_description(&shirt).unwrap(), "400");
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_BodyPartShape_ReturnsEmpty::test 4f009136d265da91afb9eafd060faf459f6c510ca0502a33ce9e128d241f9bd1 translated
#[test]
fn get_cof_link_description_body_part_shape_returns_empty() {
    let cof =
        appearance::CurrentOutfitFolder::new(Some(Arc::new(GridClient::new().unwrap()))).unwrap();
    let shape = wearable("Test Shape", WearableType::Shape, AssetType::Bodypart);
    assert_eq!(cof.get_cof_link_description(&shape).unwrap(), "");
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_AttachmentObject_ReturnsEmpty::test a838329a7a23081a4022671bc794e7e706c9ab7da51598280a8befbb824d336d translated
#[test]
fn get_cof_link_description_attachment_object_returns_empty() {
    let cof =
        appearance::CurrentOutfitFolder::new(Some(Arc::new(GridClient::new().unwrap()))).unwrap();
    assert_eq!(
        cof.get_cof_link_description(&object("Test Attachment"))
            .unwrap(),
        ""
    );
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCofLinkDescription_AllBodyPartTypes_ReturnEmpty::test aa057d55a3b34846567efe382b6e090b5d91935a4ffa6166093b34870ee5da23 translated
#[test]
fn get_cof_link_description_all_body_part_types_return_empty() {
    let cof =
        appearance::CurrentOutfitFolder::new(Some(Arc::new(GridClient::new().unwrap()))).unwrap();
    for wearable_type in [
        WearableType::Shape,
        WearableType::Skin,
        WearableType::Eyes,
        WearableType::Hair,
    ] {
        let item = wearable("Test body part", wearable_type, AssetType::Bodypart);
        assert_eq!(
            cof.get_cof_link_description(&item).unwrap(),
            "",
            "{wearable_type:?}"
        );
    }
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateInventoryLinksAsync_SuccessResponse_ReturnsAllParsedLinks::test 416a1f023a2ae3b883c4c49f0a774c84a2f1a3562abb3ec848f86fe53d929f63 translated
#[test]
fn create_inventory_links_success_response_returns_all_parsed_links() {
    let folder = UUID::random().unwrap();
    let ids = [
        (
            UUID::random().unwrap(),
            UUID::random().unwrap(),
            UUID::random().unwrap(),
            "Link1",
        ),
        (
            UUID::random().unwrap(),
            UUID::random().unwrap(),
            UUID::random().unwrap(),
            "Link2",
        ),
    ];
    let uri = format!("{CAP}/category/{}", folder.to_string());
    let (client, _) = client_with_cap(vec![(uri, 200, batch_response(&ids))]);
    let payload = OSD::Map(HashMap::from([("links".into(), OSD::Array(Vec::new()))]));
    let result = block_on(
        client
            .ais_client()
            .create_inventory_links(folder, payload, None),
    )
    .unwrap();
    assert_eq!(result.len(), 2);
    let names: Vec<String> = result.iter().map(|item| item.base.name()).collect();
    assert!(names.contains(&"Link1".into()));
    assert!(names.contains(&"Link2".into()));
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateInventoryLinksAsync_ServerError_ThrowsHttpRequestException::test 53e3956676e546ab8d69bb63a071028b29151013b6362c39f37516a36cad6641 translated
#[test]
fn create_inventory_links_server_error_throws_http_request_exception() {
    let folder = UUID::random().unwrap();
    let (client, _) = client_with_cap(vec![(
        format!("{CAP}/category/{}", folder.to_string()),
        500,
        String::new(),
    )]);
    let Err(error) = block_on(client.ais_client().create_inventory_links(
        folder,
        OSD::Map(HashMap::new()),
        None,
    )) else {
        panic!("expected HttpRequest error");
    };
    assert_eq!(error, Error::HttpRequest);
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateInventoryLinksAsync_NoCapability_ReturnsEmptyList::test d9cd50e620f69e40daab52d175886f3052d3741c5d39ef42549283ff6b2b7b44 translated
#[test]
fn create_inventory_links_no_capability_returns_empty_list() {
    let client = GridClient::new().unwrap();
    let result = block_on(client.ais_client().create_inventory_links(
        UUID::random().unwrap(),
        OSD::Map(HashMap::new()),
        None,
    ))
    .unwrap();
    assert!(result.is_empty());
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateLinksAsync_EmptyList_InvokesCallbackWithTrueWithoutHttpCall::test e7ba88352761ca9fd3e956233e3743689b41f47bba09bfbb463c66f132273c70 translated
#[test]
fn create_links_empty_list_invokes_callback_with_true_without_http_call() {
    let (client, recording) = client_with_cap(Vec::new());
    let callback = Arc::new(Mutex::new(None));
    let recorded_result = Arc::clone(&callback);
    block_on(client.inventory().create_links(
        UUID::random().unwrap(),
        Box::new(Vec::<(InventoryItem, String)>::new().into_iter()),
        Some(Box::new(move |value| {
            *recorded_result.lock().unwrap() = Some(value);
        })),
        None,
    ))
    .unwrap();
    assert_eq!(*callback.lock().unwrap(), Some(true));
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateLinksAsync_AisAvailable_SendsSingleBatchPost::test 369d99d37dca818991aca94d6ff9d7881874d74797c7a8b705c5925723e383db translated
#[test]
fn create_links_ais_available_sends_single_batch_post() {
    let folder = UUID::random().unwrap();
    let first = wearable("Shirt", WearableType::Shirt, AssetType::Clothing);
    let second = object("Object");
    let response = batch_response(&[
        (
            UUID::random().unwrap(),
            folder,
            first.base.base.uuid(),
            "Shirt",
        ),
        (
            UUID::random().unwrap(),
            folder,
            second.base.base.uuid(),
            "Object",
        ),
    ]);
    let (client, recording) = client_with_cap(vec![(
        format!("{CAP}/category/{}", folder.to_string()),
        200,
        response,
    )]);
    block_on(client.inventory().create_links(
        folder,
        Box::new(vec![(first.base, "400".into()), (second.base, String::new())].into_iter()),
        None,
        None,
    ))
    .unwrap();
    assert_eq!(
        recording
            .requests()
            .iter()
            .filter(|request| request.method == "POST")
            .count(),
        1
    );
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateLinksAsync_AisAvailable_PayloadContainsCorrectFieldsForEachLink::test b7629c3d061fe297e1c1c4f41ca4f8ce9cdeb2764525421be7bdc553a5f83063 translated
#[test]
fn create_links_ais_available_payload_contains_correct_fields_for_each_link() {
    let folder = UUID::random().unwrap();
    let linked = wearable("Gloves", WearableType::Gloves, AssetType::Clothing);
    let linked_id = linked.base.base.uuid();
    let response = batch_response(&[(UUID::random().unwrap(), folder, linked_id, "Gloves")]);
    let (client, recording) = client_with_cap(vec![(
        format!("{CAP}/category/{}", folder.to_string()),
        200,
        response,
    )]);
    block_on(client.inventory().create_links(
        folder,
        Box::new(vec![(linked.base, "900".into())].into_iter()),
        None,
        None,
    ))
    .unwrap();
    let request = recording
        .requests()
        .into_iter()
        .find(|request| request.method == "POST")
        .unwrap();
    let payload =
        OSDParser::deserialize_llsd_xml_with_string(String::from_utf8(request.body).unwrap())
            .unwrap();
    let OSD::Map(payload) = payload else {
        panic!("payload must be a map")
    };
    let OSD::Array(links) = &payload["links"] else {
        panic!("links must be an array")
    };
    assert_eq!(links.len(), 1);
    let OSD::Map(link) = &links[0] else {
        panic!("link must be a map")
    };
    assert!(matches!(&link["linked_id"], OSD::UUID(value) if *value == linked_id));
    assert!(matches!(&link["name"], OSD::String(value) if value == "Gloves"));
    assert!(matches!(&link["desc"], OSD::String(value) if value == "900"));
    assert!(matches!(&link["type"], OSD::Integer(value) if *value == AssetType::Link as i32));
    assert!(
        matches!(&link["inv_type"], OSD::Integer(value) if *value == i32::from(InventoryType::WEARABLE.0))
    );
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateLinksAsync_AisAvailable_CallbackTrueOnSuccess::test 22dc685f896ec0415e56c1c01846ad90a9757cf5d9a0ed0d3706a76ee325d92d translated
#[test]
fn create_links_ais_available_callback_true_on_success() {
    let folder = UUID::random().unwrap();
    let linked = object("Hat");
    let response = batch_response(&[(
        UUID::random().unwrap(),
        folder,
        linked.base.base.uuid(),
        "Hat",
    )]);
    let (client, _) = client_with_cap(vec![(
        format!("{CAP}/category/{}", folder.to_string()),
        200,
        response,
    )]);
    let callback = Arc::new(Mutex::new(None));
    let result = Arc::clone(&callback);
    block_on(client.inventory().create_links(
        folder,
        Box::new(vec![(linked.base, String::new())].into_iter()),
        Some(Box::new(move |value| *result.lock().unwrap() = Some(value))),
        None,
    ))
    .unwrap();
    assert_eq!(*callback.lock().unwrap(), Some(true));
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.CreateLinksAsync_ServerError_CallbackFalse::test 5fd9b2967bc61933cea80e01c55ee809f6ade246f10fb4048ec2668e0cc71dbf translated
#[test]
fn create_links_server_error_callback_false() {
    let folder = UUID::random().unwrap();
    let linked = object("Hat");
    let (client, _) = client_with_cap(vec![(
        format!("{CAP}/category/{}", folder.to_string()),
        500,
        String::new(),
    )]);
    let callback = Arc::new(Mutex::new(None));
    let result = Arc::clone(&callback);
    block_on(client.inventory().create_links(
        folder,
        Box::new(vec![(linked.base, String::new())].into_iter()),
        Some(Box::new(move |value| *result.lock().unwrap() = Some(value))),
        None,
    ))
    .unwrap();
    assert_eq!(*callback.lock().unwrap(), Some(false));
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCategoryLinks_UsesGetWithNoTidParameter::test 3079c03ab4b14d4ac10a410b3685f1593ea267c52de7b6323f9e8e8387314f29 translated
#[test]
fn get_category_links_uses_get_with_no_tid_parameter() {
    let folder = UUID::random().unwrap();
    let uri = format!("{CAP}/category/{}/links", folder.to_string());
    let response = "<llsd><map><key>_embedded</key><map></map></map></llsd>".to_owned();
    let (client, recording) = client_with_cap(vec![(uri.clone(), 200, response)]);
    let (success, _, _, _) = block_on(
        client
            .ais_client()
            .get_category_links(folder.to_string(), None),
    )
    .unwrap();
    assert!(success);
    let requests = recording.requests();
    assert_eq!(requests.len(), 1);
    assert_eq!(requests[0].method, "GET");
    assert_eq!(requests[0].uri.0, uri);
    assert!(!requests[0].uri.0.contains("?tid="));
}

// parity-case: LibreMetaverse.Tests/BatchLinkTests.cs::BatchLinkTests.GetCategory_UsesGetWithNoTidParameter::test 98cd0aa58ce9640263d07aecd45337113b07acfc76640d17c5812abe05d35c73 translated
#[test]
fn get_category_uses_get_with_no_tid_parameter() {
    let folder = UUID::random().unwrap();
    let uri = format!("{CAP}/category/{}", folder.to_string());
    let response = "<llsd><map><key>_embedded</key><map></map></map></llsd>".to_owned();
    let (client, recording) = client_with_cap(vec![(uri.clone(), 200, response)]);
    let (success, _, _, _) =
        block_on(client.ais_client().get_category(folder.to_string(), None)).unwrap();
    assert!(success);
    let requests = recording.requests();
    assert_eq!(requests.len(), 1);
    assert_eq!(requests[0].method, "GET");
    assert_eq!(requests[0].uri.0, uri);
    assert!(!requests[0].uri.0.contains("?tid="));
}
