#![allow(deprecated)]
use std::{str::FromStr, sync::Arc, time::Duration};

use anyhow::Result;
use dittolive_ditto::prelude::*;

pub fn get_ditto(app_id: Option<AppId>, shared_key: Option<&str>) -> Result<Ditto> {
    let app_id = app_id.unwrap_or_else(AppId::generate);

    let mut ditto = Ditto::builder().with_temp_dir();

    if let Some(shared_key) = shared_key {
        ditto = ditto.with_identity(|r| identity::SharedKey::new(r, app_id, shared_key))?;
    } else {
        ditto = ditto.with_identity(|r| {
            identity::OnlineWithAuthentication::new(r, app_id, AuthEventHandler {}, true, None)
        })?;
    }

    let ditto = ditto.with_minimum_log_level(LogLevel::Warning).build()?;
    if shared_key.is_some() {
        let _ = ditto.set_license_from_env("DITTO_LICENSE");
    }
    ditto.disable_sync_with_v3().unwrap();
    Ok(ditto)
}

/// This test checks that unorthodox, mixed-case AppIds will continue to work when using SharedKey
/// authentication. We have some customers who use mixed-case AppIds.
#[tokio::test]
#[ignore = "Requires DITTO_LICENSE environment variable"]
async fn mixed_case_app_id_succeeds_with_shared_key_authentication() -> Result<()> {
    let app_id = AppId::from_str("heLLo")?;
    let shared_key = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8lF58VPhhns2hBzGAyEodDCL//\
                      WUQN29Adv6QWGgm4qhRANCAATZW8DQK/3SoulndTvE7kAcrFDe4BcN3ZTzPxuOl+WZO/\
                      iQFAz+3eSFaU2nYSegvTwKGxchHYUMDSobY/USmtWF";
    let ditto1 = get_ditto(Some(app_id.clone()), Some(shared_key))?;
    let ditto2 = get_ditto(Some(app_id), Some(shared_key))?;

    ditto1.disable_sync_with_v3()?;
    ditto2.disable_sync_with_v3()?;

    let mut server = TransportConfig::new();
    server.listen.tcp = TcpListenConfig {
        enabled: true,
        interface_ip: "127.0.0.1".into(),
        port: 53644,
    };
    ditto1.set_transport_config(server);

    let mut client = TransportConfig::new();
    client
        .connect
        .tcp_servers
        .insert("localhost:53644".to_string());
    ditto2.set_transport_config(client);

    ditto1.sync().start()?;
    ditto2.sync().start()?;

    let ditto2 = Arc::new(ditto2);
    assert_connected_peers(Arc::clone(&ditto2), 1).await;

    Ok(())
}

/// AppIds are case-insensitive
#[tokio::test]
#[ignore = "Requires DITTO_LICENSE environment variable"]
async fn mixed_case_app_id_succeeds_with_shared_key_authentication_on_peers_with_different_cases(
) -> Result<()> {
    let app_id_1 = AppId::from_str("HeLLo")?;
    let app_id_2 = AppId::from_str("hello")?;
    let shared_key = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8lF58VPhhns2hBzGAyEodDCL//\
                      WUQN29Adv6QWGgm4qhRANCAATZW8DQK/3SoulndTvE7kAcrFDe4BcN3ZTzPxuOl+WZO/\
                      iQFAz+3eSFaU2nYSegvTwKGxchHYUMDSobY/USmtWF";
    let ditto1 = get_ditto(Some(app_id_1), Some(shared_key))?;
    let ditto2 = get_ditto(Some(app_id_2), Some(shared_key))?;

    ditto1.disable_sync_with_v3()?;
    ditto2.disable_sync_with_v3()?;

    let mut server = TransportConfig::new();
    server.listen.tcp = TcpListenConfig {
        enabled: true,
        interface_ip: "127.0.0.1".into(),
        port: 53645,
    };
    ditto1.set_transport_config(server);

    let mut client = TransportConfig::new();
    client
        .connect
        .tcp_servers
        .insert("localhost:53645".to_string());
    ditto2.set_transport_config(client);

    ditto1.sync().start()?;
    ditto2.sync().start()?;

    let ditto2 = Arc::new(ditto2);
    assert_connected_peers(Arc::clone(&ditto2), 1).await;

    Ok(())
}

/// This test checks that uuid-based AppIds are enforced for OnlineWithAuthentication.
#[tokio::test]
async fn app_id_must_be_a_uuid_with_online_with_authentication() -> Result<()> {
    assert!(get_ditto(Some(AppId::from_str("hello")?), None).is_err());
    assert!(get_ditto(Some(AppId::from_str("HELLO")?), None).is_err());
    assert!(get_ditto(Some(AppId::from_str("E4CAB6FB-6B3F")?), None).is_err());
    assert!(get_ditto(
        Some(AppId::from_str("E4CAB6FB_6B3F_4173_95EB_B5162034C235")?),
        None,
    )
    .is_err(),);

    // AppIds are case-insensitive
    assert!(get_ditto(
        Some(AppId::from_str("E4CAB6FB-6B3F-4173-95EB-B5162034C235")?),
        None,
    )
    .is_ok(),);
    assert!(get_ditto(
        Some(AppId::from_str("e4cab6fb-6b3f-4173-95eb-b5162034c235")?),
        None,
    )
    .is_ok());
    assert!(get_ditto(
        Some(AppId::from_str("E4CAB6FB-6b3f-4173-95eb-b5162034c235")?),
        None,
    )
    .is_ok());

    Ok(())
}

/// Check whether the provided Ditto instance has `count` number of connections. Retry for 10
/// seconds. This function will panic if the `count` requirement is not met within 10 seconds.
async fn assert_connected_peers(ditto: Arc<Ditto>, count: usize) {
    let f = tokio::spawn(async move {
        let mut cur_peers = 0;
        for _ in 0..10 {
            cur_peers = ditto.presence().graph().remote_peers.len();
            if cur_peers == count {
                return Ok(());
            }

            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }

        Err(cur_peers)
    });

    let result = tokio::time::timeout(std::time::Duration::from_secs(11), f)
        .await
        .unwrap()
        .unwrap();

    if let Err(peers_count) = result {
        panic!("Expected {count} connected peer(s), got {peers_count}");
    }
}

#[derive(Clone)]
pub struct AuthEventHandler {}

impl DittoAuthenticationEventHandler for AuthEventHandler {
    fn authentication_required(&self, _auth: DittoAuthenticator) {}

    fn authentication_expiring_soon(
        &self,
        _auth: DittoAuthenticator,
        _seconds_remaining: Duration,
    ) {
    }
}
