1use crate::config::service::ConfigService;
8use crate::{Result, config::Config};
9use std::path::{Path, PathBuf};
10use std::sync::Mutex;
11
12pub struct TestConfigService {
18 config: Mutex<Config>,
19}
20
21impl TestConfigService {
22 pub fn set_ai_settings_and_key(&self, provider: &str, model: &str, api_key: &str) {
24 let mut cfg = self.config.lock().unwrap();
25 cfg.ai.provider = provider.to_string();
26 cfg.ai.model = model.to_string();
27 cfg.ai.api_key = if api_key.is_empty() {
28 None
29 } else {
30 Some(api_key.to_string())
31 };
32 }
33
34 pub fn set_ai_settings_with_base_url(
36 &self,
37 provider: &str,
38 model: &str,
39 api_key: &str,
40 base_url: &str,
41 ) {
42 self.set_ai_settings_and_key(provider, model, api_key);
43 let mut cfg = self.config.lock().unwrap();
44 cfg.ai.base_url = base_url.to_string();
45 }
46 pub fn new(config: Config) -> Self {
52 Self {
53 config: Mutex::new(config),
54 }
55 }
56
57 pub fn with_defaults() -> Self {
61 Self::new(Config::default())
62 }
63
64 pub fn with_ai_settings(provider: &str, model: &str) -> Self {
71 let mut config = Config::default();
72 config.ai.provider = provider.to_string();
73 config.ai.model = model.to_string();
74 Self::new(config)
75 }
76
77 pub fn with_ai_settings_and_key(provider: &str, model: &str, api_key: &str) -> Self {
85 let mut config = Config::default();
86 config.ai.provider = provider.to_string();
87 config.ai.model = model.to_string();
88 config.ai.api_key = Some(api_key.to_string());
89 Self::new(config)
90 }
91
92 pub fn with_sync_settings(correlation_threshold: f32, max_offset: f32) -> Self {
99 let mut config = Config::default();
100 config.sync.correlation_threshold = correlation_threshold;
101 config.sync.max_offset_seconds = max_offset;
102 Self::new(config)
103 }
104
105 pub fn with_parallel_settings(max_workers: usize, queue_size: usize) -> Self {
112 let mut config = Config::default();
113 config.general.max_concurrent_jobs = max_workers;
114 config.parallel.task_queue_size = queue_size;
115 Self::new(config)
116 }
117
118 pub fn config(&self) -> std::sync::MutexGuard<'_, Config> {
122 self.config.lock().unwrap()
123 }
124
125 pub fn config_mut(&self) -> std::sync::MutexGuard<'_, Config> {
129 self.config.lock().unwrap()
130 }
131}
132
133impl ConfigService for TestConfigService {
134 fn get_config(&self) -> Result<Config> {
135 Ok(self.config.lock().unwrap().clone())
136 }
137
138 fn reload(&self) -> Result<()> {
139 Ok(())
141 }
142
143 fn save_config(&self) -> Result<()> {
144 Ok(())
146 }
147
148 fn save_config_to_file(&self, _path: &Path) -> Result<()> {
149 Ok(())
151 }
152
153 fn get_config_file_path(&self) -> Result<PathBuf> {
154 Ok(PathBuf::from("/tmp/subx_test_config.toml"))
156 }
157
158 fn get_config_value(&self, key: &str) -> Result<String> {
159 let config = self.config.lock().unwrap();
160 crate::config::service::read_config_value_from(&config, key)
161 }
162
163 fn reset_to_defaults(&self) -> Result<()> {
164 *self.config.lock().unwrap() = Config::default();
166 Ok(())
167 }
168
169 fn set_config_value(&self, key: &str, value: &str) -> Result<()> {
170 let mut cfg = self.load_for_repair()?;
174 self.validate_and_set_value(&mut cfg, key, value)?;
179 *self.config.lock().unwrap() = cfg;
181 Ok(())
182 }
183
184 fn load_for_repair(&self) -> Result<Config> {
185 Ok(self.config.lock().unwrap().clone())
188 }
189}
190
191impl TestConfigService {
192 fn validate_and_set_value(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
194 use crate::config::OverflowStrategy;
195 use crate::config::validation::*;
196 use crate::error::SubXError;
197
198 let parts: Vec<&str> = key.split('.').collect();
199 match parts.as_slice() {
200 ["ai", "provider"] => {
201 validate_enum(
202 value,
203 &[
204 "openai",
205 "anthropic",
206 "local",
207 "ollama",
208 "openrouter",
209 "azure-openai",
210 ],
211 )?;
212 config.ai.provider = crate::config::field_validator::normalize_ai_provider(value);
213 }
214 ["ai", "api_key"] => {
215 if !value.is_empty() {
216 validate_api_key(value)?;
217 config.ai.api_key = Some(value.to_string());
218 } else {
219 config.ai.api_key = None;
220 }
221 }
222 ["ai", "model"] => {
223 config.ai.model = value.to_string();
224 }
225 ["ai", "base_url"] => {
226 validate_url(value)?;
227 config.ai.base_url = value.to_string();
228 }
229 ["ai", "max_sample_length"] => {
230 let v = validate_usize_range(value, 100, 10000)?;
231 config.ai.max_sample_length = v;
232 }
233 ["ai", "temperature"] => {
234 let v = validate_float_range(value, 0.0, 1.0)?;
235 config.ai.temperature = v;
236 }
237 ["ai", "max_tokens"] => {
238 let v = validate_uint_range(value, 1, 100_000)?;
239 config.ai.max_tokens = v;
240 }
241 ["ai", "retry_attempts"] => {
242 let v = validate_uint_range(value, 1, 10)?;
243 config.ai.retry_attempts = v;
244 }
245 ["ai", "retry_delay_ms"] => {
246 let v = validate_u64_range(value, 100, 30000)?;
247 config.ai.retry_delay_ms = v;
248 }
249 ["ai", "request_timeout_seconds"] => {
250 let v = validate_u64_range(value, 10, 600)?;
251 config.ai.request_timeout_seconds = v;
252 }
253 ["formats", "default_output"] => {
254 validate_enum(value, &["srt", "ass", "vtt", "webvtt"])?;
255 config.formats.default_output = value.to_string();
256 }
257 ["formats", "preserve_styling"] => {
258 let v = parse_bool(value)?;
259 config.formats.preserve_styling = v;
260 }
261 ["formats", "default_encoding"] => {
262 validate_enum(value, &["utf-8", "gbk", "big5", "shift_jis"])?;
263 config.formats.default_encoding = value.to_string();
264 }
265 ["formats", "encoding_detection_confidence"] => {
266 let v = validate_float_range(value, 0.0, 1.0)?;
267 config.formats.encoding_detection_confidence = v;
268 }
269 ["sync", "max_offset_seconds"] => {
270 let v = validate_float_range(value, 0.0, 300.0)?;
271 config.sync.max_offset_seconds = v;
272 }
273 ["sync", "default_method"] => {
274 validate_enum(value, &["auto", "vad"])?;
275 config.sync.default_method = value.to_string();
276 }
277 ["sync", "vad", "enabled"] => {
278 let v = parse_bool(value)?;
279 config.sync.vad.enabled = v;
280 }
281 ["sync", "vad", "sensitivity"] => {
282 let v = validate_float_range(value, 0.0, 1.0)?;
283 config.sync.vad.sensitivity = v;
284 }
285 ["sync", "vad", "padding_chunks"] => {
286 let v = validate_uint_range(value, 0, u32::MAX)?;
287 config.sync.vad.padding_chunks = v;
288 }
289 ["sync", "vad", "min_speech_duration_ms"] => {
290 let v = validate_uint_range(value, 0, u32::MAX)?;
291 config.sync.vad.min_speech_duration_ms = v;
292 }
293 ["sync", "correlation_threshold"] => {
294 let v = validate_float_range(value, 0.0, 1.0)?;
295 config.sync.correlation_threshold = v;
296 }
297 ["sync", "dialogue_detection_threshold"] => {
298 let v = validate_float_range(value, 0.0, 1.0)?;
299 config.sync.dialogue_detection_threshold = v;
300 }
301 ["sync", "min_dialogue_duration_ms"] => {
302 let v = validate_uint_range(value, 100, 5000)?;
303 config.sync.min_dialogue_duration_ms = v;
304 }
305 ["sync", "dialogue_merge_gap_ms"] => {
306 let v = validate_uint_range(value, 50, 2000)?;
307 config.sync.dialogue_merge_gap_ms = v;
308 }
309 ["sync", "enable_dialogue_detection"] => {
310 let v = parse_bool(value)?;
311 config.sync.enable_dialogue_detection = v;
312 }
313 ["sync", "audio_sample_rate"] => {
314 let v = validate_uint_range(value, 8000, 192000)?;
315 config.sync.audio_sample_rate = v;
316 }
317 ["sync", "auto_detect_sample_rate"] => {
318 let v = parse_bool(value)?;
319 config.sync.auto_detect_sample_rate = v;
320 }
321 ["general", "backup_enabled"] => {
322 let v = parse_bool(value)?;
323 config.general.backup_enabled = v;
324 }
325 ["general", "max_concurrent_jobs"] => {
326 let v = validate_usize_range(value, 1, 64)?;
327 config.general.max_concurrent_jobs = v;
328 }
329 ["general", "task_timeout_seconds"] => {
330 let v = validate_u64_range(value, 30, 3600)?;
331 config.general.task_timeout_seconds = v;
332 }
333 ["general", "enable_progress_bar"] => {
334 let v = parse_bool(value)?;
335 config.general.enable_progress_bar = v;
336 }
337 ["general", "worker_idle_timeout_seconds"] => {
338 let v = validate_u64_range(value, 10, 3600)?;
339 config.general.worker_idle_timeout_seconds = v;
340 }
341 ["general", "max_subtitle_bytes"] => {
342 let v = validate_u64_range(value, 1024, 1_073_741_824)?;
343 config.general.max_subtitle_bytes = v;
344 }
345 ["general", "max_audio_bytes"] => {
346 let v = validate_u64_range(value, 1024, 10_737_418_240)?;
347 config.general.max_audio_bytes = v;
348 }
349 ["parallel", "max_workers"] => {
350 let v = validate_usize_range(value, 1, 64)?;
351 config.parallel.max_workers = v;
352 }
353 ["parallel", "task_queue_size"] => {
354 let v = validate_usize_range(value, 100, 10000)?;
355 config.parallel.task_queue_size = v;
356 }
357 ["parallel", "enable_task_priorities"] => {
358 let v = parse_bool(value)?;
359 config.parallel.enable_task_priorities = v;
360 }
361 ["parallel", "auto_balance_workers"] => {
362 let v = parse_bool(value)?;
363 config.parallel.auto_balance_workers = v;
364 }
365 ["parallel", "overflow_strategy"] => {
366 validate_enum(value, &["Block", "Drop", "Expand"])?;
367 config.parallel.overflow_strategy = match value {
368 "Block" => OverflowStrategy::Block,
369 "Drop" => OverflowStrategy::Drop,
370 "Expand" => OverflowStrategy::Expand,
371 _ => unreachable!(),
372 };
373 }
374 _ => {
375 return Err(SubXError::config(format!(
376 "Unknown configuration key: {key}"
377 )));
378 }
379 }
380 Ok(())
381 }
382}
383
384impl Default for TestConfigService {
385 fn default() -> Self {
386 Self::with_defaults()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_config_service_with_defaults() {
396 let service = TestConfigService::with_defaults();
397 let config = service.get_config().unwrap();
398
399 assert_eq!(config.ai.provider, "openai");
400 assert_eq!(config.ai.model, "gpt-4.1-mini");
401 }
402
403 #[test]
404 fn test_config_service_with_ai_settings() {
405 let service = TestConfigService::with_ai_settings("anthropic", "claude-3");
406 let config = service.get_config().unwrap();
407
408 assert_eq!(config.ai.provider, "anthropic");
409 assert_eq!(config.ai.model, "claude-3");
410 }
411
412 #[test]
413 fn test_config_service_with_ai_settings_and_key() {
414 let service =
415 TestConfigService::with_ai_settings_and_key("openai", "gpt-4.1", "test-api-key");
416 let config = service.get_config().unwrap();
417
418 assert_eq!(config.ai.provider, "openai");
419 assert_eq!(config.ai.model, "gpt-4.1");
420 assert_eq!(config.ai.api_key, Some("test-api-key".to_string()));
421 }
422
423 #[test]
424 fn test_config_service_with_ai_settings_and_key_openrouter() {
425 let service = TestConfigService::with_ai_settings_and_key(
426 "openrouter",
427 "deepseek/deepseek-r1-0528:free",
428 "test-openrouter-key",
429 );
430 let config = service.get_config().unwrap();
431 assert_eq!(config.ai.provider, "openrouter");
432 assert_eq!(config.ai.model, "deepseek/deepseek-r1-0528:free");
433 assert_eq!(config.ai.api_key, Some("test-openrouter-key".to_string()));
434 }
435
436 #[test]
437 fn test_config_service_with_sync_settings() {
438 let service = TestConfigService::with_sync_settings(0.8, 45.0);
439 let config = service.get_config().unwrap();
440
441 assert_eq!(config.sync.correlation_threshold, 0.8);
442 assert_eq!(config.sync.max_offset_seconds, 45.0);
443 }
444
445 #[test]
446 fn test_config_service_with_parallel_settings() {
447 let service = TestConfigService::with_parallel_settings(8, 200);
448 let config = service.get_config().unwrap();
449
450 assert_eq!(config.general.max_concurrent_jobs, 8);
451 assert_eq!(config.parallel.task_queue_size, 200);
452 }
453
454 #[test]
455 fn test_config_service_reload() {
456 let service = TestConfigService::with_defaults();
457
458 assert!(service.reload().is_ok());
460 }
461
462 #[test]
463 fn test_config_service_direct_access() {
464 let service = TestConfigService::with_defaults();
465
466 assert_eq!(service.config().ai.provider, "openai");
468
469 service.config_mut().ai.provider = "modified".to_string();
471 assert_eq!(service.config().ai.provider, "modified");
472
473 let config = service.get_config().unwrap();
474 assert_eq!(config.ai.provider, "modified");
475 }
476}