// Exact Rust translations of world capability tests owned by milestone issue #23.
// Source SHA-256: f83be0075683edda91f8e345661b2f753d81c134585f38ddd52b8b9b9e406089
// Source SHA-256: 79483c67d93295f09d281ec577f642d4967e281f88e531b2b43ee14c1444a440
// Source SHA-256: 1ef332ad434b9bdc468282eae99150393950db352a98f5f822517fa48021222f
// Source SHA-256: e49de5d4d588aa944bc27eeecd9921891b851eaecb76270abcec91f649579f7b

use libremetaverse::messages::linden::{EnvironmentData, SimConsoleResponseMessage};
use libremetaverse::{
    GridClient, HttpCapsClient, RegionRestartDays, RegionRestartSchedule, Simulator,
};
use libremetaverse_compat_tests::block_on;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{HttpMessageHandler, HttpRequest, HttpResponse, Uri};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;

const CAP: &str = "http://test.invalid/capability";
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 response(status: u16, content_type: &str, body: &str) -> HttpResponse {
    HttpResponse {
        status_code: status,
        headers: BTreeMap::new(),
        content_type: Some(content_type.into()),
        body: body.as_bytes().to_vec(),
    }
}

fn client_with_cap(
    cap_name: &str,
    expose_capability: bool,
    cap_response: HttpResponse,
) -> (GridClient, Recording) {
    let requests = Arc::new(Mutex::new(Vec::new()));
    let captured = Arc::clone(&requests);
    let cap_reply = Mutex::new(Some(cap_response));
    let seed = if expose_capability {
        format!("<llsd><map><key>{cap_name}</key><uri>{CAP}</uri></map></llsd>")
    } else {
        "<llsd><map></map></llsd>".into()
    };
    let handler = HttpMessageHandler::new(move |request, _| {
        let reply = if request.uri.0 == SEED {
            response(200, "application/llsd+xml", &seed)
        } else {
            captured.lock().unwrap().push(request);
            cap_reply
                .lock()
                .unwrap()
                .take()
                .unwrap_or_else(|| response(404, "application/octet-stream", ""))
        };
        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,
    )
    .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 only_request(recording: &Recording) -> HttpRequest {
    let requests = recording.requests();
    assert_eq!(requests.len(), 1);
    requests.into_iter().next().unwrap()
}

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

// parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.GetParcelEnvironment_UsesParcelidQueryParameter::test 5c2f2b0fa2a86955975c621b2415a92ff3da283215e669956e14bf5ee2a4c1f4 translated
#[test]
fn get_parcel_environment_uses_parcelid_query_parameter() {
    let (client, recording) = client_with_cap(
        "ExtEnvironment",
        true,
        response(
            200,
            "application/llsd+xml",
            "<llsd><map><key>success</key><boolean>true</boolean></map></llsd>",
        ),
    );
    block_on(client.environment().get_parcel_environment(42, None)).unwrap();
    let request = only_request(&recording);
    assert_eq!(request.method, "GET");
    assert!(request.uri.0.contains("parcelid=42"));
    assert!(!request.uri.0.contains("parcel_id"));
}

// parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.SetParcelEnvironment_SendsParcelidInQueryStringNotBody::test f633e2d78e4c6a0fff42917ccfe8de68e4f6133a11e7eef7f9b757c3d1622f0f translated
#[test]
fn set_parcel_environment_sends_parcelid_in_query_not_body() {
    let (client, recording) = client_with_cap(
        "ExtEnvironment",
        true,
        response(
            200,
            "application/llsd+xml",
            "<llsd><map><key>success</key><boolean>true</boolean></map></llsd>",
        ),
    );
    let mut environment = EnvironmentData::new().expect("EnvironmentData constructor");
    environment.set_day_length(7200);
    block_on(
        client
            .environment()
            .set_parcel_environment(7, environment, None),
    )
    .unwrap();
    let request = only_request(&recording);
    assert_eq!(request.method, "POST");
    assert!(request.uri.0.contains("parcelid=7"));
    let OSD::Map(body) = OSDParser::deserialize_with_bytes(request.body).unwrap() else {
        panic!("ExtEnvironment POST body is not an LLSD map")
    };
    assert!(!body.contains_key("parcel_id"));
    let Some(OSD::Map(environment)) = body.get("environment") else {
        panic!("environment map missing")
    };
    assert_eq!(
        environment.get("day_length").unwrap().as_integer().unwrap(),
        7200
    );
    assert!(!environment.contains_key("sky_track"));
}

// parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.SetRegionEnvironment_OmitsQueryStringEntirely::test bade6e06765506ad7e500ac3e5c9f168605ce25fef86647819d285a68b39f109 translated
#[test]
fn set_region_environment_omits_query_string_entirely() {
    let (client, recording) = client_with_cap(
        "ExtEnvironment",
        true,
        response(
            200,
            "application/llsd+xml",
            "<llsd><map><key>success</key><boolean>true</boolean></map></llsd>",
        ),
    );
    let environment = EnvironmentData::new().expect("EnvironmentData constructor");
    block_on(
        client
            .environment()
            .set_region_environment_with_environment_data_cancellation_token(environment, None),
    )
    .unwrap();
    let request = only_request(&recording);
    assert_eq!(request.method, "POST");
    assert_eq!(request.uri.0, CAP);
}

// parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.ResetParcelEnvironment_SendsHttpDeleteNotPost::test 4360439192c137f5d7a2ceb83b8664f3917b4b261798050cb855d596f483f082 translated
#[test]
fn reset_parcel_environment_sends_delete_not_post() {
    let (client, recording) = client_with_cap(
        "ExtEnvironment",
        true,
        response(
            200,
            "application/llsd+xml",
            "<llsd><map><key>success</key><boolean>true</boolean></map></llsd>",
        ),
    );
    assert!(block_on(client.environment().reset_parcel_environment(3, None)).unwrap());
    let request = only_request(&recording);
    assert_eq!(request.method, "DELETE");
    assert!(request.uri.0.contains("parcelid=3"));
}

// parity-case: LibreMetaverse.Tests/QueryExperiencesOnParcelTests.cs::QueryExperiencesOnParcelTests.QueryExperiencesOnParcelAsync_HappyPath_ParsesAllowedMapAndBuildsExpectedQuery::test 2571d3a4100face21268d7ca2401cc8468e711c03c808704aacc7a50006125d5 translated
#[test]
fn query_experiences_on_parcel_happy_path() {
    let first = uuid();
    let second = uuid();
    let first_text = first.to_string();
    let second_text = second.to_string();
    let body = format!(r#"{{"experiences":{{"{first_text}":true,"{second_text}":false}}}}"#);
    let (mut client, recording) = client_with_cap(
        "ExperienceQuery",
        true,
        response(200, "application/json", &body),
    );
    let result = block_on(client.self_().query_experiences_on_parcel(
        42,
        Box::new([first, second].into_iter()),
        None,
    ))
    .unwrap()
    .expect("experience result");
    assert_eq!(result.get(&first), Some(&true));
    assert_eq!(result.get(&second), Some(&false));
    let request = only_request(&recording);
    assert!(request.uri.0.contains("parcelid=42"));
    assert!(
        request
            .uri
            .0
            .contains(&format!("experiences={first_text},{second_text}"))
    );
}

// parity-case: LibreMetaverse.Tests/QueryExperiencesOnParcelTests.cs::QueryExperiencesOnParcelTests.QueryExperiencesOnParcelAsync_NoExperienceIds_OmitsExperiencesParam::test ddbaa53aa8c63262c2241588b4281c42bbb66efdecac95e297deb0fb81797455 translated
#[test]
fn query_experiences_on_parcel_no_ids_omits_parameter() {
    let (mut client, recording) = client_with_cap(
        "ExperienceQuery",
        true,
        response(200, "application/json", r#"{"experiences":{}}"#),
    );
    let result = block_on(client.self_().query_experiences_on_parcel(
        7,
        Box::new(std::iter::empty()),
        None,
    ))
    .unwrap()
    .expect("experience result");
    assert!(result.is_empty());
    let request = only_request(&recording);
    assert!(request.uri.0.contains("parcelid=7"));
    assert!(!request.uri.0.contains("experiences"));
}

// parity-case: LibreMetaverse.Tests/QueryExperiencesOnParcelTests.cs::QueryExperiencesOnParcelTests.QueryExperiencesOnParcelAsync_NoCapability_ReturnsNullWithoutRequest::test 5c6a6584fba8ddc66200367217f658d342fb2c26d6be9e49ccf3892bea420095 translated
#[test]
fn query_experiences_on_parcel_no_capability_returns_none_without_request() {
    let (mut client, recording) = client_with_cap(
        "ExperienceQuery",
        false,
        response(404, "application/json", ""),
    );
    let result = block_on(client.self_().query_experiences_on_parcel(
        1,
        Box::new([uuid()].into_iter()),
        None,
    ))
    .unwrap();
    assert!(result.is_none());
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/QueryExperiencesOnParcelTests.cs::QueryExperiencesOnParcelTests.QueryExperiencesOnParcelAsync_NonSuccessStatus_ReturnsNull::test 977fe55acd5aeb5735cc718f705a2742ae0dfab12dc1211c03a64cc6d1f3bbf5 translated
#[test]
fn query_experiences_on_parcel_non_success_returns_none() {
    let (mut client, _) = client_with_cap(
        "ExperienceQuery",
        true,
        response(500, "application/json", ""),
    );
    assert!(
        block_on(client.self_().query_experiences_on_parcel(
            1,
            Box::new([uuid()].into_iter()),
            None
        ))
        .unwrap()
        .is_none()
    );
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.GetRegionRestartScheduleAsync_WeeklySchedule_ParsesDaysAndTime::test 0f64aab66c6a6d807b3d2bbc46b636a85900e4f76e35d5e3dd8cd0d8b295618b translated
#[test]
fn get_region_restart_schedule_weekly_parses_days_and_time() {
    let (client, _) = client_with_cap(
        "RegionSchedule",
        true,
        response(
            200,
            "application/json",
            r#"{"restart":{"type":"W","days":"TR","time":7200}}"#,
        ),
    );
    let schedule = block_on(client.estate().get_region_restart_schedule(None))
        .unwrap()
        .expect("restart schedule");
    assert!(!schedule.is_daily());
    assert_eq!(
        schedule.days(),
        RegionRestartDays(RegionRestartDays::TUESDAY.0 | RegionRestartDays::THURSDAY.0)
    );
    assert_eq!(schedule.time(), Duration::from_secs(2 * 60 * 60));
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.GetRegionRestartScheduleAsync_DailySchedule_ReturnsAllDays::test cd966b6e3ba470c91ac958bf6c71aeb34f4dab060ad5334cb6060e1954b5c6d0 translated
#[test]
fn get_region_restart_schedule_daily_returns_all_days() {
    let (client, _) = client_with_cap(
        "RegionSchedule",
        true,
        response(
            200,
            "application/json",
            r#"{"restart":{"type":"D","time":3600}}"#,
        ),
    );
    let schedule = block_on(client.estate().get_region_restart_schedule(None))
        .unwrap()
        .expect("restart schedule");
    assert!(schedule.is_daily());
    assert_eq!(schedule.days(), RegionRestartDays::ALL);
    assert_eq!(schedule.time(), Duration::from_secs(60 * 60));
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.GetRegionRestartScheduleAsync_NoRestartConfigured_ReturnsNull::test 59a4b050a79659ccaf0fff13b750d634d803ed7b9e4940cbad676c1bdc04a5fc translated
#[test]
fn get_region_restart_schedule_no_restart_returns_none() {
    let (client, _) = client_with_cap(
        "RegionSchedule",
        true,
        response(200, "application/json", "{}"),
    );
    assert!(
        block_on(client.estate().get_region_restart_schedule(None))
            .unwrap()
            .is_none()
    );
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.GetRegionRestartScheduleAsync_NoCapability_ReturnsNullWithoutRequest::test e169e7846874ed4309a50cb25b03c2e722a8d758a787654a5906610ab3b655f1 translated
#[test]
fn get_region_restart_schedule_no_capability_returns_none_without_request() {
    let (client, recording) = client_with_cap(
        "RegionSchedule",
        false,
        response(404, "application/json", ""),
    );
    assert!(
        block_on(client.estate().get_region_restart_schedule(None))
            .unwrap()
            .is_none()
    );
    assert!(recording.requests().is_empty());
}

fn schedule(daily: bool, days: RegionRestartDays, time: Duration) -> RegionRestartSchedule {
    let mut schedule = RegionRestartSchedule::new().expect("RegionRestartSchedule constructor");
    schedule.set_is_daily(daily);
    schedule.set_days(days);
    schedule.set_time(time);
    schedule
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.SetRegionRestartScheduleAsync_Weekly_PostsUppercaseDaysAndSeconds::test 153fc0ac84af549c1381d0814a356bf2d9e0ead2c879e503981193366943546c translated
#[test]
fn set_region_restart_schedule_weekly_posts_uppercase_days_and_seconds() {
    let (client, recording) = client_with_cap(
        "RegionSchedule",
        true,
        response(200, "application/json", ""),
    );
    let days = RegionRestartDays(
        RegionRestartDays::MONDAY.0 | RegionRestartDays::WEDNESDAY.0 | RegionRestartDays::FRIDAY.0,
    );
    assert!(
        block_on(
            client.estate().set_region_restart_schedule(
                schedule(false, days, Duration::from_secs(5400)),
                None
            )
        )
        .unwrap()
    );
    let body = String::from_utf8(only_request(&recording).body).unwrap();
    assert!(body.contains("<key>type</key>"));
    assert!(body.contains('W'));
    assert!(body.contains("MWF"));
    assert!(body.contains("5400"));
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.SetRegionRestartScheduleAsync_DailyIgnoresDays_DoesNotPostDaysKey::test b51a1272465c3316194542eb346b2aa15c43fdc8474163ddd851b3f2204d1c2b translated
#[test]
fn set_region_restart_schedule_daily_omits_days() {
    let (client, recording) = client_with_cap(
        "RegionSchedule",
        true,
        response(200, "application/json", ""),
    );
    assert!(
        block_on(client.estate().set_region_restart_schedule(
            schedule(
                true,
                RegionRestartDays::NONE,
                Duration::from_secs(3 * 60 * 60)
            ),
            None
        ))
        .unwrap()
    );
    assert!(
        !String::from_utf8(only_request(&recording).body)
            .unwrap()
            .contains("<key>days</key>")
    );
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.SetRegionRestartScheduleAsync_EmptyDaysClearsSchedule::test 57ef3c0f8d9eeac58a7d1b3e7dbc44413016f39303d304cacd3071bd2c55f320 translated
#[test]
fn set_region_restart_schedule_empty_days_clears_schedule() {
    let (client, recording) = client_with_cap(
        "RegionSchedule",
        true,
        response(200, "application/json", ""),
    );
    assert!(
        block_on(client.estate().set_region_restart_schedule(
            schedule(false, RegionRestartDays::NONE, Duration::ZERO),
            None
        ))
        .unwrap()
    );
    let body = String::from_utf8(only_request(&recording).body).unwrap();
    assert!(body.contains("<key>days</key>"));
    assert!(body.contains("<string />") || body.contains("<string></string>"));
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.SetRegionRestartScheduleAsync_NoCapability_ReturnsFalseWithoutRequest::test be03507fe574b9f7c7174c39af44b7b693e1b7a496845db46451ef09bf9db206 translated
#[test]
fn set_region_restart_schedule_no_capability_returns_false_without_request() {
    let (client, recording) = client_with_cap(
        "RegionSchedule",
        false,
        response(404, "application/json", ""),
    );
    assert!(
        !block_on(client.estate().set_region_restart_schedule(
            schedule(true, RegionRestartDays::NONE, Duration::ZERO),
            None
        ))
        .unwrap()
    );
    assert!(recording.requests().is_empty());
}

// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.SetRegionRestartScheduleAsync_NonSuccessStatus_ReturnsFalse::test 16c8988d7aa15ea9f58deaa2e3f416b6115978e23073fd99749843f4f410dca4 translated
#[test]
fn set_region_restart_schedule_non_success_returns_false() {
    let (client, _) = client_with_cap(
        "RegionSchedule",
        true,
        response(500, "application/json", ""),
    );
    assert!(
        !block_on(client.estate().set_region_restart_schedule(
            schedule(true, RegionRestartDays::NONE, Duration::ZERO),
            None
        ))
        .unwrap()
    );
}

// parity-case: LibreMetaverse.Tests/SimConsoleTests.cs::SimConsoleTests.SimConsoleResponseMessage_Deserialize_ReadsBodyField::test 4d367bc3fd012d1579847fd9837bf883c5ea95452776a086bbf856899d7c2099 translated
#[test]
fn sim_console_response_message_deserialize_reads_body_field() {
    let map = libremetaverse_structured_data::OSDMap::new_with_dictionary(
        std::collections::HashMap::from([(
            "body".into(),
            OSD::String("console output text".into()),
        )]),
    )
    .unwrap();
    let mut message =
        SimConsoleResponseMessage::new().expect("SimConsoleResponseMessage constructor");
    message.deserialize(map).unwrap();
    assert_eq!(message.body, "console output text");
}
