use std::{fs::File, io::Read, path::Path, time::Duration};

use chrono::{DateTime, TimeZone, Utc};
pub use dittolive_ditto::{error::DittoError, fs::TempRoot, 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);

#[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_test_ditto(database_id: Option<DatabaseId>) -> Result<(TempRoot, Ditto), DittoError> {
    let (root, ditto) = get_inactive_ditto(database_id)?;
    ditto.set_license_from_env("DITTO_LICENSE")?;
    Ok((root, ditto))
}

#[allow(dead_code)]
pub fn get_test_ditto_async_sync(
    database_id: Option<DatabaseId>,
) -> Result<(TempRoot, Ditto), DittoError> {
    let (root, ditto) = block_on(get_inactive_ditto_async(database_id))?;
    ditto.set_license_from_env("DITTO_LICENSE")?;
    Ok((root, ditto))
}

#[allow(dead_code)]
pub async fn get_test_ditto_async(
    database_id: Option<DatabaseId>,
) -> Result<(TempRoot, Ditto), DittoError> {
    let (root, ditto) = get_inactive_ditto_async(database_id).await?;
    ditto.set_license_from_env("DITTO_LICENSE")?;
    Ok((root, ditto))
}

#[allow(dead_code)]
pub fn get_inactive_ditto(
    database_id: Option<DatabaseId>,
) -> Result<(TempRoot, Ditto), DittoError> {
    let database_id = database_id.unwrap_or_else(DatabaseId::generate);
    let root = TempRoot::new();
    let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
    let config = DittoConfig::new(database_id.to_string(), connect)
        .with_persistence_directory(root.root_path());
    let ditto = Ditto::open_sync(config)?;
    Ok((root, ditto))
}

#[allow(dead_code)]
pub async fn get_inactive_ditto_async(
    database_id: Option<DatabaseId>,
) -> Result<(TempRoot, Ditto), DittoError> {
    let database_id = database_id.unwrap_or_else(DatabaseId::generate);
    let root = TempRoot::new();
    let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
    let config = DittoConfig::new(database_id.to_string(), connect)
        .with_persistence_directory(root.root_path());
    let ditto = Ditto::open(config).await?;
    Ok((root, ditto))
}

#[allow(dead_code)]
pub fn get_online_ditto() -> Result<(TempRoot, Ditto), DittoError> {
    let database_id = DatabaseId::generate();
    let root = TempRoot::new();
    let config = DittoConfig::new(
        database_id.to_string(),
        DittoConfigConnect::Server {
            url: format!("https://{}.cloud.ditto.live", database_id)
                .parse()
                .unwrap(),
        },
    )
    .with_persistence_directory(root.root_path());
    let ditto = Ditto::open_sync(config)?;
    ditto
        .auth()
        .expect("Server mode should have auth")
        .set_expiration_handler(async |_ditto: &Ditto, _duration_remaining| {});
    Ok((root, ditto))
}

#[allow(dead_code)]
pub fn get_online_ditto_with_persistent_dir(db_path: &Path) -> Result<Ditto, DittoError> {
    let database_id = DatabaseId::generate();
    let config = DittoConfig::new(
        database_id.to_string(),
        DittoConfigConnect::Server {
            url: format!("https://{}.cloud.ditto.live", database_id)
                .parse()
                .unwrap(),
        },
    )
    .with_persistence_directory(db_path);
    let ditto = Ditto::open_sync(config)?;
    ditto
        .auth()
        .expect("Server mode should have auth")
        .set_expiration_handler(async |_ditto: &Ditto, _duration_remaining| {});
    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, Rng, SeedableRng};

        let mut bytes: Vec<u8> = vec![0; 2048];
        let mut rng = StdRng::seed_from_u64(123456789);
        rng.fill(bytes.as_mut_slice());

        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,
        }
    }
}
