cinchdb 0.2.4

CLI for CinchDB - database and scope management
//! Config commands: show, set, path

use crate::config::{self, ConfigFile};
use crate::output;
use anyhow::Result;
use clap::Subcommand;

#[derive(Subcommand)]
pub enum ConfigCommands {
    /// Set a config value
    Set {
        /// Config key (org, project, environment, scope, api.url)
        key: String,
        /// Config value
        value: String,
    },
    /// Print config file path
    Path,
}

/// Run config commands. None = print current context.
pub fn run(command: Option<ConfigCommands>, json: bool) -> Result<()> {
    match command {
        None => show_config(json),
        Some(ConfigCommands::Set { key, value }) => set_config(&key, &value, json),
        Some(ConfigCommands::Path) => print_path(json),
    }
}

fn show_config(json: bool) -> Result<()> {
    let config = ConfigFile::load()?;
    let context = config::resolve_context(&config);

    if json {
        let data = serde_json::json!({
            "context": {
                "org": context.org,
                "project": context.project,
                "environment": context.environment,
                "scope": context.scope,
            },
            "api": {
                "url": config.api.url.as_deref().unwrap_or(config::DEFAULT_API_URL),
            },
            "authenticated": config.auth_header_value().is_some(),
        });
        println!("{}", serde_json::to_string_pretty(&data).expect("json serialization failed"));
    } else {
        let api_url = config
            .api
            .url
            .as_deref()
            .unwrap_or(config::DEFAULT_API_URL);
        let authenticated = if config.auth_header_value().is_some() {
            "yes"
        } else {
            "no"
        };

        output::print_kv_or_json(
            &[
                ("org", context.org.as_deref().unwrap_or("(not set)")),
                ("project", context.project.as_deref().unwrap_or("(not set)")),
                (
                    "environment",
                    context.environment.as_deref().unwrap_or("(not set)"),
                ),
                ("scope", context.scope.as_deref().unwrap_or("(not set)")),
                ("api.url", api_url),
                ("authenticated", authenticated),
            ],
            &serde_json::json!(null), // unused when not json
            false,
        );
    }
    Ok(())
}

fn set_config(key: &str, value: &str, json: bool) -> Result<()> {
    config::set_config_value(key, value)?;

    if json {
        println!(
            "{}",
            serde_json::json!({ "key": key, "value": value, "status": "set" })
        );
    } else {
        println!("Set {key} = \"{value}\"");
    }
    Ok(())
}

fn print_path(json: bool) -> Result<()> {
    let path = config::config_path()?;
    let path_str = path.display().to_string();

    if json {
        println!("{}", serde_json::json!({ "path": path_str }));
    } else {
        println!("{path_str}");
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_settable_keys_are_valid() {
        // Verify all keys in SETTABLE_KEYS would match in set_config_value
        for key in config::SETTABLE_KEYS {
            match *key {
                "org" | "project" | "environment" | "scope" | "api.url" => {}
                other => panic!("unhandled settable key: {other}"),
            }
        }
    }

    #[test]
    fn test_config_set_invalid_key() {
        // Test the validation logic directly without filesystem
        let mut cfg = ConfigFile::default();
        let result = match "invalid_key" {
            "org" => { cfg.context.org = Some("v".to_string()); Ok(()) }
            "project" => { cfg.context.project = Some("v".to_string()); Ok(()) }
            "environment" => { cfg.context.environment = Some("v".to_string()); Ok(()) }
            "scope" => { cfg.context.scope = Some("v".to_string()); Ok(()) }
            "api.url" => { cfg.api.url = Some("v".to_string()); Ok(()) }
            other => Err(format!("unknown config key '{other}'")),
        };
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("unknown config key"));
    }

    #[test]
    fn test_config_set_valid_keys_roundtrip() {
        // Test config set/load roundtrip via direct file operations
        let dir = tempfile::TempDir::new().expect("tempdir");
        let path = dir.path().join("config.toml");

        let mut config = ConfigFile::default();
        config.context.org = Some("acme".to_string());
        config.context.project = Some("api".to_string());
        config.context.environment = Some("prod".to_string());
        config.context.scope = Some("default".to_string());
        config.api.url = Some("https://custom.dev".to_string());

        let content = toml::to_string_pretty(&config).expect("serialize");
        std::fs::write(&path, &content).expect("write");

        let loaded: ConfigFile =
            toml::from_str(&std::fs::read_to_string(&path).expect("read")).expect("parse");
        assert_eq!(loaded.context.org.as_deref(), Some("acme"));
        assert_eq!(loaded.context.project.as_deref(), Some("api"));
        assert_eq!(loaded.context.environment.as_deref(), Some("prod"));
        assert_eq!(loaded.context.scope.as_deref(), Some("default"));
        assert_eq!(loaded.api.url.as_deref(), Some("https://custom.dev"));
    }
}