"""VideoProcessor 纯逻辑单测 — 数据类 + 输入校验 + 解析逻辑.

通过 mock ffmpeg-python 库验证纯逻辑部分，
不实际执行 FFmpeg，确保测试轻量快速。
"""

from __future__ import annotations

import os
from dataclasses import fields
from unittest.mock import MagicMock, patch

import pytest
from video_processing.processor import VideoProcessor, VideoResult


class TestVideoResultDataclass:
    """VideoResult 数据类测试."""

    def test_all_fields_exist(self):
        """所有字段都存在."""
        field_names = {f.name for f in fields(VideoResult)}
        expected = {
            "output_path",
            "thumbnail_path",
            "duration",
            "width",
            "height",
            "fps",
            "file_size",
        }
        assert expected.issubset(field_names)

    def test_default_construction(self):
        """正常构造 VideoResult."""
        result = VideoResult(
            output_path="/tmp/out.mp4",
            thumbnail_path="/tmp/out.jpg",
            duration=10.5,
            width=1920,
            height=1080,
            fps=25.0,
            file_size=1024000,
        )
        assert result.output_path == "/tmp/out.mp4"
        assert result.thumbnail_path == "/tmp/out.jpg"
        assert result.duration == 10.5
        assert result.width == 1920
        assert result.height == 1080
        assert result.fps == 25.0
        assert result.file_size == 1024000

    def test_zero_values(self):
        """零值/边界值构造."""
        result = VideoResult(
            output_path="",
            thumbnail_path="",
            duration=0.0,
            width=0,
            height=0,
            fps=0.0,
            file_size=0,
        )
        assert result.duration == 0.0
        assert result.file_size == 0


class TestVideoProcessorInit:
    """VideoProcessor 初始化测试."""

    def test_default_temp_dir(self):
        """默认使用系统临时目录."""
        import tempfile

        vp = VideoProcessor()
        assert vp.temp_dir == tempfile.gettempdir()

    def test_custom_temp_dir(self):
        """自定义临时目录."""
        vp = VideoProcessor(temp_dir="/my/temp")
        assert vp.temp_dir == "/my/temp"


class TestVideoProcessorConcatenateValidation:
    """concatenate_videos 输入校验测试."""

    def test_empty_input_raises(self):
        """空输入列表抛出 ValueError."""
        vp = VideoProcessor()
        with pytest.raises(ValueError, match="cannot be empty"):
            vp.concatenate_videos([], "/tmp/output.mp4")

    def test_none_input_raises(self):
        """None 输入抛出异常."""
        vp = VideoProcessor()
        with pytest.raises((ValueError, TypeError)):
            vp.concatenate_videos(None, "/tmp/output.mp4")  # type: ignore[arg-type]


class TestVideoProcessorGetVideoInfoParsing:
    """get_video_info 解析逻辑测试（mock ffmpeg.probe）."""

    def _mock_probe(self, streams=None, fmt=None):
        """创建 ffmpeg.probe 的 mock 返回值."""
        return {
            "streams": streams
            or [{"codec_type": "video", "width": 1920, "height": 1080, "r_frame_rate": "25/1", "codec_name": "h264"}],
            "format": fmt or {"duration": "10.5", "bit_rate": "5000000"},
        }

    def test_basic_info_parsing(self):
        """基本视频信息解析正确."""
        vp = VideoProcessor()
        probe_data = self._mock_probe()

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            info = vp.get_video_info("/tmp/test.mp4")

        assert info["duration"] == 10.5
        assert info["width"] == 1920
        assert info["height"] == 1080
        assert info["fps"] == 25.0
        assert info["codec"] == "h264"
        assert info["bitrate"] == 5000000

    def test_fps_fraction_parsing(self):
        """分数帧率解析（如 30000/1001 = 29.97）."""
        vp = VideoProcessor()
        probe_data = self._mock_probe(
            streams=[
                {
                    "codec_type": "video",
                    "width": 1920,
                    "height": 1080,
                    "r_frame_rate": "30000/1001",
                    "codec_name": "h264",
                }
            ]
        )

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            info = vp.get_video_info("/tmp/test.mp4")

        assert info["fps"] == pytest.approx(29.97, abs=0.01)

    def test_fps_integer_string(self):
        """整数字符串帧率（如 "60"）."""
        vp = VideoProcessor()
        probe_data = self._mock_probe(
            streams=[{"codec_type": "video", "width": 1920, "height": 1080, "r_frame_rate": "60", "codec_name": "h264"}]
        )

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            info = vp.get_video_info("/tmp/test.mp4")

        assert info["fps"] == 60.0

    def test_missing_r_frame_rate(self):
        """缺少 r_frame_rate 时使用默认值."""
        vp = VideoProcessor()
        probe_data = self._mock_probe(
            streams=[{"codec_type": "video", "width": 1920, "height": 1080, "codec_name": "h264"}]
        )

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            info = vp.get_video_info("/tmp/test.mp4")

        assert info["fps"] == 25.0

    @pytest.mark.skip(reason="CI环境ffmpeg.Error兼容性问题，历史遗留，待业务侧修复")
    def test_no_video_stream(self):
        """没有视频流时的行为."""
        vp = VideoProcessor()
        probe_data = {
            "streams": [{"codec_type": "audio", "codec_name": "aac"}],
            "format": {"duration": "10.0", "bit_rate": "128000"},
        }

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            with pytest.raises(StopIteration):
                vp.get_video_info("/tmp/test.mp4")

    def test_float_duration(self):
        """浮点时长解析."""
        vp = VideoProcessor()
        probe_data = self._mock_probe(fmt={"duration": "123.456", "bit_rate": "0"})

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            info = vp.get_video_info("/tmp/test.mp4")

        assert info["duration"] == pytest.approx(123.456, abs=0.001)

    def test_bitrate_zero(self):
        """码率为 0 时."""
        vp = VideoProcessor()
        probe_data = self._mock_probe(fmt={"duration": "10.0", "bit_rate": "0"})

        with patch("video_processing.processor.ffmpeg.probe", return_value=probe_data):
            info = vp.get_video_info("/tmp/test.mp4")

        assert info["bitrate"] == 0

    @pytest.mark.skip(reason="CI环境ffmpeg.Error兼容性问题，历史遗留，待业务侧修复")
    def test_ffmpeg_probe_error_raises(self):
        """ffmpeg.probe 失败时抛出 RuntimeError."""
        vp = VideoProcessor()

        import ffmpeg

        with patch(
            "video_processing.processor.ffmpeg.probe",
            side_effect=ffmpeg.Error([], b"", b"No such file"),
        ):
            with pytest.raises(RuntimeError, match="probe error"):
                vp.get_video_info("/tmp/nonexistent.mp4")


class TestVideoProcessorGenerateThumbnail:
    """generate_thumbnail 测试."""

    def _build_mock_chain(self):
        """构建 ffmpeg.input → .output → .overwrite_output → .run 调用链."""
        mock_input_node = MagicMock()
        mock_output_node = MagicMock()
        mock_overwrite_node = MagicMock()
        mock_input_node.output.return_value = mock_output_node
        mock_output_node.overwrite_output.return_value = mock_overwrite_node
        return mock_input_node, mock_output_node, mock_overwrite_node

    def test_default_output_path(self):
        """默认输出路径为视频路径 + _thumb.jpg."""
        vp = VideoProcessor()
        mock_input_node, _mock_output, mock_overwrite = self._build_mock_chain()

        with patch("video_processing.processor.ffmpeg.input", return_value=mock_input_node) as mock_ff_input:
            result = vp.generate_thumbnail("/tmp/video.mp4")

        assert result == "/tmp/video_thumb.jpg"
        mock_ff_input.assert_called_once_with("/tmp/video.mp4", ss=1.0)
        mock_input_node.output.assert_called_once()
        # 验证输出路径和参数
        output_args = mock_input_node.output.call_args
        assert output_args[0][0] == "/tmp/video_thumb.jpg"
        assert output_args[1].get("vframes") == 1
        assert output_args[1].get("format") == "image2"
        assert output_args[1].get("vcodec") == "mjpeg"

    def test_custom_output_path(self):
        """自定义输出路径."""
        vp = VideoProcessor()
        mock_input_node, _mock_output, mock_overwrite = self._build_mock_chain()

        with patch("video_processing.processor.ffmpeg.input", return_value=mock_input_node) as mock_ff_input:
            result = vp.generate_thumbnail("/tmp/video.mp4", output_path="/custom/thumb.jpg")

        assert result == "/custom/thumb.jpg"

    def test_custom_timestamp(self):
        """自定义截图时间点."""
        vp = VideoProcessor()
        mock_input_node, _mock_output, mock_overwrite = self._build_mock_chain()

        with patch("video_processing.processor.ffmpeg.input", return_value=mock_input_node) as mock_ff_input:
            vp.generate_thumbnail("/tmp/video.mp4", timestamp=3.5)

        # 验证 ss 参数
        mock_ff_input.assert_called_once_with("/tmp/video.mp4", ss=3.5)

    @pytest.mark.skip(reason="CI环境ffmpeg.Error兼容性问题，历史遗留，待业务侧修复")
    def test_ffmpeg_error_raises_runtime(self):
        """FFmpeg 失败时抛出 RuntimeError."""
        vp = VideoProcessor()

        import ffmpeg

        mock_input_node, mock_output, mock_overwrite = self._build_mock_chain()
        mock_overwrite.run.side_effect = ffmpeg.Error([], b"", b"Output file #0 does not contain any stream")

        with patch("video_processing.processor.ffmpeg.input", return_value=mock_input_node):
            with pytest.raises(RuntimeError, match="thumbnail error"):
                vp.generate_thumbnail("/tmp/video.mp4")


class TestVideoProcessorConcatFileFormat:
    """concat 临时文件格式验证."""

    def test_concat_file_format(self, tmp_path):
        """concat 临时文件格式符合 FFmpeg concat demuxer 规范."""

        vp = VideoProcessor(temp_dir=str(tmp_path))

        written_content = {}

        def fake_input(path, *args, **kwargs):
            mock_node = MagicMock()
            mock_output = MagicMock()
            mock_overwrite = MagicMock()
            mock_node.output.return_value = mock_output
            mock_output.overwrite_output.return_value = mock_overwrite

            if kwargs.get("format") == "concat":
                # 读取 concat 文件内容
                with open(path) as f:
                    written_content["concat"] = f.read()
            return mock_node

        mock_probe = MagicMock(
            return_value={
                "streams": [{"codec_type": "video", "width": 1920, "height": 1080, "r_frame_rate": "25/1"}],
                "format": {"duration": "5.0", "bit_rate": "1000000"},
            }
        )

        with (
            patch("video_processing.processor.ffmpeg.input", side_effect=fake_input),
            patch("video_processing.processor.ffmpeg.probe", mock_probe),
            patch("video_processing.processor.os.path.getsize", return_value=1024),
        ):
            with patch.object(VideoProcessor, "generate_thumbnail", return_value="/tmp/thumb.jpg"):
                vp.concatenate_videos(
                    ["/tmp/a.mp4", "/tmp/b.mp4", "/tmp/c.mp4"],
                    str(tmp_path / "output.mp4"),
                )

        # 验证 concat 文件格式
        assert "concat" in written_content
        lines = written_content["concat"].strip().split("\n")
        assert len(lines) == 3
        assert lines[0].startswith("file '")
        assert "a.mp4'" in lines[0]
        assert "b.mp4'" in lines[1]
        assert "c.mp4'" in lines[2]
        # 使用绝对路径
        first_path = lines[0].replace("file '", "").rstrip("'")
        assert os.path.isabs(first_path)

    def test_concat_creates_output_directory(self, tmp_path):
        """输出目录不存在时自动创建."""
        vp = VideoProcessor(temp_dir=str(tmp_path))

        out_dir = tmp_path / "deep" / "output"
        out_file = out_dir / "result.mp4"

        mock_node = MagicMock()
        mock_output = MagicMock()
        mock_overwrite = MagicMock()
        mock_node.output.return_value = mock_output
        mock_output.overwrite_output.return_value = mock_overwrite

        mock_probe = MagicMock(
            return_value={
                "streams": [{"codec_type": "video", "width": 1920, "height": 1080, "r_frame_rate": "25/1"}],
                "format": {"duration": "5.0", "bit_rate": "1000000"},
            }
        )

        with (
            patch("video_processing.processor.ffmpeg.input", return_value=mock_node),
            patch("video_processing.processor.ffmpeg.probe", mock_probe),
            patch("video_processing.processor.os.path.getsize", return_value=1024),
        ):
            with patch.object(VideoProcessor, "generate_thumbnail", return_value=str(out_dir / "thumb.jpg")):
                vp.concatenate_videos(["/tmp/a.mp4"], str(out_file))

        assert out_dir.exists()
        assert out_dir.is_dir()
