"""PiP 画中画配置单测.

纯逻辑模块，覆盖：PiPLayerConfig校验、PiPConfig解析+属性、
parse_size_value尺寸解析、calculate_pip_position位置计算。
"""

from __future__ import annotations

from packages.domain.pip_config import (
    ANIMATION_FADE,
    ANIMATION_SCALE,
    ANIMATION_SLIDE_BOTTOM,
    ANIMATION_SLIDE_LEFT,
    ANIMATION_SLIDE_RIGHT,
    ANIMATION_SLIDE_TOP,
    POSITION_BOTTOM_LEFT,
    POSITION_BOTTOM_RIGHT,
    POSITION_CENTER,
    POSITION_TOP_LEFT,
    POSITION_TOP_RIGHT,
    PiPConfig,
    PiPLayerConfig,
    calculate_pip_position,
    parse_size_value,
)


class TestPiPLayerConfigDefaults:
    def test_default_source(self):
        layer = PiPLayerConfig()
        assert layer.source == ""
        assert layer.source_type == "asset_id"

    def test_default_position(self):
        layer = PiPLayerConfig()
        assert layer.position == POSITION_BOTTOM_RIGHT
        assert layer.x == 0
        assert layer.y == 0
        assert layer.margin == 20

    def test_default_size(self):
        layer = PiPLayerConfig()
        assert layer.width == "25%"
        assert layer.height == ""

    def test_default_style(self):
        layer = PiPLayerConfig()
        assert layer.opacity == 1.0
        assert layer.corner_radius == 0
        assert layer.border_width == 0
        assert layer.border_color == "white"

    def test_default_timing(self):
        layer = PiPLayerConfig()
        assert layer.start_time == 0.0
        assert layer.duration == 0.0

    def test_default_animation(self):
        layer = PiPLayerConfig()
        assert layer.animation_in == ""
        assert layer.animation_out == ""
        assert layer.animation_duration == 0.5

    def test_default_z_index(self):
        layer = PiPLayerConfig()
        assert layer.z_index == 1


class TestPiPLayerConfigValidate:
    def test_valid_with_source(self):
        layer = PiPLayerConfig(source="asset_123")
        ok, msg = layer.validate()
        assert ok is True
        assert msg == ""

    def test_empty_source_invalid(self):
        layer = PiPLayerConfig(source="")
        ok, msg = layer.validate()
        assert ok is False
        assert "source" in msg

    def test_invalid_position(self):
        layer = PiPLayerConfig(source="a", position="invalid")
        ok, msg = layer.validate()
        assert ok is False
        assert "position" in msg

    def test_custom_position_valid(self):
        layer = PiPLayerConfig(source="a", position="custom")
        ok, _ = layer.validate()
        assert ok is True

    def test_all_9_positions_valid(self):
        positions = [
            "top_left",
            "top_center",
            "top_right",
            "center_left",
            "center",
            "center_right",
            "bottom_left",
            "bottom_center",
            "bottom_right",
        ]
        for pos in positions:
            layer = PiPLayerConfig(source="a", position=pos)
            ok, _ = layer.validate()
            assert ok is True, f"position {pos} should be valid"

    def test_opacity_below_zero_invalid(self):
        layer = PiPLayerConfig(source="a", opacity=-0.1)
        ok, msg = layer.validate()
        assert ok is False
        assert "opacity" in msg

    def test_opacity_above_one_invalid(self):
        layer = PiPLayerConfig(source="a", opacity=1.5)
        ok, msg = layer.validate()
        assert ok is False
        assert "opacity" in msg

    def test_opacity_zero_valid(self):
        layer = PiPLayerConfig(source="a", opacity=0.0)
        ok, _ = layer.validate()
        assert ok is True

    def test_opacity_one_valid(self):
        layer = PiPLayerConfig(source="a", opacity=1.0)
        ok, _ = layer.validate()
        assert ok is True

    def test_negative_corner_radius_invalid(self):
        layer = PiPLayerConfig(source="a", corner_radius=-1)
        ok, msg = layer.validate()
        assert ok is False
        assert "corner_radius" in msg

    def test_negative_start_time_invalid(self):
        layer = PiPLayerConfig(source="a", start_time=-1.0)
        ok, msg = layer.validate()
        assert ok is False
        assert "start_time" in msg

    def test_negative_duration_invalid(self):
        layer = PiPLayerConfig(source="a", duration=-1.0)
        ok, msg = layer.validate()
        assert ok is False
        assert "duration" in msg

    def test_zero_duration_valid(self):
        layer = PiPLayerConfig(source="a", duration=0.0)
        ok, _ = layer.validate()
        assert ok is True

    def test_invalid_animation_in(self):
        layer = PiPLayerConfig(source="a", animation_in="invalid")
        ok, msg = layer.validate()
        assert ok is False
        assert "入场动画" in msg

    def test_invalid_animation_out(self):
        layer = PiPLayerConfig(source="a", animation_out="invalid")
        ok, msg = layer.validate()
        assert ok is False
        assert "出场动画" in msg

    def test_empty_animation_valid(self):
        layer = PiPLayerConfig(source="a", animation_in="", animation_out="")
        ok, _ = layer.validate()
        assert ok is True

    def test_all_valid_animations(self):
        anims = [
            ANIMATION_FADE,
            ANIMATION_SLIDE_LEFT,
            ANIMATION_SLIDE_RIGHT,
            ANIMATION_SLIDE_TOP,
            ANIMATION_SLIDE_BOTTOM,
            ANIMATION_SCALE,
        ]
        for anim in anims:
            layer = PiPLayerConfig(source="a", animation_in=anim, animation_out=anim)
            ok, _ = layer.validate()
            assert ok is True, f"animation {anim} should be valid"

    def test_negative_animation_duration_invalid(self):
        layer = PiPLayerConfig(source="a", animation_duration=-0.5)
        ok, msg = layer.validate()
        assert ok is False
        assert "animation_duration" in msg


class TestPiPConfigDefaults:
    def test_default_disabled(self):
        config = PiPConfig()
        assert config.enabled is False
        assert config.layers == []

    def test_default_layer_count(self):
        config = PiPConfig()
        assert config.layer_count == 0

    def test_default_max_z_index(self):
        config = PiPConfig()
        assert config.max_z_index == 0


class TestPiPConfigFromDict:
    def test_none_returns_disabled(self):
        config = PiPConfig.from_dict(None)
        assert config.enabled is False
        assert config.layer_count == 0

    def test_empty_dict_returns_disabled(self):
        config = PiPConfig.from_dict({})
        assert config.enabled is False

    def test_enabled_false_returns_disabled(self):
        config = PiPConfig.from_dict({"enabled": False})
        assert config.enabled is False

    def test_enabled_no_layers_returns_disabled(self):
        config = PiPConfig.from_dict({"enabled": True, "layers": []})
        assert config.enabled is False
        assert config.layer_count == 0

    def test_single_layer(self):
        config = PiPConfig.from_dict(
            {
                "enabled": True,
                "layers": [
                    {"source": "asset_1", "position": "top_left"},
                ],
            }
        )
        assert config.enabled is True
        assert config.layer_count == 1
        assert config.layers[0].source == "asset_1"
        assert config.layers[0].position == POSITION_TOP_LEFT

    def test_multiple_layers_sorted_by_z_index(self):
        config = PiPConfig.from_dict(
            {
                "enabled": True,
                "layers": [
                    {"source": "a", "z_index": 3},
                    {"source": "b", "z_index": 1},
                    {"source": "c", "z_index": 2},
                ],
            }
        )
        assert config.layer_count == 3
        assert config.layers[0].source == "b"  # z=1
        assert config.layers[1].source == "c"  # z=2
        assert config.layers[2].source == "a"  # z=3

    def test_invalid_layer_skipped(self):
        config = PiPConfig.from_dict(
            {
                "enabled": True,
                "layers": [
                    {"source": "valid_layer", "position": "center"},
                    {"source": "", "position": "center"},  # 空source，无效
                ],
            }
        )
        assert config.layer_count == 1
        assert config.layers[0].source == "valid_layer"

    def test_all_invalid_layers_returns_disabled(self):
        config = PiPConfig.from_dict(
            {
                "enabled": True,
                "layers": [
                    {"source": ""},  # 无效
                ],
            }
        )
        assert config.enabled is False
        assert config.layer_count == 0

    def test_layer_with_full_config(self):
        config = PiPConfig.from_dict(
            {
                "enabled": True,
                "layers": [
                    {
                        "source": "https://example.com/video.mp4",
                        "source_type": "url",
                        "position": "bottom_right",
                        "width": "30%",
                        "height": "auto",
                        "opacity": 0.8,
                        "corner_radius": 8,
                        "border_width": 2,
                        "border_color": "#00FF00",
                        "start_time": 2.5,
                        "duration": 10.0,
                        "animation_in": "fade",
                        "animation_out": "slide_right",
                        "animation_duration": 0.8,
                        "z_index": 5,
                        "margin": 30,
                    },
                ],
            }
        )
        assert config.layer_count == 1
        layer = config.layers[0]
        assert layer.source == "https://example.com/video.mp4"
        assert layer.source_type == "url"
        assert layer.width == "30%"
        assert layer.opacity == 0.8
        assert layer.corner_radius == 8
        assert layer.border_width == 2
        assert layer.start_time == 2.5
        assert layer.duration == 10.0
        assert layer.animation_in == "fade"
        assert layer.animation_out == "slide_right"
        assert layer.animation_duration == 0.8
        assert layer.z_index == 5
        assert layer.margin == 30

    def test_layer_parse_error_skipped(self):
        config = PiPConfig.from_dict(
            {
                "enabled": True,
                "layers": [
                    {"source": "ok", "opacity": "not_a_number"},  # 会抛ValueError
                    {"source": "valid"},
                ],
            }
        )
        # opacity解析失败会被跳过
        assert config.layer_count >= 1
        # 至少valid那个还在
        sources = [layer.source for layer in config.layers]
        assert "valid" in sources


class TestPiPConfigProperties:
    def test_layer_count(self):
        config = PiPConfig(
            enabled=True,
            layers=[
                PiPLayerConfig(source="a"),
                PiPLayerConfig(source="b"),
                PiPLayerConfig(source="c"),
            ],
        )
        assert config.layer_count == 3

    def test_max_z_index(self):
        config = PiPConfig(
            enabled=True,
            layers=[
                PiPLayerConfig(source="a", z_index=5),
                PiPLayerConfig(source="b", z_index=2),
                PiPLayerConfig(source="c", z_index=10),
            ],
        )
        assert config.max_z_index == 10

    def test_max_z_index_empty(self):
        config = PiPConfig(enabled=False, layers=[])
        assert config.max_z_index == 0


class TestParseSizeValue:
    def test_int_passthrough(self):
        assert parse_size_value(100, 1000) == 100

    def test_int_zero_clamped_to_1(self):
        assert parse_size_value(0, 1000) == 1

    def test_int_negative_clamped_to_1(self):
        assert parse_size_value(-10, 1000) == 1

    def test_percentage_string(self):
        assert parse_size_value("50%", 1000) == 500

    def test_percentage_25(self):
        assert parse_size_value("25%", 1920) == 480

    def test_percentage_small(self):
        assert parse_size_value("1%", 100) == 1

    def test_percentage_zero_clamped(self):
        assert parse_size_value("0%", 1000) == 1

    def test_invalid_percentage_uses_default(self):
        assert parse_size_value("abc%", 1000) == 250  # default 25% of 1000

    def test_numeric_string(self):
        assert parse_size_value("200", 1000) == 200

    def test_empty_string_uses_default(self):
        assert parse_size_value("", 1000) == 250

    def test_custom_default_pct(self):
        assert parse_size_value("invalid", 1000, default_pct=0.5) == 500

    def test_float_string(self):
        """float字符串会走int()转换路径."""
        result = parse_size_value("150.5", 1000)
        assert result >= 1  # 至少不崩


class TestCalculatePipPosition:
    def test_top_left(self):
        x, y = calculate_pip_position(POSITION_TOP_LEFT, 1920, 1080, 400, 300, margin=20)
        assert (x, y) == (20, 20)

    def test_top_right(self):
        x, y = calculate_pip_position(POSITION_TOP_RIGHT, 1920, 1080, 400, 300, margin=20)
        assert (x, y) == (1920 - 400 - 20, 20)

    def test_bottom_right(self):
        x, y = calculate_pip_position(POSITION_BOTTOM_RIGHT, 1920, 1080, 400, 300, margin=20)
        assert (x, y) == (1920 - 400 - 20, 1080 - 300 - 20)

    def test_bottom_left(self):
        x, y = calculate_pip_position(POSITION_BOTTOM_LEFT, 1920, 1080, 400, 300, margin=20)
        assert (x, y) == (20, 1080 - 300 - 20)

    def test_center(self):
        x, y = calculate_pip_position(POSITION_CENTER, 1920, 1080, 400, 300, margin=20)
        assert x == (1920 - 400) // 2
        assert y == (1080 - 300) // 2

    def test_top_center(self):
        x, y = calculate_pip_position("top_center", 1920, 1080, 400, 300, margin=20)
        assert x == (1920 - 400) // 2
        assert y == 20

    def test_bottom_center(self):
        x, y = calculate_pip_position("bottom_center", 1920, 1080, 400, 300, margin=30)
        assert x == (1920 - 400) // 2
        assert y == 1080 - 300 - 30

    def test_center_left(self):
        x, y = calculate_pip_position("center_left", 1920, 1080, 400, 300, margin=20)
        assert x == 20
        assert y == (1080 - 300) // 2

    def test_center_right(self):
        x, y = calculate_pip_position("center_right", 1920, 1080, 400, 300, margin=20)
        assert x == 1920 - 400 - 20
        assert y == (1080 - 300) // 2

    def test_custom_position_pixel_values(self):
        x, y = calculate_pip_position("custom", 1920, 1080, 400, 300, custom_x=100, custom_y=200)
        assert (x, y) == (100, 200)

    def test_custom_position_percentage(self):
        x, y = calculate_pip_position("custom", 1920, 1080, 400, 300, custom_x="10%", custom_y="20%")
        assert x == 192  # 10% of 1920
        assert y == 216  # 20% of 1080

    def test_different_margin(self):
        x, y = calculate_pip_position(POSITION_TOP_LEFT, 1920, 1080, 400, 300, margin=50)
        assert (x, y) == (50, 50)

    def test_invalid_position_defaults_to_bottom_right(self):
        x, y = calculate_pip_position("invalid_pos", 1920, 1080, 400, 300, margin=20)
        assert (x, y) == (1920 - 400 - 20, 1080 - 300 - 20)

    def test_small_output_size(self):
        x, y = calculate_pip_position(POSITION_CENTER, 100, 100, 50, 50, margin=5)
        assert x == 25
        assert y == 25
