subx_cli/config/
service.rs

1#![allow(deprecated)]
2//! Configuration service system for dependency injection and test isolation.
3//!
4//! This module provides a clean abstraction for configuration management
5//! that enables dependency injection and complete test isolation without
6//! requiring unsafe code or global state resets.
7
8use crate::config::{EnvironmentProvider, SystemEnvironmentProvider};
9use crate::{Result, config::Config, error::SubXError};
10use config::{Config as ConfigCrate, ConfigBuilder, Environment, File, builder::DefaultState};
11use log::debug;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, RwLock};
14
15/// Configuration service trait for dependency injection.
16///
17/// This trait abstracts configuration loading and reloading operations,
18/// allowing different implementations for production and testing environments.
19pub trait ConfigService: Send + Sync {
20    /// Get the current configuration.
21    ///
22    /// Returns a clone of the current configuration state. This method
23    /// may use internal caching for performance.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if configuration loading or validation fails.
28    /// Get the current configuration.
29    ///
30    /// Returns the current [`Config`] instance loaded from files,
31    /// environment variables, and defaults.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if configuration loading fails due to:
36    /// - Invalid TOML format in configuration files
37    /// - Missing required configuration values
38    /// - File system access issues
39    fn get_config(&self) -> Result<Config>;
40
41    /// Reload configuration from sources.
42    ///
43    /// Forces a reload of configuration from all sources, discarding
44    /// any cached values.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if configuration reloading fails.
49    fn reload(&self) -> Result<()>;
50
51    /// Save current configuration to the default file location.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if:
56    /// - Unable to determine config file path
57    /// - File system write permissions are insufficient
58    /// - TOML serialization fails
59    fn save_config(&self) -> Result<()>;
60
61    /// Save configuration to a specific file path.
62    ///
63    /// # Arguments
64    ///
65    /// - `path`: Target file path for the configuration
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if:
70    /// - TOML serialization fails
71    /// - Unable to create parent directories
72    /// - File write operation fails
73    fn save_config_to_file(&self, path: &Path) -> Result<()>;
74
75    /// Get the default configuration file path.
76    ///
77    /// # Returns
78    ///
79    /// Returns the path where configuration files are expected to be located,
80    /// typically `$CONFIG_DIR/subx/config.toml`.
81    fn get_config_file_path(&self) -> Result<PathBuf>;
82
83    /// Get a specific configuration value by key path.
84    ///
85    /// # Arguments
86    ///
87    /// - `key`: Dot-separated path to the configuration value (e.g., "ai.provider")
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the key is not recognized.
92    fn get_config_value(&self, key: &str) -> Result<String>;
93
94    /// Reset configuration to default values.
95    ///
96    /// This will overwrite the current configuration file with default values
97    /// and reload the configuration.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if save or reload fails.
102    fn reset_to_defaults(&self) -> Result<()>;
103
104    /// Set a specific configuration value by key path.
105    ///
106    /// # Arguments
107    ///
108    /// - `key`: Dot-separated path to the configuration value
109    /// - `value`: New value as string (will be converted to appropriate type)
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if validation or persistence fails, including:
114    /// - Unknown configuration key
115    /// - Type conversion or validation error
116    /// - Failure to persist configuration
117    fn set_config_value(&self, key: &str, value: &str) -> Result<()>;
118}
119
120/// Production configuration service implementation.
121///
122/// This service loads configuration from multiple sources in order of priority:
123/// 1. Environment variables (highest priority)
124/// 2. User configuration file
125/// 3. Default configuration file (lowest priority)
126///
127/// Configuration is cached after first load for performance.
128pub struct ProductionConfigService {
129    config_builder: ConfigBuilder<DefaultState>,
130    cached_config: Arc<RwLock<Option<Config>>>,
131    env_provider: Arc<dyn EnvironmentProvider>,
132}
133
134impl ProductionConfigService {
135    /// Create a new production configuration service.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the configuration builder cannot be initialized.
140    /// Creates a configuration service using the default environment variable provider (maintains compatibility with existing methods).
141    pub fn new() -> Result<Self> {
142        Self::with_env_provider(Arc::new(SystemEnvironmentProvider::new()))
143    }
144
145    /// Create a configuration service using the specified environment variable provider.
146    ///
147    /// # Arguments
148    /// * `env_provider` - Environment variable provider
149    pub fn with_env_provider(env_provider: Arc<dyn EnvironmentProvider>) -> Result<Self> {
150        // Check if a custom config path is specified in the environment provider
151        let config_file_path = if let Some(custom_path) = env_provider.get_var("SUBX_CONFIG_PATH") {
152            PathBuf::from(custom_path)
153        } else {
154            Self::user_config_path()
155        };
156
157        let config_builder = ConfigCrate::builder()
158            .add_source(File::with_name("config/default").required(false))
159            .add_source(File::from(config_file_path).required(false))
160            .add_source(Environment::with_prefix("SUBX").separator("_"));
161
162        Ok(Self {
163            config_builder,
164            cached_config: Arc::new(RwLock::new(None)),
165            env_provider,
166        })
167    }
168
169    /// Create a configuration service with custom sources.
170    ///
171    /// This allows adding additional configuration sources for specific use cases.
172    ///
173    /// # Arguments
174    ///
175    /// * `sources` - Additional configuration sources to add
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if the configuration builder cannot be updated.
180    pub fn with_custom_file(mut self, file_path: PathBuf) -> Result<Self> {
181        self.config_builder = self.config_builder.add_source(File::from(file_path));
182        Ok(self)
183    }
184
185    /// Get the user configuration file path.
186    ///
187    /// Returns the path to the user's configuration file, which is typically
188    /// located in the user's configuration directory.
189    fn user_config_path() -> PathBuf {
190        dirs::config_dir()
191            .unwrap_or_else(|| PathBuf::from("."))
192            .join("subx")
193            .join("config.toml")
194    }
195
196    /// Load and validate configuration from all sources.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if configuration loading or validation fails.
201    fn load_and_validate(&self) -> Result<Config> {
202        debug!("ProductionConfigService: Loading configuration from sources");
203
204        // Build configuration from all sources
205        let config_crate = self.config_builder.build_cloned().map_err(|e| {
206            debug!("ProductionConfigService: Config build failed: {}", e);
207            SubXError::config(format!("Failed to build configuration: {}", e))
208        })?;
209
210        // Start with default configuration
211        let mut app_config = Config::default();
212
213        // Try to deserialize from config crate, but fall back to defaults if needed
214        if let Ok(config) = config_crate.clone().try_deserialize::<Config>() {
215            app_config = config;
216            debug!("ProductionConfigService: Full configuration loaded successfully");
217        } else {
218            debug!("ProductionConfigService: Full deserialization failed, attempting partial load");
219
220            // Try to load partial configurations from environment
221            if let Ok(raw_map) = config_crate
222                .try_deserialize::<std::collections::HashMap<String, serde_json::Value>>()
223            {
224                // Extract AI configuration if available
225                if let Some(ai_section) = raw_map.get("ai") {
226                    if let Some(ai_obj) = ai_section.as_object() {
227                        // Extract individual AI fields that are available
228                        if let Some(api_key) = ai_obj.get("apikey").and_then(|v| v.as_str()) {
229                            app_config.ai.api_key = Some(api_key.to_string());
230                            debug!(
231                                "ProductionConfigService: AI API key loaded from SUBX_AI_APIKEY"
232                            );
233                        }
234                        if let Some(provider) = ai_obj.get("provider").and_then(|v| v.as_str()) {
235                            app_config.ai.provider = provider.to_string();
236                            debug!(
237                                "ProductionConfigService: AI provider loaded from SUBX_AI_PROVIDER"
238                            );
239                        }
240                        if let Some(model) = ai_obj.get("model").and_then(|v| v.as_str()) {
241                            app_config.ai.model = model.to_string();
242                            debug!("ProductionConfigService: AI model loaded from SUBX_AI_MODEL");
243                        }
244                        if let Some(base_url) = ai_obj.get("base_url").and_then(|v| v.as_str()) {
245                            app_config.ai.base_url = base_url.to_string();
246                            debug!(
247                                "ProductionConfigService: AI base URL loaded from SUBX_AI_BASE_URL"
248                            );
249                        }
250                    }
251                }
252            }
253        }
254
255        // Special handling for OPENAI_API_KEY environment variable
256        // This provides backward compatibility with direct OPENAI_API_KEY usage
257        if app_config.ai.api_key.is_none() {
258            if let Some(api_key) = self.env_provider.get_var("OPENAI_API_KEY") {
259                debug!("ProductionConfigService: Found OPENAI_API_KEY environment variable");
260                app_config.ai.api_key = Some(api_key);
261            }
262        }
263
264        // Special handling for OPENAI_BASE_URL environment variable
265        if let Some(base_url) = self.env_provider.get_var("OPENAI_BASE_URL") {
266            debug!("ProductionConfigService: Found OPENAI_BASE_URL environment variable");
267            app_config.ai.base_url = base_url;
268        }
269
270        // Validate the configuration
271        crate::config::validator::validate_config(&app_config).map_err(|e| {
272            debug!("ProductionConfigService: Config validation failed: {}", e);
273            SubXError::config(format!("Configuration validation failed: {}", e))
274        })?;
275
276        debug!("ProductionConfigService: Configuration loaded and validated successfully");
277        Ok(app_config)
278    }
279
280    /// Validate and set a configuration value.
281    ///
282    /// This method now delegates validation to the field_validator module.
283    fn validate_and_set_value(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
284        use crate::config::field_validator;
285
286        // Use the dedicated field validator
287        field_validator::validate_field(key, value)?;
288
289        // Set the value in the configuration
290        self.set_value_internal(config, key, value)?;
291
292        // Validate the entire configuration after the change
293        self.validate_configuration(config)?;
294
295        Ok(())
296    }
297
298    /// Internal method to set configuration values without validation.
299    fn set_value_internal(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
300        use crate::config::OverflowStrategy;
301        use crate::config::validation::*;
302        use crate::error::SubXError;
303
304        let parts: Vec<&str> = key.split('.').collect();
305        match parts.as_slice() {
306            ["ai", "provider"] => {
307                config.ai.provider = value.to_string();
308            }
309            ["ai", "api_key"] => {
310                if !value.is_empty() {
311                    config.ai.api_key = Some(value.to_string());
312                } else {
313                    config.ai.api_key = None;
314                }
315            }
316            ["ai", "model"] => {
317                config.ai.model = value.to_string();
318            }
319            ["ai", "base_url"] => {
320                config.ai.base_url = value.to_string();
321            }
322            ["ai", "max_sample_length"] => {
323                let v = value.parse().unwrap(); // Validation already done
324                config.ai.max_sample_length = v;
325            }
326            ["ai", "temperature"] => {
327                let v = value.parse().unwrap(); // Validation already done
328                config.ai.temperature = v;
329            }
330            ["ai", "max_tokens"] => {
331                let v = value.parse().unwrap(); // Validation already done
332                config.ai.max_tokens = v;
333            }
334            ["ai", "retry_attempts"] => {
335                let v = value.parse().unwrap(); // Validation already done
336                config.ai.retry_attempts = v;
337            }
338            ["ai", "retry_delay_ms"] => {
339                let v = value.parse().unwrap(); // Validation already done
340                config.ai.retry_delay_ms = v;
341            }
342            ["ai", "request_timeout_seconds"] => {
343                let v = value.parse().unwrap(); // Validation already done
344                config.ai.request_timeout_seconds = v;
345            }
346            ["formats", "default_output"] => {
347                config.formats.default_output = value.to_string();
348            }
349            ["formats", "preserve_styling"] => {
350                let v = parse_bool(value)?;
351                config.formats.preserve_styling = v;
352            }
353            ["formats", "default_encoding"] => {
354                config.formats.default_encoding = value.to_string();
355            }
356            ["formats", "encoding_detection_confidence"] => {
357                let v = value.parse().unwrap(); // Validation already done
358                config.formats.encoding_detection_confidence = v;
359            }
360            ["sync", "max_offset_seconds"] => {
361                let v = value.parse().unwrap(); // Validation already done
362                config.sync.max_offset_seconds = v;
363            }
364            ["sync", "default_method"] => {
365                config.sync.default_method = value.to_string();
366            }
367            ["sync", "vad", "enabled"] => {
368                let v = parse_bool(value)?;
369                config.sync.vad.enabled = v;
370            }
371            ["sync", "vad", "sensitivity"] => {
372                let v = value.parse().unwrap(); // Validation already done
373                config.sync.vad.sensitivity = v;
374            }
375            ["sync", "vad", "padding_chunks"] => {
376                let v = value.parse().unwrap(); // Validation already done
377                config.sync.vad.padding_chunks = v;
378            }
379            ["sync", "vad", "min_speech_duration_ms"] => {
380                let v = value.parse().unwrap(); // Validation already done
381                config.sync.vad.min_speech_duration_ms = v;
382            }
383            ["general", "backup_enabled"] => {
384                let v = parse_bool(value)?;
385                config.general.backup_enabled = v;
386            }
387            ["general", "max_concurrent_jobs"] => {
388                let v = value.parse().unwrap(); // Validation already done
389                config.general.max_concurrent_jobs = v;
390            }
391            ["general", "task_timeout_seconds"] => {
392                let v = value.parse().unwrap(); // Validation already done
393                config.general.task_timeout_seconds = v;
394            }
395            ["general", "enable_progress_bar"] => {
396                let v = parse_bool(value)?;
397                config.general.enable_progress_bar = v;
398            }
399            ["general", "worker_idle_timeout_seconds"] => {
400                let v = value.parse().unwrap(); // Validation already done
401                config.general.worker_idle_timeout_seconds = v;
402            }
403            ["parallel", "max_workers"] => {
404                let v = value.parse().unwrap(); // Validation already done
405                config.parallel.max_workers = v;
406            }
407            ["parallel", "task_queue_size"] => {
408                let v = value.parse().unwrap(); // Validation already done
409                config.parallel.task_queue_size = v;
410            }
411            ["parallel", "enable_task_priorities"] => {
412                let v = parse_bool(value)?;
413                config.parallel.enable_task_priorities = v;
414            }
415            ["parallel", "auto_balance_workers"] => {
416                let v = parse_bool(value)?;
417                config.parallel.auto_balance_workers = v;
418            }
419            ["parallel", "overflow_strategy"] => {
420                config.parallel.overflow_strategy = match value {
421                    "Block" => OverflowStrategy::Block,
422                    "Drop" => OverflowStrategy::Drop,
423                    "Expand" => OverflowStrategy::Expand,
424                    _ => unreachable!(), // Validation already done
425                };
426            }
427            _ => {
428                return Err(SubXError::config(format!(
429                    "Unknown configuration key: {}",
430                    key
431                )));
432            }
433        }
434        Ok(())
435    }
436
437    /// Validate the entire configuration.
438    fn validate_configuration(&self, config: &Config) -> Result<()> {
439        use crate::config::validator;
440        validator::validate_config(config)
441    }
442
443    /// Save configuration to file with specific config object.
444    fn save_config_to_file_with_config(
445        &self,
446        path: &std::path::Path,
447        config: &Config,
448    ) -> Result<()> {
449        let toml_content = toml::to_string_pretty(config)
450            .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
451        if let Some(parent) = path.parent() {
452            std::fs::create_dir_all(parent).map_err(|e| {
453                SubXError::config(format!("Failed to create config directory: {}", e))
454            })?;
455        }
456        std::fs::write(path, toml_content)
457            .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
458        Ok(())
459    }
460}
461
462impl ConfigService for ProductionConfigService {
463    fn get_config(&self) -> Result<Config> {
464        // Check cache first
465        {
466            let cache = self.cached_config.read().unwrap();
467            if let Some(config) = cache.as_ref() {
468                debug!("ProductionConfigService: Returning cached configuration");
469                return Ok(config.clone());
470            }
471        }
472
473        // Load configuration
474        let app_config = self.load_and_validate()?;
475
476        // Update cache
477        {
478            let mut cache = self.cached_config.write().unwrap();
479            *cache = Some(app_config.clone());
480        }
481
482        Ok(app_config)
483    }
484
485    fn reload(&self) -> Result<()> {
486        debug!("ProductionConfigService: Reloading configuration");
487
488        // Clear cache to force reload
489        {
490            let mut cache = self.cached_config.write().unwrap();
491            *cache = None;
492        }
493
494        // Trigger reload by calling get_config
495        self.get_config()?;
496
497        debug!("ProductionConfigService: Configuration reloaded successfully");
498        Ok(())
499    }
500
501    fn save_config(&self) -> Result<()> {
502        let _config = self.get_config()?;
503        let path = self.get_config_file_path()?;
504        self.save_config_to_file(&path)
505    }
506
507    fn save_config_to_file(&self, path: &Path) -> Result<()> {
508        let config = self.get_config()?;
509        let toml_content = toml::to_string_pretty(&config)
510            .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
511
512        if let Some(parent) = path.parent() {
513            std::fs::create_dir_all(parent).map_err(|e| {
514                SubXError::config(format!("Failed to create config directory: {}", e))
515            })?;
516        }
517
518        std::fs::write(path, toml_content)
519            .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
520
521        Ok(())
522    }
523
524    fn get_config_file_path(&self) -> Result<PathBuf> {
525        // Allow injection via EnvironmentProvider for testing
526        if let Some(custom) = self.env_provider.get_var("SUBX_CONFIG_PATH") {
527            return Ok(PathBuf::from(custom));
528        }
529
530        let config_dir = dirs::config_dir()
531            .ok_or_else(|| SubXError::config("Unable to determine config directory"))?;
532        Ok(config_dir.join("subx").join("config.toml"))
533    }
534
535    fn get_config_value(&self, key: &str) -> Result<String> {
536        let config = self.get_config()?;
537        let parts: Vec<&str> = key.split('.').collect();
538        match parts.as_slice() {
539            ["ai", "provider"] => Ok(config.ai.provider.clone()),
540            ["ai", "model"] => Ok(config.ai.model.clone()),
541            ["ai", "api_key"] => Ok(config.ai.api_key.clone().unwrap_or_default()),
542            ["ai", "base_url"] => Ok(config.ai.base_url.clone()),
543            ["ai", "max_sample_length"] => Ok(config.ai.max_sample_length.to_string()),
544            ["ai", "temperature"] => Ok(config.ai.temperature.to_string()),
545            ["ai", "max_tokens"] => Ok(config.ai.max_tokens.to_string()),
546            ["ai", "retry_attempts"] => Ok(config.ai.retry_attempts.to_string()),
547            ["ai", "retry_delay_ms"] => Ok(config.ai.retry_delay_ms.to_string()),
548            ["ai", "request_timeout_seconds"] => Ok(config.ai.request_timeout_seconds.to_string()),
549
550            ["formats", "default_output"] => Ok(config.formats.default_output.clone()),
551            ["formats", "default_encoding"] => Ok(config.formats.default_encoding.clone()),
552            ["formats", "preserve_styling"] => Ok(config.formats.preserve_styling.to_string()),
553            ["formats", "encoding_detection_confidence"] => {
554                Ok(config.formats.encoding_detection_confidence.to_string())
555            }
556
557            ["sync", "default_method"] => Ok(config.sync.default_method.clone()),
558            ["sync", "max_offset_seconds"] => Ok(config.sync.max_offset_seconds.to_string()),
559            ["sync", "vad", "enabled"] => Ok(config.sync.vad.enabled.to_string()),
560            ["sync", "vad", "sensitivity"] => Ok(config.sync.vad.sensitivity.to_string()),
561            ["sync", "vad", "padding_chunks"] => Ok(config.sync.vad.padding_chunks.to_string()),
562            ["sync", "vad", "min_speech_duration_ms"] => {
563                Ok(config.sync.vad.min_speech_duration_ms.to_string())
564            }
565
566            ["general", "backup_enabled"] => Ok(config.general.backup_enabled.to_string()),
567            ["general", "max_concurrent_jobs"] => {
568                Ok(config.general.max_concurrent_jobs.to_string())
569            }
570            ["general", "task_timeout_seconds"] => {
571                Ok(config.general.task_timeout_seconds.to_string())
572            }
573            ["general", "enable_progress_bar"] => {
574                Ok(config.general.enable_progress_bar.to_string())
575            }
576            ["general", "worker_idle_timeout_seconds"] => {
577                Ok(config.general.worker_idle_timeout_seconds.to_string())
578            }
579
580            ["parallel", "max_workers"] => Ok(config.parallel.max_workers.to_string()),
581            ["parallel", "task_queue_size"] => Ok(config.parallel.task_queue_size.to_string()),
582            ["parallel", "enable_task_priorities"] => {
583                Ok(config.parallel.enable_task_priorities.to_string())
584            }
585            ["parallel", "auto_balance_workers"] => {
586                Ok(config.parallel.auto_balance_workers.to_string())
587            }
588            ["parallel", "overflow_strategy"] => {
589                Ok(format!("{:?}", config.parallel.overflow_strategy))
590            }
591
592            _ => Err(SubXError::config(format!(
593                "Unknown configuration key: {}",
594                key
595            ))),
596        }
597    }
598
599    fn set_config_value(&self, key: &str, value: &str) -> Result<()> {
600        // 1. Load current configuration
601        let mut config = self.get_config()?;
602
603        // 2. Validate and set the value
604        self.validate_and_set_value(&mut config, key, value)?;
605
606        // 3. Validate the entire configuration
607        crate::config::validator::validate_config(&config)?;
608
609        // 4. Save to file
610        let path = self.get_config_file_path()?;
611        self.save_config_to_file_with_config(&path, &config)?;
612
613        // 5. Update cache
614        {
615            let mut cache = self.cached_config.write().unwrap();
616            *cache = Some(config);
617        }
618
619        Ok(())
620    }
621
622    fn reset_to_defaults(&self) -> Result<()> {
623        let default_config = Config::default();
624        let path = self.get_config_file_path()?;
625
626        let toml_content = toml::to_string_pretty(&default_config)
627            .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
628
629        if let Some(parent) = path.parent() {
630            std::fs::create_dir_all(parent).map_err(|e| {
631                SubXError::config(format!("Failed to create config directory: {}", e))
632            })?;
633        }
634
635        std::fs::write(&path, toml_content)
636            .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
637
638        self.reload()
639    }
640}
641
642impl Default for ProductionConfigService {
643    fn default() -> Self {
644        Self::new().expect("Failed to create default ProductionConfigService")
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::config::TestConfigService;
652    use crate::config::TestEnvironmentProvider;
653    use std::sync::Arc;
654
655    #[test]
656    fn test_production_config_service_creation() {
657        let service = ProductionConfigService::new();
658        assert!(service.is_ok());
659    }
660
661    #[test]
662    fn test_production_config_service_with_custom_file() {
663        let service = ProductionConfigService::new()
664            .unwrap()
665            .with_custom_file(PathBuf::from("test.toml"));
666        assert!(service.is_ok());
667    }
668
669    #[test]
670    fn test_production_service_implements_config_service_trait() {
671        let service = ProductionConfigService::new().unwrap();
672
673        // Test trait methods
674        let config1 = service.get_config();
675        assert!(config1.is_ok());
676
677        let reload_result = service.reload();
678        assert!(reload_result.is_ok());
679
680        let config2 = service.get_config();
681        assert!(config2.is_ok());
682    }
683
684    #[test]
685    fn test_config_service_with_openai_api_key() {
686        // Test configuration with OpenAI API key using TestConfigService
687        let test_service = TestConfigService::with_ai_settings_and_key(
688            "openai",
689            "gpt-4.1-mini",
690            "sk-test-openai-key-123",
691        );
692
693        let config = test_service.get_config().unwrap();
694        assert_eq!(
695            config.ai.api_key,
696            Some("sk-test-openai-key-123".to_string())
697        );
698        assert_eq!(config.ai.provider, "openai");
699        assert_eq!(config.ai.model, "gpt-4.1-mini");
700    }
701
702    #[test]
703    fn test_config_service_with_custom_base_url() {
704        // Test configuration with custom base URL
705        let mut config = Config::default();
706        config.ai.base_url = "https://custom.openai.endpoint".to_string();
707
708        let test_service = TestConfigService::new(config);
709        let loaded_config = test_service.get_config().unwrap();
710
711        assert_eq!(loaded_config.ai.base_url, "https://custom.openai.endpoint");
712    }
713
714    #[test]
715    fn test_config_service_with_both_openai_settings() {
716        // Test configuration with both API key and base URL
717        let mut config = Config::default();
718        config.ai.api_key = Some("sk-test-api-key-combined".to_string());
719        config.ai.base_url = "https://api.custom-openai.com".to_string();
720
721        let test_service = TestConfigService::new(config);
722        let loaded_config = test_service.get_config().unwrap();
723
724        assert_eq!(
725            loaded_config.ai.api_key,
726            Some("sk-test-api-key-combined".to_string())
727        );
728        assert_eq!(loaded_config.ai.base_url, "https://api.custom-openai.com");
729    }
730
731    #[test]
732    fn test_config_service_provider_precedence() {
733        // Test that manually configured values take precedence
734        let test_service =
735            TestConfigService::with_ai_settings_and_key("openai", "gpt-4.1", "sk-explicit-key");
736
737        let config = test_service.get_config().unwrap();
738        assert_eq!(config.ai.api_key, Some("sk-explicit-key".to_string()));
739        assert_eq!(config.ai.provider, "openai");
740        assert_eq!(config.ai.model, "gpt-4.1");
741    }
742
743    #[test]
744    fn test_config_service_fallback_behavior() {
745        // Test fallback to default values when no specific configuration provided
746        let test_service = TestConfigService::with_defaults();
747        let config = test_service.get_config().unwrap();
748
749        // Should use default values
750        assert_eq!(config.ai.provider, "openai");
751        assert_eq!(config.ai.model, "gpt-4.1-mini");
752        assert_eq!(config.ai.base_url, "https://api.openai.com/v1");
753        assert_eq!(config.ai.api_key, None); // No API key by default
754    }
755
756    #[test]
757    fn test_config_service_reload_functionality() {
758        // Test configuration reload capability
759        let test_service = TestConfigService::with_defaults();
760
761        // First load
762        let config1 = test_service.get_config().unwrap();
763        assert_eq!(config1.ai.provider, "openai");
764
765        // Reload should always succeed for test service
766        let reload_result = test_service.reload();
767        assert!(reload_result.is_ok());
768
769        // Second load should still work
770        let config2 = test_service.get_config().unwrap();
771        assert_eq!(config2.ai.provider, "openai");
772    }
773
774    #[test]
775    fn test_config_service_custom_base_url_override() {
776        // Test that custom base URL properly overrides default
777        let mut config = Config::default();
778        config.ai.base_url = "https://my-proxy.openai.com/v1".to_string();
779
780        let test_service = TestConfigService::new(config);
781        let loaded_config = test_service.get_config().unwrap();
782
783        assert_eq!(loaded_config.ai.base_url, "https://my-proxy.openai.com/v1");
784    }
785
786    #[test]
787    fn test_config_service_sync_settings() {
788        // Test sync configuration settings
789        let test_service = TestConfigService::with_sync_settings(0.8, 45.0);
790        let config = test_service.get_config().unwrap();
791
792        assert_eq!(config.sync.correlation_threshold, 0.8);
793        assert_eq!(config.sync.max_offset_seconds, 45.0);
794    }
795
796    #[test]
797    fn test_config_service_parallel_settings() {
798        // Test parallel processing configuration
799        let test_service = TestConfigService::with_parallel_settings(8, 200);
800        let config = test_service.get_config().unwrap();
801
802        assert_eq!(config.general.max_concurrent_jobs, 8);
803        assert_eq!(config.parallel.task_queue_size, 200);
804    }
805
806    #[test]
807    fn test_config_service_direct_access() {
808        // Test direct configuration access and mutation
809        let test_service = TestConfigService::with_defaults();
810
811        // Test direct read access
812        assert_eq!(test_service.config().ai.provider, "openai");
813
814        // Test mutable access
815        test_service.config_mut().ai.provider = "modified".to_string();
816        assert_eq!(test_service.config().ai.provider, "modified");
817
818        // Test that get_config reflects the changes
819        let config = test_service.get_config().unwrap();
820        assert_eq!(config.ai.provider, "modified");
821    }
822
823    #[test]
824    fn test_production_config_service_openai_api_key_loading() {
825        // Test OPENAI_API_KEY environment variable loading
826        let mut env_provider = TestEnvironmentProvider::new();
827        env_provider.set_var("OPENAI_API_KEY", "sk-test-openai-key-env");
828
829        // Use a non-existent config path to avoid interference from existing config files
830        env_provider.set_var(
831            "SUBX_CONFIG_PATH",
832            "/tmp/test_config_that_does_not_exist.toml",
833        );
834
835        let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
836            .expect("Failed to create config service");
837
838        let config = service.get_config().expect("Failed to get config");
839
840        assert_eq!(
841            config.ai.api_key,
842            Some("sk-test-openai-key-env".to_string())
843        );
844    }
845
846    #[test]
847    fn test_production_config_service_openai_base_url_loading() {
848        // Test OPENAI_BASE_URL environment variable loading
849        let mut env_provider = TestEnvironmentProvider::new();
850        env_provider.set_var("OPENAI_BASE_URL", "https://test.openai.com/v1");
851
852        let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
853            .expect("Failed to create config service");
854
855        let config = service.get_config().expect("Failed to get config");
856
857        assert_eq!(config.ai.base_url, "https://test.openai.com/v1");
858    }
859
860    #[test]
861    fn test_production_config_service_both_openai_env_vars() {
862        // Test setting both OPENAI environment variables simultaneously
863        let mut env_provider = TestEnvironmentProvider::new();
864        env_provider.set_var("OPENAI_API_KEY", "sk-test-key-both");
865        env_provider.set_var("OPENAI_BASE_URL", "https://both.openai.com/v1");
866
867        // Use a non-existent config path to avoid interference from existing config files
868        env_provider.set_var(
869            "SUBX_CONFIG_PATH",
870            "/tmp/test_config_both_that_does_not_exist.toml",
871        );
872
873        let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
874            .expect("Failed to create config service");
875
876        let config = service.get_config().expect("Failed to get config");
877
878        assert_eq!(config.ai.api_key, Some("sk-test-key-both".to_string()));
879        assert_eq!(config.ai.base_url, "https://both.openai.com/v1");
880    }
881
882    #[test]
883    fn test_production_config_service_no_openai_env_vars() {
884        // Test the case with no OPENAI environment variables
885        let mut env_provider = TestEnvironmentProvider::new(); // Empty provider
886
887        // Use a non-existent config path to avoid interference from existing config files
888        env_provider.set_var(
889            "SUBX_CONFIG_PATH",
890            "/tmp/test_config_no_openai_that_does_not_exist.toml",
891        );
892
893        let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
894            .expect("Failed to create config service");
895
896        let config = service.get_config().expect("Failed to get config");
897
898        // Should use default values
899        assert_eq!(config.ai.api_key, None);
900        assert_eq!(config.ai.base_url, "https://api.openai.com/v1"); // Default value
901    }
902
903    #[test]
904    fn test_production_config_service_api_key_priority() {
905        // Test API key priority: existing API key should not be overwritten
906        let mut env_provider = TestEnvironmentProvider::new();
907        env_provider.set_var("OPENAI_API_KEY", "sk-env-key");
908        // Simulate API key loaded from other sources (e.g., configuration file)
909        env_provider.set_var("SUBX_AI_APIKEY", "sk-config-key");
910
911        let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
912            .expect("Failed to create config service");
913
914        let config = service.get_config().expect("Failed to get config");
915
916        // SUBX_AI_APIKEY should have higher priority (since it's processed first)
917        // This test only verifies priority order, should at least have a value
918        assert!(config.ai.api_key.is_some());
919    }
920}