subx_cli/core/parallel/
task.rs

1//! Task definition and utilities for parallel processing
2use crate::core::fs_util::copy_file_cifs_safe;
3use async_trait::async_trait;
4use std::fmt;
5use std::path::Path;
6
7/// Trait defining a unit of work that can be executed asynchronously.
8///
9/// All tasks in the parallel processing system must implement this trait
10/// to provide execution logic and metadata.
11#[async_trait]
12pub trait Task: Send + Sync {
13    /// Executes the task and returns the result.
14    async fn execute(&self) -> TaskResult;
15    /// Returns the type identifier for this task.
16    fn task_type(&self) -> &'static str;
17    /// Returns a unique identifier for this specific task instance.
18    fn task_id(&self) -> String;
19    /// Returns an estimated duration for the task execution.
20    fn estimated_duration(&self) -> Option<std::time::Duration> {
21        None
22    }
23    /// Returns a human-readable description of the task.
24    fn description(&self) -> String {
25        format!("{} task", self.task_type())
26    }
27}
28
29/// Result of task execution indicating success, failure, or partial completion.
30///
31/// Provides detailed information about the outcome of a task execution,
32/// including success/failure status and descriptive messages.
33#[derive(Debug, Clone)]
34pub enum TaskResult {
35    /// Task completed successfully with a result message
36    Success(String),
37    /// Task failed with an error message
38    Failed(String),
39    /// Task was cancelled before completion
40    Cancelled,
41    /// Task partially completed with success and failure messages
42    PartialSuccess(String, String),
43}
44
45/// Current execution status of a task in the system.
46///
47/// Tracks the lifecycle of a task from initial queuing through completion
48/// or failure, providing detailed status information.
49#[derive(Debug, Clone)]
50pub enum TaskStatus {
51    /// Task is queued and waiting for execution
52    Pending,
53    /// Task is currently being executed
54    Running,
55    /// Task completed successfully or with partial success
56    Completed(TaskResult),
57    /// Task failed during execution
58    Failed(String),
59    /// Task was cancelled before or during execution
60    Cancelled,
61}
62
63impl fmt::Display for TaskResult {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            TaskResult::Success(msg) => write!(f, "✓ {}", msg),
67            TaskResult::Failed(msg) => write!(f, "✗ {}", msg),
68            TaskResult::Cancelled => write!(f, "⚠ Task cancelled"),
69            TaskResult::PartialSuccess(success, warn) => {
70                write!(f, "⚠ {} (warning: {})", success, warn)
71            }
72        }
73    }
74}
75
76impl fmt::Display for TaskStatus {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            TaskStatus::Pending => write!(f, "Pending"),
80            TaskStatus::Running => write!(f, "Running"),
81            TaskStatus::Completed(result) => write!(f, "Completed: {}", result),
82            TaskStatus::Failed(msg) => write!(f, "Failed: {}", msg),
83            TaskStatus::Cancelled => write!(f, "Cancelled"),
84        }
85    }
86}
87
88/// Task for processing files (convert, sync, match, validate).
89///
90/// Represents a file processing operation that can be executed
91/// asynchronously in the parallel processing system.
92pub struct FileProcessingTask {
93    /// Path to the input file to be processed
94    pub input_path: std::path::PathBuf,
95    /// Optional output path for the processed file
96    pub output_path: Option<std::path::PathBuf>,
97    /// The specific operation to perform on the file
98    pub operation: ProcessingOperation,
99}
100
101/// Supported operations for file processing tasks.
102///
103/// Defines the different types of operations that can be performed
104/// on subtitle and video files in the processing system.
105#[derive(Debug, Clone)]
106pub enum ProcessingOperation {
107    /// Convert subtitle format from one type to another
108    ConvertFormat {
109        /// Source format (e.g., "srt", "ass")
110        from: String,
111        /// Target format (e.g., "srt", "ass")
112        to: String,
113    },
114    /// Synchronize subtitle timing with audio
115    SyncSubtitle {
116        /// Path to the audio file for synchronization
117        audio_path: std::path::PathBuf,
118    },
119    /// Match subtitle files with video files
120    MatchFiles {
121        /// Whether to search recursively in subdirectories
122        recursive: bool,
123    },
124    /// Validate subtitle file format and structure
125    ValidateFormat,
126    /// Copy subtitle file to video folder
127    CopyToVideoFolder {
128        /// Path to the source subtitle file to be copied
129        source: std::path::PathBuf,
130        /// Path to the target video folder where the subtitle will be copied
131        target: std::path::PathBuf,
132    },
133    /// Move subtitle file to video folder
134    MoveToVideoFolder {
135        /// Path to the source subtitle file to be moved
136        source: std::path::PathBuf,
137        /// Path to the target video folder where the subtitle will be moved
138        target: std::path::PathBuf,
139    },
140    /// Copy a file with a new name (local copy)
141    CopyWithRename {
142        /// Source file path
143        source: std::path::PathBuf,
144        /// Target file path
145        target: std::path::PathBuf,
146    },
147    /// Create a backup of a file
148    CreateBackup {
149        /// Original file path
150        source: std::path::PathBuf,
151        /// Backup file path
152        backup: std::path::PathBuf,
153    },
154    /// Rename (move) a file
155    RenameFile {
156        /// Original file path
157        source: std::path::PathBuf,
158        /// New file path after rename
159        target: std::path::PathBuf,
160    },
161}
162
163#[async_trait]
164impl Task for FileProcessingTask {
165    async fn execute(&self) -> TaskResult {
166        match &self.operation {
167            ProcessingOperation::ConvertFormat { from, to } => {
168                match self.convert_format(from, to).await {
169                    Ok(path) => TaskResult::Success(format!(
170                        "Successfully converted {} -> {}: {}",
171                        from,
172                        to,
173                        path.display()
174                    )),
175                    Err(e) => TaskResult::Failed(format!(
176                        "Conversion failed {}: {}",
177                        self.input_path.display(),
178                        e
179                    )),
180                }
181            }
182            ProcessingOperation::SyncSubtitle { .. } => {
183                // Sync not supported in parallel tasks
184                TaskResult::Failed("Sync functionality not implemented".to_string())
185            }
186            ProcessingOperation::MatchFiles { recursive } => {
187                match self.match_files(*recursive).await {
188                    Ok(m) => TaskResult::Success(format!(
189                        "File matching completed: found {} matches",
190                        m.len()
191                    )),
192                    Err(e) => TaskResult::Failed(format!("Matching failed: {}", e)),
193                }
194            }
195            ProcessingOperation::ValidateFormat => match self.validate_format().await {
196                Ok(true) => TaskResult::Success(format!(
197                    "Format validation passed: {}",
198                    self.input_path.display()
199                )),
200                Ok(false) => TaskResult::Failed(format!(
201                    "Format validation failed: {}",
202                    self.input_path.display()
203                )),
204                Err(e) => TaskResult::Failed(format!("Validation error: {}", e)),
205            },
206            ProcessingOperation::CopyToVideoFolder { source, target } => {
207                match self.execute_copy_operation(source, target).await {
208                    Ok(_) => TaskResult::Success(format!(
209                        "Copied: {} -> {}",
210                        source.display(),
211                        target.display()
212                    )),
213                    Err(e) => TaskResult::Failed(format!("Copy failed: {}", e)),
214                }
215            }
216            ProcessingOperation::MoveToVideoFolder { source, target } => {
217                match self.execute_move_operation(source, target).await {
218                    Ok(_) => TaskResult::Success(format!(
219                        "Moved: {} -> {}",
220                        source.display(),
221                        target.display()
222                    )),
223                    Err(e) => TaskResult::Failed(format!("Move failed: {}", e)),
224                }
225            }
226            ProcessingOperation::CopyWithRename { source, target } => {
227                match self
228                    .execute_copy_with_rename_operation(source, target)
229                    .await
230                {
231                    Ok(_) => TaskResult::Success(format!(
232                        "Copied: {} -> {}",
233                        source.display(),
234                        target.display()
235                    )),
236                    Err(e) => TaskResult::Failed(format!("Copy failed: {}", e)),
237                }
238            }
239            ProcessingOperation::CreateBackup { source, backup } => {
240                match self.execute_create_backup_operation(source, backup).await {
241                    Ok(_) => TaskResult::Success(format!(
242                        "Backup created: {} -> {}",
243                        source.display(),
244                        backup.display()
245                    )),
246                    Err(e) => TaskResult::Failed(format!("Backup failed: {}", e)),
247                }
248            }
249            ProcessingOperation::RenameFile { source, target } => {
250                match self.execute_rename_file_operation(source, target).await {
251                    Ok(_) => TaskResult::Success(format!(
252                        "Renamed: {} -> {}",
253                        source.display(),
254                        target.display()
255                    )),
256                    Err(e) => TaskResult::Failed(format!("Rename failed: {}", e)),
257                }
258            }
259        }
260    }
261
262    fn task_type(&self) -> &'static str {
263        match &self.operation {
264            ProcessingOperation::ConvertFormat { .. } => "convert",
265            ProcessingOperation::SyncSubtitle { .. } => "sync",
266            ProcessingOperation::MatchFiles { .. } => "match",
267            ProcessingOperation::ValidateFormat => "validate",
268            ProcessingOperation::CopyToVideoFolder { .. } => "copy_to_video_folder",
269            ProcessingOperation::MoveToVideoFolder { .. } => "move_to_video_folder",
270            ProcessingOperation::CopyWithRename { .. } => "copy_with_rename",
271            ProcessingOperation::CreateBackup { .. } => "create_backup",
272            ProcessingOperation::RenameFile { .. } => "rename_file",
273        }
274    }
275
276    fn task_id(&self) -> String {
277        use std::collections::hash_map::DefaultHasher;
278        use std::hash::{Hash, Hasher};
279        let mut hasher = DefaultHasher::new();
280        self.input_path.hash(&mut hasher);
281        self.operation.hash(&mut hasher);
282        format!("{}_{:x}", self.task_type(), hasher.finish())
283    }
284
285    fn estimated_duration(&self) -> Option<std::time::Duration> {
286        if let Ok(meta) = std::fs::metadata(&self.input_path) {
287            let size_mb = meta.len() as f64 / 1_048_576.0;
288            let secs = match &self.operation {
289                ProcessingOperation::ConvertFormat { .. } => size_mb * 0.1,
290                ProcessingOperation::SyncSubtitle { .. } => size_mb * 0.5,
291                ProcessingOperation::MatchFiles { .. } => 2.0,
292                ProcessingOperation::ValidateFormat => size_mb * 0.05,
293                ProcessingOperation::CopyToVideoFolder { .. } => size_mb * 0.01, // Fast copy
294                ProcessingOperation::MoveToVideoFolder { .. } => size_mb * 0.005, // Even faster move
295                ProcessingOperation::CopyWithRename { .. } => size_mb * 0.01,
296                ProcessingOperation::CreateBackup { .. } => size_mb * 0.01,
297                ProcessingOperation::RenameFile { .. } => size_mb * 0.005,
298            };
299            Some(std::time::Duration::from_secs_f64(secs))
300        } else {
301            None
302        }
303    }
304
305    fn description(&self) -> String {
306        match &self.operation {
307            ProcessingOperation::ConvertFormat { from, to } => {
308                format!(
309                    "Convert {} from {} to {}",
310                    self.input_path.display(),
311                    from,
312                    to
313                )
314            }
315            ProcessingOperation::SyncSubtitle { audio_path } => format!(
316                "Sync subtitle {} with audio {}",
317                self.input_path.display(),
318                audio_path.display()
319            ),
320            ProcessingOperation::MatchFiles { recursive } => format!(
321                "Match files in {}{}",
322                self.input_path.display(),
323                if *recursive { " (recursive)" } else { "" }
324            ),
325            ProcessingOperation::ValidateFormat => {
326                format!("Validate format of {}", self.input_path.display())
327            }
328            ProcessingOperation::CopyToVideoFolder { source, target } => {
329                format!("Copy {} to {}", source.display(), target.display())
330            }
331            ProcessingOperation::MoveToVideoFolder { source, target } => {
332                format!("Move {} to {}", source.display(), target.display())
333            }
334            ProcessingOperation::CopyWithRename { source, target } => {
335                format!(
336                    "CopyWithRename {} to {}",
337                    source.display(),
338                    target.display()
339                )
340            }
341            ProcessingOperation::CreateBackup { source, backup } => {
342                format!("CreateBackup {} to {}", source.display(), backup.display())
343            }
344            ProcessingOperation::RenameFile { source, target } => {
345                format!("Rename {} to {}", source.display(), target.display())
346            }
347        }
348    }
349}
350
351impl FileProcessingTask {
352    /// Create a new file processing task with operation
353    pub fn new(
354        input_path: std::path::PathBuf,
355        output_path: Option<std::path::PathBuf>,
356        operation: ProcessingOperation,
357    ) -> Self {
358        FileProcessingTask {
359            input_path,
360            output_path,
361            operation,
362        }
363    }
364
365    /// Execute copy operation for file relocation
366    async fn execute_copy_operation(
367        &self,
368        source: &Path,
369        target: &Path,
370    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
371        // Create target directory if it doesn't exist
372        if let Some(parent) = target.parent() {
373            std::fs::create_dir_all(parent)?;
374        }
375
376        // Handle filename conflicts
377        let final_target = self.resolve_filename_conflict(target.to_path_buf()).await?;
378
379        // Execute copy operation
380        copy_file_cifs_safe(source, &final_target)?;
381        Ok(())
382    }
383
384    /// Execute move operation for file relocation
385    async fn execute_move_operation(
386        &self,
387        source: &Path,
388        target: &Path,
389    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
390        // Create target directory if it doesn't exist
391        if let Some(parent) = target.parent() {
392            std::fs::create_dir_all(parent)?;
393        }
394
395        // Handle filename conflicts
396        let final_target = self.resolve_filename_conflict(target.to_path_buf()).await?;
397
398        // Execute move operation
399        std::fs::rename(source, &final_target)?;
400        Ok(())
401    }
402
403    /// Resolve filename conflicts by adding numeric suffix
404    async fn resolve_filename_conflict(
405        &self,
406        target: std::path::PathBuf,
407    ) -> Result<std::path::PathBuf, Box<dyn std::error::Error + Send + Sync>> {
408        if !target.exists() {
409            return Ok(target);
410        }
411
412        // Extract filename components
413        let file_stem = target
414            .file_stem()
415            .and_then(|s| s.to_str())
416            .unwrap_or("file");
417        let extension = target.extension().and_then(|s| s.to_str()).unwrap_or("");
418
419        let parent = target.parent().unwrap_or_else(|| std::path::Path::new("."));
420
421        // Try adding numeric suffixes
422        for i in 1..1000 {
423            let new_name = if extension.is_empty() {
424                format!("{}.{}", file_stem, i)
425            } else {
426                format!("{}.{}.{}", file_stem, i, extension)
427            };
428            let new_path = parent.join(new_name);
429            if !new_path.exists() {
430                return Ok(new_path);
431            }
432        }
433
434        Err("Could not resolve filename conflict".into())
435    }
436
437    /// Execute a copy with rename operation (local copy) using CIFS-safe copy
438    async fn execute_copy_with_rename_operation(
439        &self,
440        source: &Path,
441        target: &Path,
442    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
443        if let Some(parent) = target.parent() {
444            std::fs::create_dir_all(parent)?;
445        }
446        copy_file_cifs_safe(source, target)?;
447        Ok(())
448    }
449
450    /// Execute a create backup operation using CIFS-safe copy
451    async fn execute_create_backup_operation(
452        &self,
453        source: &Path,
454        backup: &Path,
455    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
456        if let Some(parent) = backup.parent() {
457            std::fs::create_dir_all(parent)?;
458        }
459        copy_file_cifs_safe(source, backup)?;
460        Ok(())
461    }
462
463    /// Execute a file rename operation
464    async fn execute_rename_file_operation(
465        &self,
466        source: &Path,
467        target: &Path,
468    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
469        if let Some(parent) = target.parent() {
470            std::fs::create_dir_all(parent)?;
471        }
472        std::fs::rename(source, target)?;
473        Ok(())
474    }
475
476    async fn convert_format(&self, _from: &str, _to: &str) -> crate::Result<std::path::PathBuf> {
477        // Stub convert: simply return input path
478        Ok(self.input_path.clone())
479    }
480
481    async fn sync_subtitle(
482        &self,
483        _audio_path: &std::path::Path,
484    ) -> crate::Result<crate::core::sync::SyncResult> {
485        // Stub implementation: sync not available
486        Err(crate::error::SubXError::parallel_processing(
487            "sync_subtitle not implemented".to_string(),
488        ))
489    }
490
491    async fn match_files(&self, _recursive: bool) -> crate::Result<Vec<()>> {
492        // Stub implementation: no actual matching
493        Ok(Vec::new())
494    }
495
496    async fn validate_format(&self) -> crate::Result<bool> {
497        // Stub validate: always succeed
498        Ok(true)
499    }
500}
501
502// impl Hash for ProcessingOperation to support task_id generation
503impl std::hash::Hash for ProcessingOperation {
504    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
505        match self {
506            ProcessingOperation::ConvertFormat { from, to } => {
507                "convert".hash(state);
508                from.hash(state);
509                to.hash(state);
510            }
511            ProcessingOperation::SyncSubtitle { audio_path } => {
512                "sync".hash(state);
513                audio_path.hash(state);
514            }
515            ProcessingOperation::MatchFiles { recursive } => {
516                "match".hash(state);
517                recursive.hash(state);
518            }
519            ProcessingOperation::ValidateFormat => {
520                "validate".hash(state);
521            }
522            ProcessingOperation::CopyToVideoFolder { source, target } => {
523                "copy_to_video_folder".hash(state);
524                source.hash(state);
525                target.hash(state);
526            }
527            ProcessingOperation::MoveToVideoFolder { source, target } => {
528                "move_to_video_folder".hash(state);
529                source.hash(state);
530                target.hash(state);
531            }
532            ProcessingOperation::CopyWithRename { source, target } => {
533                "copy_with_rename".hash(state);
534                source.hash(state);
535                target.hash(state);
536            }
537            ProcessingOperation::CreateBackup { source, backup } => {
538                "create_backup".hash(state);
539                source.hash(state);
540                backup.hash(state);
541            }
542            ProcessingOperation::RenameFile { source, target } => {
543                "rename_file".hash(state);
544                source.hash(state);
545                target.hash(state);
546            }
547        }
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use std::time::Duration;
555    use tempfile::TempDir;
556
557    #[tokio::test]
558    async fn test_file_processing_task_validate_format() {
559        let tmp = TempDir::new().unwrap();
560        let test_file = tmp.path().join("test.srt");
561        tokio::fs::write(&test_file, "1\n00:00:01,000 --> 00:00:02,000\nTest\n")
562            .await
563            .unwrap();
564        let task = FileProcessingTask {
565            input_path: test_file.clone(),
566            output_path: None,
567            operation: ProcessingOperation::ValidateFormat,
568        };
569        let result = task.execute().await;
570        assert!(matches!(result, TaskResult::Success(_)));
571    }
572
573    #[tokio::test]
574    async fn test_file_processing_task_copy_with_rename() {
575        let tmp = TempDir::new().unwrap();
576        let src = tmp.path().join("orig.txt");
577        let dst = tmp.path().join("copy.txt");
578        tokio::fs::write(&src, b"hello").await.unwrap();
579        let task = FileProcessingTask::new(
580            src.clone(),
581            Some(dst.clone()),
582            ProcessingOperation::CopyWithRename {
583                source: src.clone(),
584                target: dst.clone(),
585            },
586        );
587        let result = task.execute().await;
588        assert!(matches!(result, TaskResult::Success(_)));
589        let data = tokio::fs::read(&dst).await.unwrap();
590        assert_eq!(data, b"hello");
591    }
592
593    #[tokio::test]
594    async fn test_file_processing_task_create_backup() {
595        let tmp = TempDir::new().unwrap();
596        let src = tmp.path().join("orig.txt");
597        let backup = tmp.path().join("orig.txt.bak");
598        tokio::fs::write(&src, b"backup").await.unwrap();
599        let task = FileProcessingTask::new(
600            src.clone(),
601            Some(backup.clone()),
602            ProcessingOperation::CreateBackup {
603                source: src.clone(),
604                backup: backup.clone(),
605            },
606        );
607        let result = task.execute().await;
608        assert!(matches!(result, TaskResult::Success(_)));
609        let data = tokio::fs::read(&backup).await.unwrap();
610        assert_eq!(data, b"backup");
611    }
612
613    #[tokio::test]
614    async fn test_file_processing_task_rename_file() {
615        let tmp = TempDir::new().unwrap();
616        let src = tmp.path().join("a.txt");
617        let dst = tmp.path().join("b.txt");
618        tokio::fs::write(&src, b"rename").await.unwrap();
619        let task = FileProcessingTask::new(
620            src.clone(),
621            Some(dst.clone()),
622            ProcessingOperation::RenameFile {
623                source: src.clone(),
624                target: dst.clone(),
625            },
626        );
627        let result = task.execute().await;
628        assert!(matches!(result, TaskResult::Success(_)));
629        assert!(!tokio::fs::metadata(&src).await.is_ok());
630        let data = tokio::fs::read(&dst).await.unwrap();
631        assert_eq!(data, b"rename");
632    }
633
634    /// Test task lifecycle and status transitions
635    #[tokio::test]
636    async fn test_task_lifecycle() {
637        let tmp = TempDir::new().unwrap();
638        let test_file = tmp.path().join("lifecycle.srt");
639        tokio::fs::write(
640            &test_file,
641            "1\n00:00:01,000 --> 00:00:02,000\nLifecycle test\n",
642        )
643        .await
644        .unwrap();
645
646        let task = FileProcessingTask {
647            input_path: test_file.clone(),
648            output_path: None,
649            operation: ProcessingOperation::ValidateFormat,
650        };
651
652        // Test initial task properties
653        assert_eq!(task.task_type(), "validate");
654        assert!(!task.task_id().is_empty());
655        assert!(task.description().contains("Validate format"));
656        assert!(task.description().contains("lifecycle.srt"));
657        assert!(
658            task.estimated_duration().is_some(),
659            "Should estimate duration for existing file"
660        );
661
662        // Test execution
663        let result = task.execute().await;
664        assert!(matches!(result, TaskResult::Success(_)));
665    }
666
667    /// Test task result serialization and display
668    #[test]
669    fn test_task_result_display() {
670        let success = TaskResult::Success("Operation completed".to_string());
671        let failed = TaskResult::Failed("Operation failed".to_string());
672        let cancelled = TaskResult::Cancelled;
673        let partial =
674            TaskResult::PartialSuccess("Mostly worked".to_string(), "Minor issue".to_string());
675
676        assert_eq!(format!("{}", success), "✓ Operation completed");
677        assert_eq!(format!("{}", failed), "✗ Operation failed");
678        assert_eq!(format!("{}", cancelled), "⚠ Task cancelled");
679        assert_eq!(
680            format!("{}", partial),
681            "⚠ Mostly worked (warning: Minor issue)"
682        );
683    }
684
685    /// Test task status display
686    #[test]
687    fn test_task_status_display() {
688        let pending = TaskStatus::Pending;
689        let running = TaskStatus::Running;
690        let completed = TaskStatus::Completed(TaskResult::Success("Done".to_string()));
691        let failed = TaskStatus::Failed("Error occurred".to_string());
692        let cancelled = TaskStatus::Cancelled;
693
694        assert_eq!(format!("{}", pending), "Pending");
695        assert_eq!(format!("{}", running), "Running");
696        assert_eq!(format!("{}", completed), "Completed: ✓ Done");
697        assert_eq!(format!("{}", failed), "Failed: Error occurred");
698        assert_eq!(format!("{}", cancelled), "Cancelled");
699    }
700
701    /// Test format conversion task
702    #[tokio::test]
703    async fn test_format_conversion_task() {
704        let tmp = TempDir::new().unwrap();
705        let input_file = tmp.path().join("input.srt");
706        let output_file = tmp.path().join("output.ass");
707
708        // Create valid SRT content
709        let srt_content = r#"1
71000:00:01,000 --> 00:00:03,000
711First subtitle
712
7132
71400:00:04,000 --> 00:00:06,000
715Second subtitle
716"#;
717
718        tokio::fs::write(&input_file, srt_content).await.unwrap();
719
720        let task = FileProcessingTask {
721            input_path: input_file.clone(),
722            output_path: Some(output_file.clone()),
723            operation: ProcessingOperation::ConvertFormat {
724                from: "srt".to_string(),
725                to: "ass".to_string(),
726            },
727        };
728
729        let result = task.execute().await;
730        assert!(matches!(result, TaskResult::Success(_)));
731
732        // Note: The convert_format method is a stub that returns the input path
733        // In a real implementation, this would create an actual output file
734        assert!(tokio::fs::metadata(&input_file).await.is_ok());
735    }
736
737    /// Test file matching task
738    #[tokio::test]
739    async fn test_file_matching_task() {
740        let tmp = TempDir::new().unwrap();
741        let video_file = tmp.path().join("movie.mkv");
742        let subtitle_file = tmp.path().join("movie.srt");
743
744        // Create test files
745        tokio::fs::write(&video_file, b"fake video content")
746            .await
747            .unwrap();
748        tokio::fs::write(&subtitle_file, "1\n00:00:01,000 --> 00:00:02,000\nTest\n")
749            .await
750            .unwrap();
751
752        let task = FileProcessingTask {
753            input_path: tmp.path().to_path_buf(),
754            output_path: None,
755            operation: ProcessingOperation::MatchFiles { recursive: false },
756        };
757
758        let result = task.execute().await;
759        assert!(matches!(result, TaskResult::Success(_)));
760    }
761
762    /// Test sync subtitle task (expected to fail)
763    #[tokio::test]
764    async fn test_sync_subtitle_task() {
765        let tmp = TempDir::new().unwrap();
766        let audio_file = tmp.path().join("audio.wav");
767        let subtitle_file = tmp.path().join("subtitle.srt");
768
769        tokio::fs::write(&audio_file, b"fake audio content")
770            .await
771            .unwrap();
772        tokio::fs::write(&subtitle_file, "1\n00:00:01,000 --> 00:00:02,000\nTest\n")
773            .await
774            .unwrap();
775
776        let task = FileProcessingTask {
777            input_path: subtitle_file.clone(),
778            output_path: None,
779            operation: ProcessingOperation::SyncSubtitle {
780                audio_path: audio_file,
781            },
782        };
783
784        let result = task.execute().await;
785        // Sync is not implemented, so should fail
786        assert!(matches!(result, TaskResult::Failed(_)));
787    }
788
789    /// Test task error handling
790    #[tokio::test]
791    async fn test_task_error_handling() {
792        // Test with sync operation which always fails in stub implementation
793        let tmp = TempDir::new().unwrap();
794        let test_file = tmp.path().join("test.srt");
795
796        let task = FileProcessingTask {
797            input_path: test_file,
798            output_path: None,
799            operation: ProcessingOperation::SyncSubtitle {
800                audio_path: tmp.path().join("audio.wav"),
801            },
802        };
803
804        let result = task.execute().await;
805        assert!(matches!(result, TaskResult::Failed(_)));
806    }
807
808    /// Test task timeout handling
809    #[tokio::test]
810    async fn test_task_timeout() {
811        use async_trait::async_trait;
812
813        struct SlowTask {
814            duration: Duration,
815        }
816
817        #[async_trait]
818        impl Task for SlowTask {
819            async fn execute(&self) -> TaskResult {
820                tokio::time::sleep(self.duration).await;
821                TaskResult::Success("Slow task completed".to_string())
822            }
823            fn task_type(&self) -> &'static str {
824                "slow"
825            }
826            fn task_id(&self) -> String {
827                "slow_task_1".to_string()
828            }
829            fn estimated_duration(&self) -> Option<Duration> {
830                Some(self.duration)
831            }
832        }
833
834        let slow_task = SlowTask {
835            duration: Duration::from_millis(100),
836        };
837
838        // Test estimated duration
839        assert_eq!(
840            slow_task.estimated_duration(),
841            Some(Duration::from_millis(100))
842        );
843
844        // Test execution
845        let start = std::time::Instant::now();
846        let result = slow_task.execute().await;
847        let elapsed = start.elapsed();
848
849        assert!(matches!(result, TaskResult::Success(_)));
850        assert!(elapsed >= Duration::from_millis(90)); // Allow some variance
851    }
852
853    /// Test processing operation variants
854    #[test]
855    fn test_processing_operation_variants() {
856        let convert_op = ProcessingOperation::ConvertFormat {
857            from: "srt".to_string(),
858            to: "ass".to_string(),
859        };
860
861        let sync_op = ProcessingOperation::SyncSubtitle {
862            audio_path: std::path::PathBuf::from("audio.wav"),
863        };
864
865        let match_op = ProcessingOperation::MatchFiles { recursive: true };
866        let validate_op = ProcessingOperation::ValidateFormat;
867
868        // Test debug formatting
869        assert!(format!("{:?}", convert_op).contains("ConvertFormat"));
870        assert!(format!("{:?}", sync_op).contains("SyncSubtitle"));
871        assert!(format!("{:?}", match_op).contains("MatchFiles"));
872        assert!(format!("{:?}", validate_op).contains("ValidateFormat"));
873
874        // Test cloning
875        let convert_clone = convert_op.clone();
876        assert!(format!("{:?}", convert_clone).contains("ConvertFormat"));
877    }
878
879    /// Test custom task implementation
880    #[tokio::test]
881    async fn test_custom_task_implementation() {
882        use async_trait::async_trait;
883
884        struct CustomTask {
885            id: String,
886            should_succeed: bool,
887        }
888
889        #[async_trait]
890        impl Task for CustomTask {
891            async fn execute(&self) -> TaskResult {
892                if self.should_succeed {
893                    TaskResult::Success(format!("Custom task {} succeeded", self.id))
894                } else {
895                    TaskResult::Failed(format!("Custom task {} failed", self.id))
896                }
897            }
898
899            fn task_type(&self) -> &'static str {
900                "custom"
901            }
902
903            fn task_id(&self) -> String {
904                self.id.clone()
905            }
906
907            fn description(&self) -> String {
908                format!("Custom task with ID: {}", self.id)
909            }
910
911            fn estimated_duration(&self) -> Option<Duration> {
912                Some(Duration::from_millis(1))
913            }
914        }
915
916        // Test successful custom task
917        let success_task = CustomTask {
918            id: "success_1".to_string(),
919            should_succeed: true,
920        };
921
922        assert_eq!(success_task.task_type(), "custom");
923        assert_eq!(success_task.task_id(), "success_1");
924        assert_eq!(success_task.description(), "Custom task with ID: success_1");
925        assert_eq!(
926            success_task.estimated_duration(),
927            Some(Duration::from_millis(1))
928        );
929
930        let result = success_task.execute().await;
931        assert!(matches!(result, TaskResult::Success(_)));
932
933        // Test failing custom task
934        let fail_task = CustomTask {
935            id: "fail_1".to_string(),
936            should_succeed: false,
937        };
938
939        let result = fail_task.execute().await;
940        assert!(matches!(result, TaskResult::Failed(_)));
941    }
942}