crabcode 0.0.1

(WIP) Rust AI CLI Coding Agent with a beautiful terminal UI
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKeyConfig {
    pub api_keys: HashMap<String, String>,
}

impl Default for ApiKeyConfig {
    fn default() -> Self {
        Self::new()
    }
}

impl ApiKeyConfig {
    pub fn new() -> Self {
        Self {
            api_keys: HashMap::new(),
        }
    }

    pub fn load() -> Result<Self> {
        let path = Self::config_path();
        if path.exists() {
            let content = fs::read_to_string(&path)?;
            let config: ApiKeyConfig = serde_json::from_str(&content)?;
            Ok(config)
        } else {
            Ok(Self::new())
        }
    }

    pub fn save(&self) -> Result<()> {
        let path = Self::config_path();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let content = serde_json::to_string_pretty(self)?;
        fs::write(&path, content)?;
        Ok(())
    }

    pub fn set_api_key(&mut self, provider: String, api_key: String) {
        self.api_keys.insert(provider, api_key);
    }

    pub fn get_api_key(&self, provider: &str) -> Option<&String> {
        self.api_keys.get(provider)
    }

    pub fn list_providers(&self) -> Vec<String> {
        let mut providers: Vec<String> = self.api_keys.keys().cloned().collect();
        providers.sort();
        providers
    }

    fn config_path() -> PathBuf {
        if cfg!(test) || env::var("CRABCODE_TEST_MODE").is_ok() {
            PathBuf::from("/tmp/crabcode_test_api_keys.json")
        } else {
            dirs::config_dir()
                .unwrap_or_else(|| PathBuf::from("."))
                .join("crabcode")
                .join("api_keys.json")
        }
    }

    #[cfg(test)]
    pub fn load_test() -> Result<Self> {
        let path = PathBuf::from("/tmp/crabcode_test_api_keys.json");
        if path.exists() {
            let content = fs::read_to_string(&path)?;
            let config: ApiKeyConfig = serde_json::from_str(&content)?;
            Ok(config)
        } else {
            Ok(Self::new())
        }
    }

    #[cfg(test)]
    pub fn save_test(&self) -> Result<()> {
        let path = PathBuf::from("/tmp/crabcode_test_api_keys.json");
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let content = serde_json::to_string_pretty(self)?;
        fs::write(&path, content)?;
        Ok(())
    }

    #[cfg(test)]
    pub fn cleanup_test() -> Result<()> {
        let path = PathBuf::from("/tmp/crabcode_test_api_keys.json");
        if path.exists() {
            fs::remove_file(&path)?;
        }
        Ok(())
    }
}

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

    #[test]
    fn test_api_key_config_new() {
        let config = ApiKeyConfig::new();
        assert!(config.api_keys.is_empty());
    }

    #[test]
    fn test_api_key_config_default() {
        let config = ApiKeyConfig::default();
        assert!(config.api_keys.is_empty());
    }

    #[test]
    fn test_set_api_key() {
        let mut config = ApiKeyConfig::new();
        config.set_api_key("nano-gpt".to_string(), "test-key-123".to_string());
        assert_eq!(
            config.get_api_key("nano-gpt"),
            Some(&"test-key-123".to_string())
        );
    }

    #[test]
    fn test_get_api_key_nonexistent() {
        let config = ApiKeyConfig::new();
        assert_eq!(config.get_api_key("nonexistent"), None);
    }

    #[test]
    fn test_list_providers_empty() {
        let config = ApiKeyConfig::new();
        assert!(config.list_providers().is_empty());
    }

    #[test]
    fn test_list_providers() {
        let mut config = ApiKeyConfig::new();
        config.set_api_key("z-ai".to_string(), "key1".to_string());
        config.set_api_key("nano-gpt".to_string(), "key2".to_string());
        let providers = config.list_providers();
        assert_eq!(providers.len(), 2);
        assert!(providers.contains(&"nano-gpt".to_string()));
        assert!(providers.contains(&"z-ai".to_string()));
    }

    #[test]
    fn test_list_providers_sorted() {
        let mut config = ApiKeyConfig::new();
        config.set_api_key("z-ai".to_string(), "key1".to_string());
        config.set_api_key("nano-gpt".to_string(), "key2".to_string());
        let providers = config.list_providers();
        assert_eq!(providers[0], "nano-gpt");
        assert_eq!(providers[1], "z-ai");
    }

    #[test]
    fn test_save_and_load_test() -> Result<()> {
        ApiKeyConfig::cleanup_test()?;

        let mut config = ApiKeyConfig::new();
        config.set_api_key("nano-gpt".to_string(), "test-key".to_string());
        config.save_test()?;

        let loaded = ApiKeyConfig::load_test()?;
        assert_eq!(
            loaded.get_api_key("nano-gpt"),
            Some(&"test-key".to_string())
        );

        ApiKeyConfig::cleanup_test()?;
        Ok(())
    }

    #[test]
    fn test_serialization() {
        let mut config = ApiKeyConfig::new();
        config.set_api_key("nano-gpt".to_string(), "test-key".to_string());

        let serialized = serde_json::to_string(&config).unwrap();
        let deserialized: ApiKeyConfig = serde_json::from_str(&serialized).unwrap();

        assert_eq!(
            deserialized.get_api_key("nano-gpt"),
            Some(&"test-key".to_string())
        );
    }
}