"""sticker_config 单元测试 - wave167

覆盖：
- ImageStickerConfig 数据类 / from_dict / has_time_range / end_time
- TextStickerConfig 数据类 / from_dict / has_background / has_time_range
- StickerOverlayResult 数据类
- resolve_sticker_position 位置解析（9宫格/自定义坐标/单位转换/钳制）
- parse_stickers_from_config 贴纸列表解析
- get_sticker_categories 分类列表
- 常量验证（POSITION_PRESETS / STICKER_CATEGORIES）
"""

import pytest

from packages.domain.sticker_config import (
    POSITION_PRESETS,
    STICKER_CATEGORIES,
    ImageStickerConfig,
    StickerOverlayResult,
    TextStickerConfig,
    get_sticker_categories,
    parse_stickers_from_config,
    resolve_sticker_position,
)

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


class TestConstants:
    def test_nine_position_presets(self):
        assert len(POSITION_PRESETS) == 9

    def test_positions_in_normalized_range(self):
        for name, (x, y) in POSITION_PRESETS.items():
            assert 0.0 <= x <= 1.0, f"{name} x out of range: {x}"
            assert 0.0 <= y <= 1.0, f"{name} y out of range: {y}"

    def test_top_left_is_near_zero(self):
        x, y = POSITION_PRESETS["top_left"]
        assert x < 0.1
        assert y < 0.1

    def test_bottom_right_is_near_one(self):
        x, y = POSITION_PRESETS["bottom_right"]
        assert x > 0.9
        assert y > 0.9

    def test_center_is_middle(self):
        x, y = POSITION_PRESETS["center"]
        assert x == 0.5
        assert y == 0.5

    def test_sticker_categories_not_empty(self):
        assert len(STICKER_CATEGORIES) >= 3

    def test_sticker_categories_format(self):
        for cat in STICKER_CATEGORIES:
            assert len(cat) == 2
            assert isinstance(cat[0], str)
            assert isinstance(cat[1], str)


# ============================================================
# ImageStickerConfig
# ============================================================


class TestImageStickerConfigDefaults:
    def test_default_values(self):
        cfg = ImageStickerConfig()
        assert cfg.enabled is False
        assert cfg.type == "image"
        assert cfg.position == "top_right"
        assert cfg.x is None
        assert cfg.y is None
        assert cfg.x_unit == "percent"
        assert cfg.y_unit == "percent"
        assert cfg.scale == 1.0
        assert cfg.width is None
        assert cfg.height is None
        assert cfg.opacity == 1.0
        assert cfg.start_time == 0.0
        assert cfg.duration == 0.0
        assert cfg.fade_in == 0.0
        assert cfg.fade_out == 0.0
        assert cfg.z_index == 10
        assert cfg.image_url == ""
        assert cfg.preset_id == ""

    def test_has_time_range_false_when_zero(self):
        cfg = ImageStickerConfig()
        assert cfg.has_time_range is False

    def test_has_time_range_true_when_set(self):
        cfg = ImageStickerConfig(duration=5.0)
        assert cfg.has_time_range is True

    def test_end_time_calculation(self):
        cfg = ImageStickerConfig(start_time=2.0, duration=3.0)
        assert cfg.end_time == pytest.approx(5.0)

    def test_end_time_zero_duration(self):
        cfg = ImageStickerConfig(start_time=5.0, duration=0.0)
        assert cfg.end_time == pytest.approx(5.0)


class TestImageStickerFromDict:
    def test_none_returns_default(self):
        cfg = ImageStickerConfig.from_dict(None)
        assert cfg.enabled is False
        assert cfg.position == "top_right"

    def test_empty_dict_returns_default(self):
        cfg = ImageStickerConfig.from_dict({})
        assert cfg.enabled is False

    def test_not_dict_returns_default(self):
        cfg = ImageStickerConfig.from_dict("not_a_dict")
        assert cfg.enabled is False

    def test_enabled_true(self):
        cfg = ImageStickerConfig.from_dict({"enabled": True})
        assert cfg.enabled is True

    def test_position_custom(self):
        cfg = ImageStickerConfig.from_dict({"position": "bottom_left"})
        assert cfg.position == "bottom_left"

    def test_custom_coordinates(self):
        cfg = ImageStickerConfig.from_dict({"x": 30.0, "y": 50.0})
        assert cfg.x == 30.0
        assert cfg.y == 50.0

    def test_string_coordinates_converted(self):
        cfg = ImageStickerConfig.from_dict({"x": "25.5", "y": "75.0"})
        assert cfg.x == 25.5
        assert cfg.y == 75.0

    def test_invalid_coordinates_become_none(self):
        cfg = ImageStickerConfig.from_dict({"x": "invalid", "y": "abc"})
        assert cfg.x is None
        assert cfg.y is None

    def test_scale_clamped_min(self):
        cfg = ImageStickerConfig.from_dict({"scale": 0.001})
        assert cfg.scale == 0.01

    def test_scale_normal(self):
        cfg = ImageStickerConfig.from_dict({"scale": 1.5})
        assert cfg.scale == 1.5

    def test_opacity_clamped_high(self):
        cfg = ImageStickerConfig.from_dict({"opacity": 1.5})
        assert cfg.opacity == 1.0

    def test_opacity_clamped_low(self):
        cfg = ImageStickerConfig.from_dict({"opacity": -0.5})
        assert cfg.opacity == 0.0

    def test_opacity_normal(self):
        cfg = ImageStickerConfig.from_dict({"opacity": 0.5})
        assert cfg.opacity == 0.5

    def test_start_time_clamped_non_negative(self):
        cfg = ImageStickerConfig.from_dict({"start_time": -5.0})
        assert cfg.start_time == 0.0

    def test_duration_clamped_non_negative(self):
        cfg = ImageStickerConfig.from_dict({"duration": -3.0})
        assert cfg.duration == 0.0

    def test_fade_in_non_negative(self):
        cfg = ImageStickerConfig.from_dict({"fade_in": -1.0})
        assert cfg.fade_in == 0.0

    def test_fade_out_non_negative(self):
        cfg = ImageStickerConfig.from_dict({"fade_out": -1.0})
        assert cfg.fade_out == 0.0

    def test_z_index_default_when_zero(self):
        cfg = ImageStickerConfig.from_dict({"z_index": 0})
        # 0 or None → 10
        assert cfg.z_index == 10

    def test_z_index_custom(self):
        cfg = ImageStickerConfig.from_dict({"z_index": 5})
        assert cfg.z_index == 5

    def test_width_height(self):
        cfg = ImageStickerConfig.from_dict({"width": 200, "height": 100})
        assert cfg.width == 200
        assert cfg.height == 100

    def test_width_height_none(self):
        cfg = ImageStickerConfig.from_dict({})
        assert cfg.width is None
        assert cfg.height is None

    def test_image_url_and_preset_id(self):
        cfg = ImageStickerConfig.from_dict({"image_url": "http://img.png", "preset_id": "sticker_001"})
        assert cfg.image_url == "http://img.png"
        assert cfg.preset_id == "sticker_001"

    def test_units_custom(self):
        cfg = ImageStickerConfig.from_dict({"x_unit": "pixel", "y_unit": "pixel"})
        assert cfg.x_unit == "pixel"
        assert cfg.y_unit == "pixel"

    def test_invalid_numeric_values_use_default(self):
        cfg = ImageStickerConfig.from_dict({"scale": "not_a_number"})
        assert cfg.scale == 1.0  # 回退到默认值

    def test_invalid_int_values_use_default(self):
        cfg = ImageStickerConfig.from_dict({"z_index": "invalid"})
        assert cfg.z_index == 10


# ============================================================
# TextStickerConfig
# ============================================================


class TestTextStickerConfigDefaults:
    def test_default_values(self):
        cfg = TextStickerConfig()
        assert cfg.enabled is False
        assert cfg.type == "text"
        assert cfg.text == ""
        assert cfg.font_size == 36
        assert cfg.font_color == "#FFFFFF"
        assert cfg.font_family == "sans"
        assert cfg.stroke_color == "#000000"
        assert cfg.stroke_width == 2
        assert cfg.shadow_color == "#000000"
        assert cfg.shadow_x == 2
        assert cfg.shadow_y == 2
        assert cfg.shadow_alpha == 0.5
        assert cfg.position == "center"
        assert cfg.start_time == 0.0
        assert cfg.duration == 0.0
        assert cfg.z_index == 10
        assert cfg.bg_color == ""
        assert cfg.bg_padding == 8
        assert cfg.bg_alpha == 0.8
        assert cfg.bg_corner_radius == 8

    def test_has_background_false_when_empty(self):
        cfg = TextStickerConfig()
        assert cfg.has_background is False

    def test_has_background_true_when_set(self):
        cfg = TextStickerConfig(bg_color="#FF0000")
        assert cfg.has_background is True

    def test_has_time_range_false(self):
        cfg = TextStickerConfig()
        assert cfg.has_time_range is False

    def test_has_time_range_true(self):
        cfg = TextStickerConfig(duration=10.0)
        assert cfg.has_time_range is True


class TestTextStickerFromDict:
    def test_none_returns_default(self):
        cfg = TextStickerConfig.from_dict(None)
        assert cfg.enabled is False
        assert cfg.text == ""

    def test_empty_dict_returns_default(self):
        cfg = TextStickerConfig.from_dict({})
        assert cfg.text == ""

    def test_not_dict_returns_default(self):
        cfg = TextStickerConfig.from_dict(123)
        assert cfg.text == ""

    def test_text_content(self):
        cfg = TextStickerConfig.from_dict({"text": "Hello World"})
        assert cfg.text == "Hello World"

    def test_font_size_minimum_1(self):
        cfg = TextStickerConfig.from_dict({"font_size": 0})
        assert cfg.font_size == 1

    def test_font_size_normal(self):
        cfg = TextStickerConfig.from_dict({"font_size": 48})
        assert cfg.font_size == 48

    def test_font_color(self):
        cfg = TextStickerConfig.from_dict({"font_color": "#00FF00"})
        assert cfg.font_color == "#00FF00"

    def test_stroke_width_non_negative(self):
        cfg = TextStickerConfig.from_dict({"stroke_width": -2})
        assert cfg.stroke_width == 0

    def test_stroke_width_normal(self):
        cfg = TextStickerConfig.from_dict({"stroke_width": 4})
        assert cfg.stroke_width == 4

    def test_shadow_alpha_clamped(self):
        cfg = TextStickerConfig.from_dict({"shadow_alpha": 1.5})
        assert cfg.shadow_alpha == 1.0

    def test_shadow_alpha_negative(self):
        cfg = TextStickerConfig.from_dict({"shadow_alpha": -0.5})
        assert cfg.shadow_alpha == 0.0

    def test_shadow_offset(self):
        cfg = TextStickerConfig.from_dict({"shadow_x": 5, "shadow_y": 3})
        assert cfg.shadow_x == 5
        assert cfg.shadow_y == 3

    def test_position_custom(self):
        cfg = TextStickerConfig.from_dict({"position": "top_right"})
        assert cfg.position == "top_right"

    def test_custom_coordinates(self):
        cfg = TextStickerConfig.from_dict({"x": 10, "y": 20})
        assert cfg.x == 10.0
        assert cfg.y == 20.0

    def test_start_time_non_negative(self):
        cfg = TextStickerConfig.from_dict({"start_time": -1.0})
        assert cfg.start_time == 0.0

    def test_duration_non_negative(self):
        cfg = TextStickerConfig.from_dict({"duration": -2.0})
        assert cfg.duration == 0.0

    def test_fade_in_out_non_negative(self):
        cfg = TextStickerConfig.from_dict({"fade_in": -1, "fade_out": -1})
        assert cfg.fade_in == 0.0
        assert cfg.fade_out == 0.0

    def test_z_index_default(self):
        cfg = TextStickerConfig.from_dict({})
        assert cfg.z_index == 10

    def test_bg_color(self):
        cfg = TextStickerConfig.from_dict({"bg_color": "#0000FF"})
        assert cfg.bg_color == "#0000FF"

    def test_bg_padding_non_negative(self):
        cfg = TextStickerConfig.from_dict({"bg_padding": -5})
        assert cfg.bg_padding == 0

    def test_bg_alpha_clamped(self):
        cfg = TextStickerConfig.from_dict({"bg_alpha": 2.0})
        assert cfg.bg_alpha == 1.0

    def test_bg_corner_radius_non_negative(self):
        cfg = TextStickerConfig.from_dict({"bg_corner_radius": -3})
        assert cfg.bg_corner_radius == 0

    def test_invalid_numeric_values_use_default(self):
        cfg = TextStickerConfig.from_dict({"font_size": "big"})
        assert cfg.font_size == 36  # 默认值


# ============================================================
# StickerOverlayResult
# ============================================================


class TestStickerOverlayResult:
    def test_minimal_creation(self):
        r = StickerOverlayResult(filter_str="overlay=10:20", output_label="[out]")
        assert r.filter_str == "overlay=10:20"
        assert r.output_label == "[out]"
        assert r.extra_inputs == []

    def test_with_extra_inputs(self):
        r = StickerOverlayResult(
            filter_str="overlay",
            output_label="[out]",
            extra_inputs=["img1.png", "img2.png"],
        )
        assert len(r.extra_inputs) == 2
        assert r.extra_inputs == ["img1.png", "img2.png"]

    def test_extra_inputs_independent_lists(self):
        r1 = StickerOverlayResult(filter_str="a", output_label="[o1]")
        r2 = StickerOverlayResult(filter_str="b", output_label="[o2]")
        r1.extra_inputs.append("test.png")
        assert r2.extra_inputs == []  # 独立


# ============================================================
# resolve_sticker_position
# ============================================================


class TestResolveStickerPosition:
    CANVAS_W = 1920
    CANVAS_H = 1080

    def test_preset_top_left(self):
        x, y = resolve_sticker_position(
            "top_left",
            None,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            100,
            50,
        )
        # preset x=0.05, y=0.05 → 0.05*1920 - 50 = 96-50=46, 0.05*1080-25=54-25=29
        assert x == pytest.approx(0.05 * 1920 - 50)
        assert y == pytest.approx(0.05 * 1080 - 25)

    def test_preset_center(self):
        x, y = resolve_sticker_position(
            "center",
            None,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            200,
            100,
        )
        # center 0.5,0.5 → 贴纸左上角居中
        assert x == pytest.approx(1920 / 2 - 100)
        assert y == pytest.approx(1080 / 2 - 50)

    def test_preset_bottom_right(self):
        x, y = resolve_sticker_position(
            "bottom_right",
            None,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            100,
            50,
        )
        assert x == pytest.approx(0.95 * 1920 - 50)
        assert y == pytest.approx(0.95 * 1080 - 25)

    def test_invalid_preset_defaults_center(self):
        x, y = resolve_sticker_position(
            "invalid",
            None,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            100,
            50,
        )
        # 默认居中
        assert x == pytest.approx(1920 / 2 - 50)
        assert y == pytest.approx(1080 / 2 - 25)

    def test_custom_percent_coordinates(self):
        x, y = resolve_sticker_position(
            "top_left",
            30.0,
            70.0,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        # x=30% of 1920 = 576, y=70% of 1080 = 756
        assert x == pytest.approx(576.0)
        assert y == pytest.approx(756.0)

    def test_custom_percent_clamped_0(self):
        x, y = resolve_sticker_position(
            "center",
            -10.0,
            -5.0,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        assert x == 0.0
        assert y == 0.0

    def test_custom_percent_clamped_100(self):
        x, y = resolve_sticker_position(
            "center",
            150.0,
            120.0,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        assert x == pytest.approx(1920.0)
        assert y == pytest.approx(1080.0)

    def test_custom_pixel_coordinates(self):
        x, y = resolve_sticker_position(
            "center",
            500,
            300,
            "pixel",
            "pixel",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        assert x == pytest.approx(500.0)
        assert y == pytest.approx(300.0)

    def test_x_only_override(self):
        # 只有x覆盖，y用预设
        x, y = resolve_sticker_position(
            "top_left",
            50.0,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        assert x == pytest.approx(0.5 * 1920)  # 50%
        assert y == pytest.approx(0.05 * 1080)  # 预设 top_left

    def test_y_only_override(self):
        x, y = resolve_sticker_position(
            "top_left",
            None,
            50.0,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        assert x == pytest.approx(0.05 * 1920)
        assert y == pytest.approx(0.5 * 1080)

    def test_clamped_to_canvas_with_sticker_size(self):
        # 贴纸100x50，放在最右下角，不能超出画布
        x, y = resolve_sticker_position(
            "bottom_right",
            None,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            100,
            50,
        )
        assert x <= self.CANVAS_W - 100
        assert y <= self.CANVAS_H - 50

    def test_clamped_left_top(self):
        # 很大的负偏移，被钳制在0
        x, y = resolve_sticker_position(
            "center",
            0.0,
            0.0,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            200,
            100,
        )
        # 0%位置 + 贴纸尺寸的一半偏移 = -100, -50 → 钳制到 0, 0
        assert x == 0
        assert y == 0

    def test_zero_canvas_defaults_center(self):
        x, y = resolve_sticker_position(
            "top_left",
            50,
            50,
            "pixel",
            "pixel",
            0,
            0,
            0,
            0,
        )
        # canvas_w=0 → 回退到0.5 → 0*0.5=0
        assert x == 0.0
        assert y == 0.0

    def test_sticker_size_zero(self):
        x, y = resolve_sticker_position(
            "center",
            None,
            None,
            "percent",
            "percent",
            self.CANVAS_W,
            self.CANVAS_H,
            0,
            0,
        )
        assert x == pytest.approx(960.0)
        assert y == pytest.approx(540.0)


# ============================================================
# parse_stickers_from_config
# ============================================================


class TestParseStickersFromConfig:
    def test_none_returns_empty(self):
        assert parse_stickers_from_config(None) == []

    def test_empty_dict_returns_empty(self):
        assert parse_stickers_from_config({}) == []

    def test_stickers_list(self):
        config = {"stickers": [{"type": "text", "text": "hi"}, {"type": "image", "image_url": "a.png"}]}
        result = parse_stickers_from_config(config)
        assert len(result) == 2
        assert result[0]["type"] == "text"

    def test_stickers_empty_list(self):
        config = {"stickers": []}
        assert parse_stickers_from_config(config) == []

    def test_stickers_not_list_returns_empty(self):
        config = {"stickers": "not_a_list"}
        assert parse_stickers_from_config(config) == []

    def test_other_config_ignored(self):
        config = {"other_field": "value"}
        assert parse_stickers_from_config(config) == []


# ============================================================
# get_sticker_categories
# ============================================================


class TestGetStickerCategories:
    def test_returns_list(self):
        result = get_sticker_categories()
        assert isinstance(result, list)
        assert len(result) == len(STICKER_CATEGORIES)

    def test_returns_copy(self):
        # 修改返回值不应影响常量
        result = get_sticker_categories()
        result.append(("new", "新分类"))
        assert len(STICKER_CATEGORIES) == len(STICKER_CATEGORIES)
        # 原常量不变
        assert ("new", "新分类") not in STICKER_CATEGORIES

    def test_format(self):
        for cat in get_sticker_categories():
            assert len(cat) == 2
            assert isinstance(cat[0], str)
            assert isinstance(cat[1], str)
