"""ass_subtitle_builder 单元测试 - wave169

覆盖：
- hex_to_ass_color 颜色转换
- position_to_ass_alignment 位置对齐映射
- build_ass_style Style行构建
- escape_ass_text 文本转义
- format_ass_time 时间格式化
- build_ass_content 完整ASS内容生成
"""

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,
)

# ============================================================
# hex_to_ass_color
# ============================================================


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

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

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

    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_prefix(self):
        assert hex_to_ass_color("FF0000") == "&H0000FF"

    def test_mixed_case(self):
        assert hex_to_ass_color("#aBcDeF") == "&HEFCDAB"

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

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

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

    def test_uppercase_output(self):
        result = hex_to_ass_color("#abcdef")
        assert result == result.upper()


# ============================================================
# position_to_ass_alignment
# ============================================================


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_defaults_bottom(self):
        assert position_to_ass_alignment("unknown") == 2

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


# ============================================================
# build_ass_style
# ============================================================


class TestBuildAssStyle:
    def test_minimal_style(self):
        result = build_ass_style("Default")
        assert result.startswith("Style: Default,")

    def test_contains_font_name(self):
        result = build_ass_style("S1", font_name="Arial")
        assert "Arial" in result

    def test_contains_font_size(self):
        result = build_ass_style("S1", font_size=36)
        # Style行格式：Name, Fontname, Fontsize, ...（36*1.35=48.6→49）
        parts = result.split(",")
        assert parts[2] == "49"

    def test_bold_true(self):
        result = build_ass_style("S1", bold=True)
        parts = result.split(",")
        # Bold 是第7个字段（索引7）
        assert parts[7] == "-1"

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

    def test_italic_true(self):
        result = build_ass_style("S1", italic=True)
        parts = result.split(",")
        # Italic 是第8个字段（索引8）
        assert parts[8] == "-1"

    def test_italic_false(self):
        result = build_ass_style("S1", italic=False)
        parts = result.split(",")
        assert parts[8] == "0"

    def test_alignment(self):
        result = build_ass_style("S1", alignment=5)
        parts = result.split(",")
        # Alignment 是第18个字段（索引18）
        assert parts[18] == "5"

    def test_outline_width(self):
        result = build_ass_style("S1", outline_width=3.0)
        parts = result.split(",")
        # Outline 是第16个字段（索引16）
        assert parts[16] == "3.0"

    def test_margins(self):
        result = build_ass_style("S1", margin_l=10, margin_r=20, margin_v=30)
        parts = result.split(",")
        assert parts[19] == "10"  # MarginL
        assert parts[20] == "20"  # MarginR
        assert parts[21] == "30"  # MarginV

    def test_shadow_with_blur(self):
        result = build_ass_style("S1", shadow_blur=2.0, shadow_offset=(3, 5))
        parts = result.split(",")
        # Shadow 深度 = shadow_offset[1] when blur > 0
        assert parts[17] == "5"

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

    def test_primary_color(self):
        result = build_ass_style("S1", primary_color="&H00FFFFFF")
        parts = result.split(",")
        assert parts[3] == "&H00FFFFFF"

    def test_outline_color(self):
        result = build_ass_style("S1", outline_color="&H000000FF")
        parts = result.split(",")
        assert parts[5] == "&H000000FF"

    def test_22_fields(self):
        # ASS Style 行应有23个字段（Style: 前缀 + 22个逗号分隔字段）
        result = build_ass_style("Default")
        parts = result.split(",")
        assert len(parts) >= 22  # 至少22个字段


# ============================================================
# escape_ass_text
# ============================================================


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

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

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

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

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

    def test_opening_brace_escaped(self):
        assert escape_ass_text("{hello") == "(hello"

    def test_closing_brace_escaped(self):
        assert escape_ass_text("hello}") == "hello)"

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

    def test_mixed_newlines_and_braces(self):
        result = escape_ass_text("line1\n{tag}line2")
        assert result == "line1\\N(tag)line2"

    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_backslash_n_in_input(self):
        # 文本里本身有 \n 字符串（不是换行符）
        result = escape_ass_text("\\n")
        assert result == "\\n"  # 不变，因为不是实际换行符


# ============================================================
# format_ass_time
# ============================================================


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(90.0) == "0:01:30.00"

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

    def test_multi_hours(self):
        assert format_ass_time(7384.25) == "2:03:04.25"

    def test_two_decimal_places(self):
        result = format_ass_time(1.234)
        # 两位小数
        assert result.endswith(".23") or result.endswith(".24")

    def test_minutes_two_digits(self):
        result = format_ass_time(65.0)
        parts = result.split(":")
        assert len(parts[1]) == 2
        assert parts[1] == "01"

    def test_seconds_two_digits_before_decimal(self):
        result = format_ass_time(5.0)
        parts = result.split(":")
        sec_part = parts[2]
        assert sec_part.startswith("05")

    def test_float_input(self):
        assert format_ass_time(123.45) == "0:02:03.45"

    def test_exactly_one_hour(self):
        assert format_ass_time(3600.0) == "1:00:00.00"


# ============================================================
# build_ass_content
# ============================================================


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_only(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="Test Title",
        )
        assert result != ""
        assert "[Script Info]" in result
        assert "PlayResX: 1920" in result
        assert "PlayResY: 1080" in result
        assert "[V4+ Styles]" in result
        assert "[Events]" in result
        assert "TitleStyle" in result
        assert "Test Title" in result

    def test_subtitle_only(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            subtitle_text="Hello Subtitle",
        )
        assert result != ""
        assert "SubtitleStyle" in result
        assert "Hello Subtitle" in result

    def test_both_title_and_subtitle(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="Title",
            subtitle_text="Subtitle",
        )
        assert "TitleStyle" in result
        assert "SubtitleStyle" in result
        assert "Title" in result
        assert "Subtitle" in result

    def test_whitespace_title_returns_empty(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="   ",
        )
        assert result == ""

    def test_whitespace_subtitle_returns_empty(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            subtitle_text="  \n  ",
        )
        assert result == ""

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

    def test_subtitle_disabled(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            subtitle_text="Sub",
            subtitle_config={"enabled": False},
        )
        assert result == ""

    def test_title_color(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"color": "#FF0000"},
        )
        # 红色 → &H0000FF
        assert "&H0000FF" in result

    def test_title_position_top(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"position": "top"},
        )
        # top alignment = 8
        assert "TitleStyle" in result

    def test_title_position_bottom(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"position": "bottom"},
        )
        # bottom=2, 检查Style行里有2
        assert "TitleStyle" in result

    def test_subtitle_position_bottom(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            subtitle_text="S",
            subtitle_config={"position": "bottom"},
        )
        assert "SubtitleStyle" in result

    def test_title_font_size(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"size": 72},
        )
        # 在TitleStyle行里查找字体大小（字号上限已移除，72应原样保留）
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[2] == "97"  # 72*1.35=97.2→97
                break

    def test_title_font_size_frontend_field_alias(self):
        """前端传 font_size 应归一化为内部 size 字段。"""
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"font_size": 48},
        )
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[2] == "65"  # 48*1.35=64.8→65
                break

    def test_title_font_color_frontend_field_alias(self):
        """前端传 font_color 应归一化为内部 color 字段。"""
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"font_color": "#FF0000"},
        )
        # 红色 → &H0000FF
        assert "&H0000FF" in result

    def test_title_size_takes_precedence_over_font_size(self):
        """同时传 size 和 font_size 时，size 优先。"""
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"size": 56, "font_size": 28},
        )
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[2] == "76"  # 56*1.35=75.6→76
                break

    def test_title_bold(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"bold": True},
        )
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[7] == "-1"
                break

    def test_title_stroke_enabled(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"stroke": {"enabled": True, "width": 3, "color": "#000000"}},
        )
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[16] == "3.0"
                break

    def test_title_stroke_disabled(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"stroke": {"enabled": False, "width": 3}},
        )
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[16] == "0.0"
                break

    def test_title_shadow_enabled(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=5.0,
            title_text="T",
            title_config={"shadow": {"enabled": True, "blur": 2, "offset_x": 2, "offset_y": 4}},
        )
        for line in result.split("\n"):
            if line.startswith("Style: TitleStyle"):
                parts = line.split(",")
                assert parts[17] == "4"  # Shadow = offset_y
                break

    def test_dialogue_has_correct_timing(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=65.5,
            title_text="T",
        )
        # 结束时间应该是 0:01:05.50
        assert "0:01:05.50" in result

    def test_dialogue_starts_at_zero(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            subtitle_text="S",
        )
        assert "0:00:00.00" in result

    def test_contains_script_info_header(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="T",
        )
        assert "[Script Info]" in result
        assert "ScriptType: v4.00+" in result
        assert "ScaledBorderAndShadow: yes" in result

    def test_escaped_text_in_dialogue(self):
        result = build_ass_content(
            video_width=1920,
            video_height=1080,
            video_duration=10.0,
            title_text="line1\nline2",
        )
        # 换行符应被转义为 \N
        assert "\\N" in result
        assert "line1" in result
        assert "line2" in result


# ============================================================
# 常量验证
# ============================================================


class TestConstants:
    def test_margin_values(self):
        assert TITLE_MARGIN_TOP > 0
        assert TITLE_MARGIN_BOTTOM > 0
        assert TITLE_MARGIN_SIDE > 0

    def test_margins_are_integers(self):
        assert isinstance(TITLE_MARGIN_TOP, int)
        assert isinstance(TITLE_MARGIN_BOTTOM, int)
        assert isinstance(TITLE_MARGIN_SIDE, int)


# ============================================================
# _wrap_title_text 换行逻辑验证
# ============================================================


class TestWrapTitleTextFontSizeConsistency:
    """验证换行计算使用原始 font_size，与 CSS 预览一致。"""

    def test_font_size_113_uses_original_not_compensated(self):
        """font_size=113 时，每行应容纳8个字（113px字宽），而非6个字（153px字宽）。"""
        # 标题"永康拾掇脚阔头"共7个字
        # 可用宽度 = 1080 - 60 - 60 = 960px
        # 用 font_size=113：char_width=113，960/113 ≈ 8.5，每行8个字
        # 7个字 < 8个字，应该在一行内
        title = "永康拾掇脚阔头"
        result = _wrap_title_text(title, video_width=1080, font_size=113, margin_l=60, margin_r=60)
        # 不应该有换行
        assert "\\N" not in result
        assert result == title

    def test_long_title_wraps_correctly(self):
        """长标题应该按 font_size 字宽正确换行。"""
        # 16个中文字，每行8个字，应该换行为2行
        title = "永康拾掇脚阔头来一个笑一个哈哈哈"
        result = _wrap_title_text(title, video_width=1080, font_size=113, margin_l=60, margin_r=60)
        # 应该有一个换行
        assert result.count("\\N") == 1
        # 每行8个字
        lines = result.split("\\N")
        assert len(lines) == 2
        assert len(lines[0]) == 8
        assert len(lines[1]) == 8

    def test_mixed_cjk_and_ascii(self):
        """混合中英文时，英文按半角宽度计算。"""
        # "测试test" = 2个中文(2*113=226) + 4个英文(4*113*0.55=248.6) = 474.6px
        title = "测试test"
        result = _wrap_title_text(title, video_width=1080, font_size=113, margin_l=60, margin_r=60)
        # 总宽度474.6px < 960px，应该在一行内
        assert "\\N" not in result
