// Exact Rust translation of LibreMetaverse.Tests/MarketplaceManagerTests.cs.
// Source SHA-256: 059a0cf2c190954fb6a52aed1c63ca286aa22a7eca72a6a5e9f18e3054f55f38

use libremetaverse::marketplace::{
    MarketplaceErrorEventArgs, MarketplaceListing, MarketplaceListingChangedEventArgs,
    MarketplaceListingStatus, MarketplaceListingsSyncedEventArgs, MarketplaceManager,
};
use libremetaverse::{GridClient, HttpCapsClient, Simulator};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{
    ExternalError, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use std::collections::{BTreeMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;

const CAP_BASE: &str = "http://test.invalid/direct-delivery";
const SEED: &str = "http://test.invalid/seed";
const FOLDER_1: &str = "11111111-1111-1111-1111-111111111111";
const VERSION_1: &str = "22222222-2222-2222-2222-222222222222";
const FOLDER_2: &str = "33333333-3333-3333-3333-333333333333";
const VERSION_2: &str = "44444444-4444-4444-4444-444444444444";

fn uuid(text: &str) -> UUID {
    UUID::new_with_string(text.into()).expect("UUID string constructor")
}

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

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

struct Harness {
    manager: MarketplaceManager,
    requests: Arc<Mutex<Vec<HttpRequest>>>,
    responses: Arc<Mutex<VecDeque<(String, HttpResponse)>>>,
}

impl Harness {
    fn new() -> Self {
        let requests = Arc::new(Mutex::new(Vec::new()));
        let responses = Arc::new(Mutex::new(VecDeque::new()));
        let captured = Arc::clone(&requests);
        let queued = Arc::clone(&responses);
        let handler = HttpMessageHandler::new(move |request, _| {
            let reply = if request.uri.0 == SEED {
                HttpResponse {
                    status_code: 200,
                    headers: BTreeMap::new(),
                    content_type: Some("application/llsd+xml".into()),
                    body: format!(
                        "<llsd><map><key>DirectDelivery</key><uri>{CAP_BASE}</uri></map></llsd>"
                    )
                    .into_bytes(),
                }
            } else {
                let uri = request.uri.0.clone();
                captured.lock().unwrap().push(request);
                let mut queued = queued.lock().unwrap();
                let index = queued
                    .iter()
                    .position(|(expected, _)| expected == &uri)
                    .expect("queued response for request URI");
                queued.remove(index).unwrap().1
            };
            async move { reply }
        });
        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:13".parse().unwrap(),
            0,
            None,
            None,
        )
        .expect("Simulator constructor");
        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);
        Self {
            manager: MarketplaceManager::new(client).expect("MarketplaceManager constructor"),
            requests,
            responses,
        }
    }

    fn add(&self, path: &str, status: u16, body: &str) {
        self.responses
            .lock()
            .unwrap()
            .push_back((format!("{CAP_BASE}/{path}"), response(status, body)));
    }

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

    fn last_request(&self) -> HttpRequest {
        self.requests().last().expect("captured request").clone()
    }

    fn seed_listing(&self, id: i32, folder: &str, version: &str, count: i32) {
        self.add(
            "listings",
            200,
            &format!(
                "{{\"listings\":[{{\"id\":{id},\"is_listed\":false,\"inventory_info\":{{\"listing_folder_id\":\"{folder}\",\"version_folder_id\":\"{version}\",\"count_on_hand\":{count}}}}}]}}"
            ),
        );
        block_on(self.manager.fetch_listings(None)).unwrap();
    }
}

fn listing_json(id: i32, folder: &str) -> String {
    format!(
        "{{\"listings\":[{{\"id\":{id},\"inventory_info\":{{\"listing_folder_id\":\"{folder}\"}}}}]}}"
    )
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_ListingsArrayResponse_PopulatesBothCaches::test 1ec347288488be26f8a4b923d304ae9598d1d4f40423356012c353b3527ae089 translated
#[test]
fn fetch_listings_populates_both_caches() {
    let h = Harness::new();
    h.add("listings", 200, &format!("{{\"listings\":[{{\"id\":101,\"is_listed\":true,\"edit_url\":\"http://mp.sl.com/edit/101\",\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_1}\",\"version_folder_id\":\"{VERSION_1}\",\"count_on_hand\":3}}}}]}}"));
    block_on(h.manager.fetch_listings(None)).unwrap();
    let by_id = h.manager.listings_by_id();
    let by_folder = h.manager.listings_by_folder();
    assert!(by_id.contains_key(&101));
    assert!(by_folder.contains_key(&uuid(FOLDER_1)));
    let listing = &by_id[&101];
    assert_eq!(listing.status(), MarketplaceListingStatus::Listed);
    assert_eq!(listing.stock_count(), 3);
    assert_eq!(listing.version_folder_uuid(), uuid(VERSION_1));
    assert_eq!(
        listing.edit_url().as_deref(),
        Some("http://mp.sl.com/edit/101")
    );
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_UnlistedEntry_ParsesAsUnlisted::test 3e5b22da9fcc6a38ddec3ffb215367320df035aed33c9c0690461fdce786b37d translated
#[test]
fn fetch_listings_parses_unlisted_entry() {
    let h = Harness::new();
    h.add("listings", 200, &format!("{{\"listings\":[{{\"id\":102,\"is_listed\":false,\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_2}\"}}}}]}}"));
    block_on(h.manager.fetch_listings(None)).unwrap();
    assert_eq!(
        h.manager.listings_by_id()[&102].status(),
        MarketplaceListingStatus::Unlisted
    );
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_FiresListingsSyncedEvent::test ec70956ce5eb51fe650a6e2e77924ec21051b0c06396bce7c9c82a277f6aa82a translated
#[test]
fn fetch_listings_fires_synced_event() {
    let h = Harness::new();
    h.add("listings", 200, &listing_json(101, FOLDER_1));
    let raised = Arc::new(Mutex::new(None));
    let captured = Arc::clone(&raised);
    let _subscription = h.manager.subscribe_listings_synced(Arc::new(move |args| {
        let listings = args.listings();
        *captured.lock().unwrap() = Some((listings.len(), listings[0].listing_id()));
    }));
    block_on(h.manager.fetch_listings(None)).unwrap();
    assert_eq!(*raised.lock().unwrap(), Some((1, 101)));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_HttpError_FiresErrorEventAndLeavesEmptyCache::test 0a71f4db07be054bf77752f07223268bd25b673a9c5c8cfe8a5f78ee36b4a463 translated
#[test]
fn fetch_listings_http_error_fires_error_and_leaves_empty_cache() {
    let h = Harness::new();
    h.add("listings", 500, "");
    let error = Arc::new(Mutex::new(None));
    let captured = Arc::clone(&error);
    let _subscription = h.manager.subscribe_error(Arc::new(move |args| {
        *captured.lock().unwrap() = Some(args.message());
    }));
    block_on(h.manager.fetch_listings(None)).unwrap();
    assert!(
        error
            .lock()
            .unwrap()
            .as_ref()
            .is_some_and(|message| !message.is_empty())
    );
    assert!(h.manager.listings_by_id().is_empty());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_EmptyListingsArray_ResultsInEmptyCache::test 79ab6108d67cba54f55a8603f3929f1329cbe84ce3b591717d4b4712c2bbe0a2 translated
#[test]
fn fetch_listings_empty_array_results_in_empty_cache() {
    let h = Harness::new();
    h.add("listings", 200, "{\"listings\":[]}");
    block_on(h.manager.fetch_listings(None)).unwrap();
    assert!(h.manager.listings_by_id().is_empty());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_SecondCall_ReplacesCacheWithNewData::test 2327332d62529d276a8e1e074b20c3a37a47b5c820910644e6562317fdd4651b translated
#[test]
fn fetch_listings_second_call_replaces_cache() {
    let h = Harness::new();
    h.add("listings", 200, &listing_json(101, FOLDER_1));
    block_on(h.manager.fetch_listings(None)).unwrap();
    assert!(h.manager.listings_by_id().contains_key(&101));
    h.add("listings", 200, &listing_json(202, FOLDER_2));
    block_on(h.manager.fetch_listings(None)).unwrap();
    let listings = h.manager.listings_by_id();
    assert!(!listings.contains_key(&101));
    assert!(listings.contains_key(&202));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.CreateListingAsync_SendsNestedInventoryInfoNotFlatUuidField::test efda655bf20ec6157df4738df4cf4ddcc1ee9e1ade01f1f0177630659b54b1d6 translated
#[test]
fn create_listing_sends_nested_inventory_info() {
    let h = Harness::new();
    h.add("listings", 200, &listing_json(202, FOLDER_2));
    let result = block_on(h.manager.create_listing(
        uuid(FOLDER_2),
        uuid(VERSION_2),
        "My Listing".into(),
        Some(5),
        None,
    ))
    .unwrap()
    .expect("created listing");
    assert_eq!(result.listing_id(), 202);
    let request = h.last_request();
    assert_eq!(request.method, "POST");
    assert_eq!(request.uri.0, format!("{CAP_BASE}/listings"));
    let body = String::from_utf8(request.body).unwrap();
    assert!(body.contains("\"name\":\"My Listing\""));
    assert!(body.contains(&format!("\"listing_folder_id\":\"{FOLDER_2}\"")));
    assert!(body.contains(&format!("\"version_folder_id\":\"{VERSION_2}\"")));
    assert!(body.contains("\"count_on_hand\":5"));
    assert!(!body.contains("listing_folder_uuid"));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.CreateListingAsync_FiresListingChangedEvent::test 242073ff6fbae379c518d047cd63e1ad368ff37ffc2cad379b8c007fb725b74e translated
#[test]
fn create_listing_fires_changed_event() {
    let h = Harness::new();
    h.add("listings", 200, &listing_json(202, FOLDER_2));
    let changed = Arc::new(Mutex::new(None));
    let captured = Arc::clone(&changed);
    let _subscription = h.manager.subscribe_listing_changed(Arc::new(move |args| {
        let listing = args.listing().expect("changed listing");
        *captured.lock().unwrap() = Some((args.listing_id(), listing.listing_folder_uuid()));
    }));
    block_on(
        h.manager
            .create_listing(uuid(FOLDER_2), uuid(VERSION_2), "Name".into(), None, None),
    )
    .unwrap();
    assert_eq!(*changed.lock().unwrap(), Some((202, uuid(FOLDER_2))));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.CreateListingAsync_HttpError_ReturnsNullAndFiresErrorEvent::test fa999c4cce5bee688ef3bb6342e276867268ab9f32571eff25646cb1384bcece translated
#[test]
fn create_listing_http_error_returns_none_and_fires_error() {
    let h = Harness::new();
    h.add("listings", 400, "");
    let fired = Arc::new(Mutex::new(false));
    let captured = Arc::clone(&fired);
    let _subscription = h
        .manager
        .subscribe_error(Arc::new(move |_| *captured.lock().unwrap() = true));
    let result =
        block_on(
            h.manager
                .create_listing(random_uuid(), random_uuid(), "Name".into(), None, None),
        )
        .unwrap();
    assert!(result.is_none());
    assert!(*fired.lock().unwrap());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.DeleteListingAsync_UsesSingularListingRoute::test aea0672be109f05632df326a0c2caf7b7d14d23cf0d66e6ab867902351ebb554 translated
#[test]
fn delete_listing_uses_singular_route_and_evicts_caches() {
    let h = Harness::new();
    h.add("listings", 200, &listing_json(101, FOLDER_1));
    block_on(h.manager.fetch_listings(None)).unwrap();
    let mut listing = None;
    assert!(h.manager.try_get_by_id(101, &mut listing));
    h.add("listing/101", 200, "");
    assert!(block_on(h.manager.delete_listing(101, None)).unwrap());
    assert!(!h.manager.try_get_by_id(101, &mut None));
    assert!(!h.manager.try_get_by_folder(uuid(FOLDER_1), &mut None));
    assert_eq!(h.last_request().uri.0, format!("{CAP_BASE}/listing/101"));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.DeleteListingAsync_HttpError_ReturnsFalseAndFiresErrorEvent::test 52976e09904483e7f33dbe016d695e380646f75b2fdd1b1f744c3fc69dd9d192 translated
#[test]
fn delete_listing_http_error_returns_false_and_fires_error() {
    let h = Harness::new();
    h.add("listing/999", 404, "");
    let fired = Arc::new(Mutex::new(false));
    let captured = Arc::clone(&fired);
    let _subscription = h
        .manager
        .subscribe_error(Arc::new(move |_| *captured.lock().unwrap() = true));
    assert!(!block_on(h.manager.delete_listing(999, None)).unwrap());
    assert!(*fired.lock().unwrap());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.ActivateListingAsync_SendsIsListedTrueAndCachedInventoryInfoToSingularRoute::test 4b6daed0e333c504a7aed8666980152990c37d81830d08f3a31faf39dbbc706f translated
#[test]
fn activate_listing_sends_full_cached_inventory_info() {
    let h = Harness::new();
    h.seed_listing(101, FOLDER_1, VERSION_1, 7);
    h.add("listing/101", 200, &format!("{{\"listings\":[{{\"id\":101,\"is_listed\":true,\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_1}\",\"version_folder_id\":\"{VERSION_1}\",\"count_on_hand\":7}}}}]}}"));
    assert!(block_on(h.manager.activate_listing(101, None)).unwrap());
    let request = h.last_request();
    assert_eq!(request.method, "PUT");
    assert_eq!(request.uri.0, format!("{CAP_BASE}/listing/101"));
    let body = String::from_utf8(request.body).unwrap();
    for expected in [
        "\"is_listed\":true",
        "\"id\":101",
        &format!("\"listing_folder_id\":\"{FOLDER_1}\""),
        &format!("\"version_folder_id\":\"{VERSION_1}\""),
        "\"count_on_hand\":7",
    ] {
        assert!(body.contains(expected));
    }
    let mut listing = None;
    assert!(h.manager.try_get_by_id(101, &mut listing));
    assert_eq!(listing.unwrap().status(), MarketplaceListingStatus::Listed);
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.ActivateListingAsync_NoCachedListing_ReturnsFalseWithoutSendingRequest::test 1c4d407a29ba139b13e868882f1721327cfdd64dc2d2e9a32a0c71678905bb65 translated
#[test]
fn activate_listing_without_cached_listing_sends_nothing() {
    let h = Harness::new();
    assert!(!block_on(h.manager.activate_listing(999, None)).unwrap());
    assert!(h.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.ActivateListingAsync_HttpError_ReturnsFalse::test 1f7b39a95adfd13cf159fbadac9d157a70ebbdcb360a1cf1cf4d21b891bd61d6 translated
#[test]
fn activate_listing_http_error_returns_false() {
    let h = Harness::new();
    h.seed_listing(101, FOLDER_1, VERSION_1, 0);
    h.add("listing/101", 400, "");
    assert!(!block_on(h.manager.activate_listing(101, None)).unwrap());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.DeactivateListingAsync_SendsIsListedFalse::test 1fa6eb4bfc86fb9a5fe746b18e43568b9dcc1a723e3a667b1bc57a1ebb66fb50 translated
#[test]
fn deactivate_listing_sends_is_listed_false() {
    let h = Harness::new();
    h.seed_listing(101, FOLDER_1, VERSION_1, 0);
    h.add("listing/101", 200, &format!("{{\"listings\":[{{\"id\":101,\"is_listed\":false,\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_1}\"}}}}]}}"));
    block_on(h.manager.deactivate_listing(101, None)).unwrap();
    assert!(
        String::from_utf8(h.last_request().body)
            .unwrap()
            .contains("\"is_listed\":false")
    );
    let mut listing = None;
    assert!(h.manager.try_get_by_id(101, &mut listing));
    assert_eq!(
        listing.unwrap().status(),
        MarketplaceListingStatus::Unlisted
    );
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.TryGetByFolder_AfterFetch_ReturnsMatchingListing::test cb0b2a301f9096a238ba078739f0b6488a4dcc20fecdd59d51c930f7ea28902c translated
#[test]
fn try_get_by_folder_returns_matching_listing() {
    let h = Harness::new();
    h.add("listings", 200, &listing_json(101, FOLDER_1));
    block_on(h.manager.fetch_listings(None)).unwrap();
    let mut listing = None;
    assert!(h.manager.try_get_by_folder(uuid(FOLDER_1), &mut listing));
    assert_eq!(listing.unwrap().listing_id(), 101);
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.TryGetByFolder_MissingUUID_ReturnsFalse::test 86fe3e33c410412e829efbc4e775b164492b2dd14d82421478552e9a01c35ef8 translated
#[test]
fn try_get_by_folder_missing_uuid_returns_false() {
    let h = Harness::new();
    h.add("listings", 200, "{\"listings\":[]}");
    block_on(h.manager.fetch_listings(None)).unwrap();
    assert!(!h.manager.try_get_by_folder(random_uuid(), &mut None));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.TryGetById_EmptyCache_ReturnsFalse::test 8d49aeaf5444d8d2d2098f932df01dac6e6942a86b105287c5f40cb54e501dcc translated
#[test]
fn try_get_by_id_empty_cache_returns_false() {
    let h = Harness::new();
    assert!(!h.manager.try_get_by_id(9999, &mut None));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListingsSyncedEventArgs_StoresListings::test c0744e6b9f9282613692f7be962009e9129c234e3ca863ab40b581baa90a6a68 translated
#[test]
fn listings_synced_event_args_store_listings() {
    let mut listing = MarketplaceListing::new().unwrap();
    listing.set_listing_id(1);
    let args = MarketplaceListingsSyncedEventArgs::new(vec![listing]).unwrap();
    let listings = args.listings();
    assert_eq!(listings.len(), 1);
    assert_eq!(listings[0].listing_id(), 1);
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListingChangedEventArgs_StoresListingIdAndListing::test 874578d97b250b4667e3cb0c28626948ede80fba14e9ae9add153e2dfddc1902 translated
#[test]
fn listing_changed_event_args_store_same_listing() {
    let mut listing = MarketplaceListing::new().unwrap();
    listing.set_listing_id(42);
    let listing = Arc::new(listing);
    let args = MarketplaceListingChangedEventArgs::new(42, Some(Arc::clone(&listing))).unwrap();
    assert_eq!(args.listing_id(), 42);
    assert!(Arc::ptr_eq(&args.listing().unwrap(), &listing));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListingChangedEventArgs_NullListing_IsValidForDeleteScenario::test 6a4fadad3a00e0e17977506badf7e08a53278f7c89fc4c7597a502164257f50d translated
#[test]
fn listing_changed_event_args_allow_none_for_delete() {
    let args = MarketplaceListingChangedEventArgs::new(99, None).unwrap();
    assert_eq!(args.listing_id(), 99);
    assert!(args.listing().is_none());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceErrorEventArgs_StoresMessageAndException::test cb0213208ab6b63f32a950f1771354fae4b6904c72720b1705ddf5b8355ffc7d translated
#[test]
fn error_event_args_store_same_message_and_exception() {
    let error = Arc::new(ExternalError("inner error".into()));
    let args = MarketplaceErrorEventArgs::new("Something failed.".into(), Some(Arc::clone(&error)))
        .unwrap();
    assert_eq!(args.message(), "Something failed.");
    assert!(Arc::ptr_eq(&args.exception().unwrap(), &error));
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceErrorEventArgs_WithoutException_HasNullException::test de7c8addfb291cba5dee890a98ac8b5f27b192d0b75894c4102303a49785de47 translated
#[test]
fn error_event_args_without_exception_has_none() {
    let args = MarketplaceErrorEventArgs::new("No exception here.".into(), None).unwrap();
    assert_eq!(args.message(), "No exception here.");
    assert!(args.exception().is_none());
}

// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListing_DefaultValues_AreCorrect::test 70a29163581702fdc32c54a398ad4cefa608da8b1bc9d54a4e9e191e95398fb7 translated
#[test]
fn marketplace_listing_defaults_are_correct() {
    let listing = MarketplaceListing::new().unwrap();
    assert_eq!(listing.status(), MarketplaceListingStatus::Unknown);
    assert_eq!(listing.last_sync_utc(), SystemTime::UNIX_EPOCH);
    assert!(listing.edit_url().is_none());
}
