use super::*;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{
    CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use libremetaverse_types::{InventoryType, UUID};
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex};

const CAP: &str = "http://inventory.test/ais";
const LIBRARY_CAP: &str = "http://library.test/ais";
const SEED: &str = "http://inventory.test/seed";

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(),
        }
    }
}

fn client_with_response(body: Vec<u8>) -> (GridClient, Arc<Mutex<Vec<HttpRequest>>>) {
    let requests = Arc::new(Mutex::new(Vec::new()));
    let recorded = Arc::clone(&requests);
    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 {
            recorded.lock().unwrap().push(request);
            HttpResponse {
                status_code: 200,
                headers: BTreeMap::new(),
                content_type: Some("application/llsd+xml".into()),
                body: body.clone(),
            }
        };
        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 deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
    while simulator
        .native_caps()
        .and_then(|caps| caps.capability_uri("InventoryAPIv3".into()).ok().flatten())
        .is_none()
        && std::time::Instant::now() < deadline
    {
        std::thread::sleep(std::time::Duration::from_millis(5));
    }
    let mut network = client.network();
    network.set_current_sim(Some(simulator));
    client.set_network(network);
    (client, requests)
}

#[test]
fn records_exact_ais_verbs_paths_headers_and_llsd_payloads() {
    let (client, requests) = client_with_response(Vec::new());
    let ais = client.ais_client();
    assert!(ais.is_available(), "seed capability should be visible");
    let item = UUID::random().unwrap();
    let destination = UUID::random().unwrap();
    let payload = OSD::Map(HashMap::from([(
        "name".into(),
        OSD::String("renamed".into()),
    )]));

    assert!(block_on(ais.update_item(item, payload.clone(), None)).unwrap());
    assert!(block_on(ais.copy_item(item, destination, None)).unwrap());
    assert!(
        block_on(ais.get_category_children("current".into(), 0, true, None))
            .unwrap()
            .0
    );

    let requests = requests.lock().unwrap();
    assert_eq!(requests.len(), 3);
    assert_eq!(requests[0].method, "PATCH");
    assert_eq!(requests[0].uri.0, format!("{CAP}/item/{item}"));
    assert_eq!(
        requests[0].content_type.as_deref(),
        Some("application/llsd+xml")
    );
    assert_eq!(
        OSDParser::deserialize_with_bytes(requests[0].body.clone()).unwrap(),
        payload
    );
    assert_eq!(requests[1].method, "COPY");
    assert!(
        requests[1]
            .uri
            .0
            .starts_with(&format!("{CAP}/item/{item}?tid="))
    );
    assert_eq!(
        requests[1].headers.get("Destination"),
        Some(&destination.to_string())
    );
    assert!(requests[1].body.is_empty());
    assert_eq!(requests[2].method, "GET");
    assert_eq!(
        requests[2].uri.0,
        format!("{CAP}/category/current/children?depth=50")
    );
}

#[test]
fn malformed_and_cancelled_responses_leave_store_unchanged() {
    let (client, requests) = client_with_response(b"not llsd".to_vec());
    let owner = UUID::random().unwrap();
    let item_id = UUID::random().unwrap();
    let store = Inventory::new_with_grid_client_uuid(Arc::new(client.clone()), owner).unwrap();
    let item = InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id).unwrap();
    store.update_node_for(&item).unwrap();
    client.inventory().native_set_store_for_tests(store.clone());

    let result = block_on(client.ais_client().fetch_item(item_id, None)).unwrap();
    assert!(!result.0);
    assert!(store.contains_with_uuid(item_id).unwrap());
    assert_eq!(store.count(), 1);

    let cancellation = CancellationTokenSource::new();
    cancellation.cancel();
    let result = block_on(
        client
            .ais_client()
            .fetch_item(item_id, Some(cancellation.token())),
    );
    assert!(matches!(result, Err(Error::Cancelled)));
    assert_eq!(requests.lock().unwrap().len(), 1);
    assert_eq!(store.count(), 1);
}

#[test]
fn partially_valid_response_is_rejected_before_any_reconciliation() {
    let existing = UUID::random().unwrap();
    let new_folder = UUID::random().unwrap();
    let malformed_item = UUID::random().unwrap();
    let zero = UUID::zero();
    let body = format!(
        "<llsd><map><key>_removed_items</key><array><uuid>{existing}</uuid></array><key>_embedded</key><map><key>categories</key><map><key>{new_folder}</key><map><key>category_id</key><uuid>{new_folder}</uuid><key>parent_id</key><uuid>{zero}</uuid><key>name</key><string>valid folder</string></map></map><key>items</key><map><key>{malformed_item}</key><map><key>item_id</key><string>not-a-uuid</string></map></map></map></map></llsd>"
    );
    let (client, _) = client_with_response(body.into_bytes());
    let store =
        Inventory::new_with_grid_client_uuid(Arc::new(client.clone()), UUID::random().unwrap())
            .unwrap();
    let item =
        InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, existing).unwrap();
    store.update_node_for(&item).unwrap();
    client.inventory().native_set_store_for_tests(store.clone());

    let result = block_on(client.ais_client().get_category("current".into(), None)).unwrap();
    assert!(!result.0);
    assert!(store.contains_with_uuid(existing).unwrap());
    assert!(!store.contains_with_uuid(new_folder).unwrap());
    assert_eq!(store.count(), 1);
}

#[test]
fn response_metadata_reconciles_removals_and_folder_versions_together() {
    let folder_id = UUID::random().unwrap();
    let item_id = UUID::random().unwrap();
    let body = format!(
        "<llsd><map><key>_removed_items</key><array><uuid>{item_id}</uuid></array><key>_updated_category_versions</key><map><key>{folder_id}</key><integer>17</integer></map></map></llsd>"
    );
    let (client, _) = client_with_response(body.into_bytes());
    let owner = UUID::random().unwrap();
    let store = Inventory::new_with_grid_client_uuid(Arc::new(client.clone()), owner).unwrap();
    let folder = InventoryFolder::new(folder_id).unwrap();
    let mut item =
        InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id).unwrap();
    item.base.set_parent_uuid(folder_id);
    store.update_node_for(&folder).unwrap();
    store.update_node_for(&item).unwrap();
    client.inventory().native_set_store_for_tests(store.clone());

    assert!(block_on(client.ais_client().remove_item(item_id, None)).unwrap());
    assert!(!store.contains_with_uuid(item_id).unwrap());
    let updated: Option<InventoryFolder> = store
        .get_value_or_default_with_uuid_a7c63fbe(folder_id)
        .unwrap();
    assert_eq!(updated.unwrap().version(), 17);
}
