"""ChromaKeyConfig 绿幕抠像配置单测.

纯逻辑模块，覆盖：数据类、from_dict解析、from_preset预设、has_effect、
validate校验、normalize_color颜色归一化、colorkey滤镜构建、
chromakey滤镜构建、apply_chroma_key_if_needed便捷函数、get_preset_names。
"""

from __future__ import annotations

from packages.domain.chroma_key_config import (
    CHROMA_KEY_PRESETS,
    MAX_BLEND,
    MAX_SIMILARITY,
    MAX_SPILL_SUPPRESS,
    MIN_BLEND,
    MIN_SIMILARITY,
    MIN_SPILL_SUPPRESS,
    VALID_PRESETS,
    ChromaKeyConfig,
    apply_chroma_key_if_needed,
    build_chromakey_filter,
    build_colorkey_filter,
    get_preset_names,
    normalize_color,
)


class TestChromaKeyConfigDefaults:
    def test_default_disabled(self):
        config = ChromaKeyConfig()
        assert config.enabled is False
        assert config.key_color == "#00FF00"
        assert config.similarity == 0.3
        assert config.blend == 0.1
        assert config.spill_suppress == 0.0

    def test_default_has_no_effect(self):
        config = ChromaKeyConfig()
        assert config.has_effect() is False

    def test_default_validate_passes(self):
        config = ChromaKeyConfig()
        ok, msg = config.validate()
        assert ok is True
        assert msg == ""


class TestFromDict:
    def test_none_returns_disabled(self):
        config = ChromaKeyConfig.from_dict(None)
        assert config.enabled is False

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

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

    def test_basic_enabled(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": "#FF0000"})
        assert config.enabled is True
        assert config.key_color == "#FF0000"

    def test_default_values_when_enabled(self):
        config = ChromaKeyConfig.from_dict({"enabled": True})
        assert config.enabled is True
        assert config.key_color == "#00FF00"
        assert config.similarity == 0.3
        assert config.blend == 0.1
        assert config.spill_suppress == 0.0

    def test_custom_values(self):
        config = ChromaKeyConfig.from_dict(
            {
                "enabled": True,
                "key_color": "#0000FF",
                "similarity": 0.5,
                "blend": 0.2,
                "spill_suppress": 0.3,
            }
        )
        assert config.key_color == "#0000FF"
        assert config.similarity == 0.5
        assert config.blend == 0.2
        assert config.spill_suppress == 0.3

    def test_similarity_clamped_below_min(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 0.0})
        assert config.similarity == MIN_SIMILARITY

    def test_similarity_clamped_above_max(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 2.0})
        assert config.similarity == MAX_SIMILARITY

    def test_blend_clamped_below_min(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "blend": -1.0})
        assert config.blend == MIN_BLEND

    def test_blend_clamped_above_max(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "blend": 2.0})
        assert config.blend == MAX_BLEND

    def test_spill_suppress_clamped_below_min(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": -0.5})
        assert config.spill_suppress == MIN_SPILL_SUPPRESS

    def test_spill_suppress_clamped_above_max(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": 2.0})
        assert config.spill_suppress == MAX_SPILL_SUPPRESS

    def test_invalid_float_values_use_default(self):
        config = ChromaKeyConfig.from_dict(
            {
                "enabled": True,
                "similarity": "not_a_number",
                "blend": None,
            }
        )
        assert config.similarity == 0.3
        assert config.blend == 0.1

    def test_key_color_stripped(self):
        config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": "  #FF0000  "})
        assert config.key_color == "#FF0000"


class TestFromPreset:
    def test_green_screen_preset(self):
        config = ChromaKeyConfig.from_preset("green_screen")
        assert config is not None
        assert config.enabled is True
        assert config.key_color == "#00FF00"
        assert config.similarity == 0.3
        assert config.blend == 0.1
        assert config.spill_suppress == 0.5

    def test_blue_screen_preset(self):
        config = ChromaKeyConfig.from_preset("blue_screen")
        assert config is not None
        assert config.key_color == "#0000FF"

    def test_red_screen_preset(self):
        config = ChromaKeyConfig.from_preset("red_screen")
        assert config is not None
        assert config.key_color == "#FF0000"
        assert config.spill_suppress == 0.0

    def test_invalid_preset_returns_none(self):
        config = ChromaKeyConfig.from_preset("nonexistent")
        assert config is None

    def test_all_presets_are_valid(self):
        for name in CHROMA_KEY_PRESETS:
            config = ChromaKeyConfig.from_preset(name)
            assert config is not None
            assert config.enabled is True
            assert config.has_effect() is True
            ok, _ = config.validate()
            assert ok is True


class TestHasEffect:
    def test_disabled_no_effect(self):
        config = ChromaKeyConfig(enabled=False)
        assert config.has_effect() is False

    def test_enabled_but_zero_similarity(self):
        config = ChromaKeyConfig(enabled=True, similarity=0.0)
        assert config.has_effect() is False

    def test_enabled_with_similarity_has_effect(self):
        config = ChromaKeyConfig(enabled=True, similarity=0.01)
        assert config.has_effect() is True

    def test_full_config_has_effect(self):
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#00FF00",
            similarity=0.3,
            blend=0.1,
            spill_suppress=0.5,
        )
        assert config.has_effect() is True


class TestValidate:
    def test_disabled_valid(self):
        config = ChromaKeyConfig(enabled=False)
        ok, msg = config.validate()
        assert ok is True
        assert msg == ""

    def test_valid_config(self):
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#00FF00",
            similarity=0.3,
            blend=0.1,
            spill_suppress=0.5,
        )
        ok, msg = config.validate()
        assert ok is True
        assert msg == ""

    def test_empty_key_color_invalid(self):
        config = ChromaKeyConfig(enabled=True, key_color="")
        ok, msg = config.validate()
        assert ok is False
        assert "key_color" in msg

    def test_similarity_below_min_invalid(self):
        config = ChromaKeyConfig(enabled=True, similarity=0.001)
        ok, msg = config.validate()
        assert ok is False
        assert "similarity" in msg

    def test_similarity_above_max_invalid(self):
        config = ChromaKeyConfig(enabled=True, similarity=1.5)
        ok, msg = config.validate()
        assert ok is False
        assert "similarity" in msg

    def test_blend_below_min_invalid(self):
        config = ChromaKeyConfig(enabled=True, blend=-0.1)
        ok, msg = config.validate()
        assert ok is False
        assert "blend" in msg

    def test_blend_above_max_invalid(self):
        config = ChromaKeyConfig(enabled=True, blend=1.5)
        ok, msg = config.validate()
        assert ok is False
        assert "blend" in msg

    def test_spill_suppress_below_min_invalid(self):
        config = ChromaKeyConfig(enabled=True, spill_suppress=-0.1)
        ok, msg = config.validate()
        assert ok is False
        assert "spill_suppress" in msg

    def test_spill_suppress_above_max_invalid(self):
        config = ChromaKeyConfig(enabled=True, spill_suppress=1.5)
        ok, msg = config.validate()
        assert ok is False
        assert "spill_suppress" in msg

    def test_boundary_values_valid(self):
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#000",
            similarity=MIN_SIMILARITY,
            blend=MIN_BLEND,
            spill_suppress=MIN_SPILL_SUPPRESS,
        )
        ok, _ = config.validate()
        assert ok is True

        config2 = ChromaKeyConfig(
            enabled=True,
            key_color="#fff",
            similarity=MAX_SIMILARITY,
            blend=MAX_BLEND,
            spill_suppress=MAX_SPILL_SUPPRESS,
        )
        ok2, _ = config2.validate()
        assert ok2 is True


class TestNormalizeColor:
    def test_hex_with_hash(self):
        assert normalize_color("#FF0000") == "0xFF0000"

    def test_hex_without_hash(self):
        assert normalize_color("FF0000") == "0xFF0000"

    def test_hex_lowercase(self):
        assert normalize_color("#ff0000") == "0xFF0000"

    def test_hex_mixed_case(self):
        assert normalize_color("#aBcDeF") == "0xABCDEF"

    def test_hex_with_alpha(self):
        assert normalize_color("#FF000080") == "0xFF0000"

    def test_0x_format_passthrough(self):
        assert normalize_color("0xFF0000") == "0XFF0000"

    def test_0x_lowercase(self):
        assert normalize_color("0xff0000") == "0XFF0000"

    def test_color_name_passthrough(self):
        assert normalize_color("green") == "green"

    def test_color_name_blue(self):
        assert normalize_color("blue") == "blue"

    def test_whitespace_stripped(self):
        assert normalize_color("  #FF0000  ") == "0xFF0000"

    def test_black_color(self):
        assert normalize_color("#000000") == "0x000000"

    def test_white_color(self):
        assert normalize_color("#FFFFFF") == "0xFFFFFF"


class TestBuildColorkeyFilter:
    def test_disabled_returns_copy(self):
        config = ChromaKeyConfig(enabled=False)
        result = build_colorkey_filter(config, "[0:v]", "[out]")
        assert result == "[0:v]copy[out]"

    def test_zero_similarity_returns_copy(self):
        config = ChromaKeyConfig(enabled=True, similarity=0.0)
        result = build_colorkey_filter(config, "[v0]", "[ck]")
        assert result == "[v0]copy[ck]"

    def test_basic_colorkey(self):
        config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1)
        result = build_colorkey_filter(config, "[0:v]", "[ck]")
        assert "colorkey=color=0x00FF00:similarity=0.3:blend=0.1" in result
        assert result.startswith("[0:v]")
        assert result.endswith("[ck]")

    def test_with_spill_suppress(self):
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#00FF00",
            similarity=0.3,
            blend=0.1,
            spill_suppress=0.5,
        )
        result = build_colorkey_filter(config, "[0:v]", "[ck]")
        assert "colorkey=" in result
        assert "colorchannelmixer=" in result
        # Spill suppress reduces green gain
        assert "gg=" in result
        assert "rr=" in result
        assert "bb=" in result

    def test_no_spill_suppress_no_colorchannelmixer(self):
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#00FF00",
            similarity=0.3,
            blend=0.1,
            spill_suppress=0.0,
        )
        result = build_colorkey_filter(config, "[0:v]", "[ck]")
        assert "colorchannelmixer" not in result

    def test_different_labels(self):
        config = ChromaKeyConfig(enabled=True, key_color="#FF0000", similarity=0.5)
        result = build_colorkey_filter(config, "[v_in]", "[v_out]")
        assert result.startswith("[v_in]")
        assert result.endswith("[v_out]")

    def test_spill_suppress_gain_values(self):
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#00FF00",
            similarity=0.3,
            blend=0.1,
            spill_suppress=1.0,
        )
        result = build_colorkey_filter(config, "[0:v]", "[ck]")
        # At spill=1.0: g_gain = max(0.3, 1.0 - 1.0*0.7) = max(0.3, 0.3) = 0.3
        assert "gg=0.3" in result


class TestBuildChromakeyFilter:
    def test_disabled_returns_copy(self):
        config = ChromaKeyConfig(enabled=False)
        result = build_chromakey_filter(config, "[0:v]", "[out]")
        assert result == "[0:v]copy[out]"

    def test_basic_chromakey(self):
        config = ChromaKeyConfig(enabled=True, key_color="#0000FF", similarity=0.4, blend=0.2)
        result = build_chromakey_filter(config, "[0:v]", "[ck]")
        assert "chromakey=color=0x0000FF:similarity=0.4:blend=0.2" in result
        assert result.startswith("[0:v]")
        assert result.endswith("[ck]")

    def test_spill_suppress_not_included(self):
        """chromakey滤镜不包含spill_suppress（只有colorkey有）."""
        config = ChromaKeyConfig(
            enabled=True,
            key_color="#00FF00",
            similarity=0.3,
            blend=0.1,
            spill_suppress=0.5,
        )
        result = build_chromakey_filter(config, "[0:v]", "[ck]")
        assert "colorchannelmixer" not in result
        assert "chromakey=" in result


class TestApplyChromaKeyIfNeeded:
    def test_none_clip_config_returns_none(self):
        result = apply_chroma_key_if_needed(None, "[0:v]", "[ck]")
        assert result is None

    def test_no_chroma_key_returns_none(self):
        result = apply_chroma_key_if_needed({"other": "data"}, "[0:v]", "[ck]")
        assert result is None

    def test_chroma_key_disabled_returns_none(self):
        config = {"chroma_key": {"enabled": False}}
        result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]")
        assert result is None

    def test_chroma_key_enabled_returns_filter(self):
        config = {
            "chroma_key": {
                "enabled": True,
                "key_color": "#00FF00",
                "similarity": 0.3,
                "blend": 0.1,
            }
        }
        result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]")
        assert result is not None
        assert "colorkey=" in result
        assert result.startswith("[0:v]")
        assert result.endswith("[ck]")

    def test_invalid_config_returns_none(self):
        """配置异常时应该返回None而不是抛异常."""
        config = {"chroma_key": "invalid_data"}
        result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]")
        assert result is None


class TestGetPresetNames:
    def test_returns_sorted_list(self):
        names = get_preset_names()
        assert isinstance(names, list)
        assert names == sorted(names)

    def test_contains_known_presets(self):
        names = get_preset_names()
        assert "green_screen" in names
        assert "blue_screen" in names
        assert "red_screen" in names

    def test_count_matches_presets_dict(self):
        names = get_preset_names()
        assert len(names) == len(CHROMA_KEY_PRESETS)


class TestPresetsAndConstants:
    def test_valid_presets_equals_preset_keys(self):
        assert VALID_PRESETS == set(CHROMA_KEY_PRESETS.keys())

    def test_each_preset_has_required_keys(self):
        for name, preset in CHROMA_KEY_PRESETS.items():
            assert "key_color" in preset, f"{name} missing key_color"
            assert "similarity" in preset, f"{name} missing similarity"
            assert "blend" in preset, f"{name} missing blend"
            assert "spill_suppress" in preset, f"{name} missing spill_suppress"

    def test_min_less_than_max(self):
        assert MIN_SIMILARITY < MAX_SIMILARITY
        assert MIN_BLEND <= MAX_BLEND
        assert MIN_SPILL_SUPPRESS <= MAX_SPILL_SUPPRESS

    def test_min_similarity_positive(self):
        assert MIN_SIMILARITY > 0
