"""videoconv 功能测试."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch

import pytest

from pytola_office.videoconv.cli import (
    VideoConverterConfig,
    VideoConverterRunner,
    conf,
    expand_input_pattern,
    main,
    parse_args,
)


class TestVideoConverterConfig:
    """VideoConverterConfig 类测试."""

    def test_default_output_format(self) -> None:
        """测试默认输出格式为 mp4."""
        assert VideoConverterConfig.OUTPUT_FORMAT == "mp4"

    def test_default_video_codec(self) -> None:
        """测试默认视频编解码器为 libx264."""
        assert VideoConverterConfig.VIDEO_CODEC == "libx264"

    def test_default_audio_codec(self) -> None:
        """测试默认音频编解码器为 aac."""
        assert VideoConverterConfig.AUDIO_CODEC == "aac"

    def test_default_video_bitrate(self) -> None:
        """测试默认视频比特率为 2500 kbps."""
        assert VideoConverterConfig.VIDEO_BITRATE == 2500

    def test_default_audio_bitrate(self) -> None:
        """测试默认音频比特率为 128 kbps."""
        assert VideoConverterConfig.AUDIO_BITRATE == 128

    def test_default_frame_rate(self) -> None:
        """测试默认帧率为 30 fps."""
        assert VideoConverterConfig.FRAME_RATE == 30

    def test_default_scale_factor(self) -> None:
        """测试默认缩放因子为 1.0."""
        assert pytest.approx(1.0) == VideoConverterConfig.SCALE_FACTOR

    def test_default_crf(self) -> None:
        """测试默认 CRF 为 23."""
        assert VideoConverterConfig.CRF == 23

    def test_default_preset(self) -> None:
        """测试默认预设为 medium."""
        assert VideoConverterConfig.PRESET == "medium"

    def test_default_remove_audio(self) -> None:
        """测试默认移除音频为 False."""
        assert VideoConverterConfig.REMOVE_AUDIO is False

    def test_supported_formats(self) -> None:
        """测试支持的输入格式."""
        expected_formats = {
            ".mp4",
            ".avi",
            ".mkv",
            ".mov",
            ".wmv",
            ".flv",
            ".webm",
            ".m4s",
            ".mpeg",
            ".mpg",
            ".3gp",
            ".vob",
            ".mts",
            ".m2ts",
            ".divx",
            ".asf",
            ".rm",
            ".rmvb",
        }
        assert expected_formats == VideoConverterConfig.SUPPORTED_FORMATS

    def test_output_formats(self) -> None:
        """测试支持的输出格式."""
        expected_formats = {".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".gif"}
        assert expected_formats == VideoConverterConfig.OUTPUT_FORMATS


class TestVideoConverterRunner:
    """VideoConverterRunner 类测试."""

    @pytest.fixture
    def temp_video_file(self, tmp_path: Path) -> Path:
        """创建临时视频文件用于测试."""
        video_path = tmp_path / "test_video.mp4"
        # 创建虚拟文件 (不是真实视频，仅用于测试文件存在性)
        video_path.write_bytes(b"dummy video content")
        return video_path

    @pytest.fixture
    def temp_output_dir(self, tmp_path: Path) -> Path:
        """创建临时输出目录."""
        output_dir = tmp_path / "output"
        output_dir.mkdir()
        return output_dir

    def test_runner_initialization(self, temp_video_file: Path) -> None:
        """测试 VideoConverterRunner 初始化."""
        runner = VideoConverterRunner(input_video=temp_video_file)

        assert runner.input_video == temp_video_file
        assert runner.output_dir is None
        assert runner.output_format is None
        assert runner.video_codec is None
        assert runner.audio_codec is None
        assert runner.remove_audio is None

    def test_runner_with_custom_parameters(self, temp_video_file: Path, temp_output_dir: Path) -> None:
        """测试带自定义参数的 VideoConverterRunner."""
        runner = VideoConverterRunner(
            input_video=temp_video_file,
            output_dir=temp_output_dir,
            output_format=".mkv",
            video_codec="libx265",
            audio_codec="mp3",
            video_bitrate=5000,
            audio_bitrate=256,
            frame_rate=60,
            scale_factor=0.5,
            crf=18,
            preset="slow",
            remove_audio=True,
        )

        assert runner.output_dir == temp_output_dir
        assert runner.output_format == ".mkv"
        assert runner.video_codec == "libx265"
        assert runner.audio_codec == "mp3"
        assert runner.video_bitrate == 5000
        assert runner.audio_bitrate == 256
        assert runner.frame_rate == 60
        assert runner.scale_factor == pytest.approx(0.5)
        assert runner.crf == 18
        assert runner.preset == "slow"
        assert runner.remove_audio is True

    def test_validate_input_nonexistent_file(self, tmp_path: Path) -> None:
        """测试验证不存在的文件."""
        fake_path = tmp_path / "nonexistent.mp4"
        runner = VideoConverterRunner(input_video=fake_path)

        result = runner._validate_input()
        assert result is False

    def test_validate_input_directory(self, tmp_path: Path) -> None:
        """测试验证目录而非文件."""
        runner = VideoConverterRunner(input_video=tmp_path)

        result = runner._validate_input()
        assert result is False

    def test_validate_input_unsupported_format(self, temp_video_file: Path) -> None:
        """测试验证不支持的格式."""
        # 更改文件扩展名为不支持的格式
        unsupported_path = temp_video_file.parent / "test.unsupported"
        temp_video_file.rename(unsupported_path)

        runner = VideoConverterRunner(input_video=unsupported_path)
        result = runner._validate_input()

        assert result is False

    def test_get_output_path_default(self, temp_video_file: Path, tmp_path: Path) -> None:
        """测试默认输出路径."""
        runner = VideoConverterRunner(input_video=temp_video_file)

        output_path = runner._get_output_path()

        assert output_path is not None
        # 默认格式应为支持的格式
        assert output_path.suffix in conf.OUTPUT_FORMATS
        assert output_path.stem == f"{temp_video_file.stem}_converted"

    def test_get_output_path_custom_format(self, temp_video_file: Path, temp_output_dir: Path) -> None:
        """测试自定义格式的输出路径."""
        runner = VideoConverterRunner(input_video=temp_video_file, output_dir=temp_output_dir, output_format=".mkv")

        output_path = runner._get_output_path()

        assert output_path is not None
        assert output_path.suffix == ".mkv"
        assert output_path.parent == temp_output_dir

    def test_get_output_path_invalid_format(self, temp_video_file: Path) -> None:
        """测试无效格式的输出路径."""
        runner = VideoConverterRunner(input_video=temp_video_file, output_format=".invalid")

        output_path = runner._get_output_path()
        assert output_path is None

    def test_build_scale_filter_no_scaling(self, temp_video_file: Path) -> None:
        """测试构建无缩放的缩放滤镜."""
        runner = VideoConverterRunner(input_video=temp_video_file, scale_factor=1.0)

        scale_filter = runner._build_scale_filter()
        assert scale_filter is None

    def test_build_scale_filter_downscale(self, temp_video_file: Path) -> None:
        """测试构建缩小缩放滤镜."""
        runner = VideoConverterRunner(input_video=temp_video_file, scale_factor=0.5)

        scale_filter = runner._build_scale_filter()
        assert scale_filter is not None
        assert scale_filter == "scale=iw*0.5:ih*0.5"

    def test_build_scale_filter_upscale(self, temp_video_file: Path) -> None:
        """测试构建放大缩放滤镜."""
        runner = VideoConverterRunner(input_video=temp_video_file, scale_factor=2.0)

        scale_filter = runner._build_scale_filter()
        assert scale_filter is not None
        assert scale_filter == "scale=iw*2.0:ih*2.0"

    def test_build_ffmpeg_command_basic(self, temp_video_file: Path) -> None:
        """测试构建基础 ffmpeg 命令."""
        runner = VideoConverterRunner(input_video=temp_video_file)

        cmd = runner._build_ffmpeg_command()

        assert cmd is not None
        assert "ffmpeg" in cmd[0]
        assert "-i" in cmd
        assert str(temp_video_file) in cmd
        assert "-c:v" in cmd
        assert "-c:a" in cmd

    def test_build_ffmpeg_command_with_custom_params(self, temp_video_file: Path) -> None:
        """测试构建带自定义参数的 ffmpeg 命令."""
        runner = VideoConverterRunner(
            input_video=temp_video_file,
            video_codec="libx265",
            audio_codec="mp3",
            video_bitrate=5000,
            audio_bitrate=256,
            frame_rate=60,
            crf=18,
            preset="slow",
        )

        cmd = runner._build_ffmpeg_command()

        assert cmd is not None
        assert "libx265" in cmd
        assert "mp3" in cmd
        assert "5000k" in cmd
        assert "256k" in cmd
        assert "60" in cmd
        assert "18" in cmd
        assert "slow" in cmd

    def test_build_ffmpeg_command_remove_audio(self, temp_video_file: Path) -> None:
        """测试构建移除音频的 ffmpeg 命令."""
        runner = VideoConverterRunner(input_video=temp_video_file, remove_audio=True)

        cmd = runner._build_ffmpeg_command()

        assert cmd is not None
        assert "-an" in cmd  # 无音频标志

    def test_build_ffmpeg_command_gif_output(self, temp_video_file: Path) -> None:
        """测试构建 GIF 输出的 ffmpeg 命令."""
        runner = VideoConverterRunner(input_video=temp_video_file, output_format=".gif")

        cmd = runner._build_ffmpeg_command()

        assert cmd is not None
        # GIF 不应有 CRF 参数
        assert "-crf" not in cmd

    def test_run_with_mocked_ffmpeg(self, temp_video_file: Path, tmp_path: Path) -> None:
        """测试运行转换 (mocked ffmpeg)."""
        tmp_path / "output.mp4"
        runner = VideoConverterRunner(input_video=temp_video_file, output_dir=tmp_path)

        # Mock subprocess.run 模拟成功转换
        with patch("subprocess.run") as mock_run:
            mock_run.return_value.returncode = 0
            mock_run.return_value.stderr = ""

            result = runner.run()

            assert result is True
            mock_run.assert_called_once()

    def test_run_with_failed_ffmpeg(self, temp_video_file: Path, tmp_path: Path) -> None:
        """测试运行失败的 ffmpeg 命令."""
        runner = VideoConverterRunner(input_video=temp_video_file, output_dir=tmp_path)

        # Mock subprocess.run 模拟失败转换
        with patch("subprocess.run") as mock_run:
            mock_run.return_value.returncode = 1
            mock_run.return_value.stderr = "Error message"

            result = runner.run()

            assert result is False

    def test_run_with_ffmpeg_not_found(self, temp_video_file: Path, tmp_path: Path) -> None:
        """测试 ffmpeg 未找到时的运行."""
        runner = VideoConverterRunner(input_video=temp_video_file, output_dir=tmp_path)

        # Mock subprocess.run 抛出 FileNotFoundError
        with patch("subprocess.run") as mock_run:
            mock_run.side_effect = FileNotFoundError("ffmpeg not found")

            result = runner.run()

            assert result is False

    def test_video_info_property(self, temp_video_file: Path) -> None:
        """测试 video_info 属性."""
        runner = VideoConverterRunner(input_video=temp_video_file)

        # Mock ffprobe 调用
        with patch("subprocess.run") as mock_run:
            mock_run.return_value.returncode = 0
            mock_run.return_value.stdout = '{"streams": [], "format": {}}'

            info = runner.video_info()

            assert info is not None
            assert "streams" in info  # type: ignore[operator]
            assert "format" in info  # type: ignore[operator]

    def test_video_info_property_failure(self, temp_video_file: Path) -> None:
        """测试 ffprobe 失败时的 video_info 属性."""
        runner = VideoConverterRunner(input_video=temp_video_file)

        # Mock ffprobe 调用失败
        with patch("subprocess.run") as mock_run:
            mock_run.return_value.returncode = 1

            info = runner.video_info()

            assert info is None


class TestExpandInputPattern:
    """expand_input_pattern 函数测试."""

    @pytest.fixture
    def temp_video_files(self, tmp_path: Path) -> list[Path]:
        """创建多个临时视频文件."""
        files = []
        for ext in [".mp4", ".avi", ".mkv"]:
            file_path = tmp_path / f"test{ext}"
            file_path.write_bytes(b"dummy content")
            files.append(file_path)
        return files

    def test_expand_single_file(self, temp_video_files: list[Path]) -> None:
        """测试扩展单个文件路径."""
        result = expand_input_pattern(str(temp_video_files[0]))

        assert len(result) == 1
        assert result[0] == temp_video_files[0]

    def test_expand_wildcard_pattern(self, temp_video_files: list[Path], tmp_path: Path) -> None:
        """测试扩展通配符模式."""
        # 切换到临时目录以避免匹配真实文件
        import os

        original_dir = Path.cwd()
        try:
            os.chdir(tmp_path)
            result = expand_input_pattern("*.mp4")

            assert len(result) >= 1
            assert all(f.suffix == ".mp4" for f in result)
        finally:
            os.chdir(original_dir)

    def test_expand_recursive_pattern(self, temp_video_files: list[Path], tmp_path: Path) -> None:
        """测试扩展递归通配符模式."""
        # 创建包含视频文件的子目录
        subdir = tmp_path / "subdir"
        subdir.mkdir()
        nested_video = subdir / "nested.mp4"
        nested_video.write_bytes(b"dummy content")

        import os

        original_dir = Path.cwd()
        try:
            os.chdir(tmp_path)
            result = expand_input_pattern("**/*.mp4")

            assert len(result) >= 2  # 至少原始文件和嵌套文件
            assert all(f.suffix == ".mp4" for f in result)
        finally:
            os.chdir(original_dir)

    def test_expand_no_matching_files(self, tmp_path: Path) -> None:
        """测试扩展无匹配文件的模式."""
        import os

        original_dir = Path.cwd()
        try:
            os.chdir(tmp_path)
            result = expand_input_pattern("*.nonexistent")

            assert len(result) == 0
        finally:
            os.chdir(original_dir)

    def test_expand_filters_unsupported_formats(self, tmp_path: Path) -> None:
        """测试过滤不支持的格式."""
        # 创建不支持的文件
        unsupported_file = tmp_path / "test.unsupported"
        unsupported_file.write_bytes(b"dummy content")

        import os

        original_dir = Path.cwd()
        try:
            os.chdir(tmp_path)
            result = expand_input_pattern("*.unsupported")

            # 应过滤掉不支持的格式
            assert len(result) == 0
        finally:
            os.chdir(original_dir)


class TestParseArgs:
    """parse_args 函数测试."""

    def test_parse_args_basic(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试解析基础参数."""
        monkeypatch.setattr("sys.argv", ["videoconv", "input.mp4"])

        args = parse_args()

        assert args.input == "input.mp4"
        assert args.output is None
        # 格式默认值为 mp4 (不带点号)
        assert args.format == "mp4"  # 配置默认值
        assert args.video_codec == "libx264"
        assert args.audio_codec == "aac"

    def test_parse_args_with_output_dir(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试解析带输出目录的参数."""
        monkeypatch.setattr("sys.argv", ["videoconv", "input.mp4", "./output"])

        args = parse_args()

        assert args.input == "input.mp4"
        assert args.output == "./output"

    def test_parse_args_with_format(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试解析带输出格式的参数."""
        # 需要使用带点前缀的格式如 argparse choices
        monkeypatch.setattr("sys.argv", ["videoconv", "input.mp4", "-f", ".mkv"])

        args = parse_args()

        assert args.format == ".mkv"

    def test_parse_args_with_all_options(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试解析带所有选项的参数."""
        # 需要使用带点前缀的格式如 argparse choices
        monkeypatch.setattr(
            "sys.argv",
            [
                "videoconv",
                "input.avi",
                "./output",
                "-f",
                ".mp4",
                "-c:v",
                "libx265",
                "-c:a",
                "mp3",
                "-b:v",
                "5000",
                "-b:a",
                "256",
                "-r",
                "60",
                "--scale",
                "0.5",
                "-crf",
                "18",
                "--preset",
                "slow",
                "--no-audio",
                "-v",
            ],
        )

        args = parse_args()

        assert args.input == "input.avi"
        assert args.output == "./output"
        assert args.format == ".mp4"
        assert args.video_codec == "libx265"
        assert args.audio_codec == "mp3"
        assert args.video_bitrate == 5000
        assert args.audio_bitrate == 256
        assert args.frame_rate == 60
        assert args.scale == pytest.approx(0.5)
        assert args.crf == 18
        assert args.preset == "slow"
        assert args.remove_audio is True
        assert args.verbose is True

    def test_parse_args_with_invalid_format(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试解析无效格式的参数."""
        monkeypatch.setattr("sys.argv", ["videoconv", "input.mp4", "-f", "invalid"])

        # 应因无效选择而抛出 SystemExit
        with pytest.raises(SystemExit):
            parse_args()

    def test_parse_args_with_invalid_preset(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """测试解析无效预设的参数."""
        monkeypatch.setattr("sys.argv", ["videoconv", "input.mp4", "--preset", "invalid"])

        # 应因无效选择而抛出 SystemExit
        with pytest.raises(SystemExit):
            parse_args()


class TestMainFunction:
    """main 函数测试."""

    def test_main_with_nonexistent_input(
        self,
        monkeypatch: pytest.MonkeyPatch,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """测试 main 函数处理不存在的输入文件."""
        import logging

        # 启用 pytola logger 的传播以便 caplog 捕获
        pytola_logger = logging.getLogger("pytola")
        original_propagate = pytola_logger.propagate
        pytola_logger.propagate = True

        try:
            monkeypatch.setattr("sys.argv", ["videoconv", "nonexistent.mp4"])

            # 应以代码 1 退出
            with pytest.raises(SystemExit) as exc_info:
                main()

            assert exc_info.value.code == 1
            # 检查错误消息关于文件不存在
            assert "输入文件不存在" in caplog.text or "没有有效的输入" in caplog.text
        finally:
            # 恢复原始传播设置
            pytola_logger.propagate = original_propagate

    def test_main_with_valid_input_mocked(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        """测试 main 函数处理有效输入 (mocked 转换)."""
        # 创建虚拟视频文件
        video_file = tmp_path / "test.mp4"
        video_file.write_bytes(b"dummy content")

        monkeypatch.setattr("sys.argv", ["videoconv", str(video_file)])

        # Mock runner 避免实际 ffmpeg 调用
        with patch.object(VideoConverterRunner, "run", return_value=True):
            # 不应抛出异常
            with pytest.raises(SystemExit) as exc_info:
                main()

            # 应以代码 0 退出 (成功)
            assert exc_info.value.code == 0

    def test_main_with_multiple_files_mocked(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        """测试 main 函数处理多个输入文件 (mocked 转换)."""
        # 创建多个虚拟视频文件
        for i in range(3):
            video_file = tmp_path / f"test_{i}.mp4"
            video_file.write_bytes(b"dummy content")

        # 使用相对路径模式进行 rglob
        import os

        original_dir = Path.cwd()
        try:
            os.chdir(tmp_path)
            monkeypatch.setattr("sys.argv", ["videoconv", "*.mp4"])

            # Mock runner
            with patch.object(VideoConverterRunner, "run", return_value=True):
                with pytest.raises(SystemExit) as exc_info:
                    main()

                assert exc_info.value.code == 0
        finally:
            os.chdir(original_dir)

    def test_main_with_failed_conversion_mocked(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        """测试 main 函数处理失败的转换 (mocked)."""
        video_file = tmp_path / "test.mp4"
        video_file.write_bytes(b"dummy content")

        monkeypatch.setattr("sys.argv", ["videoconv", str(video_file)])

        # Mock runner 失败
        with patch.object(VideoConverterRunner, "run", return_value=False):
            with pytest.raises(SystemExit) as exc_info:
                main()

            # 应以代码 1 退出 (失败)
            assert exc_info.value.code == 1

    def test_main_with_verbose_flag(
        self,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """测试 main 函数带详细输出标志."""
        video_file = tmp_path / "test.mp4"
        video_file.write_bytes(b"dummy content")

        monkeypatch.setattr("sys.argv", ["videoconv", str(video_file), "-v"])

        # Mock runner
        with patch.object(VideoConverterRunner, "run", return_value=True), pytest.raises(SystemExit):
            main()

            # 应启用详细模式
            # 注意：实际日志级别检查取决于实现


class TestEdgeCases:
    """边界条件和边缘情况测试."""

    def test_config_inherits_from_configmixin(self) -> None:
        """测试配置继承自 ConfigMixin."""
        config = VideoConverterConfig()

        # 检查继承
        from pytola_core.config import ConfigMixin

        assert isinstance(config, ConfigMixin)

    def test_runner_with_none_values(self, tmp_path: Path) -> None:
        """测试 runner 正确处理 None 值."""
        video_file = tmp_path / "test.mp4"
        video_file.write_bytes(b"dummy content")

        # 所有可选参数为 None
        runner = VideoConverterRunner(
            input_video=video_file,
            output_dir=None,
            output_format=None,
            video_codec=None,
            audio_codec=None,
            video_bitrate=None,
            audio_bitrate=None,
            frame_rate=None,
            scale_factor=None,
            crf=None,
            preset=None,
            remove_audio=None,
        )

        # 应使用配置默认值
        assert runner.output_format is None
        assert runner.video_codec is None

    def test_output_format_without_dot_prefix(self, tmp_path: Path) -> None:
        """测试输出格式处理带和不带点前缀."""
        video_file = tmp_path / "test.mp4"
        video_file.write_bytes(b"dummy content")

        # 不带点
        runner1 = VideoConverterRunner(input_video=video_file, output_format="mkv")
        output1 = runner1._get_output_path()
        assert output1 is not None
        assert output1.suffix == ".mkv"

        # 带点
        runner2 = VideoConverterRunner(input_video=video_file, output_format=".avi")
        output2 = runner2._get_output_path()
        assert output2 is not None
        assert output2.suffix == ".avi"

    def test_scale_factor_edge_cases(self, tmp_path: Path) -> None:
        """测试缩放因子边缘情况."""
        video_file = tmp_path / "test.mp4"
        video_file.write_bytes(b"dummy content")

        # 非常小的缩放因子
        runner_small = VideoConverterRunner(input_video=video_file, scale_factor=0.1)
        filter_small = runner_small._build_scale_filter()
        assert filter_small is not None

        # 非常大的缩放因子
        runner_large = VideoConverterRunner(input_video=video_file, scale_factor=10.0)
        filter_large = runner_large._build_scale_filter()
        assert filter_large is not None

        # 恰好 1.0 (应返回 None)
        runner_exact = VideoConverterRunner(input_video=video_file, scale_factor=1.0)
        filter_exact = runner_exact._build_scale_filter()
        assert filter_exact is None
