org-cli 0.0.3

Command-line tool for searching, reading, and managing org-mode knowledge bases with fuzzy text search and structured content access
use config::{Config as ConfigRs, ConfigError};
use org_core::{
    LoggingConfig, OrgConfig, OrgModeError,
    config::{build_config_with_file_and_env, load_logging_config, load_org_config},
};
use serde::{Deserialize, Serialize};

/// CLI-specific configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliConfig {
    #[serde(default = "default_output_format")]
    pub default_format: String,
}

fn default_output_format() -> String {
    "plain".to_string()
}

impl Default for CliConfig {
    fn default() -> Self {
        Self {
            default_format: default_output_format(),
        }
    }
}

/// Complete CLI application configuration
#[derive(Debug, Clone)]
pub struct CliAppConfig {
    pub org: OrgConfig,
    pub cli: CliConfig,
    pub logging: LoggingConfig,
}

impl CliAppConfig {
    /// Load CLI configuration from file and environment with CLI argument overrides
    pub fn load(
        config_file: Option<String>,
        root_directory: Option<String>,
        log_level: Option<String>,
    ) -> Result<Self, OrgModeError> {
        let org = load_org_config(config_file.as_deref(), root_directory.as_deref())?;
        let cli = Self::load_cli_config(config_file.as_deref())?;
        let logging = load_logging_config(config_file.as_deref(), log_level.as_deref())?;

        Ok(Self { org, cli, logging })
    }

    pub fn load_cli_config(config_file: Option<&str>) -> Result<CliConfig, OrgModeError> {
        let builder = ConfigRs::builder().set_default("cli.default_format", "plain")?;

        let config = build_config_with_file_and_env(config_file, builder)?;

        config.get("cli").map_err(|e: ConfigError| {
            OrgModeError::ConfigError(format!("Failed to deserialize cli config: {e}"))
        })
    }

    /// Generate a default configuration as TOML string
    pub fn generate_default_config() -> Result<String, OrgModeError> {
        #[derive(Serialize)]
        struct DefaultConfig {
            org: OrgConfig,
            cli: CliConfig,
            logging: LoggingConfig,
        }

        let config = DefaultConfig {
            org: OrgConfig::default(),
            cli: CliConfig::default(),
            logging: LoggingConfig::default(),
        };

        toml::to_string_pretty(&config).map_err(|e| {
            OrgModeError::ConfigError(format!("Failed to serialize default config: {e}"))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serial_test::serial;
    use tempfile::tempdir;

    #[test]
    fn test_default_cli_config() {
        let config = CliConfig::default();
        assert_eq!(config.default_format, "plain");
    }

    #[test]
    fn test_generate_default_config() {
        let toml_str = CliAppConfig::generate_default_config().unwrap();
        assert!(toml_str.contains("org_directory"));
        assert!(toml_str.contains("[cli]"));
        assert!(toml_str.contains("[logging]"));
    }

    #[test]
    #[serial]
    fn test_load_from_file() {
        let temp_dir = tempdir().unwrap();
        let config_path = temp_dir.path().join("config.toml");

        let path_str = temp_dir.path().to_str().unwrap().replace('\\', "/");
        let test_config = format!(
            r#"
[org]
org_directory = "{}"

[cli]
default_format = "json"

[logging]
level = "debug"
"#,
            path_str
        );

        std::fs::write(&config_path, test_config).unwrap();

        let config =
            CliAppConfig::load(Some(config_path.to_str().unwrap().to_string()), None, None)
                .unwrap();

        assert_eq!(config.org.org_directory, path_str);
        assert_eq!(config.cli.default_format, "json");
        assert_eq!(config.logging.level, "debug");
    }

    #[test]
    #[serial]
    fn test_cli_overrides() {
        let temp_dir = tempdir().unwrap();
        let config_path = temp_dir.path().join("config.toml");

        let test_config = format!(
            r#"
[org]
org_directory = "{}"

[cli]
default_format = "json"
"#,
            temp_dir.path().to_str().unwrap().replace('\\', "/")
        );

        std::fs::write(&config_path, test_config).unwrap();

        let override_dir = tempdir().unwrap();
        let config = CliAppConfig::load(
            Some(config_path.to_str().unwrap().to_string()),
            Some(override_dir.path().to_str().unwrap().to_string()),
            Some("trace".to_string()),
        )
        .unwrap();

        assert_eq!(
            config.org.org_directory,
            override_dir.path().to_str().unwrap()
        );
        assert_eq!(config.logging.level, "trace");
    }

    #[test]
    #[serial]
    #[cfg_attr(
        target_os = "windows",
        ignore = "Environment variable handling unreliable in Windows tests"
    )]
    fn test_env_var_cli_override() {
        use temp_env::with_vars;

        let temp_dir = tempdir().unwrap();
        let temp_dir_path = temp_dir.path().to_str().unwrap();

        with_vars(
            [
                ("ORG_ORG__ORG_DIRECTORY", Some(temp_dir_path)),
                ("ORG_CLI__DEFAULT_FORMAT", Some("json")),
            ],
            || {
                let config = CliAppConfig::load(None, None, None).unwrap();
                assert_eq!(config.org.org_directory, temp_dir_path);
                assert_eq!(config.cli.default_format, "json");
            },
        );
    }

    #[test]
    #[serial]
    fn test_load_cli_config_extension_fallback() {
        let temp_dir = tempdir().unwrap();
        let config_dir = temp_dir.path().join(".config");
        std::fs::create_dir_all(&config_dir).unwrap();

        let yaml_config = r#"
cli:
  default_format: "json"
org:
  org_directory: "/tmp"
"#;

        let yaml_path = config_dir.join("config.yaml");
        std::fs::write(&yaml_path, yaml_config).unwrap();

        let org_dir = tempdir().unwrap();
        let config = CliAppConfig::load(
            Some(config_dir.join("config").to_str().unwrap().to_string()),
            Some(org_dir.path().to_str().unwrap().to_string()),
            None,
        );

        assert!(config.is_ok());
        let config = config.unwrap();
        assert_eq!(config.cli.default_format, "json");
    }
}