"""VoiceExtractor 纯逻辑单测 — 命令构建 + 边界用例.

通过 mock run_ffmpeg 验证 FFmpeg 命令参数是否正确，
不实际执行 FFmpeg，确保测试轻量快速。
"""

from __future__ import annotations

import os
from unittest.mock import MagicMock, patch

import pytest
from worker_app.tasks.voice_extraction import VoiceExtractor


class TestVoiceExtractorExtractVoiceCommand:
    """extract_voice 命令构建测试."""

    def test_default_params_correct_command(self):
        """默认参数下 FFmpeg 命令正确."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            result = extractor.extract_voice("/tmp/input.mp4", "/tmp/output.mp3")

        assert result == "/tmp/output.mp3"
        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]

        # 基本结构验证
        assert cmd[0] == "ffmpeg"
        assert "-y" in cmd
        assert cmd[cmd.index("-i") + 1] == "/tmp/input.mp4"
        assert "-vn" in cmd  # 无视频流
        assert cmd[-1] == "/tmp/output.mp3"

        # 音频滤镜验证
        af_idx = cmd.index("-af")
        af_value = cmd[af_idx + 1]
        assert "highpass=f=200" in af_value
        assert "afftdn=bn=20" in af_value
        assert "bandpass=f=300:width_type=h:width=3000" in af_value
        assert "loudnorm" in af_value

        # 编码验证
        assert "libmp3lame" in cmd
        assert "-q:a" in cmd
        assert cmd[cmd.index("-q:a") + 1] == "2"

    def test_custom_highpass(self):
        """自定义 highpass 频率."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", highpass=500)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "highpass=f=500" in af_value

    def test_custom_bandpass_freq(self):
        """自定义 bandpass 中心频率."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", bandpass_freq=500)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "bandpass=f=500:" in af_value

    def test_custom_bandpass_width(self):
        """自定义 bandpass 宽度."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", bandpass_width=5000)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "width=5000" in af_value

    def test_custom_noise_reduction(self):
        """自定义降噪强度."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", noise_reduction=30)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "afftdn=bn=30" in af_value

    def test_filter_order_is_correct(self):
        """滤镜顺序：highpass → 降噪 → bandpass → loudnorm."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3")

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]

        hp_pos = af_value.index("highpass")
        dn_pos = af_value.index("afftdn")
        bp_pos = af_value.index("bandpass")
        ln_pos = af_value.index("loudnorm")

        assert hp_pos < dn_pos < bp_pos < ln_pos

    def test_creates_output_directory(self, tmp_path):
        """输出目录不存在时自动创建."""
        out_dir = tmp_path / "nested" / "deep"
        out_file = out_dir / "voice.mp3"

        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg"):
            extractor.extract_voice("/tmp/in.mp4", str(out_file))

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

    def test_returns_output_path(self):
        """返回值为输出路径."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg"):
            result = extractor.extract_voice("/tmp/in.mp4", "/tmp/voice.mp3")

        assert result == "/tmp/voice.mp3"


class TestVoiceExtractorExtractBackgroundCommand:
    """extract_background 命令构建测试."""

    def test_default_params_correct_command(self):
        """默认参数下 FFmpeg 命令正确."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            result = extractor.extract_background("/tmp/input.mp4", "/tmp/output.mp3")

        assert result == "/tmp/output.mp3"
        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]

        # 基本结构
        assert cmd[0] == "ffmpeg"
        assert "-y" in cmd
        assert cmd[cmd.index("-i") + 1] == "/tmp/input.mp4"
        assert "-vn" in cmd
        assert cmd[-1] == "/tmp/output.mp3"

        # 音频滤镜
        af_idx = cmd.index("-af")
        af_value = cmd[af_idx + 1]
        assert "lowpass=f=200" in af_value
        assert "loudnorm" in af_value

        # 编码
        assert "libmp3lame" in cmd

    def test_custom_lowpass_freq(self):
        """自定义 lowpass 频率."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_background("/tmp/in.mp4", "/tmp/out.mp3", lowpass=500)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "lowpass=f=500" in af_value

    def test_filter_order_background(self):
        """背景音滤镜顺序：lowpass → loudnorm."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_background("/tmp/in.mp4", "/tmp/out.mp3")

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]

        lp_pos = af_value.index("lowpass")
        ln_pos = af_value.index("loudnorm")
        assert lp_pos < ln_pos

    def test_background_creates_output_directory(self, tmp_path):
        """背景音输出目录不存在时自动创建."""
        out_dir = tmp_path / "bgm" / "tracks"
        out_file = out_dir / "bg.mp3"

        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg"):
            extractor.extract_background("/tmp/in.mp4", str(out_file))

        assert out_dir.exists()


class TestVoiceExtractorEdgeCases:
    """边界情况测试."""

    def test_zero_highpass(self):
        """highpass=0 时的行为（极端低值）."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", highpass=0)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "highpass=f=0" in af_value

    def test_zero_bandpass_freq(self):
        """bandpass_freq=0 时的极端情况."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", bandpass_freq=0)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "bandpass=f=0:" in af_value

    def test_very_high_noise_reduction(self):
        """极高降噪强度."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3", noise_reduction=100)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "afftdn=bn=100" in af_value

    def test_negative_lowpass_allowed(self):
        """lowpass 负值（由调用方保证合法性，函数不做校验）."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg") as mock_run:
            extractor.extract_background("/tmp/in.mp4", "/tmp/out.mp3", lowpass=-10)

        cmd = mock_run.call_args[0][0]
        af_value = cmd[cmd.index("-af") + 1]
        assert "lowpass=f=-10" in af_value

    def test_run_ffmpeg_propagates_error(self):
        """_run_ffmpeg 抛出异常时向上传递."""
        extractor = VoiceExtractor()
        with patch.object(VoiceExtractor, "_run_ffmpeg", side_effect=RuntimeError("FFmpeg failed")):
            with pytest.raises(RuntimeError, match="FFmpeg failed"):
                extractor.extract_voice("/tmp/in.mp4", "/tmp/out.mp3")

    def test_voice_extractor_is_static_method(self):
        """_run_ffmpeg 是静态方法，可在类上直接调用."""
        # 验证 VoiceExtractor 可以直接实例化（无需参数）
        extractor = VoiceExtractor()
        assert extractor is not None

    def test_multiple_extractions_same_instance(self):
        """同一个实例可多次执行提取."""
        extractor = VoiceExtractor()
        call_count = 0

        def fake_run(cmd):
            nonlocal call_count
            call_count += 1

        with patch.object(VoiceExtractor, "_run_ffmpeg", side_effect=fake_run):
            extractor.extract_voice("/tmp/a.mp4", "/tmp/a_voice.mp3")
            extractor.extract_background("/tmp/a.mp4", "/tmp/a_bg.mp3")

        assert call_count == 2
