"""字幕生成器 + Mock ASR 单元测试。"""

import tempfile
from pathlib import Path

import pytest

from apps.worker.video_processing.subtitle_generator import (
    _wrap_text,
    generate_ass_from_timeline,
)
from packages.adapters.asr.mock_asr_service import MockASRService
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
from packages.ports.asr_service import ASRServiceError


class TestMockASRService:
    def test_transcribe_with_mock_text(self):
        service = MockASRService(mock_text="你好世界！这是一段测试语音识别的文字。用来验证Mock ASR是否正常工作。")

        # 创建一个假的音频文件（mock不真的读内容）
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            f.write(b"fake audio data")
            audio_path = Path(f.name)

        try:
            timeline = service.transcribe(audio_path, language="zh")
            assert timeline is not None
            assert timeline.language == "zh"
            assert timeline.segment_count > 0
            assert timeline.total_duration > 0
            # 总字数应该对得上
            assert timeline.total_chars == len("你好世界！这是一段测试语音识别的文字。用来验证Mock ASR是否正常工作。")
        finally:
            audio_path.unlink()

    def test_transcribe_file_not_found(self):
        service = MockASRService()
        with pytest.raises(ASRServiceError):
            service.transcribe(Path("/nonexistent/audio.wav"))

    def test_transcribe_with_word_timestamps(self):
        service = MockASRService(mock_text="你好世界！")

        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            f.write(b"fake")
            audio_path = Path(f.name)

        try:
            timeline = service.transcribe(audio_path, with_word_timestamps=True)
            # 每段应该有词级时间戳
            for seg in timeline.segments:
                if seg.words:
                    assert len(seg.words) > 0
                    assert seg.words[0].start >= seg.start
                    assert seg.words[-1].end <= seg.end
        finally:
            audio_path.unlink()

    def test_auto_detect_language(self):
        service = MockASRService(mock_text="Hello world. This is a test.")

        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            f.write(b"fake")
            audio_path = Path(f.name)

        try:
            timeline = service.transcribe(audio_path, language=None)
            # None 时默认 zh
            assert timeline.language == "zh"
        finally:
            audio_path.unlink()


class TestWrapText:
    def test_short_text_no_wrap(self):
        result = _wrap_text("你好世界", 20)
        assert result == ["你好世界"]

    def test_wrap_at_punctuation(self):
        result = _wrap_text("你好世界！这是一段很长的测试文字。", 10)
        assert len(result) == 2
        assert "！" in result[0]

    def test_hard_wrap_no_punctuation(self):
        result = _wrap_text("一二三四五六七八九十一二三四五六七八九十", 10)
        assert len(result) == 2
        assert len(result[0]) == 10
        assert len(result[1]) == 10

    def test_exact_length(self):
        result = _wrap_text("一二三四五六七八九十", 10)
        assert len(result) == 1


class TestGenerateAssFromTimeline:
    def test_generate_basic(self):
        timeline = SubtitleTimeline(
            segments=[
                SubtitleSegment(text="你好世界", start=0.0, end=2.0),
                SubtitleSegment(text="这是测试", start=2.0, end=4.0),
            ],
            language="zh",
            total_duration=4.0,
        )

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "test.ass"
            result = generate_ass_from_timeline(
                output_path,
                timeline,
                video_width=1920,
                video_height=1080,
            )

            assert result.exists()
            content = result.read_text(encoding="utf-8")
            assert "[Script Info]" in content
            assert "[V4+ Styles]" in content
            assert "[Events]" in content
            assert "你好世界" in content
            assert "这是测试" in content
            assert "PlayResX: 1920" in content
            assert "PlayResY: 1080" in content

    def test_empty_timeline(self):
        timeline = SubtitleTimeline(segments=[])

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "empty.ass"
            result = generate_ass_from_timeline(output_path, timeline, video_width=1920, video_height=1080)
            assert result.exists()
            assert result.read_text(encoding="utf-8") == ""

    def test_with_custom_style(self):
        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="测试", start=0, end=1)],
            total_duration=1.0,
        )

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "style.ass"
            generate_ass_from_timeline(
                output_path,
                timeline,
                video_width=1280,
                video_height=720,
                subtitle_config={
                    "font": "微软雅黑",
                    "size": 32,
                    "color": "#ff0000",
                    "position": "bottom",
                },
            )

            content = output_path.read_text(encoding="utf-8")
            assert "微软雅黑" in content
            assert "32" in content

    def test_time_format(self):
        timeline = SubtitleTimeline(
            segments=[
                SubtitleSegment(text="测试", start=0.5, end=1.25),
                SubtitleSegment(text="长字幕", start=3661.0, end=3662.5),  # 超过1小时
            ],
            total_duration=3662.5,
        )

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = Path(tmpdir) / "time.ass"
            generate_ass_from_timeline(output_path, timeline, video_width=1920, video_height=1080)

            content = output_path.read_text(encoding="utf-8")
            # 0:00:00.50 格式
            assert "0:00:00.50" in content
            assert "0:00:01.25" in content
            # 1:01:01.00 格式（3661秒 = 1小时1分1秒）
            assert "1:01:01.00" in content


# ── _hex_to_ass_color 深度测试 ──────────────────────────────────────────────


class TestHexToAssColor:
    """_hex_to_ass_color 颜色转换测试"""

    def test_standard_hex_with_hash(self):
        """带#号的标准6位HEX"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        # #RRGGBB → &HBBGGRR
        assert _hex_to_ass_color("#FF0000") == "&H0000FF"  # 红
        assert _hex_to_ass_color("#00FF00") == "&H00FF00"  # 绿
        assert _hex_to_ass_color("#0000FF") == "&HFF0000"  # 蓝

    def test_standard_hex_without_hash(self):
        """不带#号的6位HEX"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        assert _hex_to_ass_color("FF0000") == "&H0000FF"
        assert _hex_to_ass_color("00FF00") == "&H00FF00"
        assert _hex_to_ass_color("0000FF") == "&HFF0000"

    def test_white_and_black(self):
        """白色和黑色"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        assert _hex_to_ass_color("#FFFFFF") == "&HFFFFFF"  # 白
        assert _hex_to_ass_color("#000000") == "&H000000"  # 黑

    def test_lowercase_hex(self):
        """小写字母HEX"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        result = _hex_to_ass_color("#aabbcc")
        # 输出是大写的
        assert result == "&HCCBBAA"

    def test_mixed_case_hex(self):
        """大小写混合HEX"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        result = _hex_to_ass_color("#AaBbCc")
        assert result == "&HCCBBAA"

    def test_short_hex_returns_default(self):
        """长度不足6位返回默认白色"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        assert _hex_to_ass_color("#FFF") == "&H00FFFFFF"  # 3位
        assert _hex_to_ass_color("FF") == "&H00FFFFFF"  # 2位

    def test_long_hex_returns_default(self):
        """长度超过6位返回默认白色"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        assert _hex_to_ass_color("#AABBCCDD") == "&H00FFFFFF"

    def test_empty_string_returns_default(self):
        """空字符串返回默认白色"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        assert _hex_to_ass_color("") == "&H00FFFFFF"

    def test_gray_color(self):
        """灰色调"""
        from apps.worker.video_processing.subtitle_generator import _hex_to_ass_color

        assert _hex_to_ass_color("#808080") == "&H808080"


# ── _position_to_ass_alignment 测试 ────────────────────────────────────────


class TestPositionToAssAlignment:
    """_position_to_ass_alignment 位置映射测试"""

    def test_top_maps_to_8(self):
        """顶部 → 8"""
        from apps.worker.video_processing.subtitle_generator import _position_to_ass_alignment

        assert _position_to_ass_alignment("top") == 8

    def test_center_maps_to_5(self):
        """居中 → 5"""
        from apps.worker.video_processing.subtitle_generator import _position_to_ass_alignment

        assert _position_to_ass_alignment("center") == 5

    def test_bottom_maps_to_2(self):
        """底部 → 2"""
        from apps.worker.video_processing.subtitle_generator import _position_to_ass_alignment

        assert _position_to_ass_alignment("bottom") == 2

    def test_unknown_position_defaults_to_bottom(self):
        """未知位置默认底部(2)"""
        from apps.worker.video_processing.subtitle_generator import _position_to_ass_alignment

        assert _position_to_ass_alignment("left") == 2
        assert _position_to_ass_alignment("right") == 2
        assert _position_to_ass_alignment("middle") == 2

    def test_empty_string_defaults_to_bottom(self):
        """空字符串默认底部"""
        from apps.worker.video_processing.subtitle_generator import _position_to_ass_alignment

        assert _position_to_ass_alignment("") == 2

    def test_uppercase_not_matched(self):
        """大写不匹配，走默认"""
        from apps.worker.video_processing.subtitle_generator import _position_to_ass_alignment

        assert _position_to_ass_alignment("TOP") == 2
        assert _position_to_ass_alignment("CENTER") == 2


# ── _format_ass_time 测试 ──────────────────────────────────────────────────


class TestFormatAssTime:
    """_format_ass_time 时间格式化测试"""

    def test_zero_seconds(self):
        """0秒"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        assert _format_ass_time(0.0) == "0:00:00.00"

    def test_fractional_seconds(self):
        """小数秒"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        assert _format_ass_time(0.5) == "0:00:00.50"
        assert _format_ass_time(1.25) == "0:00:01.25"

    def test_whole_seconds(self):
        """整秒"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        assert _format_ass_time(5.0) == "0:00:05.00"
        assert _format_ass_time(30.0) == "0:00:30.00"

    def test_minutes_level(self):
        """分钟级"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        assert _format_ass_time(60.0) == "0:01:00.00"
        assert _format_ass_time(90.5) == "0:01:30.50"
        assert _format_ass_time(599.0) == "0:09:59.00"

    def test_hours_level(self):
        """小时级"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        assert _format_ass_time(3600.0) == "1:00:00.00"
        assert _format_ass_time(3661.5) == "1:01:01.50"
        assert _format_ass_time(7384.0) == "2:03:04.00"

    def test_centisecond_precision(self):
        """百分秒精度"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        assert _format_ass_time(1.01) == "0:00:01.01"
        assert _format_ass_time(1.99) == "0:00:01.99"

    def test_sub_centisecond_truncated(self):
        """毫秒级精度会被格式化截断到百分秒"""
        from apps.worker.video_processing.subtitle_generator import _format_ass_time

        # Python 的 %05.2f 会四舍五入
        result = _format_ass_time(1.123)
        assert result.startswith("0:00:01.")
        assert len(result.split(".")[-1]) == 2


# ── _escape_ass_text 测试 ──────────────────────────────────────────────────


class TestEscapeAssText:
    """_escape_ass_text 转义测试"""

    def test_plain_text_unchanged(self):
        """普通文本不改变"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("你好世界") == "你好世界"
        assert _escape_ass_text("Hello World") == "Hello World"

    def test_newline_to_ass_newline(self):
        """\\n 转 \\N"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("第一行\n第二行") == "第一行\\N第二行"

    def test_crlf_to_ass_newline(self):
        """\\r\\n 转 \\N"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("第一行\r\n第二行") == "第一行\\N第二行"

    def test_cr_to_ass_newline(self):
        """\\r 转 \\N"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("第一行\r第二行") == "第一行\\N第二行"

    def test_curly_braces_escaped(self):
        """花括号转圆括号（防止ASS标签注入）"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("{text}") == "(text)"
        assert _escape_ass_text("{{double}}") == "((double))"

    def test_mixed_escapes(self):
        """混合转义"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        text = "你好\n世界{测试}\r\n结束"
        result = _escape_ass_text(text)
        assert "\\N" in result
        assert "(测试)" in result
        assert "\n" not in result
        assert "{" not in result

    def test_empty_string(self):
        """空字符串"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("") == ""

    def test_only_braces(self):
        """只有花括号"""
        from apps.worker.video_processing.subtitle_generator import _escape_ass_text

        assert _escape_ass_text("{}") == "()"


# ── _wrap_text 补充测试 ────────────────────────────────────────────────────


class TestWrapTextMore:
    """_wrap_text 补充边界测试"""

    def test_empty_string_returns_empty_list(self):
        """空字符串返回空列表"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        assert _wrap_text("", 10) == [""]

    def test_single_character(self):
        """单字符"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        assert _wrap_text("好", 10) == ["好"]

    def test_max_chars_equals_one(self):
        """max_chars=1 每个字一行"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        result = _wrap_text("一二三四", 1)
        assert len(result) == 4
        assert result[0] == "一"
        assert result[1] == "二"

    def test_punctuation_in_middle(self):
        """标点在正中间优先从标点断开"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        # 前10字内有句号，优先在句号后断开
        text = "一二三四五六七八九十。后面的内容继续写下去"
        result = _wrap_text(text, 15)
        # 第一行应该包含句号
        assert "。" in result[0]

    def test_punctuation_at_start_ignored(self):
        """标点在开头位置（前半部分）不会触发断开"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "。一二三四五六七八九十"
        result = _wrap_text(text, 10)
        # 标点在第0位，不会在max_chars//2到max_chars范围内
        assert len(result) == 2
        assert len(result[0]) == 10

    def test_no_punctuation_long_text(self):
        """完全没有标点的长文本硬切"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "一二三四五六七八九十一二三四五六七八九十一二三四五"
        result = _wrap_text(text, 10)
        assert len(result) == 3
        assert len(result[0]) == 10
        assert len(result[1]) == 10
        assert len(result[2]) == 5

    def test_exactly_two_lines(self):
        """恰好两行"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "一" * 20
        result = _wrap_text(text, 10)
        assert len(result) == 2
        assert len(result[0]) == 10
        assert len(result[1]) == 10

    def test_three_lines(self):
        """三行"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "一" * 25
        result = _wrap_text(text, 10)
        assert len(result) == 3
        assert len(result[0]) == 10
        assert len(result[1]) == 10
        assert len(result[2]) == 5

    def test_multiple_punctuation_points(self):
        """多个标点，选择最后一个合适的"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "你好。再见。谢谢。抱歉。好的不行了"
        result = _wrap_text(text, 12)
        # 应该在最靠后的（在范围内的）标点处断开
        assert "。" in result[0]

    def test_english_punctuation_wrap(self):
        """英文标点也会触发换行"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "Hello world. This is a test sentence."
        result = _wrap_text(text, 20)
        assert len(result) >= 2
        assert result[0].endswith(".") or "." in result[0]

    def test_total_length_preserved(self):
        """换行后总字符数不变"""
        from apps.worker.video_processing.subtitle_generator import _wrap_text

        text = "这是一段用于测试换行功能的中文文本，包含了各种标点符号。看看效果如何？"
        result = _wrap_text(text, 10)
        assert "".join(result) == text


# ── generate_ass_from_timeline 补充测试 ────────────────────────────────────


class TestGenerateAssFromTimelineMore:
    """generate_ass_from_timeline 补充测试"""

    def test_single_segment(self):
        """单段字幕"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="你好", start=0.0, end=1.0)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "single.ass"
            generate_ass_from_timeline(output, timeline, video_width=1920, video_height=1080)
            content = output.read_text(encoding="utf-8")
            assert "Dialogue:" in content
            assert "你好" in content
            assert content.count("Dialogue:") == 1

    def test_multiple_segments(self):
        """多段字幕"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text=f"第{i}段", start=float(i), end=float(i + 1)) for i in range(5)],
            total_duration=5.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "multi.ass"
            generate_ass_from_timeline(output, timeline, video_width=1920, video_height=1080)
            content = output.read_text(encoding="utf-8")
            assert content.count("Dialogue:") == 5

    def test_long_text_auto_wrap(self):
        """长字幕自动换行"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        long_text = "这是一段非常非常长的字幕文本，用来测试自动换行功能是否正常工作。"
        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text=long_text, start=0, end=5)],
            total_duration=5.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "wrap.ass"
            generate_ass_from_timeline(
                output,
                timeline,
                video_width=1920,
                video_height=1080,
                subtitle_config={"max_chars_per_line": 10},
            )
            content = output.read_text(encoding="utf-8")
            # 应该包含 \N 换行符
            assert "\\N" in content

    def test_custom_color_hex(self):
        """自定义颜色正确转换"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="红", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "red.ass"
            generate_ass_from_timeline(
                output,
                timeline,
                video_width=1920,
                video_height=1080,
                subtitle_config={"color": "#FF0000"},
            )
            content = output.read_text(encoding="utf-8")
            # 红色 #FF0000 → &H0000FF (BBGGRR)
            assert "&H0000FF" in content

    def test_position_top(self):
        """顶部位置"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="顶部", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "top.ass"
            generate_ass_from_timeline(
                output,
                timeline,
                video_width=1920,
                video_height=1080,
                subtitle_config={"position": "top"},
            )
            content = output.read_text(encoding="utf-8")
            # 顶部对齐是 \\an8 → Style 中 Alignment=8
            assert "1,1.5,0,8," in content or ",0,8," in content

    def test_position_center(self):
        """居中位置"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="居中", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "center.ass"
            generate_ass_from_timeline(
                output,
                timeline,
                video_width=1920,
                video_height=1080,
                subtitle_config={"position": "center"},
            )
            content = output.read_text(encoding="utf-8")
            # 居中对齐是 Alignment=5
            assert ",0,5," in content

    def test_custom_font_size(self):
        """自定义字体大小"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="大", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "big.ass"
            generate_ass_from_timeline(
                output,
                timeline,
                video_width=1920,
                video_height=1080,
                subtitle_config={"size": 48},
            )
            content = output.read_text(encoding="utf-8")
            # Style 行中字号应该是48
            assert "Default,思源黑体,48," in content

    def test_720p_resolution(self):
        """720p分辨率"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="720p", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "720p.ass"
            generate_ass_from_timeline(output, timeline, video_width=1280, video_height=720)
            content = output.read_text(encoding="utf-8")
            assert "PlayResX: 1280" in content
            assert "PlayResY: 720" in content

    def test_special_characters_in_text(self):
        """字幕文本含特殊字符（花括号、换行）"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="你好{tag}\n世界", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "special.ass"
            generate_ass_from_timeline(output, timeline, video_width=1920, video_height=1080)
            content = output.read_text(encoding="utf-8")
            # 花括号被转义
            assert "(tag)" in content
            # 换行被转义成 \N
            assert "\\N" in content

    def test_output_path_creates_parent_dirs(self):
        """自动创建父目录"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="测试", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "nested" / "deep" / "out.ass"
            result = generate_ass_from_timeline(output, timeline, video_width=1920, video_height=1080)
            assert result.exists()
            assert result.parent.exists()

    def test_returns_output_path(self):
        """返回值等于输出路径"""
        import tempfile
        from pathlib import Path

        from apps.worker.video_processing.subtitle_generator import generate_ass_from_timeline
        from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline

        timeline = SubtitleTimeline(
            segments=[SubtitleSegment(text="返回", start=0, end=1)],
            total_duration=1.0,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            output = Path(tmpdir) / "ret.ass"
            result = generate_ass_from_timeline(output, timeline, video_width=1920, video_height=1080)
            assert result == output
