#![allow(deprecated)]
use std::{fs::File, io::Read, path::Path, sync::Arc, time::Duration};

use anyhow::Error;
use chrono::{DateTime, TimeZone, Utc};
use culpa::throws;
pub use dittolive_ditto::{error::DittoError, identity, prelude::*};
use futures::executor::block_on;
pub use safer_ffi::prelude::*;
use serde::{Deserialize, Serialize};

#[allow(dead_code)]
pub const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(10);

#[throws]
#[allow(deprecated)]
pub fn eq_ignoring_id<D: DittoDocument>(doc: D, value: serde_json::Value) -> bool {
    let remove_id = |value: &mut serde_json::Value| {
        let object = value
            .as_object_mut()
            .expect("doc contents should be an object");
        object.remove("_id");
    };

    let mut doc_value = doc.typed::<serde_json::Value>()?;
    remove_id(&mut doc_value);

    let mut expected_value = value;
    remove_id(&mut expected_value);

    doc_value == expected_value
}

#[throws]
#[allow(dead_code, deprecated)]
pub fn assert_eq_ignoring_id<D: DittoDocument>(doc: D, value: serde_json::Value) {
    assert!(eq_ignoring_id(doc, value)?);
}

#[allow(dead_code)]
pub fn test_license_from_file<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
    let mut f = File::open(path)?;
    let mut key = String::new();
    let _bytes = f.read_to_string(&mut key)?;
    let result = key.trim().to_string();
    if result.is_empty() {
        Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "License Key is empty",
        ))
    } else {
        Ok(result)
    }
}

#[allow(dead_code)]
pub fn get_builder_ditto(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let ditto = get_inactive_builder_ditto(app_id)?;
    let _ = ditto.set_license_from_env("DITTO_LICENSE");
    ditto.disable_sync_with_v3().unwrap();
    Ok(ditto)
}

#[allow(dead_code)]
pub fn get_revamp_ditto(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let ditto = get_inactive_revamp_ditto(app_id)?;
    let _ = ditto.set_license_from_env("DITTO_LICENSE");
    ditto.disable_sync_with_v3().unwrap();
    Ok(ditto)
}

#[allow(dead_code)]
pub fn get_revamp_ditto_async_sync(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let ditto = block_on(get_inactive_revamp_ditto_async(app_id))?;
    let _ = ditto.set_license_from_env("DITTO_LICENSE");
    Ok(ditto)
}

#[allow(dead_code)]
pub async fn get_revamp_ditto_async(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let ditto = get_inactive_revamp_ditto_async(app_id).await?;
    let _ = ditto.set_license_from_env("DITTO_LICENSE");
    Ok(ditto)
}

#[allow(dead_code)]
pub fn get_inactive_builder_ditto(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let app_id = app_id.unwrap_or_else(AppId::generate);
    let ditto = Ditto::builder()
        .with_temp_dir()
        .with_identity(|r| identity::OfflinePlayground::new(r, app_id))?
        .with_minimum_log_level(LogLevel::Warning)
        .build()?;
    ditto.disable_sync_with_v3().unwrap();
    Ok(ditto)
}

#[allow(dead_code)]
pub fn get_inactive_revamp_ditto(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let app_id = app_id.unwrap_or_else(AppId::generate);
    let tempdir = tempfile::tempdir().expect("should create tempdir");
    let persistent_dir = tempdir.keep();
    let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
    let config =
        DittoConfig::new(app_id.to_string(), connect).with_persistence_directory(persistent_dir);
    let ditto = Ditto::open_sync(config)?;
    ditto.disable_sync_with_v3().unwrap();
    Ok(ditto)
}

#[allow(dead_code)]
pub async fn get_inactive_revamp_ditto_async(app_id: Option<AppId>) -> Result<Ditto, DittoError> {
    let app_id = app_id.unwrap_or_else(AppId::generate);
    let tempdir = tempfile::tempdir().expect("should create tempdir");
    let persistent_dir = tempdir.keep();
    let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
    let config =
        DittoConfig::new(app_id.to_string(), connect).with_persistence_directory(persistent_dir);
    let ditto = Ditto::open(config).await?;
    ditto.disable_sync_with_v3().unwrap();
    Ok(ditto)
}

#[allow(dead_code)]
pub fn get_online_ditto() -> Result<Ditto, DittoError> {
    let shared_token = String::from("SHARED_TOKEN");
    let ditto = Ditto::builder()
        .with_temp_dir()
        .with_identity(|ditto_root| {
            identity::OnlinePlayground::new(
                ditto_root,
                AppId::generate(),
                shared_token,
                false,
                None,
            )
        })?
        .with_minimum_log_level(LogLevel::Error)
        .build()?;
    Ok(ditto)
}

#[allow(dead_code)]
pub fn get_online_ditto_with_persistent_dir(db_path: &Path) -> Result<Ditto, DittoError> {
    let shared_token = String::from("SHARED_TOKEN");
    let ditto = Ditto::builder()
        .with_root(Arc::new(PersistentRoot::new(db_path)?))
        .with_identity(|ditto_root| {
            identity::OnlinePlayground::new(
                ditto_root,
                AppId::generate(),
                shared_token,
                false,
                None,
            )
        })?
        .with_minimum_log_level(LogLevel::Error)
        .build()?;
    Ok(ditto)
}

// A Re-usable test document type
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum TestColor {
    Red,
    Blue,
    Crimson,
    Orange,
    Yellow,
    Pink,
    White,
    Black,
    Brown,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct TestType {
    pub make: String,
    pub color: TestColor,
    pub age: u8,
    pub mileage: i64,
    pub purchased_on: DateTime<Utc>,
    pub tags: Vec<String>,
    pub data: Vec<u8>, // most common repr for bin data in std lib
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<DocumentId>,
}

impl TestType {
    #[allow(dead_code)] // This actually is used
    pub fn random() -> TestType {
        TestType::random_with_id(None)
    }

    #[allow(dead_code)] // This actually is used
    pub fn random_with_id(id: Option<DocumentId>) -> TestType {
        use rand::{rngs::StdRng, Fill, SeedableRng};

        let mut bytes: Vec<u8> = Vec::with_capacity(2048);
        let mut rng = StdRng::seed_from_u64(123456789);
        bytes.try_fill(&mut rng).unwrap();

        TestType {
            make: String::from("Honda"),
            color: TestColor::Red,
            age: 12,
            mileage: 52000,
            purchased_on: Utc::now(),
            tags: vec![
                String::from("a"),
                String::from("-1"),
                String::from("test_tag"),
            ],
            data: bytes,
            id,
        }
    }
}

impl Default for TestType {
    fn default() -> TestType {
        TestType {
            make: String::default(),
            color: TestColor::Red,
            age: 0,
            mileage: 0,
            purchased_on: Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(),
            tags: Vec::new(),
            data: Vec::new(),
            id: None,
        }
    }
}
