"""ASS 字幕构建领域模型单元测试 — 纯逻辑，无文件IO."""

from __future__ import annotations

import pytest

from packages.domain.ass_subtitle_builder import (
    TITLE_MARGIN_BOTTOM,
    TITLE_MARGIN_SIDE,
    TITLE_MARGIN_TOP,
    _wrap_title_text,
    build_ass_content,
    build_ass_style,
    escape_ass_text,
    format_ass_time,
    hex_to_ass_color,
    position_to_ass_alignment,
)

# ── 颜色转换 ──────────────────────────────────────────────────────────────────


class TestHexToAssColor:
    def test_red(self):
        assert hex_to_ass_color("#FF0000") == "&H0000FF"

    def test_green(self):
        assert hex_to_ass_color("#00FF00") == "&H00FF00"

    def test_blue(self):
        assert hex_to_ass_color("#0000FF") == "&HFF0000"

    def test_white(self):
        assert hex_to_ass_color("#FFFFFF") == "&HFFFFFF"

    def test_black(self):
        assert hex_to_ass_color("#000000") == "&H000000"

    def test_without_hash(self):
        assert hex_to_ass_color("FF0000") == "&H0000FF"

    def test_lowercase(self):
        assert hex_to_ass_color("#ff0000") == "&H0000FF"

    def test_invalid_length_short(self):
        assert hex_to_ass_color("#FFF") == "&H000000"

    def test_invalid_length_long(self):
        assert hex_to_ass_color("#FFFFFFFF") == "&H000000"

    def test_empty(self):
        assert hex_to_ass_color("") == "&H000000"


# ── 位置对齐 ──────────────────────────────────────────────────────────────────


class TestPositionToAssAlignment:
    def test_top(self):
        assert position_to_ass_alignment("top") == 8

    def test_center(self):
        assert position_to_ass_alignment("center") == 5

    def test_bottom(self):
        assert position_to_ass_alignment("bottom") == 2

    def test_unknown_default_bottom(self):
        assert position_to_ass_alignment("unknown") == 2

    def test_empty_default_bottom(self):
        assert position_to_ass_alignment("") == 2


# ── Style 行构建 ─────────────────────────────────────────────────────────────


class TestBuildAssStyle:
    def test_minimal_style(self):
        result = build_ass_style("TestStyle")
        assert result.startswith("Style: TestStyle,")
        assert "Noto Sans SC" in result
        assert ",65," in result  # 48*1.35=64.8→65

    def test_custom_font_size(self):
        result = build_ass_style("Title", font_size=64)
        assert ",86," in result  # 64*1.35=86.4→86

    def test_bold_enabled(self):
        result = build_ass_style("BoldStyle", bold=True)
        parts = result.split(",")
        # Bold 是第 8 个字段（index 7）
        assert parts[7] == "-1"

    def test_bold_disabled(self):
        result = build_ass_style("NormalStyle", bold=False)
        parts = result.split(",")
        assert parts[7] == "0"

    def test_italic_enabled(self):
        result = build_ass_style("ItalicStyle", italic=True)
        parts = result.split(",")
        assert parts[8] == "-1"

    def test_alignment(self):
        result = build_ass_style("AlignBottom", alignment=2)
        parts = result.split(",")
        # Alignment 是第 19 个字段（index 18）
        assert parts[18] == "2"

    def test_margins(self):
        result = build_ass_style("MarginStyle", margin_v=100, margin_l=50, margin_r=50)
        parts = result.split(",")
        # MarginL, MarginR, MarginV 分别是 index 19, 20, 21
        assert parts[19] == "50"
        assert parts[20] == "50"
        assert parts[21] == "100"

    def test_outline_width(self):
        result = build_ass_style("OutlineStyle", outline_width=3.5)
        # Outline 是 index 16
        parts = result.split(",")
        assert parts[16] == "3.5"

    def test_shadow_with_blur(self):
        result = build_ass_style("ShadowStyle", shadow_blur=4.0, shadow_offset=(2, 3))
        parts = result.split(",")
        # Shadow 深度（纵向偏移）是 index 17
        assert parts[17] == "3"

    def test_shadow_without_blur(self):
        result = build_ass_style("NoShadowStyle", shadow_blur=0.0, shadow_offset=(2, 3))
        parts = result.split(",")
        assert parts[17] == "0"

    def test_primary_color(self):
        result = build_ass_style("ColorStyle", primary_color="&H00FFFFFF")
        # PrimaryColour 是 index 3
        parts = result.split(",")
        assert parts[3] == "&H00FFFFFF"

    def test_outline_color(self):
        result = build_ass_style("StrokeStyle", outline_color="&H00000000")
        # OutlineColour 是 index 5
        parts = result.split(",")
        assert parts[5] == "&H00000000"

    def test_field_count(self):
        """验证 Style 行有正确的字段数（23 个字段）."""
        result = build_ass_style("FullStyle")
        parts = result.split(",")
        # Style: 行有 23 个字段（去掉 "Style: " 前缀后）
        assert len(parts) == 23

    def test_font_name_mapping_siyuan(self):
        """思源黑体 → Noto Sans SC"""
        result = build_ass_style("Test", font_name="思源黑体")
        assert "Noto Sans SC" in result

    def test_font_name_mapping_apple(self):
        """苹方 → Noto Sans SC"""
        result = build_ass_style("Test", font_name="苹方")
        assert "Noto Sans SC" in result

    def test_font_name_mapping_msyh(self):
        """微软雅黑 → Noto Sans SC"""
        result = build_ass_style("Test", font_name="微软雅黑")
        assert "Noto Sans SC" in result

    def test_font_name_mapping_unknown_passthrough(self):
        """未映射字体原样使用"""
        result = build_ass_style("Test", font_name="CustomFont")
        assert "CustomFont" in result


# ── 文本转义 ──────────────────────────────────────────────────────────────────


class TestEscapeAssText:
    def test_plain_text(self):
        assert escape_ass_text("Hello World") == "Hello World"

    def test_newline_lf(self):
        assert escape_ass_text("line1\nline2") == "line1\\Nline2"

    def test_newline_crlf(self):
        assert escape_ass_text("line1\r\nline2") == "line1\\Nline2"

    def test_newline_cr(self):
        assert escape_ass_text("line1\rline2") == "line1\\Nline2"

    def test_curly_braces(self):
        assert escape_ass_text("text {tag} text") == "text (tag) text"

    def test_left_brace_only(self):
        assert escape_ass_text("{start") == "(start"

    def test_right_brace_only(self):
        assert escape_ass_text("end}") == "end)"

    def test_multiple_braces(self):
        assert escape_ass_text("{a}{b}{c}") == "(a)(b)(c)"

    def test_mixed_newline_and_braces(self):
        assert escape_ass_text("line1\n{tag}\nline2") == "line1\\N(tag)\\Nline2"

    def test_empty_string(self):
        assert escape_ass_text("") == ""

    def test_chinese_text(self):
        assert escape_ass_text("你好世界") == "你好世界"

    def test_slash_converted_to_newline(self):
        """半角斜杠 / 应转为 ASS 硬换行。"""
        assert escape_ass_text("第一行/第二行") == "第一行\\N第二行"

    def test_fullwidth_slash_converted_to_newline(self):
        """全角斜杠 ／ 应转为 ASS 硬换行。"""
        assert escape_ass_text("第一行／第二行") == "第一行\\N第二行"

    def test_mixed_slashes_and_newlines(self):
        """斜杠和换行符都应转为硬换行。"""
        result = escape_ass_text("A/B\nC／D")
        assert result == "A\\NB\\NC\\ND"


# ── 时间格式化 ────────────────────────────────────────────────────────────────


class TestFormatAssTime:
    def test_zero(self):
        assert format_ass_time(0) == "0:00:00.00"

    def test_seconds_only(self):
        assert format_ass_time(5.5) == "0:00:05.50"

    def test_minutes(self):
        assert format_ass_time(125.0) == "0:02:05.00"

    def test_hours(self):
        assert format_ass_time(3661.5) == "1:01:01.50"

    def test_one_hour_exact(self):
        assert format_ass_time(3600) == "1:00:00.00"

    def test_sub_second_precision(self):
        result = format_ass_time(1.23)
        assert result == "0:00:01.23"

    def test_59_seconds(self):
        assert format_ass_time(59.99) == "0:00:59.99"

    def test_60_seconds(self):
        assert format_ass_time(60.0) == "0:01:00.00"

    def test_90_minutes(self):
        assert format_ass_time(5400.0) == "1:30:00.00"


# ── 完整 ASS 内容生成 ─────────────────────────────────────────────────────────


class TestBuildAssContent:
    def test_no_subtitles_returns_empty(self):
        result = build_ass_content(video_width=1920, video_height=1080, video_duration=10.0)
        assert result == ""

    def test_title_disabled_returns_empty(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Title",
            title_config={"enabled": False},
        )
        assert result == ""

    def test_empty_title_text_returns_empty(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="   ",
            title_config={"enabled": True},
        )
        assert result == ""

    def test_with_title(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=30.0,
            title_text="My Title",
            title_config={"enabled": True, "color": "#FFFFFF"},
        )
        assert "[Script Info]" in result
        assert "PlayResX: 1920" in result
        assert "PlayResY: 1080" in result
        assert "[V4+ Styles]" in result
        assert "TitleStyle" in result
        assert "[Events]" in result
        assert "Dialogue:" in result
        assert "My Title" in result

    def test_with_subtitle(self):
        result = build_ass_content(
            video_width=1280,
            video_height=720,
            video_duration=15.0,
            subtitle_text="Subtitle Text",
            subtitle_config={"enabled": True},
        )
        assert "PlayResX: 1280" in result
        assert "PlayResY: 720" in result
        assert "SubtitleStyle" in result
        assert "Subtitle Text" in result

    def test_with_both_title_and_subtitle(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=60.0,
            title_text="Big Title",
            title_config={"enabled": True},
            subtitle_text="Small subtitle",
            subtitle_config={"enabled": True},
        )
        assert "TitleStyle" in result
        assert "SubtitleStyle" in result
        assert "Big Title" in result
        assert "Small subtitle" in result
        # 两个 Dialogue 行
        assert result.count("Dialogue:") == 2

    def test_title_position_bottom(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Bottom Title",
            title_config={"enabled": True, "position": "bottom"},
        )
        # 对齐方式为 2（底部居中）
        assert "TitleStyle" in result

    def test_title_with_stroke(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Stroke Title",
            title_config={
                "enabled": True,
                "stroke": {"enabled": True, "color": "#000000", "width": 3},
            },
        )
        assert "Stroke Title" in result

    def test_title_with_shadow(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Shadow Title",
            title_config={
                "enabled": True,
                "shadow": {"enabled": True, "blur": 4, "offset_x": 2, "offset_y": 3},
            },
        )
        assert "Shadow Title" in result

    def test_title_bold_default(self):
        """标题默认启用粗体."""
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Bold Title",
            title_config={"enabled": True},
        )
        # 在 TitleStyle 行中找 bold=-1
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[7] == "-1"
                break
        else:
            pytest.fail("TitleStyle not found")

    def test_subtitle_not_bold(self):
        """字幕默认不启用粗体."""
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            subtitle_text="Normal Subtitle",
            subtitle_config={"enabled": True},
        )
        for line in result.split("\n"):
            if line.startswith("Style: SubtitleStyle"):
                parts = line.split(",")
                assert parts[7] == "0"
                break
        else:
            pytest.fail("SubtitleStyle not found")

    def test_duration_format_in_dialogue(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=125.5,
            title_text="Timed",
            title_config={"enabled": True},
        )
        # 结束时间应该是 0:02:05.50
        assert "0:02:05.50" in result

    def test_title_text_escaped(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Line1\n{tag}Line2",
            title_config={"enabled": True},
        )
        assert "Line1\\N(tag)Line2" in result

    def test_default_title_enabled(self):
        """不传 enabled 时默认为 True."""
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Default Enabled",
            title_config={},
        )
        assert result != ""
        assert "Default Enabled" in result

    def test_subtitle_position_top(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            subtitle_text="Top Subtitle",
            subtitle_config={"enabled": True, "position": "top"},
        )
        assert "Top Subtitle" in result

    def test_scaled_border_and_shadow(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Test",
            title_config={"enabled": True},
        )
        assert "ScaledBorderAndShadow: yes" in result

    def test_wrap_style(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="Test",
            title_config={"enabled": True},
        )
        assert "WrapStyle: 2" in result


# ── 常量 ──────────────────────────────────────────────────────────────────────


class TestConstants:
    def test_title_margin_top(self):
        assert TITLE_MARGIN_TOP == 120

    def test_title_margin_bottom(self):
        assert TITLE_MARGIN_BOTTOM == 60

    def test_title_margin_side(self):
        assert TITLE_MARGIN_SIDE == 40


# ── 标题自动换行 ──────────────────────────────────────────────────────────────


class TestWrapTitleText:
    """测试标题自动换行逻辑。"""

    def test_short_title_no_wrap(self):
        """短标题不需要换行。"""
        result = _wrap_title_text("你好世界", video_width=1080, font_size=48)
        assert "\\N" not in result
        assert result == "你好世界"

    def test_long_title_wraps(self):
        """长标题应该被换行。"""
        # 1080p竖屏 480px宽，字号48，边距40*2
        # 可用宽度 = 480 - 40 - 40 = 400
        # 每个中文字符 = 48px，最多约 8 个字符一行
        long_title = "这是一个非常非常长的标题需要换行处理"
        result = _wrap_title_text(long_title, video_width=480, font_size=48)
        assert "\\N" in result
        lines = result.split("\\N")
        assert len(lines) >= 2

    def test_empty_text(self):
        """空文本直接返回。"""
        assert _wrap_title_text("", 1080, 48) == ""

    def test_english_half_width(self):
        """英文字符按半角计算。"""
        # 英文宽度 = 48 * 0.55 = 26.4px
        # 可用宽度 = 480 - 80 = 400, 约15个字符
        result = _wrap_title_text("a" * 20, video_width=480, font_size=48)
        # 20个英文字符 * 26.4 = 528 > 400，应该换行
        assert "\\N" in result

    def test_zero_width(self):
        """视频宽度为0时直接返回原文。"""
        assert _wrap_title_text("测试", 0, 48) == "测试"

    def test_zero_font_size(self):
        """字号为0时直接返回原文。"""
        assert _wrap_title_text("测试", 1080, 0) == "测试"

    def test_preserves_explicit_newline(self):
        """已有的 \\N 换行标记应保留，不被当普通字符算宽度。"""
        text = "第一行\\N第二行"
        result = _wrap_title_text(text, video_width=1080, font_size=48)
        assert result == text

    def test_explicit_newline_each_segment_wraps_independently(self):
        """\\N 分段后，每段各自自动换行。"""
        # 480px 宽，48px 字号，可用 400px，每段约8个中文字
        text = "这是第一段很长很长很长的内容\\N这是第二段也很长很长的内容"
        result = _wrap_title_text(text, video_width=480, font_size=48)
        # 应该有多个 \N：用户手动的 + 自动换行的
        assert "\\N" in result
        segments = result.split("\\N")
        # 至少3行（两段都需要换行）
        assert len(segments) >= 3
        # 验证包含两段的文字
        joined = result.replace("\\N", "")
        assert "第一段" in joined
        assert "第二段" in joined

    def test_multiple_explicit_newlines(self):
        """多个 \\N 分段都应保留。"""
        text = "A\\NB\\NC"
        result = _wrap_title_text(text, video_width=1080, font_size=48)
        assert result == text
        assert result.count("\\N") == 2

    def test_build_ass_content_integration(self):
        """集成测试：build_ass_content 中的标题应该自动换行。"""
        long_title = "这是一段非常长的标题文字用于测试自动换行功能是否正常工作"
        content = build_ass_content(
            video_width=480,
            video_height=854,
            video_duration=10.0,
            title_text=long_title,
            title_config={"size": 48, "position": "top"},
        )
        # 检查 Dialogue 行中包含 \N 换行
        for line in content.split("\n"):
            if "Dialogue" in line and "TitleStyle" in line:
                assert "\\N" in line, f"标题应该包含换行符: {line}"
                break
        else:
            pytest.fail("未找到 TitleStyle Dialogue 行")


class TestFontsizeCompensation:
    """ASS Fontsize 补偿系数（CSS 字号 → ASS em-square 字号）。"""

    def test_default_48_compensated_to_65(self):
        result = build_ass_style("S")
        parts = result.split(",")
        assert parts[2] == "65"  # round(48*1.35)=65

    def test_89_compensated_to_120(self):
        """实测对齐点：font_size=89 → ASS Fontsize=120。"""
        result = build_ass_style("S", font_size=89)
        parts = result.split(",")
        assert parts[2] == "120"

    def test_subtitle_also_compensated(self):
        content = build_ass_content(
            video_width=1080,
            video_height=1920,
            video_duration=5.0,
            subtitle_text="字幕",
            subtitle_config={"size": 24},
        )
        sub_line = [line for line in content.splitlines() if line.startswith("Style: SubtitleStyle")][0]
        fields = [f.strip() for f in sub_line.split(",")]
        assert fields[2] == "32"  # round(24*1.35)=32

    def test_minimum_fontsize_at_least_one(self):
        result = build_ass_style("S", font_size=0)
        parts = result.split(",")
        assert int(parts[2]) >= 1


# ── 标题自由位置拖拽（工单 #1405 方案 B）──────────────────────────────────────


def _title_style_line(content: str) -> str:
    return [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]


def _title_dialogue_line(content: str) -> str:
    return [line for line in content.splitlines() if line.startswith("Dialogue:") and "TitleStyle" in line][0]


class TestTitleFreePosition:
    """pos_x/pos_y 合法时注入 \\pos 且 Alignment=5；非法/缺失时回退原逻辑。"""

    def _base_kwargs(self):
        return dict(
            video_width=1080,
            video_height=1920,
            video_duration=8.0,
            title_text="测试标题",
        )

    def test_valid_position_injects_pos_tag_and_alignment_5(self):
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "top", "size": 36, "pos_x": 540, "pos_y": 300},
        )
        # Dialogue 文本前注入 {\pos(540,300)}
        dialogue = _title_dialogue_line(content)
        assert "{\\pos(540,300)}" in dialogue
        # TitleStyle Alignment 固定 5（\an5 中对齐，\pos 锚点为文本块中心）
        fields = [f.strip() for f in _title_style_line(content).split(",")]
        assert fields[18] == "5"

    def test_boundary_coordinates_zero_and_max_accepted(self):
        """边界值 0 和 video_width/video_height 合法（闭区间）。"""
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={"pos_x": 0, "pos_y": 1920},
        )
        assert "{\\pos(0,1920)}" in _title_dialogue_line(content)

        content2 = build_ass_content(
            **self._base_kwargs(),
            title_config={"pos_x": 1080, "pos_y": 0},
        )
        assert "{\\pos(1080,0)}" in _title_dialogue_line(content2)

    def test_no_coords_output_identical_to_before(self):
        """不传坐标 → 输出与现有断言完全一致（回归保护）。"""
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "top", "size": 36},
        )
        # 无 \pos 注入
        assert "\\pos(" not in content
        # Alignment 走 position 映射（top → 8）
        fields = [f.strip() for f in _title_style_line(content).split(",")]
        assert fields[18] == "8"

    def test_out_of_bounds_falls_back(self):
        """越界坐标 → 回退 position 三档逻辑，输出与无坐标一致。"""
        base = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "top", "size": 36},
        )
        for pos_x, pos_y in [(-1, 300), (540, -1), (1081, 300), (540, 1921), (99999, 99999)]:
            content = build_ass_content(
                **self._base_kwargs(),
                title_config={"position": "top", "size": 36, "pos_x": pos_x, "pos_y": pos_y},
            )
            assert "\\pos(" not in content, f"({pos_x},{pos_y}) should be rejected"
            assert content == base, f"({pos_x},{pos_y}) output differs from fallback"

    def test_invalid_coords_falls_back(self):
        """非法类型坐标 → 回退原逻辑。"""
        base = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "top", "size": 36},
        )
        for pos_x, pos_y in [("abc", 300), (540, None), (None, None), (True, 300), (540, False), (540.5, 300.9)]:
            content = build_ass_content(
                **self._base_kwargs(),
                title_config={"position": "top", "size": 36, "pos_x": pos_x, "pos_y": pos_y},
            )
            assert "\\pos(" not in content, f"({pos_x!r},{pos_y!r}) should be rejected"
            assert content == base, f"({pos_x!r},{pos_y!r}) output differs from fallback"

    def test_only_one_coord_falls_back(self):
        """只传 pos_x 或 pos_y → 回退原逻辑。"""
        base = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "center", "size": 36},
        )
        content_x = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "center", "size": 36, "pos_x": 540},
        )
        content_y = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "center", "size": 36, "pos_y": 300},
        )
        assert content_x == base
        assert content_y == base
        assert "\\pos(" not in content_x
        assert "\\pos(" not in content_y

    def test_position_three_levels_unchanged_without_coords(self):
        """无坐标时 top/center/bottom 三档 Alignment 输出不变。"""
        for position, expected_align in [("top", "8"), ("center", "5"), ("bottom", "2")]:
            content = build_ass_content(
                **self._base_kwargs(),
                title_config={"position": position, "size": 36},
            )
            fields = [f.strip() for f in _title_style_line(content).split(",")]
            assert fields[18] == expected_align

    def test_pos_overrides_position_alignment(self):
        """有合法坐标时，无论 position 是什么，Alignment 都固定为 5。"""
        for position in ["top", "center", "bottom"]:
            content = build_ass_content(
                **self._base_kwargs(),
                title_config={"position": position, "size": 36, "pos_x": 100, "pos_y": 200},
            )
            fields = [f.strip() for f in _title_style_line(content).split(",")]
            assert fields[18] == "5"
            assert "{\\pos(100,200)}" in _title_dialogue_line(content)

    def test_subtitle_not_affected_by_pos(self):
        """pos_x/pos_y 只影响 Title，Subtitle 输出不变。"""
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={"pos_x": 540, "pos_y": 300},
            subtitle_text="配音字幕",
            subtitle_config={"position": "bottom", "size": 24},
        )
        sub_style = [line for line in content.splitlines() if line.startswith("Style: SubtitleStyle")][0]
        sub_fields = [f.strip() for f in sub_style.split(",")]
        assert sub_fields[18] == "2"  # bottom
        sub_dialogue = [
            line for line in content.splitlines() if line.startswith("Dialogue:") and "SubtitleStyle" in line
        ][0]
        assert "\\pos(" not in sub_dialogue


class TestDefaultPositionBottom:
    """默认 position 应为 bottom（alignment=2），与前端 DEFAULT_TITLE_SETTINGS 对齐。"""

    def _base_kwargs(self):
        return dict(
            video_width=1080,
            video_height=1920,
            video_duration=10.0,
            title_text="测试标题",
        )

    def test_no_position_defaults_to_bottom_alignment(self):
        """不传 position 时，Alignment 应为 2（bottom）。"""
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={"size": 36},
        )
        style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
        fields = [f.strip() for f in style_line.split(",")]
        assert fields[18] == "2", f"Expected alignment 2 (bottom), got {fields[18]}"

    def test_no_position_no_coords_defaults_to_bottom(self):
        """不传 position 也不传坐标时，走 bottom 三档逻辑。"""
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={},
        )
        style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
        fields = [f.strip() for f in style_line.split(",")]
        assert fields[18] == "2"

    def test_explicit_top_still_works(self):
        """显式传 position='top' 仍然得到 alignment=8。"""
        content = build_ass_content(
            **self._base_kwargs(),
            title_config={"position": "top", "size": 36},
        )
        style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
        fields = [f.strip() for f in style_line.split(",")]
        assert fields[18] == "8"
