"""subtitle_style 领域模型单测 — 纯逻辑."""

from __future__ import annotations

import pytest

from packages.domain.subtitle_style import (
    ALLOWED_SUBTITLE_EXTENSIONS,
    DEFAULT_COLOR,
    DEFAULT_FONT,
    DEFAULT_FONT_SIZE,
    DEFAULT_MAX_CHARS_PER_LINE,
    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 TestConstants:
    def test_position_alignment_has_9_positions(self):
        assert len(POSITION_ALIGNMENT) == 9
        assert POSITION_ALIGNMENT["bottom_center"] == 2
        assert POSITION_ALIGNMENT["top_center"] == 8
        assert POSITION_ALIGNMENT["center"] == 5

    def test_position_aliases(self):
        assert POSITION_ALIASES["top"] == "top_center"
        assert POSITION_ALIASES["bottom"] == "bottom_center"
        assert POSITION_ALIASES["middle"] == "center"

    def test_default_values(self):
        assert DEFAULT_FONT == "思源黑体"
        assert DEFAULT_FONT_SIZE == 24
        assert DEFAULT_COLOR == "#FFFFFF"
        assert DEFAULT_POSITION == "bottom_center"

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


# ── 工具函数测试 ──────────────────────────────────────────────────────────────


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

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

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

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

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

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

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

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

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


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

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

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

    def test_invalid_length(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_above_1_clamped(self):
        assert opacity_to_ass_alpha(1.5) == "00"

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


class TestEscapeAssText:
    def test_newline_unix(self):
        assert escape_ass_text("hello\nworld") == "hello\\Nworld"

    def test_newline_windows(self):
        assert escape_ass_text("hello\r\nworld") == "hello\\Nworld"

    def test_newline_mac(self):
        assert escape_ass_text("hello\rworld") == "hello\\Nworld"

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

    def test_mixed(self):
        assert escape_ass_text("hello\n{world}\r\nend") == "hello\\N(world)\\Nend"

    def test_empty(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_minutes(self):
        assert format_ass_time(65.25) == "0:01:05.25"

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

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

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


class TestWrapText:
    def test_short_text_no_wrap(self):
        assert wrap_text("你好", 10) == ["你好"]

    def test_exact_length_no_wrap(self):
        text = "你" * 10
        result = wrap_text(text, 10)
        assert len(result) == 1
        assert len(result[0]) == 10

    def test_long_text_breaks_at_max(self):
        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_breaks_at_punctuation(self):
        # "一二三四五六。七八九十"共10字，"。"在索引6
        # max_chars=8 时，从 8 往回找到 4，会命中索引6的"。"
        text = "一二三四五六。七八九十"
        result = wrap_text(text, 8)
        assert len(result) == 2
        assert result[0] == "一二三四五六。"
        assert result[1] == "七八九十"

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

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

    def test_zero_max_chars(self):
        assert wrap_text("hello", 0) == ["hello"]

    def test_negative_max_chars(self):
        result = wrap_text("hello", -5)
        assert isinstance(result, list)
        assert len(result) == 1


# ── SubtitleStyle 测试 ───────────────────────────────────────────────────────


class TestSubtitleStyleDefaults:
    def test_default_values(self):
        style = SubtitleStyle()
        assert style.font_name == DEFAULT_FONT
        assert style.font_size == DEFAULT_FONT_SIZE
        assert style.font_color == DEFAULT_COLOR
        assert style.bold is False
        assert style.italic is False
        assert style.stroke_enabled is True
        assert style.stroke_color == DEFAULT_STROKE_COLOR
        assert style.stroke_width == DEFAULT_STROKE_WIDTH
        assert style.position == DEFAULT_POSITION
        assert style.max_chars_per_line == DEFAULT_MAX_CHARS_PER_LINE


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

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

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

    def test_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_config(self):
        style = SubtitleStyle.from_dict(
            {
                "stroke_enabled": False,
                "stroke_color": "#00FF00",
                "stroke_width": 2.0,
            }
        )
        assert style.stroke_enabled is False
        assert style.stroke_color == "#00FF00"
        assert style.stroke_width == 2.0

    def test_shadow_config(self):
        style = SubtitleStyle.from_dict(
            {
                "shadow_enabled": True,
                "shadow_color": "#111111",
                "shadow_offset_x": 4,
                "shadow_offset_y": 4,
                "shadow_blur": 1.5,
            }
        )
        assert style.shadow_enabled is True
        assert style.shadow_color == "#111111"
        assert style.shadow_offset_x == 4
        assert style.shadow_offset_y == 4
        assert style.shadow_blur == 1.5

    def test_background_config(self):
        style = SubtitleStyle.from_dict(
            {
                "background_enabled": True,
                "background_color": "#000000",
                "background_opacity": 0.7,
                "background_padding": 10,
                "background_radius": 6,
            }
        )
        assert style.background_enabled is True
        assert style.background_opacity == 0.7
        assert style.background_padding == 10

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

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

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

    def test_position_invalid_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": 50, "margin_r": 50})
        assert style.margin_v == 80
        assert style.margin_l == 50
        assert style.margin_r == 50

    def test_max_chars_per_line(self):
        style = SubtitleStyle.from_dict({"max_chars_per_line": 15})
        assert style.max_chars_per_line == 15

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

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

    def test_fade_negative_clamped(self):
        style = SubtitleStyle.from_dict({"fade_in": -1, "fade_out": -2})
        assert style.fade_in == 0.0
        assert style.fade_out == 0.0

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

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

    def test_invalid_float_falls_back(self):
        style = SubtitleStyle.from_dict({"stroke_width": "abc"})
        assert style.stroke_width == DEFAULT_STROKE_WIDTH


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_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="#00FF00")
        assert style.ass_stroke_color == "&H0000FF00"

    def test_ass_shadow_color(self):
        style = SubtitleStyle(shadow_color="#0000FF")
        assert style.ass_shadow_color == "&H00FF0000"

    def test_ass_background_color(self):
        style = SubtitleStyle(background_color="#FF0000", background_opacity=0.5)
        # alpha = 255 - 127 = 128 = 0x80, bgr of red = 0000FF
        assert style.ass_background_color == "&H800000FF"


# ── SubtitleSegment 测试 ─────────────────────────────────────────────────────


class TestSubtitleSegment:
    def test_basic(self):
        seg = SubtitleSegment(start=1.0, end=3.0, text="你好")
        assert seg.start == 1.0
        assert seg.end == 3.0
        assert seg.text == "你好"
        assert seg.style_name == "Default"

    def test_custom_style(self):
        seg = SubtitleSegment(start=0, end=2, text="hi", style_name="Title")
        assert seg.style_name == "Title"

    def test_duration(self):
        seg = SubtitleSegment(start=1.5, end=4.0, 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=0, end=2, text="hello")
        assert seg.is_valid is True

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

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