#![allow(deprecated)]
use std::sync::Arc;

use dittolive_ditto::{prelude::*, transport::ConnectionType};
use serde_json::{json, Value as JsonValue};

mod common;

use common::*;

#[::tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn peer_metadata_returns_empty_dictionary_by_default() {
    let ditto = &get_builder_ditto(None).unwrap();
    peer_metadata_returns_empty_dictionary_by_default_impl(ditto);

    let ditto = &get_revamp_ditto(None).unwrap();
    peer_metadata_returns_empty_dictionary_by_default_impl(ditto);

    let ditto = &common::get_revamp_ditto_async(None).await.unwrap();
    peer_metadata_returns_empty_dictionary_by_default_impl(ditto);
}

fn peer_metadata_returns_empty_dictionary_by_default_impl(ditto: &dittolive_ditto::prelude::Ditto) {
    assert_eq!(ditto.presence().peer_metadata_json_str(), "{}");
    assert!(ditto.presence().peer_metadata().is_empty());
}

#[::tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn set_peer_metadata_updates_the_peer_metadata_for_the_local_peer() {
    let ditto = &get_builder_ditto(None).unwrap();
    set_peer_metadata_updates_the_peer_metadata_for_the_local_peer_impl(ditto);

    let ditto = &get_revamp_ditto(None).unwrap();
    set_peer_metadata_updates_the_peer_metadata_for_the_local_peer_impl(ditto);

    let ditto = &common::get_revamp_ditto_async(None).await.unwrap();
    set_peer_metadata_updates_the_peer_metadata_for_the_local_peer_impl(ditto);
}

fn set_peer_metadata_updates_the_peer_metadata_for_the_local_peer_impl(
    ditto: &dittolive_ditto::prelude::Ditto,
) {
    let metadata_json_str = r#"{"blah": "blub"}"#;
    ditto
        .presence()
        .set_peer_metadata_json_str(metadata_json_str)
        .unwrap();
    assert_eq!(ditto.presence().peer_metadata_json_str(), metadata_json_str);

    let JsonValue::Object(metadata) = json!({ "foo": "bar" }) else {
        unreachable!();
    };
    ditto.presence().set_peer_metadata(&metadata).unwrap();
    assert_eq!(*ditto.presence().peer_metadata(), metadata);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn connection_request_handler_is_called_with_a_connection_request() {
    let uuid = || ::uuid::Uuid::new_v4().to_string();
    let app_id = uuid();

    let temp_dir = &::tempfile::tempdir().unwrap();
    let iter = || [(); 2].into_iter();
    // This clippy lint is infuriatingly stupid: `AsRef` is about references anyways.
    #[allow(clippy::needless_borrows_for_generic_args)]
    let persistence_dirs = iter()
        .map(|()| {
            temp_dir
                .path()
                .join(&format!("ditto_presence_tests.{app_id}"))
                .join(&uuid())
        })
        .collect::<Vec<_>>();
    let dittos = &persistence_dirs
        .iter()
        .map(|dir| {
            let ditto = Ditto::builder()
                .with_root(Arc::new(PersistentRoot::new(dir).unwrap()))
                .with_identity(|ditto_root| {
                    OfflinePlayground::new(ditto_root, app_id.parse().unwrap())
                })
                .unwrap()
                .with_minimum_log_level(LogLevel::Error)
                .build()
                .unwrap();
            ditto.set_license_from_env("DITTO_LICENSE").ok();
            ditto.disable_sync_with_v3().unwrap();
            ditto
        })
        .collect::<Vec<_>>();

    let [ditto0, ditto1] = &dittos[..] else {
        unreachable!()
    };
    let dittos = (ditto0, ditto1);

    let peer_key_strings = (
        dittos.0.presence().graph().local_peer.peer_key_string,
        dittos.1.presence().graph().local_peer.peer_key_string,
    );

    let metadatas = (json!({ "number": 0 }), json!({ "number": 1 }));

    dittos.0.set_device_name("dittos.0");
    dittos.1.set_device_name("dittos.1");

    dittos.0.presence().set_peer_metadata(&metadatas.0).unwrap();
    dittos.1.presence().set_peer_metadata(&metadatas.1).unwrap();

    let connection_requests = (::std::sync::mpsc::channel(), ::std::sync::mpsc::channel());

    dittos
        .0
        .presence()
        .set_connection_request_handler(move |connection_request| {
            connection_requests.0 .0.send(connection_request).ok();
            ConnectionRequestAuthorization::Allow
        });

    dittos
        .1
        .presence()
        .set_connection_request_handler(move |connection_request| {
            connection_requests.1 .0.send(connection_request).ok();
            ConnectionRequestAuthorization::Allow
        });

    let mut _0_saw_1 = ::tokio::sync::mpsc::unbounded_channel();
    let presence_observer_0 = dittos.0.presence().register_observer(move |graph| {
        if let Some(seen_ditto_1) = graph
            .remote_peers
            .iter()
            .find(|peer| peer.device_name == "dittos.1")
        {
            _0_saw_1.0.send(()).ok();
            assert_eq!(seen_ditto_1.peer_metadata, metadatas.1);
            assert_eq!(seen_ditto_1.peer_key_string, peer_key_strings.1);
        }
        _0_saw_1.0.send(()).ok();
    });

    let mut _1_saw_0 = ::tokio::sync::mpsc::unbounded_channel();
    let presence_observer_1 = dittos.1.presence().register_observer(move |graph| {
        if let Some(seen_ditto_0) = graph
            .remote_peers
            .iter()
            .find(|peer| peer.device_name == "dittos.0")
        {
            _1_saw_0.0.send(()).ok();
            assert_eq!(seen_ditto_0.peer_metadata, metadatas.0);
            assert_eq!(seen_ditto_0.peer_key_string, peer_key_strings.0);
        }
        _1_saw_0.0.send(()).ok();
    });

    dittos.0.sync().start().unwrap();
    dittos.1.sync().start().unwrap();

    ::tokio::time::timeout(::std::time::Duration::from_secs(10), async {
        _0_saw_1.1.recv().await.unwrap();
        _1_saw_0.1.recv().await.unwrap();
    })
    .await
    .unwrap();

    dittos.0.sync().stop();
    dittos.1.sync().stop();

    drop(presence_observer_0);
    drop(presence_observer_1);

    dittos.0.presence().set_connection_request_handler(None);
    dittos.1.presence().set_connection_request_handler(None);

    // -- Aftermath

    let mut first_connection_request = None;
    for conn_req in connection_requests.0 .1.into_iter().map(Arc::new) {
        let first_connection_request =
            first_connection_request.get_or_insert_with(|| conn_req.clone());
        assert!(conn_req.peer_key_string().len() >= 2);
        assert_eq!(
            conn_req.peer_key_string(),
            first_connection_request.peer_key_string(),
        );
        assert_eq!(
            conn_req
                .peer_metadata()
                .get("number")
                .expect("number")
                .as_u64()
                .expect("u64"),
            1,
        );
        assert!(conn_req.identity_service_metadata().is_empty());
        assert!([ConnectionType::AccessPoint, ConnectionType::P2PWiFi]
            .contains(&conn_req.connection_type()));
    }

    let mut first_connection_request = None;
    for conn_req in connection_requests.1 .1.into_iter().map(Arc::new) {
        let first_connection_request =
            first_connection_request.get_or_insert_with(|| conn_req.clone());
        assert!(conn_req.peer_key_string().len() >= 2);
        assert_eq!(
            conn_req.peer_key_string(),
            first_connection_request.peer_key_string(),
        );
        assert_eq!(
            conn_req
                .peer_metadata()
                .get("number")
                .expect("number")
                .as_u64()
                .expect("u64"),
            0,
        );
        assert!(conn_req.identity_service_metadata().is_empty());
        assert!([ConnectionType::AccessPoint, ConnectionType::P2PWiFi]
            .contains(&conn_req.connection_type()));
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
#[ignore]
async fn connection_request_handler_prevents_a_peer_from_connecting_if_authorization_denied() {
    let _ditto = &get_builder_ditto(None).unwrap();

    // without long waiting times. We probably need some internal
    // APIs to check for this condition (i.e. access to some
    // internal structures denylisting the peer in question?).
    // Punting on this for now.
}
