#![allow(
    clippy::float_cmp,
    clippy::needless_pass_by_value,
    clippy::too_many_lines
)]

// Exact BSD-3-Clause fixture provenance at LibreMetaverse
// 2aa70bb68513b39795da5d13c88f31b86e85a3ba (see LICENSE.md):
// GLTFMaterialOverrideCapTests.cs 4d717ded7cc7bd4e68210417f722a1ceec6391b19dc3b1dfeb558ae16e6f115b
// GLTFMaterialUploadTests.cs      0602d1504e405868d7bcb608eb7c0db9f47e9ec6636846f34d48669aa5a27025
// TerrainMaterialOverrideTests.cs b9409ba65906e2b9d30d42b350b4b3e9c1655d4a503c4ff87bd548dd229dad88
// SendPostcardTests.cs            56691373dfa8f817f0b1079b8ec40f16e0186d6cfb13f772da0ffe3935188b4d
// UploadThumbnailTests.cs         8a88c5e4d45e7e1d4f8a41092528eefee347c7bd5f5e6f3af171ac19455e894b
// ConstructorNullArgumentTests.cs 28deea0bf4d9353a2424842c6184abab8fe82746ceb12e093912728a247f21bc

use libremetaverse::assets::AssetMaterial;
use libremetaverse::{
    AssetManager, GLTFMaterialUpdate, GridClient, HttpCapsClient, InventoryFolder, Simulator,
};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::compat::{HttpMessageHandler, HttpRequest, HttpResponse, Uri};
use libremetaverse_types::{Color4, Error, UUID, Vector3};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::fmt::Write as _;
use std::sync::{Arc, Mutex};

const MODIFY_MATERIAL: &str = "http://test.invalid/modify-material-params";
const MATERIAL_AGENT: &str = "http://test.invalid/update-material-agent-inventory";
const MATERIAL_TASK: &str = "http://test.invalid/update-material-task-inventory";
const MATERIAL_UPLOADER: &str = "http://test.invalid/update-material/upload/abc123";
const MODIFY_REGION: &str = "http://test.invalid/modify-region";
const POSTCARD: &str = "http://test.invalid/send-postcard";
const POSTCARD_UPLOADER: &str = "http://test.invalid/send-postcard/upload/abc123";
const THUMBNAIL: &str = "http://test.invalid/inventory-thumbnail-upload";
const THUMBNAIL_UPLOADER: &str = "http://test.invalid/inventory-thumbnail-upload/upload/abc123";
const SEED_CAPS: &str = "http://test.invalid/seed";
const OTHER_CAP: &str = "http://test.invalid/other";

#[derive(Clone)]
struct RecordingHttp {
    requests: Arc<Mutex<Vec<HttpRequest>>>,
}

impl RecordingHttp {
    fn new(
        capabilities: &[(&str, &str)],
        responses: &[(&str, &str)],
    ) -> (Self, HttpMessageHandler) {
        let requests = Arc::new(Mutex::new(Vec::new()));
        let recorded = Arc::clone(&requests);
        let mut queued = HashMap::<String, VecDeque<HttpResponse>>::new();
        for (uri, body) in responses {
            queued
                .entry((*uri).into())
                .or_default()
                .push_back(response(body));
        }
        let queued = Mutex::new(queued);
        let mut seed_body = "<llsd><map>".to_owned();
        for (name, uri) in capabilities {
            write!(seed_body, "<key>{name}</key><uri>{uri}</uri>")
                .expect("writing to a String cannot fail");
        }
        seed_body.push_str("</map></llsd>");
        let handler = HttpMessageHandler::new(move |request, _| {
            let uri = request.uri.0.clone();
            let response = if uri == SEED_CAPS {
                HttpResponse {
                    status_code: 200,
                    headers: BTreeMap::new(),
                    content_type: Some("application/llsd+xml".into()),
                    body: seed_body.as_bytes().to_vec(),
                }
            } else {
                recorded.lock().expect("request recording").push(request);
                queued
                    .lock()
                    .expect("response queue")
                    .get_mut(&uri)
                    .and_then(VecDeque::pop_front)
                    .unwrap_or_else(|| HttpResponse {
                        status_code: 404,
                        headers: BTreeMap::new(),
                        content_type: None,
                        body: Vec::new(),
                    })
            };
            async move { response }
        });
        (Self { requests }, handler)
    }

    fn requests(&self) -> Vec<HttpRequest> {
        self.requests.lock().expect("request recording").clone()
    }
}

fn response(body: &str) -> HttpResponse {
    HttpResponse {
        status_code: 200,
        headers: BTreeMap::from([("Content-Length".into(), body.len().to_string())]),
        content_type: Some("application/json".into()),
        body: body.as_bytes().to_vec(),
    }
}

fn client_with_fixture(
    capabilities: &[(&str, &str)],
    responses: &[(&str, &str)],
) -> (GridClient, RecordingHttp) {
    let (recording, handler) = RecordingHttp::new(capabilities, responses);
    let mut client = GridClient::new().expect("GridClient constructor");
    client.set_http_caps_client(
        HttpCapsClient::new(handler.clone()).expect("HttpCapsClient constructor"),
    );
    if !capabilities.is_empty() {
        let mut simulator_client = GridClient::new().expect("simulator GridClient constructor");
        simulator_client.set_http_caps_client(
            HttpCapsClient::new(handler).expect("simulator HttpCapsClient constructor"),
        );
        let simulator = Simulator::new(
            simulator_client,
            "127.0.0.1:0".parse().expect("loopback endpoint"),
            0,
            None,
            None,
        )
        .expect("fixture simulator");
        simulator
            .set_seed_caps(Some(Uri(SEED_CAPS.into())), Some(true))
            .expect("seed fixture capabilities");
        let mut network = client.network();
        network.set_current_sim(Some(simulator));
        client.set_network(network);
    }
    (client, recording)
}

fn body(request: &HttpRequest) -> String {
    String::from_utf8(request.body.clone()).expect("UTF-8 request body")
}

fn uuid_eq(actual: &UUID, expected: &UUID) {
    assert_eq!(
        actual.get_bytes().expect("actual UUID bytes"),
        expected.get_bytes().expect("expected UUID bytes")
    );
}

fn happy_upload_responses<'a>(
    capability: &'a str,
    uploader: &'a str,
    new_asset: &UUID,
) -> [(&'a str, String); 2] {
    [
        (
            capability,
            format!(r#"{{"state":"upload","uploader":"{uploader}"}}"#),
        ),
        (
            uploader,
            format!(
                r#"{{"state":"complete","new_asset":"{}"}}"#,
                new_asset.to_string()
            ),
        ),
    ]
}

fn response_refs<'a, const N: usize>(
    responses: &'a [(&'a str, String); N],
) -> Vec<(&'a str, &'a str)> {
    responses
        .iter()
        .map(|(uri, body)| (*uri, body.as_str()))
        .collect()
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs::GLTFMaterialOverrideCapTests.SetMaterialOverrideAsync_HappyPath_PostsBareArrayWithGltfJson::test 0c51b7b793111f5433e52057f26fa5a98d983e7230fefeb61a7b3408e97f28ee translated
#[test]
fn set_material_override_posts_bare_array_with_gltf_json() {
    let (client, recording) = client_with_fixture(
        &[("ModifyMaterialParams", MODIFY_MATERIAL)],
        &[(MODIFY_MATERIAL, r#"{"success":true}"#)],
    );
    let object_id = UUID::random().expect("UUID Random");
    let expected_id = object_id.to_string();
    let mut material = AssetMaterial::new_with_constructor().expect("AssetMaterial constructor");
    material
        .set_base_color_factor_with_color4_boolean(
            Color4::new_with_single_single_single_single(0.1, 0.2, 0.3, 1.0)
                .expect("Color4 constructor"),
            Some(true),
        )
        .expect("SetBaseColorFactor");
    let result = block_on(client.objects().set_material_override(
        client.network().current_sim().expect("current simulator"),
        object_id,
        3,
        material,
        None,
    ))
    .expect("SetMaterialOverrideAsync");
    assert!(result);
    let requests = recording.requests();
    assert_eq!(requests.len(), 1);
    let body = body(&requests[0]);
    assert!(body.contains("<key>object_id</key>"));
    assert!(body.contains(&expected_id));
    assert!(body.contains("<key>side</key>"));
    assert!(body.contains("<key>gltf_json</key>"));
    assert!(!body.contains("<key>asset_id</key>"));
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs::GLTFMaterialOverrideCapTests.ClearMaterialOverrideAsync_SendsEmptyGltfJson::test 8394d063ea71447d9ce2b76fd00be1d10967f9193e1eb909c5b9f2da56eec812 translated
#[test]
fn clear_material_override_sends_empty_gltf_json() {
    let (client, recording) = client_with_fixture(
        &[("ModifyMaterialParams", MODIFY_MATERIAL)],
        &[(MODIFY_MATERIAL, r#"{"success":true}"#)],
    );
    assert!(
        block_on(client.objects().clear_material_override(
            client.network().current_sim().expect("current simulator"),
            UUID::random().expect("UUID Random"),
            0,
            None,
        ))
        .expect("ClearMaterialOverrideAsync")
    );
    let body = body(&recording.requests()[0]);
    assert!(body.contains("<key>gltf_json</key>"));
    assert!(body.contains("<string />") || body.contains("<string></string>"));
    assert!(!body.contains("<key>asset_id</key>"));
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs::GLTFMaterialOverrideCapTests.ApplyMaterialAsync_WithoutOverride_SendsAssetIdOnly::test 1db03ab6710ecf532644398d1fef5e82520e1206058d66fb15c8885fdc8f8363 translated
#[test]
fn apply_material_without_override_sends_asset_id_only() {
    let (client, recording) = client_with_fixture(
        &[("ModifyMaterialParams", MODIFY_MATERIAL)],
        &[(MODIFY_MATERIAL, r#"{"success":true}"#)],
    );
    let asset_id = UUID::random().expect("UUID Random");
    let expected_id = asset_id.to_string();
    assert!(
        block_on(client.objects().apply_material(
            client.network().current_sim().expect("current simulator"),
            UUID::random().expect("UUID Random"),
            1,
            asset_id,
            None,
            None,
        ))
        .expect("ApplyMaterialAsync")
    );
    let body = body(&recording.requests()[0]);
    assert!(body.contains("<key>asset_id</key>"));
    assert!(body.contains(&expected_id));
    assert!(!body.contains("<key>gltf_json</key>"));
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs::GLTFMaterialOverrideCapTests.SendMaterialUpdatesAsync_MultipleEntries_SendsSingleArrayBody::test 621dca88e7f1c9e024563204ca05c3c8d9a03c4b5a7b31ec1c006abe6732b47b translated
#[test]
fn material_updates_send_one_array_body() {
    let (client, recording) = client_with_fixture(
        &[("ModifyMaterialParams", MODIFY_MATERIAL)],
        &[(MODIFY_MATERIAL, r#"{"success":true}"#)],
    );
    let mut first = GLTFMaterialUpdate::new().expect("GLTFMaterialUpdate constructor");
    first.set_object_id(UUID::random().expect("UUID Random"));
    first.set_side(0);
    let mut second = GLTFMaterialUpdate::new().expect("GLTFMaterialUpdate constructor");
    second.set_object_id(UUID::random().expect("UUID Random"));
    second.set_side(1);
    second.set_asset_id(Some(Some(UUID::random().expect("UUID Random"))));
    assert!(
        block_on(client.objects().send_material_updates(
            client.network().current_sim().expect("current simulator"),
            Box::new(vec![first, second].into_iter()),
            None,
        ))
        .expect("SendMaterialUpdatesAsync")
    );
    assert_eq!(recording.requests().len(), 1);
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs::GLTFMaterialOverrideCapTests.SendMaterialUpdatesAsync_ServerRejects_ReturnsFalse::test e9fbd95fff84695e2cb1a35943bda768f00cf9015b25e0ae1505dce4dde603a4 translated
#[test]
fn material_updates_server_rejection_returns_false() {
    let (client, _) = client_with_fixture(
        &[("ModifyMaterialParams", MODIFY_MATERIAL)],
        &[(
            MODIFY_MATERIAL,
            r#"{"success":false,"message":"no rez rights"}"#,
        )],
    );
    assert!(
        !block_on(client.objects().clear_material_override(
            client.network().current_sim().expect("current simulator"),
            UUID::random().expect("UUID Random"),
            0,
            None,
        ))
        .expect("ClearMaterialOverrideAsync")
    );
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs::GLTFMaterialOverrideCapTests.SendMaterialUpdatesAsync_NoCapability_ReturnsFalseWithoutRequest::test fdceb6bf865cb23386a6b82a9741f230e5c22b0ec6d37bc2ac8478f29292a49f translated
#[test]
fn material_updates_without_capability_make_no_request() {
    let (client, recording) = client_with_fixture(&[("SomeOtherCap", OTHER_CAP)], &[]);
    assert!(
        !block_on(client.objects().clear_material_override(
            client.network().current_sim().expect("current simulator"),
            UUID::random().expect("UUID Random"),
            0,
            None,
        ))
        .expect("ClearMaterialOverrideAsync")
    );
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialUploadTests.cs::GLTFMaterialUploadTests.RequestUpdateMaterialAgentInventoryAsync_HappyPath_PostsMetadataThenJsonAndReturnsNewAsset::test 5ac0e3c27fa6ebfddc9d7582aefb0684a676413948b055382c43b1b0b1cd7e7e translated
#[test]
fn material_agent_upload_posts_metadata_then_json() {
    let item_id = UUID::random().expect("UUID Random");
    let new_asset = UUID::random().expect("UUID Random");
    let responses = happy_upload_responses(MATERIAL_AGENT, MATERIAL_UPLOADER, &new_asset);
    let refs = response_refs(&responses);
    let (client, recording) = client_with_fixture(
        &[
            ("UpdateMaterialAgentInventory", MATERIAL_AGENT),
            ("UpdateMaterialTaskInventory", MATERIAL_TASK),
        ],
        &refs,
    );
    let result = block_on(client.inventory().request_update_material_agent_inventory(
        AssetMaterial::new_with_constructor().expect("AssetMaterial constructor"),
        item_id,
        None,
        None,
    ))
    .expect("RequestUpdateMaterialAgentInventoryAsync");
    assert!(result.0);
    uuid_eq(&result.2, &new_asset);
    let requests = recording.requests();
    assert_eq!(requests.len(), 2);
    let metadata = body(&requests[0]);
    assert!(metadata.contains("<key>item_id</key>"));
    assert!(!metadata.contains("<key>task_id</key>"));
    assert_eq!(requests[1].uri, Uri(MATERIAL_UPLOADER.into()));
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialUploadTests.cs::GLTFMaterialUploadTests.RequestUpdateMaterialTaskInventoryAsync_HappyPath_PostsItemIdAndTaskId::test 7ac0e7d210b1aefa795e479f4da0d2dd4caae0b3bbe84ca124707c3ae005bbbd translated
#[test]
fn material_task_upload_posts_item_and_task_ids() {
    let new_asset = UUID::random().expect("UUID Random");
    let responses = happy_upload_responses(MATERIAL_TASK, MATERIAL_UPLOADER, &new_asset);
    let refs = response_refs(&responses);
    let (client, recording) = client_with_fixture(
        &[
            ("UpdateMaterialAgentInventory", MATERIAL_AGENT),
            ("UpdateMaterialTaskInventory", MATERIAL_TASK),
        ],
        &refs,
    );
    let result = block_on(client.inventory().request_update_material_task_inventory(
        AssetMaterial::new_with_constructor().expect("AssetMaterial constructor"),
        UUID::random().expect("UUID Random"),
        UUID::random().expect("UUID Random"),
        None,
        None,
    ))
    .expect("RequestUpdateMaterialTaskInventoryAsync");
    assert!(result.0);
    uuid_eq(&result.2, &new_asset);
    let metadata = body(&recording.requests()[0]);
    assert!(metadata.contains("<key>item_id</key>"));
    assert!(metadata.contains("<key>task_id</key>"));
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialUploadTests.cs::GLTFMaterialUploadTests.RequestUpdateMaterialAgentInventoryAsync_NoCapability_Throws::test 325269807d778264e571d1518ca9a2ca69a27ba67ad689e580f2636ab595f335 translated
#[test]
fn material_agent_upload_without_capability_is_invalid_operation() {
    let (client, _) = client_with_fixture(&[], &[]);
    let result = block_on(client.inventory().request_update_material_agent_inventory(
        AssetMaterial::new_with_constructor().expect("AssetMaterial constructor"),
        UUID::random().expect("UUID Random"),
        None,
        None,
    ));
    match result {
        Err(error) => assert_eq!(error, Error::InvalidOperation),
        Ok(_) => panic!("expected InvalidOperation, got success"),
    }
}

// parity-case: LibreMetaverse.Tests/GLTFMaterialUploadTests.cs::GLTFMaterialUploadTests.RequestUpdateMaterialAgentInventoryAsync_UploadedJsonRoundTripsMaterial::test fd059dfc8c9299c3b6583b3b47ba18ae9c92e3ad923f88634019b0ab9f34d401 translated
#[test]
fn uploaded_material_json_round_trips() {
    let new_asset = UUID::random().expect("UUID Random");
    let responses = happy_upload_responses(MATERIAL_AGENT, MATERIAL_UPLOADER, &new_asset);
    let refs = response_refs(&responses);
    let (client, recording) = client_with_fixture(
        &[
            ("UpdateMaterialAgentInventory", MATERIAL_AGENT),
            ("UpdateMaterialTaskInventory", MATERIAL_TASK),
        ],
        &refs,
    );
    let mut material = AssetMaterial::new_with_constructor().expect("AssetMaterial constructor");
    material.set_name("My Material".into());
    material
        .set_base_color_factor_with_color4_boolean(
            Color4::new_with_single_single_single_single(0.2, 0.4, 0.6, 1.0)
                .expect("Color4 constructor"),
            None,
        )
        .expect("SetBaseColorFactor");
    block_on(client.inventory().request_update_material_agent_inventory(
        material,
        UUID::random().expect("UUID Random"),
        None,
        None,
    ))
    .expect("RequestUpdateMaterialAgentInventoryAsync");
    let uploaded = recording.requests()[1].body.clone();
    let round_trip =
        AssetMaterial::new_with_uuid_bytes(UUID::random().expect("UUID Random"), uploaded)
            .expect("AssetMaterial JSON constructor");
    assert_eq!(round_trip.name(), "My Material");
    assert_eq!(
        [
            round_trip.base_color_factor().r,
            round_trip.base_color_factor().g,
            round_trip.base_color_factor().b,
            round_trip.base_color_factor().a,
        ],
        [0.2, 0.4, 0.6, 1.0]
    );
}

// parity-case: LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs::TerrainMaterialOverrideTests.GetTerrainMaterialOverridesAsync_ParsesFourSlotsWithSomeEmpty::test 0b7f2b5a7a95c70f4ed97b9b028f3ec16f1cd427a9d4d1b133523bd331f6b9f2 translated
#[test]
fn terrain_get_parses_four_slots_with_empty_entries() {
    let (client, _) = client_with_fixture(
        &[("ModifyRegion", MODIFY_REGION)],
        &[(
            MODIFY_REGION,
            r#"{"success":true,"overrides":[{},{"mf":0.25},{},{}]}"#,
        )],
    );
    let overrides = block_on(client.terrain().get_terrain_material_overrides(None))
        .expect("GetTerrainMaterialOverridesAsync")
        .expect("successful override response");
    assert_eq!(overrides.len(), 4);
    assert!(overrides[0].is_none());
    assert_eq!(
        overrides[1].as_ref().expect("slot one").metallic_factor(),
        0.25
    );
    assert!(overrides[2].is_none());
    assert!(overrides[3].is_none());
}

// parity-case: LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs::TerrainMaterialOverrideTests.GetTerrainMaterialOverridesAsync_ServerReportsFailure_ReturnsNull::test c66b84662333288b7328822d1f9fa79450075d44a1178a9b0b661671072efe5b translated
#[test]
fn terrain_get_server_failure_returns_none() {
    let (client, _) = client_with_fixture(
        &[("ModifyRegion", MODIFY_REGION)],
        &[(MODIFY_REGION, r#"{"success":false,"message":"nope"}"#)],
    );
    assert!(
        block_on(client.terrain().get_terrain_material_overrides(None))
            .expect("GetTerrainMaterialOverridesAsync")
            .is_none()
    );
}

// parity-case: LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs::TerrainMaterialOverrideTests.GetTerrainMaterialOverridesAsync_NoCapability_ReturnsNullWithoutRequest::test 677f80689e1b6a67893d100e1116805fb9f63c5b6ec33e64a4dc9a4cf24c1237 translated
#[test]
fn terrain_get_without_capability_makes_no_request() {
    let (client, recording) = client_with_fixture(&[("SomeOtherCap", OTHER_CAP)], &[]);
    assert!(
        block_on(client.terrain().get_terrain_material_overrides(None))
            .expect("GetTerrainMaterialOverridesAsync")
            .is_none()
    );
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs::TerrainMaterialOverrideTests.SetTerrainMaterialOverridesAsync_MixOfNullAndSet_PostsFourEntries::test a4ce031e565ca7eb1864064d299dbf0c7b715581ce330a57be9c3aa93987e67c translated
#[test]
fn terrain_set_posts_four_mixed_entries() {
    let (client, recording) = client_with_fixture(
        &[("ModifyRegion", MODIFY_REGION)],
        &[(MODIFY_REGION, r#"{"success":true}"#)],
    );
    let mut material = AssetMaterial::new_with_constructor().expect("AssetMaterial constructor");
    material
        .set_roughness_factor_with_single_boolean(0.1, Some(true))
        .expect("SetRoughnessFactor");
    assert!(
        block_on(
            client
                .terrain()
                .set_terrain_material_overrides(vec![None, Some(material), None, None], None,)
        )
        .expect("SetTerrainMaterialOverridesAsync")
    );
    let body = body(&recording.requests()[0]);
    assert!(body.contains("<key>overrides</key>"));
    assert!(body.contains("<key>rf</key>"));
}

// parity-case: LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs::TerrainMaterialOverrideTests.SetTerrainMaterialOverridesAsync_WrongLength_Throws::test 836099a369b37555049b1c1ddfb9562c57cdef88381b7a79e8b80fe62b169a44 translated
#[test]
fn terrain_set_wrong_length_is_argument_error() {
    let (client, _) = client_with_fixture(&[("ModifyRegion", MODIFY_REGION)], &[]);
    let result = block_on(
        client
            .terrain()
            .set_terrain_material_overrides(vec![None, None], None),
    );
    match result {
        Err(error) => assert_eq!(error, Error::Argument),
        Ok(_) => panic!("expected Argument, got success"),
    }
}

// parity-case: LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs::TerrainMaterialOverrideTests.SetTerrainMaterialOverridesAsync_NoCapability_ReturnsFalseWithoutRequest::test 37948cbc0240910d63782d88c0fb9f1d239215cea34a5aa84ac68a10d0555e87 translated
#[test]
fn terrain_set_without_capability_makes_no_request() {
    let (client, recording) = client_with_fixture(&[("SomeOtherCap", OTHER_CAP)], &[]);
    assert!(
        !block_on(
            client
                .terrain()
                .set_terrain_material_overrides(vec![None, None, None, None], None,)
        )
        .expect("SetTerrainMaterialOverridesAsync")
    );
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/SendPostcardTests.cs::SendPostcardTests.SendPostcardAsync_HappyPath_PostsMetadataThenImageAndReturnsTrue::test 2e02bd16ef1431c2ca82d7804dffc764be0175cff5daff8c38d0a61641439e06 translated
#[test]
fn postcard_posts_metadata_then_image() {
    let (mut client, recording) = client_with_fixture(
        &[("SendPostcard", POSTCARD)],
        &[
            (
                POSTCARD,
                r#"{"state":"upload","uploader":"http://test.invalid/send-postcard/upload/abc123"}"#,
            ),
            (POSTCARD_UPLOADER, r#"{"state":"complete"}"#),
        ],
    );
    assert!(
        block_on(client.self_().send_postcard(
            vec![0xff, 0xd8, 0xff, 0xd9],
            "friend@example.com".into(),
            "Jim Radford".into(),
            "Postcard from the edge".into(),
            "Hello, how are you today?".into(),
            Vector3::one(),
            None,
        ))
        .expect("SendPostcardAsync")
    );
    let requests = recording.requests();
    assert_eq!(requests.len(), 2);
    assert_eq!(requests[0].method, "POST");
    assert_eq!(requests[0].uri, Uri(POSTCARD.into()));
    let metadata = body(&requests[0]);
    assert!(!metadata.contains("<key>from</key>"));
    for field in ["to", "name", "subject", "msg", "pos-global"] {
        assert!(metadata.contains(field));
    }
    assert_eq!(requests[1].method, "POST");
    assert_eq!(requests[1].uri, Uri(POSTCARD_UPLOADER.into()));
}

// parity-case: LibreMetaverse.Tests/SendPostcardTests.cs::SendPostcardTests.SendPostcardAsync_UploaderMissingFromResponse_ReturnsFalseWithoutSecondRequest::test 2f9498d57d7a512399f9b108765e6e8da8e4c937402f9671d580c66332630432 translated
#[test]
fn postcard_missing_uploader_returns_false_after_one_request() {
    let (mut client, recording) = client_with_fixture(
        &[("SendPostcard", POSTCARD)],
        &[(POSTCARD, r#"{"state":"upload"}"#)],
    );
    assert!(
        !block_on(client.self_().send_postcard(
            vec![1, 2, 3],
            "friend@example.com".into(),
            "Jim Radford".into(),
            "Subject".into(),
            "Message".into(),
            Vector3::zero(),
            None,
        ))
        .expect("SendPostcardAsync")
    );
    assert_eq!(recording.requests().len(), 1);
}

// parity-case: LibreMetaverse.Tests/SendPostcardTests.cs::SendPostcardTests.SendPostcardAsync_UploadDoesNotComplete_ReturnsFalse::test 937b682b406506ec9fba9570852bbf8689450a11cbecb85979af97609b1794f9 translated
#[test]
fn postcard_incomplete_upload_returns_false() {
    let (mut client, _) = client_with_fixture(
        &[("SendPostcard", POSTCARD)],
        &[
            (
                POSTCARD,
                r#"{"state":"upload","uploader":"http://test.invalid/send-postcard/upload/abc123"}"#,
            ),
            (POSTCARD_UPLOADER, r#"{"state":"failed"}"#),
        ],
    );
    assert!(
        !block_on(client.self_().send_postcard(
            vec![1, 2, 3],
            "friend@example.com".into(),
            "Jim Radford".into(),
            "Subject".into(),
            "Message".into(),
            Vector3::zero(),
            None,
        ))
        .expect("SendPostcardAsync")
    );
}

// parity-case: LibreMetaverse.Tests/SendPostcardTests.cs::SendPostcardTests.SendPostcardAsync_NoCapability_ReturnsFalseWithoutRequest::test 8d3ef84e873096d9388bb7db3eaeeb79c661fd9951e338f71bbf3251ae6580e8 translated
#[test]
fn postcard_without_capability_makes_no_request() {
    let (mut client, recording) = client_with_fixture(&[], &[]);
    assert!(
        !block_on(client.self_().send_postcard(
            vec![1, 2, 3],
            "friend@example.com".into(),
            "Jim Radford".into(),
            "Subject".into(),
            "Message".into(),
            Vector3::zero(),
            None,
        ))
        .expect("SendPostcardAsync")
    );
    assert!(recording.requests().is_empty());
}

fn thumbnail_responses(new_asset: &UUID) -> [(&str, String); 2] {
    [
        (
            THUMBNAIL,
            format!(r#"{{"uploader":"{THUMBNAIL_UPLOADER}"}}"#),
        ),
        (
            THUMBNAIL_UPLOADER,
            format!(
                r#"{{"state":"complete","new_asset":"{}"}}"#,
                new_asset.to_string()
            ),
        ),
    ]
}

// parity-case: LibreMetaverse.Tests/UploadThumbnailTests.cs::UploadThumbnailTests.UploadThumbnailAsync_AgentInventoryItem_SendsBareItemIdAndReturnsNewAsset::test 40ca874a37dade3bfbb391a2b4a75c684cfd7509b269286ccc14d747340d095d translated
#[test]
fn thumbnail_agent_item_sends_bare_item_id() {
    let new_asset = UUID::random().expect("UUID Random");
    let responses = thumbnail_responses(&new_asset);
    let refs = response_refs(&responses);
    let (client, recording) =
        client_with_fixture(&[("InventoryThumbnailUpload", THUMBNAIL)], &refs);
    let item_id = UUID::random().expect("UUID Random");
    let result = block_on(client.inventory().upload_thumbnail(
        item_id,
        UUID::zero(),
        vec![1, 2, 3],
        None,
        None,
    ))
    .expect("UploadThumbnailAsync")
    .flatten()
    .expect("new thumbnail asset");
    uuid_eq(&result, &new_asset);
    let requests = recording.requests();
    assert_eq!(requests.len(), 2);
    let metadata = body(&requests[0]);
    assert!(metadata.contains("<key>item_id</key>"));
    assert!(!metadata.contains("<key>task_id</key>"));
    assert!(!metadata.contains("<key>category_id</key>"));
    assert_eq!(requests[1].uri, Uri(THUMBNAIL_UPLOADER.into()));
}

// parity-case: LibreMetaverse.Tests/UploadThumbnailTests.cs::UploadThumbnailTests.UploadThumbnailAsync_TaskInventoryItem_SendsItemIdAndTaskId::test dae80f9a7ba6222a651165f779c123d8472f88a17490e69a1112acc3cec6d108 translated
#[test]
fn thumbnail_task_item_sends_item_and_task_ids() {
    let new_asset = UUID::random().expect("UUID Random");
    let responses = thumbnail_responses(&new_asset);
    let refs = response_refs(&responses);
    let (client, recording) =
        client_with_fixture(&[("InventoryThumbnailUpload", THUMBNAIL)], &refs);
    let item_id = UUID::random().expect("UUID Random");
    let task_id = UUID::random().expect("UUID Random");
    let result =
        block_on(
            client
                .inventory()
                .upload_thumbnail(item_id, task_id, vec![1, 2, 3], None, None),
        )
        .expect("UploadThumbnailAsync")
        .flatten()
        .expect("new thumbnail asset");
    uuid_eq(&result, &new_asset);
    let metadata = body(&recording.requests()[0]);
    assert!(metadata.contains("<key>item_id</key>"));
    assert!(metadata.contains("<key>task_id</key>"));
    assert!(!metadata.contains("<key>category_id</key>"));
}

// parity-case: LibreMetaverse.Tests/UploadThumbnailTests.cs::UploadThumbnailTests.UploadThumbnailAsync_KnownLocalFolder_SendsCategoryId::test 1ccca44bbead54fd5041c41fc6560478ba17f9e5d9bacff21096b254d2f526db translated
#[test]
fn thumbnail_known_local_folder_sends_category_id() {
    let folder_id = UUID::random().expect("UUID Random");
    let new_asset = UUID::random().expect("UUID Random");
    let responses = thumbnail_responses(&new_asset);
    let refs = response_refs(&responses);
    let (client, recording) =
        client_with_fixture(&[("InventoryThumbnailUpload", THUMBNAIL)], &refs);
    let mut folder = InventoryFolder::new(folder_id).expect("InventoryFolder constructor");
    folder.base.set_name("Test Folder".into());
    folder.base.set_parent_uuid(UUID::zero());
    let mut store = client.inventory().store().expect("fixture inventory store");
    store.set_item(folder_id, folder.base);
    let result = block_on(client.inventory().upload_thumbnail(
        folder_id,
        UUID::zero(),
        vec![1, 2, 3],
        None,
        None,
    ))
    .expect("UploadThumbnailAsync")
    .flatten()
    .expect("new thumbnail asset");
    uuid_eq(&result, &new_asset);
    let metadata = body(&recording.requests()[0]);
    assert!(metadata.contains("<key>category_id</key>"));
    assert!(!metadata.contains("<key>item_id</key>"));
    assert!(!metadata.contains("<key>task_id</key>"));
}

// parity-case: LibreMetaverse.Tests/UploadThumbnailTests.cs::UploadThumbnailTests.UploadThumbnailAsync_NoCapability_ReturnsNullWithoutRequest::test 2bc1ad18b159c5d62a18d2208ed3cf3c88d3f666a0e0dad8730778cd22b59eda translated
#[test]
fn thumbnail_without_capability_makes_no_request() {
    let (client, recording) = client_with_fixture(&[], &[]);
    let result = block_on(client.inventory().upload_thumbnail(
        UUID::random().expect("UUID Random"),
        UUID::zero(),
        vec![1],
        None,
        None,
    ))
    .expect("UploadThumbnailAsync")
    .flatten();
    assert!(result.is_none());
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/UploadThumbnailTests.cs::UploadThumbnailTests.UploadThumbnailAsync_MetadataResponseMissingUploader_ReturnsNullWithoutSecondRequest::test 8db3eddf1204ad795e9d5ef15365f422f96191a8b4c24ef3393b03c10d92d662 translated
#[test]
fn thumbnail_missing_uploader_returns_none_after_one_request() {
    let (client, recording) = client_with_fixture(
        &[("InventoryThumbnailUpload", THUMBNAIL)],
        &[(THUMBNAIL, r#"{"state":"upload"}"#)],
    );
    let result = block_on(client.inventory().upload_thumbnail(
        UUID::random().expect("UUID Random"),
        UUID::zero(),
        vec![1],
        None,
        None,
    ))
    .expect("UploadThumbnailAsync")
    .flatten();
    assert!(result.is_none());
    assert_eq!(recording.requests().len(), 1);
}

// parity-case: LibreMetaverse.Tests/UploadThumbnailTests.cs::UploadThumbnailTests.UploadThumbnailAsync_UploadDoesNotComplete_ReturnsNull::test e870f6913e3f45676ce490069930427e6c625072fc1d6ebbf16f503be51df4ae translated
#[test]
fn thumbnail_incomplete_upload_returns_none() {
    let (client, _) = client_with_fixture(
        &[("InventoryThumbnailUpload", THUMBNAIL)],
        &[
            (
                THUMBNAIL,
                r#"{"uploader":"http://test.invalid/inventory-thumbnail-upload/upload/abc123"}"#,
            ),
            (
                THUMBNAIL_UPLOADER,
                r#"{"state":"failed","message":"bad image"}"#,
            ),
        ],
    );
    let result = block_on(client.inventory().upload_thumbnail(
        UUID::random().expect("UUID Random"),
        UUID::zero(),
        vec![1],
        None,
        None,
    ))
    .expect("UploadThumbnailAsync")
    .flatten();
    assert!(result.is_none());
}

// parity-case: LibreMetaverse.Tests/ConstructorNullArgumentTests.cs::ConstructorNullArgumentTests.AssetManager_Null_Throws::test 4fa1be8b233dacf6a5955359f2197ff77ae85ffbf2c3c68fcd8cbe672c2c5f91 translated
#[test]
fn asset_manager_none_client_is_argument_null() {
    match AssetManager::new(None) {
        Err(error) => assert_eq!(error, Error::ArgumentNull),
        Ok(_) => panic!("expected ArgumentNull, got success"),
    }
}
