//! Load and parse kaze.toml from disk.

use std::fs;
use std::path::{Path, PathBuf};

use crate::config::schema::FileConfig;

/// Errors while loading kaze.toml.
#[derive(Debug)]
pub enum LoadError {
    /// Failed to read the config file.
    Io {
        /// Path to the configuration file.
        path: PathBuf,
        /// Underlying I/O error.
        source: std::io::Error,
    },
    /// Failed to parse TOML.
    Toml {
        /// Path to the configuration file.
        path: PathBuf,
        /// Underlying TOML parse error.
        source: toml::de::Error,
    },
}

impl std::fmt::Display for LoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match *self {
            Self::Io {
                ref path,
                ref source,
            } => write!(f, "failed to read {}: {source}", path.display()),
            Self::Toml {
                ref path,
                ref source,
            } => write!(f, "failed to parse {}: {source}", path.display()),
        }
    }
}

impl std::error::Error for LoadError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match *self {
            Self::Io { ref source, .. } => Some(source),
            Self::Toml { ref source, .. } => Some(source),
        }
    }
}

/**
 * Loads the kaze.toml configuration for a project.
 *
 * # Errors
 * Returns `LoadError::Io` when the file cannot be read and
 * `LoadError::Toml` when parsing fails.
 */
pub fn load_config(project_dir: &Path) -> Result<FileConfig, LoadError> {
    let path = project_dir.join("kaze.toml");
    let s = fs::read_to_string(&path).map_err(|e| LoadError::Io {
        path: path.clone(),
        source: e,
    })?;
    toml::from_str::<FileConfig>(&s).map_err(|e| LoadError::Toml { path, source: e })
}

#[cfg(test)]
mod tests {
    use std::sync::atomic::{AtomicUsize, Ordering};

    use super::*;

    fn make_temp_dir(label: &str) -> PathBuf {
        static COUNTER: AtomicUsize = AtomicUsize::new(0);
        let mut dir = std::env::temp_dir();
        dir.push(format!(
            "nishikaze-test-{}-{}",
            label,
            COUNTER.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(&dir).expect("create temp dir");
        dir
    }

    #[test]
    fn load_config_success() {
        let dir = make_temp_dir("load-ok");
        let path = dir.join("kaze.toml");
        fs::write(&path, "").expect("write config");

        let cfg = load_config(&dir).expect("load config");
        assert_eq!(cfg.project.board, None);
    }

    #[test]
    fn load_config_missing_file() {
        let dir = make_temp_dir("load-missing");
        let err = load_config(&dir).expect_err("missing file");
        assert!(matches!(err, LoadError::Io { .. }));
    }

    #[test]
    fn load_config_invalid_toml() {
        let dir = make_temp_dir("load-invalid");
        let path = dir.join("kaze.toml");
        fs::write(&path, "not = [").expect("write config");

        let err = load_config(&dir).expect_err("invalid toml");
        assert!(matches!(err, LoadError::Toml { .. }));
    }

    #[test]
    fn load_error_display_includes_path() {
        let dir = make_temp_dir("load-display");
        let err = load_config(&dir).expect_err("missing file");
        let msg = err.to_string();
        assert!(msg.contains("kaze.toml"));
    }
}
