"""watermark_config 单元测试 - wave163

覆盖：
- WatermarkConfig 数据类 / from_dict / validate / has_effect
- calc_position 9宫格位置计算
- calc_scroll_x 滚动计算
- build_image_watermark_filter 图片水印滤镜
- build_text_watermark_filter 文字水印滤镜
- get_position_names / get_position_display_name
- 常量验证
"""

import pytest

from packages.domain.watermark_config import (
    DEFAULT_FONT_COLOR,
    DEFAULT_FONT_SIZE,
    DEFAULT_MARGIN_X,
    DEFAULT_MARGIN_Y,
    DEFAULT_MODE,
    DEFAULT_OPACITY,
    DEFAULT_POSITION,
    DEFAULT_SCALE,
    DEFAULT_SCROLL_SPEED,
    VALID_POSITIONS,
    WATERMARK_POSITIONS,
    WatermarkConfig,
    build_image_watermark_filter,
    build_text_watermark_filter,
    calc_position,
    calc_scroll_x,
    get_position_display_name,
    get_position_names,
)

# ============================================================
# 常量
# ============================================================


class TestConstants:
    def test_nine_positions(self):
        assert len(VALID_POSITIONS) == 9

    def test_watermark_positions_contains_all(self):
        assert set(WATERMARK_POSITIONS.keys()) == VALID_POSITIONS

    def test_default_position_valid(self):
        assert DEFAULT_POSITION in VALID_POSITIONS

    def test_default_values(self):
        assert DEFAULT_MODE in ("text", "image")
        assert 0 < DEFAULT_SCALE <= 1.0
        assert 0 <= DEFAULT_OPACITY <= 1.0
        assert DEFAULT_FONT_SIZE > 0
        assert DEFAULT_MARGIN_X >= 0
        assert DEFAULT_MARGIN_Y >= 0
        assert DEFAULT_SCROLL_SPEED > 0
        assert isinstance(DEFAULT_FONT_COLOR, str)


# ============================================================
# WatermarkConfig 默认值
# ============================================================


class TestWatermarkConfigDefaults:
    def test_default_constructor(self):
        config = WatermarkConfig()
        assert config.mode == DEFAULT_MODE
        assert config.position == DEFAULT_POSITION
        assert config.scale == DEFAULT_SCALE
        assert config.opacity == DEFAULT_OPACITY
        assert config.font_size == DEFAULT_FONT_SIZE
        assert config.font_color == DEFAULT_FONT_COLOR
        assert config.margin_x == DEFAULT_MARGIN_X
        assert config.margin_y == DEFAULT_MARGIN_Y
        assert config.scroll is False
        assert config.scroll_speed == DEFAULT_SCROLL_SPEED

    def test_text_mode_default(self):
        config = WatermarkConfig()
        assert config.mode == "text"
        assert config.text == ""
        assert config.font_path == ""

    def test_image_mode_default(self):
        config = WatermarkConfig(mode="image")
        assert config.image_path == ""


# ============================================================
# from_dict
# ============================================================


class TestFromDict:
    def test_none_returns_none(self):
        assert WatermarkConfig.from_dict(None) is None

    def test_empty_dict_returns_none(self):
        assert WatermarkConfig.from_dict({}) is None

    def test_disabled_returns_none(self):
        assert WatermarkConfig.from_dict({"enabled": False}) is None

    def test_image_mode_missing_path_returns_none(self):
        result = WatermarkConfig.from_dict({"enabled": True, "mode": "image"})
        assert result is None

    def test_image_mode_with_image_field(self):
        result = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image": "/path/to/wm.png"})
        assert result is not None
        assert result.mode == "image"
        assert result.image_path == "/path/to/wm.png"

    def test_image_mode_with_image_path_field(self):
        result = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image_path": "/path/wm.png"})
        assert result is not None
        assert result.image_path == "/path/wm.png"

    def test_text_mode_missing_text_returns_none(self):
        result = WatermarkConfig.from_dict({"enabled": True, "mode": "text"})
        assert result is None

    def test_text_mode_with_text(self):
        result = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": "hello"})
        assert result is not None
        assert result.mode == "text"
        assert result.text == "hello"

    def test_invalid_position_falls_back_default(self):
        result = WatermarkConfig.from_dict(
            {
                "enabled": True,
                "mode": "text",
                "text": "hello",
                "position": "invalid_pos",
            }
        )
        assert result is not None
        assert result.position == DEFAULT_POSITION

    def test_custom_values_propagated(self):
        result = WatermarkConfig.from_dict(
            {
                "enabled": True,
                "mode": "text",
                "text": "测试",
                "position": "top_left",
                "opacity": 0.5,
                "font_size": 32,
                "font_color": "red",
                "margin_x": 30,
                "margin_y": 40,
                "scroll": True,
                "scroll_speed": 100,
            }
        )
        assert result is not None
        assert result.position == "top_left"
        assert result.opacity == 0.5
        assert result.font_size == 32
        assert result.font_color == "red"
        assert result.margin_x == 30
        assert result.margin_y == 40
        assert result.scroll is True
        assert result.scroll_speed == 100

    def test_image_mode_custom_values(self):
        result = WatermarkConfig.from_dict(
            {
                "enabled": True,
                "mode": "image",
                "image_path": "/wm.png",
                "scale": 0.3,
                "opacity": 0.6,
            }
        )
        assert result is not None
        assert result.scale == 0.3
        assert result.opacity == 0.6

    def test_default_mode_when_unspecified(self):
        # 只给enabled和text，mode默认text
        result = WatermarkConfig.from_dict({"enabled": True, "text": "hello"})
        assert result is not None
        assert result.mode == DEFAULT_MODE

    def test_text_empty_string_returns_none(self):
        result = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": ""})
        assert result is None


# ============================================================
# validate
# ============================================================


class TestValidate:
    def test_valid_text_config(self):
        config = WatermarkConfig(mode="text", text="hello")
        ok, msg = config.validate()
        assert ok is True
        assert msg == ""

    def test_valid_image_config(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png")
        ok, msg = config.validate()
        assert ok is True
        assert msg == ""

    def test_invalid_position(self):
        config = WatermarkConfig(mode="text", text="hi", position="nowhere")
        ok, msg = config.validate()
        assert ok is False
        assert "位置" in msg

    def test_opacity_too_high(self):
        config = WatermarkConfig(mode="text", text="hi", opacity=1.5)
        ok, msg = config.validate()
        assert ok is False
        assert "透明度" in msg

    def test_opacity_negative(self):
        config = WatermarkConfig(mode="text", text="hi", opacity=-0.1)
        ok, msg = config.validate()
        assert ok is False
        assert "透明度" in msg

    def test_opacity_boundary_zero(self):
        config = WatermarkConfig(mode="text", text="hi", opacity=0.0)
        ok, _ = config.validate()
        assert ok is True

    def test_opacity_boundary_one(self):
        config = WatermarkConfig(mode="text", text="hi", opacity=1.0)
        ok, _ = config.validate()
        assert ok is True

    def test_image_missing_path(self):
        config = WatermarkConfig(mode="image")
        ok, msg = config.validate()
        assert ok is False
        assert "图片路径" in msg

    def test_image_scale_too_small(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.001)
        ok, msg = config.validate()
        assert ok is False
        assert "缩放" in msg

    def test_image_scale_too_large(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scale=1.5)
        ok, msg = config.validate()
        assert ok is False
        assert "缩放" in msg

    def test_image_scale_boundary_low(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.01)
        ok, _ = config.validate()
        assert ok is True

    def test_image_scale_boundary_high(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scale=1.0)
        ok, _ = config.validate()
        assert ok is True

    def test_text_empty(self):
        config = WatermarkConfig(mode="text", text="")
        ok, msg = config.validate()
        assert ok is False
        assert "文字内容" in msg

    def test_font_size_zero(self):
        config = WatermarkConfig(mode="text", text="hi", font_size=0)
        ok, msg = config.validate()
        assert ok is False
        assert "字体大小" in msg

    def test_font_size_negative(self):
        config = WatermarkConfig(mode="text", text="hi", font_size=-5)
        ok, msg = config.validate()
        assert ok is False
        assert "字体大小" in msg

    def test_unknown_mode(self):
        config = WatermarkConfig(mode="video", text="hi")
        ok, msg = config.validate()
        assert ok is False
        assert "模式" in msg


# ============================================================
# has_effect
# ============================================================


class TestHasEffect:
    def test_text_has_effect(self):
        config = WatermarkConfig(mode="text", text="hello", opacity=0.5, font_size=24)
        assert config.has_effect() is True

    def test_text_empty_no_effect(self):
        config = WatermarkConfig(mode="text", text="")
        assert config.has_effect() is False

    def test_text_zero_opacity_no_effect(self):
        config = WatermarkConfig(mode="text", text="hi", opacity=0.0)
        assert config.has_effect() is False

    def test_text_zero_font_size_no_effect(self):
        config = WatermarkConfig(mode="text", text="hi", font_size=0)
        assert config.has_effect() is False

    def test_image_has_effect(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.5)
        assert config.has_effect() is True

    def test_image_no_path_no_effect(self):
        config = WatermarkConfig(mode="image")
        assert config.has_effect() is False

    def test_image_zero_opacity_no_effect(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.0)
        assert config.has_effect() is False

    def test_unknown_mode_no_effect(self):
        config = WatermarkConfig(mode="invalid")
        assert config.has_effect() is False


# ============================================================
# calc_position
# ============================================================


class TestCalcPosition:
    OUT_W = 1920
    OUT_H = 1080
    WM_W = 200
    WM_H = 50
    MX = 20
    MY = 20

    def test_top_left(self):
        x, y = calc_position("top_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 20
        assert y == 20

    def test_top_center(self):
        x, y = calc_position("top_center", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == (1920 - 200) // 2
        assert y == 20

    def test_top_right(self):
        x, y = calc_position("top_right", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 1920 - 200 - 20
        assert y == 20

    def test_center_left(self):
        x, y = calc_position("center_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 20
        assert y == (1080 - 50) // 2

    def test_center(self):
        x, y = calc_position("center", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == (1920 - 200) // 2
        assert y == (1080 - 50) // 2

    def test_center_right(self):
        x, y = calc_position("center_right", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 1920 - 200 - 20
        assert y == (1080 - 50) // 2

    def test_bottom_left(self):
        x, y = calc_position("bottom_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 20
        assert y == 1080 - 50 - 20

    def test_bottom_center(self):
        x, y = calc_position("bottom_center", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == (1920 - 200) // 2
        assert y == 1080 - 50 - 20

    def test_bottom_right(self):
        x, y = calc_position("bottom_right", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 1920 - 200 - 20
        assert y == 1080 - 50 - 20

    def test_invalid_position_defaults_bottom_right(self):
        x, y = calc_position("invalid", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
        assert x == 1920 - 200 - 20
        assert y == 1080 - 50 - 20

    def test_zero_margins(self):
        x, y = calc_position("top_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, 0, 0)
        assert x == 0
        assert y == 0

    def test_all_nine_positions_are_unique(self):
        positions = set()
        for pos in VALID_POSITIONS:
            pos_xy = calc_position(pos, self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
            positions.add(pos_xy)
        assert len(positions) == 9


# ============================================================
# calc_scroll_x
# ============================================================


class TestCalcScrollX:
    def test_returns_string(self):
        result = calc_scroll_x("bottom_left", 1920, 200, 50)
        assert isinstance(result, str)

    def test_contains_output_width(self):
        result = calc_scroll_x("bottom_left", 1920, 200, 50)
        assert "1920" in result

    def test_contains_wm_width(self):
        result = calc_scroll_x("bottom_left", 1920, 200, 50)
        assert "200" in result

    def test_contains_speed(self):
        result = calc_scroll_x("bottom_left", 1920, 200, 50)
        assert "50" in result

    def test_contains_mod_keyword(self):
        result = calc_scroll_x("bottom_left", 1920, 200, 50)
        assert "mod" in result


# ============================================================
# build_image_watermark_filter
# ============================================================


class TestBuildImageWatermarkFilter:
    def test_returns_tuple(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", position="top_left")
        result = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert isinstance(result, tuple)
        assert len(result) == 2

    def test_filter_contains_overlay(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", position="bottom_right")
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert "overlay" in filter_str

    def test_filter_contains_scale(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.3)
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert "scale=" in filter_str

    def test_opacity_applied_when_below_one(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.5)
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert "colorchannelmixer" in filter_str
        assert "aa=0.5" in filter_str

    def test_opacity_one_no_mixer(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=1.0)
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert "colorchannelmixer" not in filter_str

    def test_input_args_contain_image_path(self):
        config = WatermarkConfig(mode="image", image_path="/path/to/wm.png")
        _, input_args = build_image_watermark_filter("[in]", "/path/to/wm.png", 1920, 1080, "[out]", config)
        assert input_args == ["-i", "/path/to/wm.png"]

    def test_output_label_present(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png")
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[final]", config)
        assert "[final]" in filter_str

    def test_scroll_mode_contains_t_variable(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scroll=True, scroll_speed=60)
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert "*t" in filter_str or "t*" in filter_str

    def test_static_mode_no_t_variable_in_overlay_x(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png", scroll=False)
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        # 静态模式 x 是具体数值，不包含 t 变量
        overlay_part = filter_str.split("overlay=")[1]
        assert "t" not in overlay_part.split(":")[0] or "wm" in overlay_part.split(":")[0]

    def test_semicolon_separated_filters(self):
        config = WatermarkConfig(mode="image", image_path="/wm.png")
        filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
        assert ";" in filter_str


# ============================================================
# build_text_watermark_filter
# ============================================================


class TestBuildTextWatermarkFilter:
    def test_returns_string(self):
        config = WatermarkConfig(mode="text", text="hello")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert isinstance(result, str)

    def test_contains_drawtext(self):
        config = WatermarkConfig(mode="text", text="hello")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "drawtext=" in result

    def test_contains_text(self):
        config = WatermarkConfig(mode="text", text="测试水印")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "测试水印" in result

    def test_fontsize_in_filter(self):
        config = WatermarkConfig(mode="text", text="hi", font_size=36)
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "fontsize=36" in result

    def test_fontcolor_with_opacity(self):
        config = WatermarkConfig(mode="text", text="hi", font_color="red", opacity=0.5)
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "fontcolor=red@0.5" in result

    def test_font_path_included(self):
        config = WatermarkConfig(mode="text", text="hi", font_path="/fonts/simsun.ttf")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "fontfile=" in result
        assert "simsun.ttf" in result

    def test_no_font_path_when_empty(self):
        config = WatermarkConfig(mode="text", text="hi", font_path="")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "fontfile=" not in result

    def test_text_colon_escaped(self):
        config = WatermarkConfig(mode="text", text="time: 00:00")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        # 冒号应该被转义
        assert "time\\:" in result or "time\\\\:" in result

    def test_scroll_mode_contains_mod(self):
        config = WatermarkConfig(mode="text", text="scrolling", scroll=True, scroll_speed=50)
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "mod(" in result

    def test_static_mode_has_numeric_x_y(self):
        config = WatermarkConfig(mode="text", text="hi", position="top_left")
        result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
        assert "x=" in result
        assert "y=" in result

    def test_output_label_present(self):
        config = WatermarkConfig(mode="text", text="hi")
        result = build_text_watermark_filter("[in]", "[final_out]", config, 1920, 1080)
        assert "[final_out]" in result

    def test_input_label_present(self):
        config = WatermarkConfig(mode="text", text="hi")
        result = build_text_watermark_filter("[video_in]", "[out]", config, 1920, 1080)
        assert result.startswith("[video_in]")


# ============================================================
# 工具函数
# ============================================================


class TestGetPositionNames:
    def test_returns_nine_names(self):
        names = get_position_names()
        assert len(names) == 9

    def test_order_is_correct(self):
        names = get_position_names()
        # 按从上到下、从左到右
        assert names[0] == "top_left"
        assert names[1] == "top_center"
        assert names[2] == "top_right"
        assert names[3] == "center_left"
        assert names[4] == "center"
        assert names[5] == "center_right"
        assert names[6] == "bottom_left"
        assert names[7] == "bottom_center"
        assert names[8] == "bottom_right"

    def test_all_valid(self):
        names = get_position_names()
        assert set(names) == VALID_POSITIONS


class TestGetPositionDisplayName:
    def test_known_position(self):
        assert get_position_display_name("top_left") == "左上"
        assert get_position_display_name("center") == "中心"
        assert get_position_display_name("bottom_right") == "右下"

    def test_unknown_position_returns_original(self):
        assert get_position_display_name("invalid") == "invalid"
