"""subtitle_style 字幕样式单测.

纯逻辑模块，覆盖：颜色转换、透明度转换、文本转义、时间格式化、
文本换行、SubtitleStyle数据类+from_dict、SubtitleSegment片段。
"""

from __future__ import annotations

from packages.domain.subtitle_style import (
    ALLOWED_SUBTITLE_EXTENSIONS,
    DEFAULT_COLOR,
    DEFAULT_FONT,
    DEFAULT_FONT_SIZE,
    DEFAULT_POSITION,
    DEFAULT_STROKE_COLOR,
    DEFAULT_STROKE_WIDTH,
    POSITION_ALIASES,
    POSITION_ALIGNMENT,
    SubtitleSegment,
    SubtitleStyle,
    escape_ass_text,
    format_ass_time,
    hex_to_ass_bgr,
    hex_to_ass_color,
    opacity_to_ass_alpha,
    wrap_text,
)


class TestHexToAssColor:
    def test_standard_red(self):
        assert hex_to_ass_color("#FF0000") == "&H000000FF"

    def test_standard_green(self):
        assert hex_to_ass_color("#00FF00") == "&H0000FF00"

    def test_standard_blue(self):
        assert hex_to_ass_color("#0000FF") == "&H00FF0000"

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

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

    def test_no_hash_prefix(self):
        assert hex_to_ass_color("FF0000") == "&H000000FF"

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

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

    def test_invalid_short_length(self):
        assert hex_to_ass_color("#FFF") == "&H00FFFFFF"

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

    def test_alpha_is_00(self):
        """默认不透明（alpha=00）."""
        result = hex_to_ass_color("#123456")
        assert result.startswith("&H00")


class TestHexToAssBgr:
    def test_red(self):
        assert hex_to_ass_bgr("#FF0000") == "0000FF"

    def test_green(self):
        assert hex_to_ass_bgr("#00FF00") == "00FF00"

    def test_blue(self):
        assert hex_to_ass_bgr("#0000FF") == "FF0000"

    def test_white(self):
        assert hex_to_ass_bgr("#FFFFFF") == "FFFFFF"

    def test_no_hash(self):
        assert hex_to_ass_bgr("FF0000") == "0000FF"

    def test_non_hex_chars_still_processed(self):
        """函数只校验长度，不校验字符有效性."""
        assert hex_to_ass_bgr("#GGGGGG") == "GGGGGG"

    def test_short_returns_white(self):
        assert hex_to_ass_bgr("#FFF") == "FFFFFF"


class TestOpacityToAssAlpha:
    def test_fully_opaque(self):
        assert opacity_to_ass_alpha(1.0) == "00"

    def test_fully_transparent(self):
        assert opacity_to_ass_alpha(0.0) == "FF"

    def test_half(self):
        assert opacity_to_ass_alpha(0.5) == "80"

    def test_quarter(self):
        assert opacity_to_ass_alpha(0.25) == "C0"

    def test_below_zero_clamped(self):
        assert opacity_to_ass_alpha(-0.5) == "FF"

    def test_above_one_clamped(self):
        assert opacity_to_ass_alpha(1.5) == "00"

    def test_zero_point_one(self):
        assert opacity_to_ass_alpha(0.1) == "E6"


class TestEscapeAssText:
    def test_plain_text(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(self):
        assert escape_ass_text("{start") == "(start"

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

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

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

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

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


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

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

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

    def test_minutes_and_seconds(self):
        assert format_ass_time(125.5) == "0:02:05.50"

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

    def test_multi_hours(self):
        assert format_ass_time(7384.5) == "2:03:04.50"

    def test_negative_becomes_zero(self):
        assert format_ass_time(-1.0) == "0:00:00.00"

    def test_two_decimal_places(self):
        assert format_ass_time(1.234) == "0:00:01.23"

    def test_minutes_two_digits(self):
        assert format_ass_time(599.0) == "0:09:59.00"

    def test_float_input(self):
        assert format_ass_time(120.5) == "0:02:00.50"


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

    def test_empty_text(self):
        result = wrap_text("", 20)
        assert result == [""]

    def test_zero_max_chars(self):
        result = wrap_text("你好世界", 0)
        assert result == ["你好世界"]

    def test_negative_max_chars(self):
        result = wrap_text("你好世界", -5)
        assert result == ["你好世界"]

    def test_exact_length(self):
        text = "一二三四五六七八九十"
        result = wrap_text(text, 10)
        assert result == [text]

    def test_long_text_wrap_at_max(self):
        text = "一二三四五六七八九十一二三四五六七八九十一二三四五"
        result = wrap_text(text, 10)
        assert len(result) == 3
        assert len(result[0]) == 10

    def test_wrap_at_punctuation(self):
        """标点在搜索范围内时，优先在标点处断开."""
        text = "一二三四五六七八，九十一二三四五六"
        result = wrap_text(text, 15)
        # 搜索范围是15到7（15//2+1=8），"，"在位置8（0-indexed），在范围内
        assert result[0].endswith("，")
        assert len(result[0]) == 9  # 包括标点

    def test_punctuation_out_of_range_breaks_at_max(self):
        """标点在搜索范围外时，在max_chars处断开."""
        text = "你好，世界！这是一个测试。"
        result = wrap_text(text, 10)
        # "！"在位置5（< 10//2 = 5，不在搜索范围6-10内）
        assert len(result[0]) == 10

    def test_multiple_lines(self):
        text = "一" * 50
        result = wrap_text(text, 10)
        assert len(result) == 5
        for line in result:
            assert len(line) <= 10

    def test_punctuation_preferred_in_range(self):
        """搜索范围内有句号时，在句号处断开而不是中间截断."""
        # 总长度12（"一二三四五六七八九十。" + "二三四五六。"）
        text = "一二三四五六七八九十。二三四五六。"
        result = wrap_text(text, 15)
        # "。"在位置10，在搜索范围15到7（8-15）内
        assert result[0] == "一二三四五六七八九十。"
        assert len(result[0]) == 11


class TestSubtitleStyleDefaults:
    def test_default_font(self):
        style = SubtitleStyle()
        assert style.font_name == DEFAULT_FONT

    def test_default_size(self):
        style = SubtitleStyle()
        assert style.font_size == DEFAULT_FONT_SIZE

    def test_default_color(self):
        style = SubtitleStyle()
        assert style.font_color == DEFAULT_COLOR

    def test_default_bold_italic(self):
        style = SubtitleStyle()
        assert style.bold is False
        assert style.italic is False

    def test_default_stroke(self):
        style = SubtitleStyle()
        assert style.stroke_enabled is True
        assert style.stroke_color == DEFAULT_STROKE_COLOR
        assert style.stroke_width == DEFAULT_STROKE_WIDTH

    def test_default_shadow(self):
        style = SubtitleStyle()
        assert style.shadow_enabled is False
        assert style.shadow_offset_x == 2
        assert style.shadow_offset_y == 2

    def test_default_background(self):
        style = SubtitleStyle()
        assert style.background_enabled is False
        assert style.background_opacity == 0.5

    def test_default_position(self):
        style = SubtitleStyle()
        assert style.position == DEFAULT_POSITION

    def test_default_margins(self):
        style = SubtitleStyle()
        assert style.margin_v == 60
        assert style.margin_l == 40
        assert style.margin_r == 40

    def test_default_animation(self):
        style = SubtitleStyle()
        assert style.fade_in == 0.0
        assert style.fade_out == 0.0
        assert style.animation_type == "none"


class TestSubtitleStyleProperties:
    def test_alignment_bottom_center(self):
        style = SubtitleStyle(position="bottom_center")
        assert style.alignment == 2

    def test_alignment_top_center(self):
        style = SubtitleStyle(position="top_center")
        assert style.alignment == 8

    def test_alignment_center(self):
        style = SubtitleStyle(position="center")
        assert style.alignment == 5

    def test_ass_font_color(self):
        style = SubtitleStyle(font_color="#FF0000")
        assert style.ass_font_color == "&H000000FF"

    def test_ass_stroke_color(self):
        style = SubtitleStyle(stroke_color="#000000")
        assert style.ass_stroke_color == "&H00000000"

    def test_ass_shadow_color(self):
        style = SubtitleStyle(shadow_color="#FFFFFF")
        assert style.ass_shadow_color == "&H00FFFFFF"

    def test_ass_background_color(self):
        style = SubtitleStyle(
            background_enabled=True,
            background_color="#000000",
            background_opacity=0.5,
        )
        # alpha=80 (50%透明), bgr=000000
        assert style.ass_background_color == "&H80000000"

    def test_ass_background_color_full_opaque(self):
        style = SubtitleStyle(background_color="#FF0000", background_opacity=1.0)
        assert style.ass_background_color == "&H000000FF"


class TestSubtitleStyleFromDict:
    def test_none_returns_default(self):
        style = SubtitleStyle.from_dict(None)
        assert style == SubtitleStyle()

    def test_empty_dict_returns_default(self):
        style = SubtitleStyle.from_dict({})
        assert style == SubtitleStyle()

    def test_not_dict_returns_default(self):
        style = SubtitleStyle.from_dict("not a dict")
        assert style == SubtitleStyle()

    def test_custom_font(self):
        style = SubtitleStyle.from_dict({"font": "微软雅黑", "size": 32})
        assert style.font_name == "微软雅黑"
        assert style.font_size == 32

    def test_custom_color(self):
        style = SubtitleStyle.from_dict({"color": "#FF0000"})
        assert style.font_color == "#FF0000"

    def test_bold_italic(self):
        style = SubtitleStyle.from_dict({"bold": True, "italic": True})
        assert style.bold is True
        assert style.italic is True

    def test_stroke_custom(self):
        style = SubtitleStyle.from_dict(
            {
                "stroke_enabled": False,
                "stroke_color": "#FF0000",
                "stroke_width": 3.0,
            }
        )
        assert style.stroke_enabled is False
        assert style.stroke_color == "#FF0000"
        assert style.stroke_width == 3.0

    def test_shadow_custom(self):
        style = SubtitleStyle.from_dict(
            {
                "shadow_enabled": True,
                "shadow_color": "#0000FF",
                "shadow_offset_x": 4,
                "shadow_offset_y": 4,
                "shadow_blur": 2.0,
            }
        )
        assert style.shadow_enabled is True
        assert style.shadow_color == "#0000FF"
        assert style.shadow_offset_x == 4
        assert style.shadow_offset_y == 4
        assert style.shadow_blur == 2.0

    def test_background_custom(self):
        style = SubtitleStyle.from_dict(
            {
                "background_enabled": True,
                "background_color": "#00FF00",
                "background_opacity": 0.8,
                "background_padding": 12,
                "background_radius": 8,
            }
        )
        assert style.background_enabled is True
        assert style.background_color == "#00FF00"
        assert style.background_opacity == 0.8
        assert style.background_padding == 12
        assert style.background_radius == 8

    def test_background_opacity_clamped(self):
        style = SubtitleStyle.from_dict({"background_opacity": -0.5})
        assert style.background_opacity == 0.0
        style2 = SubtitleStyle.from_dict({"background_opacity": 2.0})
        assert style2.background_opacity == 1.0

    def test_position_alias_top(self):
        style = SubtitleStyle.from_dict({"position": "top"})
        assert style.position == "top_center"

    def test_position_alias_bottom(self):
        style = SubtitleStyle.from_dict({"position": "bottom"})
        assert style.position == "bottom_center"

    def test_position_alias_left(self):
        style = SubtitleStyle.from_dict({"position": "left"})
        assert style.position == "middle_left"

    def test_invalid_position_falls_back(self):
        style = SubtitleStyle.from_dict({"position": "invalid_pos"})
        assert style.position == DEFAULT_POSITION

    def test_margins(self):
        style = SubtitleStyle.from_dict(
            {
                "margin_v": 80,
                "margin_l": 60,
                "margin_r": 60,
            }
        )
        assert style.margin_v == 80
        assert style.margin_l == 60
        assert style.margin_r == 60

    def test_line_spacing(self):
        style = SubtitleStyle.from_dict({"line_spacing": 4})
        assert style.line_spacing == 4

    def test_fade_in_out_clamped(self):
        style = SubtitleStyle.from_dict({"fade_in": -1.0, "fade_out": -0.5})
        assert style.fade_in == 0.0
        assert style.fade_out == 0.0

    def test_invalid_int_uses_default(self):
        style = SubtitleStyle.from_dict({"size": "not_a_number"})
        assert style.font_size == DEFAULT_FONT_SIZE

    def test_none_str_uses_default(self):
        style = SubtitleStyle.from_dict({"color": None})
        assert style.font_color == DEFAULT_COLOR

    def test_animation_type(self):
        style = SubtitleStyle.from_dict({"animation_type": "fade"})
        assert style.animation_type == "fade"


class TestSubtitleSegment:
    def test_basic_segment(self):
        seg = SubtitleSegment(start=1.0, end=3.5, text="你好世界")
        assert seg.start == 1.0
        assert seg.end == 3.5
        assert seg.text == "你好世界"
        assert seg.style_name == "Default"

    def test_custom_style(self):
        seg = SubtitleSegment(start=0.0, end=2.0, text="test", style_name="Title")
        assert seg.style_name == "Title"

    def test_duration(self):
        seg = SubtitleSegment(start=1.0, end=3.5, text="test")
        assert seg.duration == 2.5

    def test_duration_zero_when_end_before_start(self):
        seg = SubtitleSegment(start=5.0, end=3.0, text="test")
        assert seg.duration == 0.0

    def test_is_valid_true(self):
        seg = SubtitleSegment(start=1.0, end=3.0, text="hello")
        assert seg.is_valid is True

    def test_is_valid_empty_text(self):
        seg = SubtitleSegment(start=1.0, end=3.0, text="")
        assert seg.is_valid is False

    def test_is_valid_zero_duration(self):
        seg = SubtitleSegment(start=1.0, end=1.0, text="hello")
        assert seg.is_valid is False

    def test_is_valid_negative_duration(self):
        seg = SubtitleSegment(start=5.0, end=3.0, text="hello")
        assert seg.is_valid is False


class TestConstants:
    def test_position_alignment_has_9_positions(self):
        assert len(POSITION_ALIGNMENT) == 9

    def test_position_aliases_resolve_to_valid(self):
        for _alias, full in POSITION_ALIASES.items():
            assert full in POSITION_ALIGNMENT

    def test_allowed_extensions_contains_common(self):
        assert ".srt" in ALLOWED_SUBTITLE_EXTENSIONS
        assert ".ass" in ALLOWED_SUBTITLE_EXTENSIONS
        assert ".vtt" in ALLOWED_SUBTITLE_EXTENSIONS

    def test_defaults_are_valid(self):
        assert isinstance(DEFAULT_FONT, str)
        assert isinstance(DEFAULT_FONT_SIZE, int)
        assert DEFAULT_FONT_SIZE > 0
        assert DEFAULT_COLOR.startswith("#")
        assert DEFAULT_POSITION in POSITION_ALIGNMENT
