use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;
use std::time::Instant;
use std::pin::Pin;
use std::boxed::Box;
use std::future::Future;
use tracing::{info, warn, error, debug};
use ffmpeg_sidecar::{
    command::FfmpegCommand,
    download::auto_download,
};

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

/// 帧提取类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrameType {
    First,
    Last,
    Custom,
    Multiple,
}

/// 时间点配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TimePoint {
    #[serde(rename = "seconds")]
    Seconds { value: f64 },
    #[serde(rename = "percentage")]
    Percentage { value: f64 },
    #[serde(rename = "frame")]
    Frame { value: u32 },
}

/// 图片格式
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageFormat {
    Jpg,
    Png,
    WebP,
    Bmp,
}

/// 图片质量设置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageQuality {
    pub format: ImageFormat,
    pub quality: u8,
    pub compression_level: Option<u8>,
}

/// 输出尺寸配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputSize {
    pub width: Option<u32>,
    pub height: Option<u32>,
    pub maintain_aspect_ratio: bool,
    pub scale_filter: Option<String>,
}

/// 帧提取配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrameExtractionConfig {
    pub frame_type: FrameType,
    pub time_points: Option<Vec<TimePoint>>,
    pub custom_time: Option<f64>,
    pub output_format: ImageQuality,
    pub output_size: Option<OutputSize>,
    pub output_directory: String,
    pub filename_pattern: String,
    pub overwrite_existing: bool,
}

/// 视频文件信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoFileInfo {
    pub path: String,
    pub filename: String,
    pub size: u64,
    pub duration: f64,
    pub width: u32,
    pub height: u32,
    pub fps: f64,
    pub format: String,
    pub is_valid: bool,
    pub error_message: Option<String>,
}

/// 单个帧提取结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrameExtractionResult {
    pub video_path: String,
    pub frame_type: FrameType,
    pub timestamp: f64,
    pub output_path: String,
    pub success: bool,
    pub error_message: Option<String>,
    pub processing_time_ms: u64,
    pub file_size: u64,
}

/// 关键帧提取服务
/// 遵循 Tauri 开发规范的基础设施层设计
pub struct FrameExtractorService;

impl FrameExtractorService {
    /// 创建隐藏控制台窗口的命令
    /// 在 Windows 上防止命令行闪现
    fn create_hidden_command(program: &str) -> Command {
        let mut cmd = Command::new(program);

        #[cfg(target_os = "windows")]
        {
            // 在 Windows 上隐藏控制台窗口
            // CREATE_NO_WINDOW = 0x08000000
            cmd.creation_flags(0x08000000);
        }

        cmd
    }

    /// 初始化 FFmpeg
    pub async fn initialize() -> Result<()> {
        info!("初始化 FrameExtractorService");
        
        // 自动下载 FFmpeg 二进制文件
        match auto_download() {
            Ok(_) => {
                info!("FFmpeg 二进制文件准备就绪");
                Ok(())
            }
            Err(e) => {
                error!("FFmpeg 初始化失败: {}", e);
                Err(anyhow!("FFmpeg 初始化失败: {}", e))
            }
        }
    }

    /// 检查 FFmpeg 是否可用
    pub fn is_available() -> bool {
        match FfmpegCommand::new().arg("-version").spawn() {
            Ok(mut child) => {
                match child.wait() {
                    Ok(status) => status.success(),
                    Err(_) => false,
                }
            }
            Err(_) => false,
        }
    }

    /// 获取视频文件信息
    pub async fn get_video_info(video_path: &str) -> Result<VideoFileInfo> {
        let path = Path::new(video_path);
        
        if !path.exists() {
            return Ok(VideoFileInfo {
                path: video_path.to_string(),
                filename: path.file_name()
                    .unwrap_or_default()
                    .to_string_lossy()
                    .to_string(),
                size: 0,
                duration: 0.0,
                width: 0,
                height: 0,
                fps: 0.0,
                format: String::new(),
                is_valid: false,
                error_message: Some("文件不存在".to_string()),
            });
        }

        let metadata = std::fs::metadata(path)?;
        let filename = path.file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .to_string();

        // 使用 ffprobe 获取视频信息
        let mut cmd = Self::create_hidden_command("ffprobe");
        cmd.arg("-v")
           .arg("quiet")
           .arg("-print_format")
           .arg("json")
           .arg("-show_format")
           .arg("-show_streams")
           .arg(video_path);

        match cmd.output() {
            Ok(output) => {
                if output.status.success() {
                    Self::parse_video_info(&output.stdout, video_path, &filename, metadata.len())
                } else {
                    Ok(VideoFileInfo {
                        path: video_path.to_string(),
                        filename,
                        size: metadata.len(),
                        duration: 0.0,
                        width: 0,
                        height: 0,
                        fps: 0.0,
                        format: String::new(),
                        is_valid: false,
                        error_message: Some("无法解析视频信息".to_string()),
                    })
                }
            }
            Err(e) => {
                error!("启动 ffprobe 失败: {}", e);
                Ok(VideoFileInfo {
                    path: video_path.to_string(),
                    filename,
                    size: metadata.len(),
                    duration: 0.0,
                    width: 0,
                    height: 0,
                    fps: 0.0,
                    format: String::new(),
                    is_valid: false,
                    error_message: Some(format!("启动 ffprobe 失败: {}", e)),
                })
            }
        }
    }

    /// 解析视频信息
    fn parse_video_info(
        json_data: &[u8],
        video_path: &str,
        filename: &str,
        file_size: u64,
    ) -> Result<VideoFileInfo> {
        let json_str = String::from_utf8_lossy(json_data);
        let json: serde_json::Value = serde_json::from_str(&json_str)?;

        let format = json["format"].as_object()
            .ok_or_else(|| anyhow!("无法解析格式信息"))?;
        
        let duration = format["duration"]
            .as_str()
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.0);

        let format_name = format["format_name"]
            .as_str()
            .unwrap_or("unknown")
            .to_string();

        // 查找视频流
        let streams = json["streams"].as_array()
            .ok_or_else(|| anyhow!("无法解析流信息"))?;

        let video_stream = streams.iter()
            .find(|stream| stream["codec_type"].as_str() == Some("video"))
            .ok_or_else(|| anyhow!("未找到视频流"))?;

        let width = video_stream["width"].as_u64().unwrap_or(0) as u32;
        let height = video_stream["height"].as_u64().unwrap_or(0) as u32;
        
        // 解析帧率
        let fps = if let Some(r_frame_rate) = video_stream["r_frame_rate"].as_str() {
            Self::parse_fraction(r_frame_rate).unwrap_or(0.0)
        } else {
            0.0
        };

        Ok(VideoFileInfo {
            path: video_path.to_string(),
            filename: filename.to_string(),
            size: file_size,
            duration,
            width,
            height,
            fps,
            format: format_name,
            is_valid: duration > 0.0 && width > 0 && height > 0,
            error_message: None,
        })
    }

    /// 解析分数格式的帧率 (如 "30/1")
    fn parse_fraction(fraction_str: &str) -> Option<f64> {
        let parts: Vec<&str> = fraction_str.split('/').collect();
        if parts.len() == 2 {
            if let (Ok(numerator), Ok(denominator)) = (
                parts[0].parse::<f64>(),
                parts[1].parse::<f64>()
            ) {
                if denominator != 0.0 {
                    return Some(numerator / denominator);
                }
            }
        }
        None
    }

    /// 扫描文件夹中的视频文件
    pub async fn scan_video_files(
        folder_path: &str,
        recursive: bool,
        supported_formats: &[String],
    ) -> Result<Vec<VideoFileInfo>> {
        let path = Path::new(folder_path);
        if !path.exists() || !path.is_dir() {
            return Err(anyhow!("指定路径不存在或不是文件夹"));
        }

        let mut video_files = Vec::new();
        Self::scan_directory_recursive(path, recursive, supported_formats, &mut video_files).await?;

        info!("扫描完成，找到 {} 个视频文件", video_files.len());
        Ok(video_files)
    }

    /// 递归扫描目录
    fn scan_directory_recursive<'a>(
        dir: &'a Path,
        recursive: bool,
        supported_formats: &'a [String],
        video_files: &'a mut Vec<VideoFileInfo>,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
        Box::pin(async move {
        let entries = std::fs::read_dir(dir)?;

        for entry in entries {
            let entry = entry?;
            let path = entry.path();

            if path.is_file() {
                if let Some(extension) = path.extension() {
                    let ext = extension.to_string_lossy().to_lowercase();
                    if supported_formats.contains(&ext) {
                        match Self::get_video_info(&path.to_string_lossy()).await {
                            Ok(info) => video_files.push(info),
                            Err(e) => {
                                warn!("无法获取视频信息 {}: {}", path.display(), e);
                            }
                        }
                    }
                }
            } else if path.is_dir() && recursive {
                Self::scan_directory_recursive(&path, recursive, supported_formats, video_files).await?;
            }
        }

        Ok(())
        })
    }

    /// 提取单个帧
    pub async fn extract_frame(
        video_path: &str,
        timestamp: f64,
        output_path: &str,
        config: &FrameExtractionConfig,
    ) -> Result<FrameExtractionResult> {
        let start_time = Instant::now();

        debug!(
            "开始提取帧: video={}, timestamp={}, output={}",
            video_path, timestamp, output_path
        );

        // 确保输出目录存在
        if let Some(parent) = Path::new(output_path).parent() {
            std::fs::create_dir_all(parent)?;
        }

        // 构建 FFmpeg 命令
        let mut cmd = FfmpegCommand::new();

        // 输入文件
        cmd.arg("-i").arg(video_path);

        // 寻找到指定时间戳
        cmd.arg("-ss").arg(timestamp.to_string());

        // 只提取一帧
        cmd.arg("-vframes").arg("1");

        // 构建视频过滤器
        let mut filters = Vec::new();

        // 尺寸调整
        if let Some(size) = &config.output_size {
            let scale_filter = if let (Some(width), Some(height)) = (size.width, size.height) {
                if size.maintain_aspect_ratio {
                    format!("scale={}:{}:force_original_aspect_ratio=decrease", width, height)
                } else {
                    format!("scale={}:{}", width, height)
                }
            } else if let Some(width) = size.width {
                format!("scale={}:-1", width)
            } else if let Some(height) = size.height {
                format!("scale=-1:{}", height)
            } else {
                String::new()
            };

            if !scale_filter.is_empty() {
                filters.push(scale_filter);
            }
        }

        // 应用过滤器
        if !filters.is_empty() {
            cmd.arg("-vf").arg(filters.join(","));
        }

        // 设置像素格式
        match config.output_format.format {
            ImageFormat::Jpg => {
                cmd.arg("-pix_fmt").arg("yuvj420p");
                cmd.arg("-q:v").arg(Self::jpeg_quality_to_qscale(config.output_format.quality).to_string());
            }
            ImageFormat::Png => {
                cmd.arg("-pix_fmt").arg("rgba");
                cmd.arg("-compression_level").arg(config.output_format.quality.to_string());
            }
            ImageFormat::WebP => {
                cmd.arg("-pix_fmt").arg("yuv420p");
                cmd.arg("-quality").arg(config.output_format.quality.to_string());
                if let Some(level) = config.output_format.compression_level {
                    cmd.arg("-compression_level").arg(level.to_string());
                }
            }
            ImageFormat::Bmp => {
                cmd.arg("-pix_fmt").arg("bgr24");
            }
        }

        // 覆盖输出文件
        if config.overwrite_existing {
            cmd.arg("-y");
        } else {
            cmd.arg("-n");
        }

        // 输出文件
        cmd.arg(output_path);

        // 执行命令
        match cmd.spawn() {
            Ok(mut child) => {
                match child.wait() {
                    Ok(status) => {
                        let processing_time = start_time.elapsed().as_millis() as u64;

                        if status.success() {
                            let file_size = std::fs::metadata(output_path)
                                .map(|m| m.len())
                                .unwrap_or(0);

                            info!(
                                "帧提取成功: {} -> {} ({}ms)",
                                video_path, output_path, processing_time
                            );

                            Ok(FrameExtractionResult {
                                video_path: video_path.to_string(),
                                frame_type: config.frame_type.clone(),
                                timestamp,
                                output_path: output_path.to_string(),
                                success: true,
                                error_message: None,
                                processing_time_ms: processing_time,
                                file_size,
                            })
                        } else {
                            let error_msg = "FFmpeg 执行失败".to_string();
                            error!("{}: {}", error_msg, video_path);

                            Ok(FrameExtractionResult {
                                video_path: video_path.to_string(),
                                frame_type: config.frame_type.clone(),
                                timestamp,
                                output_path: output_path.to_string(),
                                success: false,
                                error_message: Some(error_msg),
                                processing_time_ms: processing_time,
                                file_size: 0,
                            })
                        }
                    }
                    Err(e) => {
                        let error_msg = format!("等待 FFmpeg 进程失败: {}", e);
                        error!("{}", error_msg);

                        Ok(FrameExtractionResult {
                            video_path: video_path.to_string(),
                            frame_type: config.frame_type.clone(),
                            timestamp,
                            output_path: output_path.to_string(),
                            success: false,
                            error_message: Some(error_msg),
                            processing_time_ms: start_time.elapsed().as_millis() as u64,
                            file_size: 0,
                        })
                    }
                }
            }
            Err(e) => {
                let error_msg = format!("启动 FFmpeg 进程失败: {}", e);
                error!("{}", error_msg);

                Ok(FrameExtractionResult {
                    video_path: video_path.to_string(),
                    frame_type: config.frame_type.clone(),
                    timestamp,
                    output_path: output_path.to_string(),
                    success: false,
                    error_message: Some(error_msg),
                    processing_time_ms: start_time.elapsed().as_millis() as u64,
                    file_size: 0,
                })
            }
        }
    }

    /// 将 JPEG 质量值转换为 FFmpeg 的 qscale 值
    fn jpeg_quality_to_qscale(quality: u8) -> u8 {
        // FFmpeg qscale: 1 (最高质量) 到 31 (最低质量)
        // 质量值: 1-100
        let clamped_quality = quality.clamp(1, 100) as u32;
        (31 - ((clamped_quality - 1) * 30 / 99)) as u8
    }

    /// 计算提取时间点
    pub fn calculate_extraction_timestamps(
        video_info: &VideoFileInfo,
        config: &FrameExtractionConfig,
    ) -> Result<Vec<f64>> {
        let mut timestamps = Vec::new();

        match &config.frame_type {
            FrameType::First => {
                timestamps.push(0.0);
            }
            FrameType::Last => {
                // 最后一帧，稍微提前一点以确保能提取到
                let last_timestamp = (video_info.duration - 0.1).max(0.0);
                timestamps.push(last_timestamp);
            }
            FrameType::Custom => {
                if let Some(custom_time) = config.custom_time {
                    let timestamp = custom_time.clamp(0.0, video_info.duration);
                    timestamps.push(timestamp);
                } else {
                    // 默认提取中间帧
                    timestamps.push(video_info.duration / 2.0);
                }
            }
            FrameType::Multiple => {
                if let Some(time_points) = &config.time_points {
                    for time_point in time_points {
                        let timestamp = Self::time_point_to_seconds(time_point, video_info)?;
                        timestamps.push(timestamp);
                    }
                }
            }
        }

        Ok(timestamps)
    }

    /// 将时间点配置转换为秒数
    fn time_point_to_seconds(time_point: &TimePoint, video_info: &VideoFileInfo) -> Result<f64> {
        match time_point {
            TimePoint::Seconds { value } => {
                Ok(value.clamp(0.0, video_info.duration))
            }
            TimePoint::Percentage { value } => {
                let percentage = value.clamp(0.0, 1.0);
                Ok(video_info.duration * percentage)
            }
            TimePoint::Frame { value } => {
                if video_info.fps > 0.0 {
                    let timestamp = (*value as f64) / video_info.fps;
                    Ok(timestamp.clamp(0.0, video_info.duration))
                } else {
                    Err(anyhow!("无法计算帧时间戳：视频帧率信息不可用"))
                }
            }
        }
    }

    /// 生成输出文件路径
    pub fn generate_output_path(
        video_info: &VideoFileInfo,
        config: &FrameExtractionConfig,
        timestamp: f64,
        index: Option<usize>,
    ) -> Result<String> {
        let video_path = Path::new(&video_info.path);
        let video_name = video_path
            .file_stem()
            .ok_or_else(|| anyhow!("无法获取视频文件名"))?
            .to_string_lossy();

        let frame_type_str = match &config.frame_type {
            FrameType::First => "first",
            FrameType::Last => "last",
            FrameType::Custom => "custom",
            FrameType::Multiple => "frame",
        };

        let extension = match config.output_format.format {
            ImageFormat::Jpg => "jpg",
            ImageFormat::Png => "png",
            ImageFormat::WebP => "webp",
            ImageFormat::Bmp => "bmp",
        };

        // 替换文件名模式中的变量
        let mut filename = config.filename_pattern.clone();
        filename = filename.replace("{name}", &video_name);
        filename = filename.replace("{frame_type}", frame_type_str);
        filename = filename.replace("{timestamp}", &format!("{:.2}", timestamp));

        if let Some(idx) = index {
            filename = filename.replace("{index}", &format!("{:03}", idx + 1));
        }

        // 确保文件名有效
        filename = Self::sanitize_filename(&filename);

        let output_path = Path::new(&config.output_directory)
            .join(format!("{}.{}", filename, extension));

        Ok(output_path.to_string_lossy().to_string())
    }

    /// 清理文件名，移除无效字符
    fn sanitize_filename(filename: &str) -> String {
        filename
            .chars()
            .map(|c| match c {
                '<' | '>' | ':' | '"' | '|' | '?' | '*' => '_',
                '/' | '\\' => '_',
                c if c.is_control() => '_',
                c => c,
            })
            .collect()
    }

    /// 批量提取帧
    pub async fn extract_frames_batch(
        video_files: &[VideoFileInfo],
        config: &FrameExtractionConfig,
    ) -> Result<Vec<FrameExtractionResult>> {
        let mut results = Vec::new();

        for (index, video_info) in video_files.iter().enumerate() {
            info!(
                "处理视频 {}/{}: {}",
                index + 1,
                video_files.len(),
                video_info.filename
            );

            if !video_info.is_valid {
                warn!("跳过无效视频文件: {}", video_info.path);
                continue;
            }

            // 计算提取时间点
            let timestamps = match Self::calculate_extraction_timestamps(video_info, config) {
                Ok(timestamps) => timestamps,
                Err(e) => {
                    error!("计算时间戳失败 {}: {}", video_info.path, e);
                    continue;
                }
            };

            // 为每个时间点提取帧
            for (timestamp_index, timestamp) in timestamps.iter().enumerate() {
                let output_path = match Self::generate_output_path(
                    video_info,
                    config,
                    *timestamp,
                    if timestamps.len() > 1 { Some(timestamp_index) } else { None },
                ) {
                    Ok(path) => path,
                    Err(e) => {
                        error!("生成输出路径失败: {}", e);
                        continue;
                    }
                };

                // 检查文件是否已存在
                if !config.overwrite_existing && Path::new(&output_path).exists() {
                    info!("跳过已存在的文件: {}", output_path);
                    continue;
                }

                // 提取帧
                match Self::extract_frame(&video_info.path, *timestamp, &output_path, config).await {
                    Ok(result) => {
                        results.push(result);
                    }
                    Err(e) => {
                        error!("提取帧失败 {}: {}", video_info.path, e);
                        results.push(FrameExtractionResult {
                            video_path: video_info.path.clone(),
                            frame_type: config.frame_type.clone(),
                            timestamp: *timestamp,
                            output_path,
                            success: false,
                            error_message: Some(e.to_string()),
                            processing_time_ms: 0,
                            file_size: 0,
                        });
                    }
                }
            }
        }

        Ok(results)
    }
}

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

    use tempfile::TempDir;

    fn create_test_video_info() -> VideoFileInfo {
        VideoFileInfo {
            path: "/test/video.mp4".to_string(),
            filename: "video.mp4".to_string(),
            size: 1024 * 1024, // 1MB
            duration: 60.0, // 60 seconds
            width: 1920,
            height: 1080,
            fps: 30.0,
            format: "mp4".to_string(),
            is_valid: true,
            error_message: None,
        }
    }

    fn create_test_config() -> FrameExtractionConfig {
        FrameExtractionConfig {
            frame_type: FrameType::First,
            time_points: None,
            custom_time: None,
            output_format: ImageQuality {
                format: ImageFormat::Jpg,
                quality: 85,
                compression_level: None,
            },
            output_size: Some(OutputSize {
                width: Some(640),
                height: Some(360),
                maintain_aspect_ratio: true,
                scale_filter: Some("lanczos".to_string()),
            }),
            output_directory: "/tmp".to_string(),
            filename_pattern: "{name}_{frame_type}".to_string(),
            overwrite_existing: false,
        }
    }

    #[test]
    fn test_parse_fraction() {
        assert_eq!(FrameExtractorService::parse_fraction("30/1"), Some(30.0));
        assert_eq!(FrameExtractorService::parse_fraction("25/1"), Some(25.0));
        assert_eq!(FrameExtractorService::parse_fraction("30000/1001"), Some(29.970029970029973));
        assert_eq!(FrameExtractorService::parse_fraction("invalid"), None);
        assert_eq!(FrameExtractorService::parse_fraction("30/0"), None);
    }

    #[test]
    fn test_jpeg_quality_to_qscale() {
        assert_eq!(FrameExtractorService::jpeg_quality_to_qscale(100), 1);
        assert_eq!(FrameExtractorService::jpeg_quality_to_qscale(1), 31);
        assert_eq!(FrameExtractorService::jpeg_quality_to_qscale(85), 6);
        assert_eq!(FrameExtractorService::jpeg_quality_to_qscale(50), 16);
    }

    #[test]
    fn test_calculate_extraction_timestamps_first_frame() {
        let video_info = create_test_video_info();
        let config = FrameExtractionConfig {
            frame_type: FrameType::First,
            ..create_test_config()
        };

        let timestamps = FrameExtractorService::calculate_extraction_timestamps(&video_info, &config)
            .expect("Should calculate timestamps");

        assert_eq!(timestamps.len(), 1);
        assert_eq!(timestamps[0], 0.0);
    }

    #[test]
    fn test_calculate_extraction_timestamps_last_frame() {
        let video_info = create_test_video_info();
        let config = FrameExtractionConfig {
            frame_type: FrameType::Last,
            ..create_test_config()
        };

        let timestamps = FrameExtractorService::calculate_extraction_timestamps(&video_info, &config)
            .expect("Should calculate timestamps");

        assert_eq!(timestamps.len(), 1);
        assert_eq!(timestamps[0], 59.9); // duration - 0.1
    }

    #[test]
    fn test_calculate_extraction_timestamps_custom() {
        let video_info = create_test_video_info();
        let config = FrameExtractionConfig {
            frame_type: FrameType::Custom,
            custom_time: Some(30.0),
            ..create_test_config()
        };

        let timestamps = FrameExtractorService::calculate_extraction_timestamps(&video_info, &config)
            .expect("Should calculate timestamps");

        assert_eq!(timestamps.len(), 1);
        assert_eq!(timestamps[0], 30.0);
    }

    #[test]
    fn test_calculate_extraction_timestamps_custom_default() {
        let video_info = create_test_video_info();
        let config = FrameExtractionConfig {
            frame_type: FrameType::Custom,
            custom_time: None,
            ..create_test_config()
        };

        let timestamps = FrameExtractorService::calculate_extraction_timestamps(&video_info, &config)
            .expect("Should calculate timestamps");

        assert_eq!(timestamps.len(), 1);
        assert_eq!(timestamps[0], 30.0); // duration / 2
    }

    #[test]
    fn test_time_point_to_seconds() {
        let video_info = create_test_video_info();

        // Test seconds
        let time_point = TimePoint::Seconds { value: 15.5 };
        let result = FrameExtractorService::time_point_to_seconds(&time_point, &video_info)
            .expect("Should convert seconds");
        assert_eq!(result, 15.5);

        // Test percentage
        let time_point = TimePoint::Percentage { value: 0.5 };
        let result = FrameExtractorService::time_point_to_seconds(&time_point, &video_info)
            .expect("Should convert percentage");
        assert_eq!(result, 30.0); // 50% of 60 seconds

        // Test frame number
        let time_point = TimePoint::Frame { value: 900 };
        let result = FrameExtractorService::time_point_to_seconds(&time_point, &video_info)
            .expect("Should convert frame");
        assert_eq!(result, 30.0); // 900 frames / 30 fps
    }

    #[test]
    fn test_generate_output_path() {
        let video_info = create_test_video_info();
        let config = create_test_config();

        let output_path = FrameExtractorService::generate_output_path(
            &video_info,
            &config,
            15.5,
            None,
        ).expect("Should generate output path");

        assert!(output_path.contains("video_first"));
        assert!(output_path.contains("15.50"));
        assert!(output_path.ends_with(".jpg"));
    }

    #[test]
    fn test_generate_output_path_with_index() {
        let video_info = create_test_video_info();
        let config = FrameExtractionConfig {
            filename_pattern: "{name}_{frame_type}_{index}".to_string(),
            ..create_test_config()
        };

        let output_path = FrameExtractorService::generate_output_path(
            &video_info,
            &config,
            15.5,
            Some(2),
        ).expect("Should generate output path");

        assert!(output_path.contains("video_first_003"));
    }

    #[test]
    fn test_sanitize_filename() {
        assert_eq!(
            FrameExtractorService::sanitize_filename("normal_filename"),
            "normal_filename"
        );
        assert_eq!(
            FrameExtractorService::sanitize_filename("file<>name"),
            "file__name"
        );
        assert_eq!(
            FrameExtractorService::sanitize_filename("file/with\\path"),
            "file_with_path"
        );
        assert_eq!(
            FrameExtractorService::sanitize_filename("file:with|special*chars?"),
            "file_with_special_chars_"
        );
    }

    #[test]
    fn test_calculate_extraction_timestamps_multiple() {
        let video_info = create_test_video_info();
        let time_points = vec![
            TimePoint::Seconds { value: 10.0 },
            TimePoint::Percentage { value: 0.5 },
            TimePoint::Frame { value: 1500 },
        ];
        let config = FrameExtractionConfig {
            frame_type: FrameType::Multiple,
            time_points: Some(time_points),
            ..create_test_config()
        };

        let timestamps = FrameExtractorService::calculate_extraction_timestamps(&video_info, &config)
            .expect("Should calculate timestamps");

        assert_eq!(timestamps.len(), 3);
        assert_eq!(timestamps[0], 10.0);
        assert_eq!(timestamps[1], 30.0); // 50% of 60 seconds
        assert_eq!(timestamps[2], 50.0); // 1500 frames / 30 fps
    }

    #[test]
    fn test_time_point_clamping() {
        let video_info = create_test_video_info();

        // Test seconds clamping
        let time_point = TimePoint::Seconds { value: 100.0 }; // Beyond duration
        let result = FrameExtractorService::time_point_to_seconds(&time_point, &video_info)
            .expect("Should convert seconds");
        assert_eq!(result, 60.0); // Clamped to duration

        // Test negative seconds
        let time_point = TimePoint::Seconds { value: -5.0 };
        let result = FrameExtractorService::time_point_to_seconds(&time_point, &video_info)
            .expect("Should convert seconds");
        assert_eq!(result, 0.0); // Clamped to 0

        // Test percentage clamping
        let time_point = TimePoint::Percentage { value: 1.5 }; // > 1.0
        let result = FrameExtractorService::time_point_to_seconds(&time_point, &video_info)
            .expect("Should convert percentage");
        assert_eq!(result, 60.0); // 100% of duration
    }
}
