"""音频合并器单元测试."""

from __future__ import annotations

import os
import tempfile
from unittest.mock import MagicMock, patch

import pytest

from packages.application.tts_job.audio_merger import AudioMergeError, AudioMerger


@pytest.fixture
def sample_audio_dir():
    """创建临时目录，放几个模拟音频文件"""
    tmpdir = tempfile.mkdtemp()
    files = []
    for i in range(3):
        fpath = os.path.join(tmpdir, f"part{i}.mp3")
        with open(fpath, "wb") as f:
            f.write(f"audio_data_{i}".encode() * 100)
        files.append(fpath)
    yield files
    import shutil

    shutil.rmtree(tmpdir, ignore_errors=True)


class TestAudioMerger:
    """AudioMerger 测试"""

    def test_empty_list_raises_error(self):
        """空列表抛出 AudioMergeError"""
        merger = AudioMerger()
        with pytest.raises(AudioMergeError, match="没有可合并的音频文件"):
            merger.merge([])

    def test_single_file_returns_content(self, sample_audio_dir):
        """单文件直接返回文件内容"""
        merger = AudioMerger()
        result = merger.merge([sample_audio_dir[0]])

        with open(sample_audio_dir[0], "rb") as f:
            expected = f.read()

        assert result == expected

    def test_single_file_no_ffmpeg_needed(self, sample_audio_dir):
        """单文件不需要调用 FFmpeg"""
        with patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg:
            merger = AudioMerger()
            merger.merge([sample_audio_dir[0]])
            mock_ffmpeg.assert_not_called()

    def test_merge_multiple_files(self, sample_audio_dir):
        """多文件合并调用 FFmpeg"""
        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
        ):
            # 模拟 FFmpeg 成功：在 output_path 写点数据
            def fake_run_ffmpeg(cmd, timeout=120):
                output_idx = cmd.index("-c") + 2  # -c copy 后面是 output_path
                output_path = cmd[-1]
                with open(output_path, "wb") as f:
                    f.write(b"merged_audio_data")
                return MagicMock(stdout=b"", stderr=b"")

            mock_ffmpeg.side_effect = fake_run_ffmpeg

            merger = AudioMerger()
            result = merger.merge(sample_audio_dir)

            assert result == b"merged_audio_data"
            mock_ffmpeg.assert_called_once()

    def test_merge_concat_list_generated(self, sample_audio_dir):
        """生成正确的 concat demuxer 列表文件"""
        import subprocess

        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
        ):

            captured_list_content = []

            def fake_run_ffmpeg(cmd, timeout=120):
                # 找到 -i 参数后面的文件路径
                # 命令结构: ffmpeg -y -f concat -safe 0 -i LIST_PATH -c copy OUTPUT
                for i, arg in enumerate(cmd):
                    if arg == "-i" and i + 1 < len(cmd):
                        list_path = cmd[i + 1]
                        if list_path.endswith(".txt"):
                            with open(list_path, "r") as f:
                                captured_list_content.append(f.read())
                        break
                # 写输出文件
                output_path = cmd[-1]
                with open(output_path, "wb") as f:
                    f.write(b"fake")
                return MagicMock(stdout=b"", stderr=b"")

            mock_ffmpeg.side_effect = fake_run_ffmpeg

            merger = AudioMerger()
            merger.merge(sample_audio_dir, output_format="mp3")

            # 检查列表文件包含所有输入文件
            assert len(captured_list_content) == 1
            list_content = captured_list_content[0]
            for fpath in sample_audio_dir:
                assert fpath in list_content.replace("'\\''", "'")

    def test_merge_ffmpeg_failure_raises(self, sample_audio_dir):
        """FFmpeg 失败抛出 AudioMergeError"""
        from subprocess import CalledProcessError

        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
        ):
            mock_ffmpeg.side_effect = CalledProcessError(returncode=1, cmd=["ffmpeg"], stderr=b"error message")

            merger = AudioMerger()
            with pytest.raises(AudioMergeError, match="FFmpeg 合并失败"):
                merger.merge(sample_audio_dir)

    def test_merge_timeout_raises(self, sample_audio_dir):
        """合并超时抛出 AudioMergeError"""
        from subprocess import TimeoutExpired

        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
        ):
            mock_ffmpeg.side_effect = TimeoutExpired(cmd=["ffmpeg"], timeout=120)

            merger = AudioMerger()
            with pytest.raises(AudioMergeError, match="超时"):
                merger.merge(sample_audio_dir)

    def test_merge_cleanup_temp_dir(self, sample_audio_dir):
        """合并完成后清理临时目录"""
        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
            patch("packages.application.tts_job.audio_merger.shutil.rmtree") as mock_rmtree,
        ):

            def fake_run_ffmpeg(cmd, timeout=120):
                output_path = cmd[-1]
                with open(output_path, "wb") as f:
                    f.write(b"data")
                return MagicMock()

            mock_ffmpeg.side_effect = fake_run_ffmpeg

            merger = AudioMerger()
            merger.merge(sample_audio_dir)

            mock_rmtree.assert_called_once()
            # 第一个参数是临时目录路径
            temp_dir_path = mock_rmtree.call_args[0][0]
            assert "tts_merge_" in temp_dir_path

    def test_merge_cleanup_on_error(self, sample_audio_dir):
        """合并失败也清理临时目录"""
        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
            patch("packages.application.tts_job.audio_merger.shutil.rmtree") as mock_rmtree,
        ):
            from subprocess import CalledProcessError

            mock_ffmpeg.side_effect = CalledProcessError(1, ["ffmpeg"])

            merger = AudioMerger()
            try:
                merger.merge(sample_audio_dir)
            except AudioMergeError:
                pass

            mock_rmtree.assert_called_once()

    def test_merge_custom_output_format(self, sample_audio_dir):
        """自定义输出格式"""
        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
        ):

            def fake_run_ffmpeg(cmd, timeout=120):
                output_path = cmd[-1]
                assert output_path.endswith(".wav")
                with open(output_path, "wb") as f:
                    f.write(b"data")
                return MagicMock()

            mock_ffmpeg.side_effect = fake_run_ffmpeg

            merger = AudioMerger()
            merger.merge(sample_audio_dir, output_format="wav")

    def test_merge_two_files(self, sample_audio_dir):
        """两个文件合并"""
        with (
            patch("packages.application.tts_job.audio_merger.run_ffmpeg") as mock_ffmpeg,
            patch("packages.application.tts_job.audio_merger.FFMPEG_BIN", "ffmpeg"),
        ):

            def fake_run_ffmpeg(cmd, timeout=120):
                output_path = cmd[-1]
                with open(output_path, "wb") as f:
                    f.write(b"two_files_merged")
                return MagicMock()

            mock_ffmpeg.side_effect = fake_run_ffmpeg

            merger = AudioMerger()
            result = merger.merge(sample_audio_dir[:2])
            assert result == b"two_files_merged"
