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