void-cli 0.0.2

CLI for void — anonymous encrypted source control
//! Config command implementation.
//!
//! Get, set, list, and unset configuration values stored in `.void/config.json`.

use std::collections::HashMap;
use std::path::Path;

use serde::Serialize;
use void_core::config;

use crate::context::find_void_dir;
use crate::output::{run_command, CliError, CliOptions};

/// JSON output for config list operation.
#[derive(Debug, Serialize)]
pub struct ConfigListOutput {
    /// All configuration values as key-value pairs.
    pub values: HashMap<String, String>,
}

/// JSON output for config get operation.
#[derive(Debug, Serialize)]
pub struct ConfigGetOutput {
    /// The configuration key.
    pub key: String,
    /// The configuration value.
    pub value: String,
}

/// JSON output for config set operation.
#[derive(Debug, Serialize)]
pub struct ConfigSetOutput {
    /// The configuration key.
    pub key: String,
    /// The new configuration value.
    pub value: String,
    /// The previous value, if any.
    #[serde(rename = "previousValue")]
    pub previous_value: Option<String>,
}

/// JSON output for config unset operation.
#[derive(Debug, Serialize)]
pub struct ConfigUnsetOutput {
    /// The configuration key that was removed.
    pub key: String,
    /// Whether the key was removed.
    pub removed: bool,
}

/// Arguments for the config command.
#[derive(Debug, Clone)]
pub struct ConfigArgs {
    /// List all config values.
    pub list: bool,
    /// Unset a config key.
    pub unset: Option<String>,
    /// Key to get or set.
    pub key: Option<String>,
    /// Value to set (if provided with key).
    pub value: Option<String>,
}

/// Run the config command.
///
/// Modes:
/// - `--list`: List all config values
/// - `<key>`: Get a config value
/// - `<key> <value>`: Set a config value
/// - `--unset <key>`: Remove a config value
///
/// # Arguments
///
/// * `cwd` - Current working directory
/// * `args` - Config command arguments
/// * `opts` - CLI options
pub fn run(cwd: &Path, args: ConfigArgs, opts: &CliOptions) -> Result<(), CliError> {
    // Determine mode and dispatch
    if args.list {
        return run_list(cwd, opts);
    }

    if let Some(key) = &args.unset {
        return run_unset(cwd, key, opts);
    }

    match (&args.key, &args.value) {
        (Some(key), Some(value)) => run_set(cwd, key, value, opts),
        (Some(key), None) => run_get(cwd, key, opts),
        (None, _) => {
            // No key provided and not --list or --unset
            Err(CliError::invalid_args(
                "usage: void config --list | void config <key> | void config <key> <value> | void config --unset <key>",
            ))
        }
    }
}

/// Run the config --list operation.
fn run_list(cwd: &Path, opts: &CliOptions) -> Result<(), CliError> {
    run_command("config", opts, |ctx| {
        let void_dir = find_void_dir(cwd)?;

        ctx.progress("Loading configuration...");

        let values = config::list(&void_dir).map_err(|e| CliError::internal(e.to_string()))?;

        // Human-readable output
        if !ctx.use_json() {
            if values.is_empty() {
                ctx.info("No configuration values set.");
            } else {
                // Sort keys for consistent output
                let mut keys: Vec<_> = values.keys().collect();
                keys.sort();

                for key in keys {
                    if let Some(value) = values.get(key) {
                        ctx.info(format!("{}={}", key, value));
                    }
                }
            }
        }

        Ok(ConfigListOutput { values })
    })
}

/// Run the config get operation.
fn run_get(cwd: &Path, key: &str, opts: &CliOptions) -> Result<(), CliError> {
    run_command("config", opts, |ctx| {
        let void_dir = find_void_dir(cwd)?;

        let value = config::get(&void_dir, key).map_err(|e| CliError::internal(e.to_string()))?;

        match value {
            Some(v) => {
                // Human-readable output
                if !ctx.use_json() {
                    ctx.info(&v);
                }

                Ok(ConfigGetOutput {
                    key: key.to_string(),
                    value: v,
                })
            }
            None => Err(CliError::not_found(format!("key not found: {}", key))),
        }
    })
}

/// Run the config set operation.
fn run_set(cwd: &Path, key: &str, value: &str, opts: &CliOptions) -> Result<(), CliError> {
    run_command("config", opts, |ctx| {
        let void_dir = find_void_dir(cwd)?;

        // Get previous value before setting
        let previous_value =
            config::get(&void_dir, key).map_err(|e| CliError::internal(e.to_string()))?;

        // Set the new value
        config::set(&void_dir, key, value).map_err(|e| {
            // Convert serialization errors to appropriate CLI errors
            let msg = e.to_string();
            if msg.contains("read-only") {
                CliError::invalid_args(msg)
            } else if msg.contains("unknown config key") {
                CliError::invalid_args(msg)
            } else if msg.contains("invalid") {
                CliError::invalid_args(msg)
            } else {
                CliError::internal(msg)
            }
        })?;

        // Human-readable output
        if !ctx.use_json() {
            match &previous_value {
                Some(prev) => {
                    ctx.info(format!("{}={} (was: {})", key, value, prev));
                }
                None => {
                    ctx.info(format!("{}={}", key, value));
                }
            }
        }

        Ok(ConfigSetOutput {
            key: key.to_string(),
            value: value.to_string(),
            previous_value,
        })
    })
}

/// Run the config --unset operation.
fn run_unset(cwd: &Path, key: &str, opts: &CliOptions) -> Result<(), CliError> {
    run_command("config", opts, |ctx| {
        let void_dir = find_void_dir(cwd)?;

        // Unset the value
        config::unset(&void_dir, key).map_err(|e| {
            // Convert serialization errors to appropriate CLI errors
            let msg = e.to_string();
            if msg.contains("read-only") {
                CliError::invalid_args(msg)
            } else if msg.contains("unknown config key") {
                CliError::invalid_args(msg)
            } else {
                CliError::internal(msg)
            }
        })?;

        // Human-readable output
        if !ctx.use_json() {
            ctx.info(format!("Removed: {}", key));
        }

        Ok(ConfigUnsetOutput {
            key: key.to_string(),
            removed: true,
        })
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::output::CliOptions;
    use std::fs;
    use tempfile::tempdir;

    fn default_opts() -> CliOptions {
        CliOptions {
            human: true,
            ..Default::default()
        }
    }

    fn setup_void_repo() -> tempfile::TempDir {
        let dir = tempdir().unwrap();
        let void_dir = dir.path().join(".void");
        fs::create_dir_all(&void_dir).unwrap();
        // Create minimal config
        fs::write(void_dir.join("config.json"), "{}").unwrap();
        dir
    }

    #[test]
    fn test_config_list_empty() {
        let dir = setup_void_repo();
        let args = ConfigArgs {
            list: true,
            unset: None,
            key: None,
            value: None,
        };
        let result = run(dir.path(), args, &default_opts());
        assert!(result.is_ok());
    }

    #[test]
    fn test_config_set_and_get() {
        let dir = setup_void_repo();

        // Set a value
        let set_args = ConfigArgs {
            list: false,
            unset: None,
            key: Some("user.name".to_string()),
            value: Some("Test User".to_string()),
        };
        let result = run(dir.path(), set_args, &default_opts());
        assert!(result.is_ok());

        // Get the value
        let get_args = ConfigArgs {
            list: false,
            unset: None,
            key: Some("user.name".to_string()),
            value: None,
        };
        let result = run(dir.path(), get_args, &default_opts());
        assert!(result.is_ok());
    }

    #[test]
    fn test_config_unset() {
        let dir = setup_void_repo();

        // Set a value first
        let set_args = ConfigArgs {
            list: false,
            unset: None,
            key: Some("user.email".to_string()),
            value: Some("test@example.com".to_string()),
        };
        run(dir.path(), set_args, &default_opts()).unwrap();

        // Unset the value
        let unset_args = ConfigArgs {
            list: false,
            unset: Some("user.email".to_string()),
            key: None,
            value: None,
        };
        let result = run(dir.path(), unset_args, &default_opts());
        assert!(result.is_ok());
    }

    #[test]
    fn test_config_get_not_found() {
        let dir = setup_void_repo();

        let args = ConfigArgs {
            list: false,
            unset: None,
            key: Some("nonexistent.key".to_string()),
            value: None,
        };
        let result = run(dir.path(), args, &default_opts());
        assert!(result.is_err());
    }

    #[test]
    fn test_config_set_invalid_key() {
        let dir = setup_void_repo();

        let args = ConfigArgs {
            list: false,
            unset: None,
            key: Some("invalid.nonexistent.key".to_string()),
            value: Some("value".to_string()),
        };
        let result = run(dir.path(), args, &default_opts());
        assert!(result.is_err());
    }

    #[test]
    fn test_config_set_readonly_key() {
        let dir = setup_void_repo();

        let args = ConfigArgs {
            list: false,
            unset: None,
            key: Some("version".to_string()),
            value: Some("2".to_string()),
        };
        let result = run(dir.path(), args, &default_opts());
        assert!(result.is_err());
    }

    #[test]
    fn test_config_no_args() {
        let dir = setup_void_repo();

        let args = ConfigArgs {
            list: false,
            unset: None,
            key: None,
            value: None,
        };
        let result = run(dir.path(), args, &default_opts());
        assert!(result.is_err());
    }

    #[test]
    fn test_config_not_initialized() {
        let dir = tempdir().unwrap();
        // No .void directory

        let args = ConfigArgs {
            list: true,
            unset: None,
            key: None,
            value: None,
        };
        let result = run(dir.path(), args, &default_opts());
        assert!(result.is_err());
    }
}