use libremetaverse::{GridClient, HttpCapsClient, Simulator, UserReport, UserReportType};
use libremetaverse_compat_tests::block_on;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{
    CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use libremetaverse_types::{Error, UUID, Vector3};
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex};

const SEED: &str = "http://test.invalid/seed";
const CATEGORIES: &str = "http://test.invalid/abuse-categories";
const SEND: &str = "http://test.invalid/send-report";
const SEND_SCREENSHOT: &str = "http://test.invalid/send-report-with-screenshot";

fn response(status_code: u16, body: Vec<u8>) -> HttpResponse {
    HttpResponse {
        status_code,
        headers: BTreeMap::new(),
        content_type: Some("application/llsd+xml".into()),
        body,
    }
}

fn fixture(
    capabilities: &[(&str, &str)],
    categories: OSD,
) -> (GridClient, Arc<Mutex<Vec<HttpRequest>>>) {
    let capabilities = capabilities
        .iter()
        .map(|(name, uri)| ((*name).to_owned(), OSD::Uri(Uri((*uri).to_owned()))))
        .collect::<HashMap<_, _>>();
    let seed_body =
        OSDParser::serialize_llsd_xml_bytes(OSD::Map(capabilities)).expect("seed capability LLSD");
    let categories_body =
        OSDParser::serialize_llsd_xml_bytes(categories).expect("category response LLSD");
    let requests = Arc::new(Mutex::new(Vec::new()));
    let captured = Arc::clone(&requests);
    let handler = HttpMessageHandler::new(move |request, _token| {
        let response = if request.uri.0 == SEED {
            response(200, seed_body.clone())
        } else if request.uri.0.starts_with(CATEGORIES) {
            response(200, categories_body.clone())
        } else if request.uri.0 == SEND || request.uri.0 == SEND_SCREENSHOT {
            response(200, Vec::new())
        } else {
            response(404, Vec::new())
        };
        captured.lock().expect("request recording").push(request);
        async move { response }
    });
    let http = HttpCapsClient::new(handler).expect("HTTP fixture");
    let mut client = GridClient::new().expect("GridClient constructor");
    client.set_http_caps_client(http.clone());
    let mut simulator_client = GridClient::new().expect("simulator GridClient constructor");
    simulator_client.set_http_caps_client(http);
    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.into())), Some(true))
        .expect("fixture seed capability");
    let mut network = client.network();
    network.set_current_sim(Some(simulator));
    client.set_network(network);
    (client, requests)
}

fn report(screenshot_id: UUID) -> UserReport {
    UserReport {
        report_type: UserReportType::Complaint,
        category: 7,
        screenshot_id,
        object_id: UUID::random().expect("object UUID"),
        abuser_id: UUID::random().expect("abuser UUID"),
        abuse_region_name: "Recorded Region".into(),
        abuse_region_id: UUID::random().expect("region UUID"),
        position: Vector3 {
            x: 12.5,
            y: 64.0,
            z: 21.25,
        },
        summary: "summary".into(),
        details: "details".into(),
    }
}

#[test]
fn categories_preserve_localized_descriptions_and_language_query() {
    let categories = OSD::Map(HashMap::from([(
        "categories".into(),
        OSD::Array(vec![
            OSD::Map(HashMap::from([
                (
                    "description_localized".into(),
                    OSD::String("Harassment".into()),
                ),
                ("category".into(), OSD::String("7".into())),
            ])),
            OSD::Map(HashMap::from([
                ("description_localized".into(), OSD::String("Fraud".into())),
                ("category".into(), OSD::String("11".into())),
            ])),
        ]),
    )]));
    let (client, requests) = fixture(&[("AbuseCategories", CATEGORIES)], categories);

    let result = block_on(
        client
            .user_reports()
            .fetch_abuse_report_categories(Some("en-US"), None),
    )
    .expect("category request");

    assert_eq!(result.get("Harassment").map(String::as_str), Some("7"));
    assert_eq!(result.get("Fraud").map(String::as_str), Some("11"));
    assert!(
        requests
            .lock()
            .expect("request recording")
            .iter()
            .any(|request| request.uri.0 == format!("{CATEGORIES}?lc=en-US"))
    );
}

#[test]
fn screenshot_report_selects_capability_and_preserves_exact_payload() {
    let screenshot_id = UUID::random().expect("screenshot UUID");
    let expected = report(screenshot_id);
    let (client, requests) = fixture(
        &[
            ("SendUserReport", SEND),
            ("SendUserReportWithScreenshot", SEND_SCREENSHOT),
        ],
        OSD::Map(HashMap::new()),
    );

    assert!(
        block_on(
            client
                .user_reports()
                .send_user_report(expected.clone(), None)
        )
        .expect("report submission")
    );
    let requests = requests.lock().expect("request recording");
    let request = requests
        .iter()
        .find(|request| request.uri.0 == SEND_SCREENSHOT)
        .expect("screenshot capability POST");
    assert_eq!(request.method, "POST");
    let OSD::Map(payload) =
        OSDParser::deserialize_with_bytes(request.body.clone()).expect("report payload LLSD")
    else {
        panic!("report payload must be a map");
    };
    assert_eq!(payload.get("report-type").unwrap().as_integer().unwrap(), 3);
    assert_eq!(payload.get("category").unwrap().as_integer().unwrap(), 7);
    assert_eq!(payload.get("check-flags").unwrap().as_integer().unwrap(), 0);
    assert_eq!(
        payload.get("screenshot-id").unwrap().as_uuid().unwrap(),
        screenshot_id
    );
    assert_eq!(
        payload.get("object-id").unwrap().as_uuid().unwrap(),
        expected.object_id
    );
    assert_eq!(
        payload.get("abuser-id").unwrap().as_uuid().unwrap(),
        expected.abuser_id
    );
    assert_eq!(
        payload
            .get("abuse-region-name")
            .unwrap()
            .as_string()
            .unwrap(),
        ""
    );
    assert_eq!(
        payload.get("abuse-region-id").unwrap().as_uuid().unwrap(),
        UUID::zero()
    );
    assert_eq!(
        payload.get("position").unwrap().as_vector3().unwrap(),
        expected.position
    );
    assert_eq!(
        payload.get("summary").unwrap().as_string().unwrap(),
        "summary"
    );
    assert_eq!(
        payload.get("version-string").unwrap().as_string().unwrap(),
        "<3 LibreMetaverse"
    );
    assert_eq!(
        payload.get("details").unwrap().as_string().unwrap(),
        "details"
    );
    assert!(!requests.iter().any(|request| request.uri.0 == SEND));
}

#[test]
fn live_report_submission_requires_explicit_opt_in() {
    let live_cap = "https://example.com/send-report";
    let (client, requests) = fixture(&[("SendUserReport", live_cap)], OSD::Map(HashMap::new()));

    let error = block_on(
        client
            .user_reports()
            .send_user_report(report(UUID::zero()), None),
    )
    .expect_err("live report must be gated");
    assert_eq!(error, Error::InvalidOperation);
    assert!(
        !requests
            .lock()
            .expect("request recording")
            .iter()
            .any(|request| request.uri.0 == live_cap)
    );
}

#[test]
fn category_request_propagates_preexisting_cancellation() {
    let (client, _) = fixture(&[("AbuseCategories", CATEGORIES)], OSD::Map(HashMap::new()));
    let cancellation = CancellationTokenSource::new();
    cancellation.cancel();

    let error = block_on(
        client
            .user_reports()
            .fetch_abuse_report_categories(None, Some(cancellation.token())),
    )
    .expect_err("cancellation must be propagated");
    assert_eq!(error, Error::Cancelled);
}
