#![allow(clippy::needless_pass_by_value)]

// Exact fixture provenance at LibreMetaverse 2aa70bb68513b39795da5d13c88f31b86e85a3ba:
// DownloadManagerTests.cs                 07f31d4db35335602bc8d0a70ef440d38ce7a59f2c7765a184bc33f03b6c6015
// GridClientTests.cs                      adfee7ccb929270d5e88dccdb354c686bd008a167a3a1028b536664813c6d1
// HttpCapsClientNonHttpLocationTests.cs   92cac055067808883132d7415dc87d17e6ad05a62b85cd454e552879f568016f
// NetworkTests.cs                         325586ce3cf9af1dbcb8dd36fc2be5647e524f4baeeb260520e44e66172142dc

use libremetaverse::http::DownloadManager;
use libremetaverse::packets::PacketType;
use libremetaverse::{GridClient, GridLayerType, HttpCapsClient, NetworkManager};
use libremetaverse_compat_tests::{block_on, live_grid_credentials};
use libremetaverse_types::compat::{
    CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use std::collections::BTreeMap;
use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::{Duration, Instant};

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

struct Delay {
    state: Arc<Mutex<(bool, Option<Waker>)>>,
}

impl Delay {
    fn new(duration: Duration) -> Self {
        let state = Arc::new(Mutex::new((false, None::<Waker>)));
        let worker_state = Arc::clone(&state);
        thread::spawn(move || {
            thread::sleep(duration);
            let waker = {
                let mut state = worker_state
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                state.0 = true;
                state.1.take()
            };
            if let Some(waker) = waker {
                waker.wake();
            }
        });
        Self { state }
    }
}

impl Future for Delay {
    type Output = ();

    fn poll(self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if state.0 {
            Poll::Ready(())
        } else {
            state.1 = Some(context.waker().clone());
            Poll::Pending
        }
    }
}

impl FakeHandler {
    fn new(data: &[u8], delay: Duration) -> (Self, HttpMessageHandler) {
        let requests = Arc::new(Mutex::new(Vec::new()));
        let recorded = Arc::clone(&requests);
        let response = HttpResponse {
            status_code: 200,
            headers: BTreeMap::from([("Content-Length".into(), data.len().to_string())]),
            content_type: None,
            body: data.to_vec(),
        };
        let handler = HttpMessageHandler::new(move |request, _cancellation_token| {
            let recorded = Arc::clone(&recorded);
            let response = response.clone();
            async move {
                recorded
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .push(request);
                if !delay.is_zero() {
                    Delay::new(delay).await;
                }
                response
            }
        });
        (Self { requests }, handler)
    }

    fn send_count(&self) -> usize {
        self.requests
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .len()
    }

    fn only_request(&self) -> HttpRequest {
        let requests = self
            .requests
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        assert_eq!(requests.len(), 1, "expected exactly one HTTP request");
        requests[0].clone()
    }
}

fn block_on_timeout<F>(future: F, timeout: Duration) -> F::Output
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,
{
    let (sender, receiver) = mpsc::sync_channel(1);
    let worker = thread::spawn(move || {
        let _ = sender.send(block_on(future));
    });
    let output = receiver.recv_timeout(timeout).expect("operation timed out");
    worker.join().expect("operation worker panicked");
    output
}

async fn join<F1, F2>(first: F1, second: F2) -> (F1::Output, F2::Output)
where
    F1: Future,
    F2: Future,
{
    let mut first = std::pin::pin!(first);
    let mut second = std::pin::pin!(second);
    let mut first_output = None;
    let mut second_output = None;
    std::future::poll_fn(|context| {
        if first_output.is_none()
            && let Poll::Ready(output) = first.as_mut().poll(context)
        {
            first_output = Some(output);
        }
        if second_output.is_none()
            && let Poll::Ready(output) = second.as_mut().poll(context)
        {
            second_output = Some(output);
        }
        if first_output.is_some() && second_output.is_some() {
            Poll::Ready((
                first_output.take().expect("first future output"),
                second_output.take().expect("second future output"),
            ))
        } else {
            Poll::Pending
        }
    })
    .await
}

fn live_credentials() -> (String, String, String, String) {
    live_grid_credentials()
}

// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_SingleDownload_CompletesSuccessfully::test f8feb54d38f79624ddf1d368e0599259ff9d474c08ea0249a8f244460a1f533f translated
#[test]
fn queue_download_single_completes_successfully() {
    let bytes = b"hello world".to_vec();
    let (handler, message_handler) = FakeHandler::new(&bytes, Duration::ZERO);
    let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
    let mut client = GridClient::new().expect("GridClient constructor");
    client.set_http_caps_client(http_caps);
    let downloads = Arc::new(DownloadManager::new(client).expect("DownloadManager constructor"));
    let worker_downloads = Arc::clone(&downloads);

    let result = block_on_timeout(
        async move {
            worker_downloads
                .queue_download_with_uri_string_i_progress_cancellation_token_int32(
                    Uri("http://example.test/one".into()),
                    None,
                    None,
                    Some(CancellationToken::default()),
                    Some(1),
                )
                .await
        },
        Duration::from_secs(5),
    );
    let dispose_result = downloads.dispose();
    let (response, data) = result.expect("QueueDownloadAsync");

    assert_eq!(data, bytes);
    assert!(response.is_success_status_code());
    assert_eq!(response.headers["Content-Length"], bytes.len().to_string());
    let request = handler.only_request();
    assert_eq!(request.method, "GET");
    assert_eq!(request.uri, Uri("http://example.test/one".into()));
    dispose_result.expect("DownloadManager Dispose");
}

// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_DeduplicatesRequests_SameUri_OneHttpCall::test dc3ffee1f6171833a8364750cfac2c6d3bfa812b404129e0425066ea212f2337 translated
#[test]
fn queue_download_deduplicates_same_uri() {
    let bytes = b"duplicate payload".to_vec();
    let (handler, message_handler) = FakeHandler::new(&bytes, Duration::from_millis(200));
    let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
    let mut client = GridClient::new().expect("GridClient constructor");
    client.set_http_caps_client(http_caps);
    let downloads = Arc::new(DownloadManager::new(client).expect("DownloadManager constructor"));
    let uri = Uri("http://example.test/dup".into());
    let worker_downloads = Arc::clone(&downloads);
    let (first_result, second_result) = block_on_timeout(
        async move {
            let first = worker_downloads
                .queue_download_with_uri_string_i_progress_cancellation_token_int32(
                    uri.clone(),
                    None,
                    None,
                    Some(CancellationToken::default()),
                    Some(1),
                );
            let second = worker_downloads
                .queue_download_with_uri_string_i_progress_cancellation_token_int32(
                    uri,
                    None,
                    None,
                    Some(CancellationToken::default()),
                    Some(1),
                );
            join(first, second).await
        },
        Duration::from_secs(5),
    );
    let dispose_result = downloads.dispose();
    let (_, first_data) = first_result.expect("first download");
    let (_, second_data) = second_result.expect("second download");

    assert_eq!(first_data, bytes);
    assert_eq!(second_data, bytes);
    assert_eq!(handler.send_count(), 1);
    let request = handler.only_request();
    assert_eq!(request.method, "GET");
    assert_eq!(request.uri, Uri("http://example.test/dup".into()));
    dispose_result.expect("DownloadManager Dispose");
}

// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_Cancellation_PropagatesToTask::test 9eb3791b1def609d6f45be0fff8ae67ea04332045c0c419c6c2e7d118aeb6271 translated
#[test]
fn queue_download_cancellation_propagates() {
    let bytes = b"will cancel".to_vec();
    let (handler, message_handler) = FakeHandler::new(&bytes, Duration::ZERO);
    let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
    let mut client = GridClient::new().expect("GridClient constructor");
    client.set_http_caps_client(http_caps);
    let downloads = DownloadManager::new(client).expect("DownloadManager constructor");
    let cancellation = CancellationTokenSource::new();
    cancellation.cancel();

    let result = block_on(
        downloads.queue_download_with_uri_string_i_progress_cancellation_token_int32(
            Uri("http://example.test/cancel".into()),
            None,
            None,
            Some(cancellation.token()),
            Some(1),
        ),
    );
    let dispose_result = downloads.dispose();

    assert!(
        matches!(&result, Err(libremetaverse::Error::Cancelled)),
        "expected Cancelled, got {result:?}"
    );
    assert_eq!(handler.send_count(), 0);
    dispose_result.expect("DownloadManager Dispose");
}

// parity-case: LibreMetaverse.Tests/HttpCapsClientNonHttpLocationTests.cs::HttpCapsClientNonHttpLocationTests.PutAsync_ResponseWithNonHttpLocationHeader_DoesNotThrow::test 9dbcf4023212af6f2294fa048068fe2f4d9e7e3f976d8eefb1dacbc9f6899aa8 translated
#[test]
fn put_response_with_non_http_location_does_not_throw() {
    const SLCAPS: &str = "slcaps://11111111-1111-1111-1111-111111111111/category/x";
    let body = b"<llsd><map/></llsd>";
    let response = HttpResponse {
        status_code: 201,
        headers: BTreeMap::from([
            ("Location".into(), SLCAPS.into()),
            ("Content-Location".into(), SLCAPS.into()),
            ("Content-Length".into(), body.len().to_string()),
        ]),
        content_type: Some("application/llsd+xml".into()),
        body: body.to_vec(),
    };
    let requests = Arc::new(Mutex::new(Vec::new()));
    let recorded = Arc::clone(&requests);
    let message_handler = HttpMessageHandler::new(move |request, _cancellation_token| {
        let recorded = Arc::clone(&recorded);
        let response = response.clone();
        async move {
            recorded
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .push(request);
            response
        }
    });
    let mut client = GridClient::new().expect("GridClient constructor");
    let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
    client.set_http_caps_client(http_caps);

    let (reply, reply_body) = block_on(
        client
            .http_caps_client()
            .put_with_uri_string_bytes_cancellation_token_i_progress(
                Uri("http://127.0.0.1/".into()),
                "application/llsd+xml".into(),
                b"test".to_vec(),
                CancellationToken::default(),
                None,
            ),
    )
    .expect("HttpCapsClient PutAsync");

    assert_eq!(reply.status_code, 201);
    assert_eq!(reply.headers["Location"], SLCAPS);
    assert_eq!(reply.headers["Content-Location"], SLCAPS);
    assert_eq!(reply.content_type.as_deref(), Some("application/llsd+xml"));
    assert_eq!(reply_body, body);
    let request = {
        let requests = requests
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        assert_eq!(requests.len(), 1, "expected exactly one HTTP request");
        requests[0].clone()
    };
    assert_eq!(request.method, "PUT");
    assert_eq!(request.uri, Uri("http://127.0.0.1/".into()));
    assert_eq!(
        request.content_type.as_deref(),
        Some("application/llsd+xml")
    );
    assert_eq!(request.body, b"test");
}

// parity-case: LibreMetaverse.Tests/GridClientTests.cs::GridClientTests.GetGridRegion::test b0b72fc63fe829984167a479b763784c7daae8691399f3f3d2feb824090ad59f ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn get_grid_region_live() {
    let mut client = GridClient::new().expect("GridClient constructor");
    client.self_().movement.set_fly(true);
    let network = client.network();
    let (first, last, password, login_url) = live_credentials();
    let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
        .expect("NetworkManager StartLocation");
    let mut login = network
        .default_login_params(
            first,
            last,
            password,
            "Unit Test Framework".into(),
            "contact@radegast.life".into(),
        )
        .expect("NetworkManager DefaultLoginParams");
    login.start = start;
    login.uri = login_url;
    let logged_in = block_on(network.login_with_login_params_cancellation_token(login, None))
        .expect("NetworkManager LoginAsync");
    assert!(
        logged_in,
        "client failed to log in: {}",
        network.login_message()
    );
    assert!(network.connected(), "client is not connected to the grid");
    let simulator = network
        .current_sim()
        .expect("CurrentSim is null after successful login");
    if simulator.name.is_empty() {
        eprintln!("CurrentSim.Name is empty after login, proceeding with tests");
    } else if !simulator.name.eq_ignore_ascii_case("hooper") {
        eprintln!(
            "logged in to region '{}' instead of 'Hooper', proceeding with tests",
            simulator.name
        );
    }

    let region = block_on(
        client
            .grid()
            .get_grid_region_with_string_grid_layer_type_cancellation_token(
                "Hippo Hollow".into(),
                GridLayerType::Terrain,
                None,
            ),
    )
    .expect("GridManager GetGridRegionAsync");
    if let Some(Some(region)) = region {
        assert_eq!(region.name.to_lowercase(), "hippo hollow");
    } else {
        eprintln!("Grid region 'Hippo Hollow' not found; skipping assertion");
    }

    network.logout_with_method().expect("NetworkManager Logout");
    let _ = client.dispose_with_method();
}

// parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.DetectObjects::test 8b72f1ca30feda2fad0812faf3a2f83d691d3cca9268054b3cc3bf89ad786e87 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn detect_objects_live() {
    let mut client = GridClient::new().expect("GridClient constructor");
    client.settings().timing().login_timeout = 30 * 1_000;
    client.self_().movement.set_fly(true);
    let detected = Arc::new(AtomicBool::new(false));
    let callback_detected = Arc::clone(&detected);
    let network = Arc::new(client.network());
    network
        .register_callback_with_packet_type_event_handler(
            PacketType::ObjectUpdate,
            Arc::new(move |_event| callback_detected.store(true, Ordering::SeqCst)),
        )
        .expect("register ObjectUpdate callback");
    let (first, last, password, login_url) = live_credentials();
    let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
        .expect("NetworkManager StartLocation");
    let mut login = network
        .default_login_params(
            first,
            last,
            password,
            "Unit Test Framework".into(),
            "admin@radegast.life".into(),
        )
        .expect("NetworkManager DefaultLoginParams");
    login.start = start;
    login.uri = login_url;
    let login_network = Arc::clone(&network);
    let logged_in = block_on_timeout(
        async move {
            login_network
                .login_with_login_params_cancellation_token(login, None)
                .await
        },
        Duration::from_secs(45),
    )
    .expect("NetworkManager LoginAsync");
    assert!(
        logged_in,
        "client failed to log in: {}",
        network.login_message()
    );
    assert!(network.connected(), "client is not connected to the grid");
    thread::sleep(Duration::from_secs(1));
    let simulator = network
        .current_sim()
        .expect("CurrentSim is null after successful login");
    if simulator.name.is_empty() {
        eprintln!("CurrentSim.Name is empty, but proceeding with tests");
    } else if !simulator.name.eq_ignore_ascii_case("hooper") {
        eprintln!(
            "logged in to region '{}' instead of 'Hooper', but proceeding with tests",
            simulator.name
        );
    }

    let started = Instant::now();
    while !detected.load(Ordering::SeqCst) {
        assert!(
            started.elapsed() <= Duration::from_secs(20),
            "timeout waiting for an ObjectUpdate packet after 20 seconds"
        );
        thread::sleep(Duration::from_millis(100));
    }
    assert!(
        detected.load(Ordering::SeqCst),
        "successfully detected objects"
    );

    network.logout_with_method().expect("NetworkManager Logout");
    let _ = client.dispose_with_method();
}

// parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.CapsQueue::test cd5af4e7d69df3e938540711877d167fe28efb9af8af7518a62b8f406e46cc18 ignored-live
#[test]
#[cfg_attr(
    not(live_grid_credentials),
    ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn caps_queue_live() {
    let mut client = GridClient::new().expect("GridClient constructor");
    client.settings().timing().login_timeout = 30 * 1_000;
    client.self_().movement.set_fly(true);
    let detected = Arc::new(AtomicBool::new(false));
    let callback_detected = Arc::clone(&detected);
    let network = Arc::new(client.network());
    network
        .register_callback_with_packet_type_event_handler(
            PacketType::ObjectUpdate,
            Arc::new(move |_event| callback_detected.store(true, Ordering::SeqCst)),
        )
        .expect("register ObjectUpdate callback");
    let (first, last, password, login_url) = live_credentials();
    let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
        .expect("NetworkManager StartLocation");
    let mut login = network
        .default_login_params(
            first,
            last,
            password,
            "Unit Test Framework".into(),
            "admin@radegast.life".into(),
        )
        .expect("NetworkManager DefaultLoginParams");
    login.start = start;
    login.uri = login_url;
    let login_network = Arc::clone(&network);
    let logged_in = block_on_timeout(
        async move {
            login_network
                .login_with_login_params_cancellation_token(login, None)
                .await
        },
        Duration::from_secs(45),
    )
    .expect("NetworkManager LoginAsync");
    assert!(
        logged_in,
        "client failed to log in: {}",
        network.login_message()
    );
    assert!(network.connected(), "client is not connected to the grid");
    thread::sleep(Duration::from_secs(1));
    let simulator = network
        .current_sim()
        .expect("CurrentSim is null after successful login");
    if simulator.name.is_empty() {
        eprintln!("CurrentSim.Name is empty, but proceeding with tests");
    } else if !simulator.name.eq_ignore_ascii_case("hooper") {
        eprintln!(
            "logged in to region '{}' instead of 'Hooper', but proceeding with tests",
            simulator.name
        );
    }

    let already_running = network
        .current_sim()
        .and_then(|simulator| simulator.caps)
        .is_some_and(|caps| caps.is_event_queue_running());
    let event_queue_running = if already_running {
        true
    } else {
        let (sender, receiver) = mpsc::sync_channel(1);
        let _subscription = network.subscribe_event_queue_running(Arc::new(move |_event| {
            let _ = sender.send(());
        }));
        receiver
            .recv_timeout(Duration::from_secs(10))
            .expect("timeout waiting for event queue to start");
        true
    };
    assert!(
        event_queue_running,
        "CAPS Event Queue is not running and failed to start"
    );

    network.logout_with_method().expect("NetworkManager Logout");
    let _ = client.dispose_with_method();
}
