use async_trait::async_trait;
use config::{AsyncSource, Config, ConfigError, FileFormat, Format, Map, Value};
use std::{env, fs, path, str::FromStr};
use tokio::fs::read_to_string;

#[derive(Debug)]
struct AsyncFile {
    path: String,
    format: FileFormat,
}

/// This is a test only implementation to be used in tests
impl AsyncFile {
    pub fn new(path: String, format: FileFormat) -> Self {
        Self { path, format }
    }
}

#[async_trait]
impl AsyncSource for AsyncFile {
    async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
        let mut path = env::current_dir().unwrap();
        let local = path::PathBuf::from_str(&self.path).unwrap();

        path.extend(local.iter());
        let path = fs::canonicalize(path).map_err(|e| ConfigError::Foreign(Box::new(e)))?;

        let text = read_to_string(path)
            .await
            .map_err(|e| ConfigError::Foreign(Box::new(e)))?;

        self.format
            .parse(Some(&self.path), &text)
            .map_err(|e| ConfigError::Foreign(e))
    }
}

#[tokio::test]
async fn test_single_async_file_source() {
    let config = Config::builder()
        .add_async_source(AsyncFile::new(
            "tests/Settings.json".to_owned(),
            FileFormat::Json,
        ))
        .build()
        .await
        .unwrap();

    assert!(config.get::<bool>("debug").unwrap());
}

#[tokio::test]
async fn test_two_async_file_sources() {
    let config = Config::builder()
        .add_async_source(AsyncFile::new(
            "tests/Settings.json".to_owned(),
            FileFormat::Json,
        ))
        .add_async_source(AsyncFile::new(
            "tests/Settings.toml".to_owned(),
            FileFormat::Toml,
        ))
        .build()
        .await
        .unwrap();

    assert_eq!("Torre di Pisa", config.get::<String>("place.name").unwrap());
    assert!(config.get::<bool>("debug_json").unwrap());
    assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[tokio::test]
async fn test_sync_to_async_file_sources() {
    let config = Config::builder()
        .add_source(config::File::new("tests/Settings", FileFormat::Json))
        .add_async_source(AsyncFile::new(
            "tests/Settings.toml".to_owned(),
            FileFormat::Toml,
        ))
        .build()
        .await
        .unwrap();

    assert_eq!("Torre di Pisa", config.get::<String>("place.name").unwrap());
    assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[tokio::test]
async fn test_async_to_sync_file_sources() {
    let config = Config::builder()
        .add_async_source(AsyncFile::new(
            "tests/Settings.toml".to_owned(),
            FileFormat::Toml,
        ))
        .add_source(config::File::new("tests/Settings", FileFormat::Json))
        .build()
        .await
        .unwrap();

    assert_eq!("Torre di Pisa", config.get::<String>("place.name").unwrap());
    assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[tokio::test]
async fn test_async_file_sources_with_defaults() {
    let config = Config::builder()
        .set_default("place.name", "Tower of London")
        .unwrap()
        .set_default("place.sky", "blue")
        .unwrap()
        .add_async_source(AsyncFile::new(
            "tests/Settings.toml".to_owned(),
            FileFormat::Toml,
        ))
        .build()
        .await
        .unwrap();

    assert_eq!("Torre di Pisa", config.get::<String>("place.name").unwrap());
    assert_eq!("blue", config.get::<String>("place.sky").unwrap());
    assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[tokio::test]
async fn test_async_file_sources_with_overrides() {
    let config = Config::builder()
        .set_override("place.name", "Tower of London")
        .unwrap()
        .add_async_source(AsyncFile::new(
            "tests/Settings.toml".to_owned(),
            FileFormat::Toml,
        ))
        .build()
        .await
        .unwrap();

    assert_eq!(
        "Tower of London",
        config.get::<String>("place.name").unwrap()
    );
    assert_eq!(1, config.get::<i32>("place.number").unwrap());
}
