"""PR #312 安全债务修复 单元测试.

测试4个P1安全修复：
1. 多轨道混音：audio_path 路径安全 + 轨道数量上限
2. 视频拼接：video_path 路径安全 + 段数上限
3. 字幕渲染：字幕文件路径白名单校验
"""

import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker"))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

import pytest
from video_processing.path_security import PathSecurityError

# ── Fixtures ──────────────────────────────────────────────────────────────────


@pytest.fixture
def work_dir(tmp_path):
    return tmp_path


@pytest.fixture
def sample_audio(work_dir):
    """生成一个测试音频文件."""
    import subprocess

    path = work_dir / "test.aac"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=440:duration=1:sample_rate=44100",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=30,
    )
    return path


@pytest.fixture
def sample_video(work_dir):
    """生成一个测试视频文件."""
    import subprocess

    path = work_dir / "test.mp4"
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-f",
            "lavfi",
            "-i",
            "testsrc=duration=1:size=320x240:rate=30",
            "-f",
            "lavfi",
            "-i",
            "sine=frequency=440:duration=1:sample_rate=44100",
            "-c:v",
            "libx264",
            "-preset",
            "ultrafast",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            "-shortest",
            str(path),
        ],
        capture_output=True,
        check=True,
        timeout=60,
    )
    return path


# ══════════════════════════════════════════════════════════════════════════════
# 1. 多轨道混音安全测试
# ══════════════════════════════════════════════════════════════════════════════


class TestMultiTrackSecurity:
    """多轨道混音安全测试."""

    def test_track_count_limit_exceeded(self, work_dir, sample_audio):
        """超过最大轨道数时应截断到上限."""
        from unittest.mock import MagicMock, patch

        from video_processing.multi_track_mixer import (
            MAX_AUDIO_TRACKS,
            AudioTrack,
            MultiTrackMixConfig,
            mix_multi_track,
        )

        # 创建超过上限的轨道数
        tracks = []
        for i in range(MAX_AUDIO_TRACKS + 5):
            tracks.append(
                AudioTrack(
                    track_id=f"track_{i}",
                    track_type="sfx",
                    audio_path=str(sample_audio),
                    volume=0.5,
                )
            )

        config = MultiTrackMixConfig(tracks=tracks)

        ctx = MagicMock()
        ctx.work_dir = work_dir
        ctx.plan_id = "test_plan"

        # mock _prepare_single_track 避免实际跑ffmpeg
        with patch("video_processing.multi_track_mixer._prepare_single_track", return_value=True):
            with patch("video_processing.multi_track_mixer.run_ffmpeg"):
                import shutil

                with patch("shutil.copy2"):
                    result = mix_multi_track(ctx, sample_audio, config, 10.0)

        # 验证轨道被截断到上限
        assert len(config.tracks) == MAX_AUDIO_TRACKS
        assert result is not None

    def test_track_count_within_limit(self, work_dir, sample_audio):
        """轨道数在限制内时正常处理."""
        from unittest.mock import MagicMock, patch

        from video_processing.multi_track_mixer import (
            MAX_AUDIO_TRACKS,
            AudioTrack,
            MultiTrackMixConfig,
            mix_multi_track,
        )

        tracks = []
        for i in range(3):
            tracks.append(
                AudioTrack(
                    track_id=f"track_{i}",
                    track_type="sfx",
                    audio_path=str(sample_audio),
                    volume=0.5,
                )
            )

        config = MultiTrackMixConfig(tracks=tracks)

        ctx = MagicMock()
        ctx.work_dir = work_dir
        ctx.plan_id = "test_plan"

        with patch("video_processing.multi_track_mixer._prepare_single_track", return_value=True):
            with patch("video_processing.multi_track_mixer.run_ffmpeg"):
                result = mix_multi_track(ctx, sample_audio, config, 10.0)

        assert len(config.tracks) == 3
        assert result is not None

    def test_audio_path_traversal_attack(self, work_dir, sample_audio):
        """路径遍历攻击应被拦截."""
        from video_processing.multi_track_mixer import _validate_audio_path

        # 路径遍历
        with pytest.raises(PathSecurityError):
            _validate_audio_path("../../../etc/passwd", work_dir)

        # local:// 路径遍历
        with pytest.raises(PathSecurityError):
            _validate_audio_path("local://../../../etc/passwd", work_dir)

    def test_audio_path_allowed_extension(self, work_dir, sample_audio):
        """允许的音频扩展名应通过校验."""
        from video_processing.multi_track_mixer import _validate_audio_path

        # 在work_dir内的音频文件
        test_file = work_dir / "test.mp3"
        test_file.touch()
        _validate_audio_path(str(test_file), work_dir)  # 不应抛异常

        test_file2 = work_dir / "test.wav"
        test_file2.touch()
        _validate_audio_path(str(test_file2), work_dir)  # 不应抛异常

    def test_audio_path_disallowed_extension(self, work_dir):
        """不允许的文件扩展名应被拦截."""
        from video_processing.multi_track_mixer import _validate_audio_path

        test_file = work_dir / "test.exe"
        test_file.touch()
        with pytest.raises(PathSecurityError):
            _validate_audio_path(str(test_file), work_dir)

        test_file2 = work_dir / "test.php"
        test_file2.touch()
        with pytest.raises(PathSecurityError):
            _validate_audio_path(str(test_file2), work_dir)

    def test_audio_path_empty(self, work_dir):
        """空路径应被拦截."""
        from video_processing.multi_track_mixer import _validate_audio_path

        with pytest.raises(PathSecurityError):
            _validate_audio_path("", work_dir)

        with pytest.raises(PathSecurityError):
            _validate_audio_path(None, work_dir)

    def test_audio_path_traversal_bypass_startswith(self, work_dir):
        """【P1绕过】用../构造伪work_dir前缀路径，真实路径逃逸，必须被拦截.

        漏洞：旧代码用 startswith(str(work_dir)) 比原始字符串，
        /tmp/work/../../opt/secret.aac 会通过 startswith 检查，跳过白名单校验。
        修复：用 realpath 规范化后再比较。
        """
        from video_processing.multi_track_mixer import _validate_audio_path

        evil_path = str(work_dir / "../../../../opt/secret.aac")
        with pytest.raises(PathSecurityError, match="不在允许目录"):
            _validate_audio_path(evil_path, work_dir)


# ══════════════════════════════════════════════════════════════════════════════
# 2. 视频拼接安全测试
# ══════════════════════════════════════════════════════════════════════════════


class TestConcatSecurity:
    """视频拼接安全测试."""

    def test_segment_count_limit_exceeded(self, work_dir, sample_video):
        """超过最大段数时应报错."""
        from video_processing.concat_engine import (
            MAX_CONCAT_SEGMENTS,
            ConcatConfig,
            ConcatEngine,
            ConcatSegment,
        )

        # 创建超过上限的段数
        segments = []
        for _ in range(MAX_CONCAT_SEGMENTS + 5):
            segments.append(ConcatSegment(video_path=str(sample_video)))

        config = ConcatConfig(segments=segments)
        engine = ConcatEngine(work_dir)
        output_path = work_dir / "output.mp4"

        with pytest.raises(ValueError, match="Too many concat segments"):
            engine.concat_videos(config, output_path)

    def test_segment_count_within_limit(self, work_dir, sample_video):
        """段数在限制内时正常处理."""
        from unittest.mock import patch

        from video_processing.concat_engine import (
            MAX_CONCAT_SEGMENTS,
            ConcatConfig,
            ConcatEngine,
            ConcatSegment,
        )

        segments = [
            ConcatSegment(video_path=str(sample_video)),
            ConcatSegment(video_path=str(sample_video)),
            ConcatSegment(video_path=str(sample_video)),
        ]

        config = ConcatConfig(segments=segments)
        engine = ConcatEngine(work_dir)
        output_path = work_dir / "output.mp4"

        # mock ffmpeg执行
        with patch.object(engine, "_concat_filter", return_value=output_path):
            with patch.object(engine, "_can_use_stream_copy", return_value=False):
                result = engine.concat_videos(config, output_path)

        assert result == output_path

    def test_video_path_traversal_attack(self, work_dir):
        """路径遍历攻击应被拦截."""
        from video_processing.concat_engine import _validate_video_path

        with pytest.raises(PathSecurityError):
            _validate_video_path("../../../etc/passwd", work_dir)

        with pytest.raises(PathSecurityError):
            _validate_video_path("local://../../../etc/passwd", work_dir)

    def test_video_path_allowed_extension(self, work_dir):
        """允许的视频扩展名应通过校验."""
        from video_processing.concat_engine import _validate_video_path

        for ext in [".mp4", ".mov", ".avi", ".mkv", ".webm"]:
            test_file = work_dir / f"test{ext}"
            test_file.touch()
            _validate_video_path(str(test_file), work_dir)  # 不应抛异常

    def test_video_path_disallowed_extension(self, work_dir):
        """不允许的文件扩展名应被拦截."""
        from video_processing.concat_engine import _validate_video_path

        test_file = work_dir / "test.exe"
        test_file.touch()
        with pytest.raises(PathSecurityError):
            _validate_video_path(str(test_file), work_dir)

        test_file2 = work_dir / "test.js"
        test_file2.touch()
        with pytest.raises(PathSecurityError):
            _validate_video_path(str(test_file2), work_dir)

    def test_video_path_empty(self, work_dir):
        """空路径应被拦截."""
        from video_processing.concat_engine import _validate_video_path

        with pytest.raises(PathSecurityError):
            _validate_video_path("", work_dir)

        with pytest.raises(PathSecurityError):
            _validate_video_path(None, work_dir)

    def test_video_path_traversal_bypass_startswith(self, work_dir):
        """【P1绕过】视频路径../遍历绕过startswith检查，必须被拦截.

        漏洞：旧代码用 startswith(str(work_dir)) 比原始字符串，
        /tmp/work/../../opt/secret.mp4 会通过 startswith 检查，跳过白名单校验。
        修复：用 realpath 规范化后再比较。
        """
        from video_processing.concat_engine import _validate_video_path

        evil_path = str(work_dir / "../../../../opt/secret.mp4")
        with pytest.raises(PathSecurityError, match="不在允许目录"):
            _validate_video_path(evil_path, work_dir)

    def test_invalid_segments_skipped(self, work_dir, sample_video):
        """路径不安全的片段应被跳过."""
        from unittest.mock import patch

        from video_processing.concat_engine import (
            ConcatConfig,
            ConcatEngine,
            ConcatSegment,
        )

        segments = [
            ConcatSegment(video_path=str(sample_video)),
            ConcatSegment(video_path="../../../etc/passwd"),  # 不安全路径
            ConcatSegment(video_path=str(sample_video)),
        ]

        config = ConcatConfig(segments=segments)
        engine = ConcatEngine(work_dir)
        output_path = work_dir / "output.mp4"

        with patch.object(engine, "_concat_filter", return_value=output_path):
            with patch.object(engine, "_can_use_stream_copy", return_value=False):
                result = engine.concat_videos(config, output_path)

        # 验证只有2个安全片段保留
        assert len(config.segments) == 2
        assert result == output_path


# ══════════════════════════════════════════════════════════════════════════════
# 3. 字幕渲染安全测试
# ══════════════════════════════════════════════════════════════════════════════


class TestSubtitleSecurity:
    """字幕渲染安全测试."""

    def test_subtitle_path_traversal_attack(self, work_dir):
        """路径遍历攻击应被拦截."""
        from video_processing.subtitle_render_engine import _validate_subtitle_path

        with pytest.raises(PathSecurityError):
            _validate_subtitle_path("../../../etc/passwd", work_dir)

        with pytest.raises(PathSecurityError):
            _validate_subtitle_path("local://../../../etc/shadow", work_dir)

    def test_subtitle_path_allowed_extension(self, work_dir):
        """允许的字幕扩展名应通过校验."""
        from video_processing.subtitle_render_engine import _validate_subtitle_path

        for ext in [".srt", ".ass", ".vtt", ".sub"]:
            test_file = work_dir / f"test{ext}"
            test_file.touch()
            _validate_subtitle_path(str(test_file), work_dir)  # 不应抛异常

    def test_subtitle_path_disallowed_extension(self, work_dir):
        """不允许的文件扩展名应被拦截."""
        from video_processing.subtitle_render_engine import _validate_subtitle_path

        test_file = work_dir / "test.exe"
        test_file.touch()
        with pytest.raises(PathSecurityError):
            _validate_subtitle_path(str(test_file), work_dir)

        test_file2 = work_dir / "test.mp4"
        test_file2.touch()
        with pytest.raises(PathSecurityError):
            _validate_subtitle_path(str(test_file2), work_dir)

    def test_subtitle_remote_url_blocked(self, work_dir):
        """远程URL字幕应被拦截."""
        from video_processing.subtitle_render_engine import _validate_subtitle_path

        with pytest.raises(PathSecurityError, match="远程URL"):
            _validate_subtitle_path("http://evil.com/evil.ass", work_dir)

        with pytest.raises(PathSecurityError, match="远程URL"):
            _validate_subtitle_path("https://evil.com/evil.srt", work_dir)

    def test_subtitle_path_empty(self, work_dir):
        """空路径应被拦截."""
        from video_processing.subtitle_render_engine import _validate_subtitle_path

        with pytest.raises(PathSecurityError):
            _validate_subtitle_path("", work_dir)

        with pytest.raises(PathSecurityError):
            _validate_subtitle_path(None, work_dir)

    def test_build_filter_with_safe_path(self, work_dir):
        """安全路径应正常生成滤镜字符串."""
        from video_processing.subtitle_render_engine import build_subtitle_filter

        ass_file = work_dir / "subtitle.ass"
        ass_file.write_text("test", encoding="utf-8")

        result = build_subtitle_filter(ass_file, work_dir=work_dir)
        assert "subtitles=" in result
        assert "subtitle.ass" in result
        assert "[subtitled]" in result

    def test_build_filter_with_unsafe_path_raises(self, work_dir):
        """不安全路径应抛出异常."""
        from video_processing.subtitle_render_engine import build_subtitle_filter

        with pytest.raises(PathSecurityError):
            build_subtitle_filter("../../../etc/passwd", work_dir=work_dir)

    def test_build_filter_work_dir_required(self, work_dir):
        """不传work_dir时必须报错（防止自证清白绕过）."""
        from video_processing.subtitle_render_engine import build_subtitle_filter

        ass_file = work_dir / "sub.ass"
        ass_file.write_text("test", encoding="utf-8")

        # 不传 work_dir 必须报错
        with pytest.raises(PathSecurityError, match="work_dir"):
            build_subtitle_filter(ass_file)  # type: ignore[call-arg]

        # 传 None 也必须报错
        with pytest.raises(PathSecurityError, match="work_dir"):
            build_subtitle_filter(ass_file, work_dir=None)  # type: ignore[arg-type]

        # 传空字符串也必须报错
        with pytest.raises(PathSecurityError, match="work_dir"):
            build_subtitle_filter(ass_file, work_dir="")

    def test_subtitle_path_traversal_bypass_startswith(self, work_dir):
        """【P1绕过】字幕路径../遍历绕过startswith检查，必须被拦截."""
        from video_processing.subtitle_render_engine import _validate_subtitle_path

        evil_path = str(work_dir / "../../../../opt/secret.srt")
        with pytest.raises(PathSecurityError, match="不在允许目录"):
            _validate_subtitle_path(evil_path, work_dir)
