"""ColorGradeConfig 色彩调色配置单测.

纯逻辑模块，覆盖：数据类、resolve_params参数解析、has_effect效果判断、
from_dict字典解析、validate校验、预设查询函数、clamp_param钳制。
"""

from __future__ import annotations

from packages.domain.color_grade_config import (
    ALL_PARAM_KEYS,
    DEFAULT_PARAMS,
    PARAM_RANGES,
    PRESET_DISPLAY_NAMES,
    PRESET_PARAMS,
    VALID_PRESETS,
    ColorGradeConfig,
    clamp_param,
    get_preset_names,
    get_preset_params,
)


class TestColorGradeConfigDefaults:
    def test_default_disabled(self):
        config = ColorGradeConfig()
        assert config.enabled is False
        assert config.preset == ""
        assert config.brightness is None
        assert config.contrast is None
        assert config.saturation is None
        assert config.temperature is None
        assert config.hue is None

    def test_default_resolve_returns_defaults(self):
        config = ColorGradeConfig()
        params = config.resolve_params()
        for key in ALL_PARAM_KEYS:
            assert params[key] == DEFAULT_PARAMS[key]

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

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


class TestResolveParams:
    def test_disabled_still_resolves(self):
        """禁用状态下仍能解析参数."""
        config = ColorGradeConfig(enabled=False, brightness=50.0)
        params = config.resolve_params()
        assert params["brightness"] == 50.0

    def test_preset_fresh(self):
        config = ColorGradeConfig(enabled=True, preset="fresh")
        params = config.resolve_params()
        assert params["brightness"] == 8
        assert params["contrast"] == 10
        assert params["saturation"] == 120
        assert params["temperature"] == -8
        assert params["hue"] == 5

    def test_preset_black_white(self):
        config = ColorGradeConfig(enabled=True, preset="black_white")
        params = config.resolve_params()
        assert params["saturation"] == 0
        assert params["contrast"] == 15

    def test_preset_warm(self):
        config = ColorGradeConfig(enabled=True, preset="warm")
        params = config.resolve_params()
        assert params["temperature"] == 30

    def test_preset_cool(self):
        config = ColorGradeConfig(enabled=True, preset="cool")
        params = config.resolve_params()
        assert params["temperature"] == -25

    def test_invalid_preset_uses_defaults(self):
        config = ColorGradeConfig(enabled=True, preset="nonexistent")
        params = config.resolve_params()
        for key in ALL_PARAM_KEYS:
            assert params[key] == DEFAULT_PARAMS[key]

    def test_custom_override_preset(self):
        config = ColorGradeConfig(
            enabled=True,
            preset="fresh",
            brightness=50.0,
        )
        params = config.resolve_params()
        # custom覆盖了预设
        assert params["brightness"] == 50.0
        # 其他参数仍用预设值
        assert params["contrast"] == 10
        assert params["saturation"] == 120

    def test_multiple_custom_overrides(self):
        config = ColorGradeConfig(
            enabled=True,
            preset="vintage",
            brightness=20.0,
            saturation=150.0,
            hue=10.0,
        )
        params = config.resolve_params()
        assert params["brightness"] == 20.0
        assert params["saturation"] == 150.0
        assert params["hue"] == 10.0
        # 未覆盖的保留预设值
        assert params["contrast"] == 5
        assert params["temperature"] == 25

    def test_custom_without_preset(self):
        config = ColorGradeConfig(
            enabled=True,
            preset="",
            brightness=30.0,
            contrast=-20.0,
        )
        params = config.resolve_params()
        assert params["brightness"] == 30.0
        assert params["contrast"] == -20.0
        # 未设置的用默认值
        assert params["saturation"] == 100.0
        assert params["temperature"] == 0.0
        assert params["hue"] == 0.0

    def test_clamping_brightness_above_max(self):
        config = ColorGradeConfig(enabled=True, brightness=200.0)
        params = config.resolve_params()
        assert params["brightness"] == 100.0

    def test_clamping_brightness_below_min(self):
        config = ColorGradeConfig(enabled=True, brightness=-200.0)
        params = config.resolve_params()
        assert params["brightness"] == -100.0

    def test_clamping_saturation_below_zero(self):
        config = ColorGradeConfig(enabled=True, saturation=-10.0)
        params = config.resolve_params()
        assert params["saturation"] == 0.0

    def test_clamping_saturation_above_max(self):
        config = ColorGradeConfig(enabled=True, saturation=300.0)
        params = config.resolve_params()
        assert params["saturation"] == 200.0

    def test_clamping_hue_above_max(self):
        config = ColorGradeConfig(enabled=True, hue=200.0)
        params = config.resolve_params()
        assert params["hue"] == 180.0

    def test_clamping_hue_below_min(self):
        config = ColorGradeConfig(enabled=True, hue=-200.0)
        params = config.resolve_params()
        assert params["hue"] == -180.0

    def test_clamping_preset_plus_custom(self):
        """预设值+自定义值超出范围时仍会钳制."""
        config = ColorGradeConfig(
            enabled=True,
            preset="fresh",
            saturation=250.0,  # 超出200上限
        )
        params = config.resolve_params()
        assert params["saturation"] == 200.0

    def test_returns_new_dict_each_time(self):
        config = ColorGradeConfig(enabled=True, brightness=10.0)
        p1 = config.resolve_params()
        p2 = config.resolve_params()
        assert p1 is not p2
        p1["brightness"] = 999
        assert p2["brightness"] == 10.0


class TestHasEffect:
    def test_default_no_effect(self):
        config = ColorGradeConfig()
        assert config.has_effect() is False

    def test_enabled_but_all_defaults(self):
        config = ColorGradeConfig(
            enabled=True,
            brightness=0.0,
            contrast=0.0,
            saturation=100.0,
            temperature=0.0,
            hue=0.0,
        )
        assert config.has_effect() is False

    def test_brightness_change_has_effect(self):
        config = ColorGradeConfig(enabled=True, brightness=1.0)
        assert config.has_effect() is True

    def test_contrast_change_has_effect(self):
        config = ColorGradeConfig(enabled=True, contrast=1.0)
        assert config.has_effect() is True

    def test_saturation_change_has_effect(self):
        config = ColorGradeConfig(enabled=True, saturation=99.0)
        assert config.has_effect() is True

    def test_temperature_change_has_effect(self):
        config = ColorGradeConfig(enabled=True, temperature=1.0)
        assert config.has_effect() is True

    def test_hue_change_has_effect(self):
        config = ColorGradeConfig(enabled=True, hue=1.0)
        assert config.has_effect() is True

    def test_all_presets_have_effect(self):
        for preset in VALID_PRESETS:
            config = ColorGradeConfig(enabled=True, preset=preset)
            assert config.has_effect() is True, f"preset {preset} should have effect"

    def test_very_small_change_no_effect(self):
        """小于0.001的浮点误差视为无效果."""
        config = ColorGradeConfig(enabled=True, brightness=0.0001)
        assert config.has_effect() is False


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

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

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

    def test_basic_enabled(self):
        config = ColorGradeConfig.from_dict({"enabled": True})
        assert config.enabled is True
        assert config.preset == ""
        assert config.brightness is None

    def test_with_preset(self):
        config = ColorGradeConfig.from_dict({"enabled": True, "preset": "warm"})
        assert config.enabled is True
        assert config.preset == "warm"

    def test_invalid_preset_ignored(self):
        config = ColorGradeConfig.from_dict({"enabled": True, "preset": "invalid"})
        assert config.enabled is True
        assert config.preset == ""

    def test_with_custom_params(self):
        config = ColorGradeConfig.from_dict(
            {
                "enabled": True,
                "brightness": 50,
                "contrast": -10.5,
                "saturation": 150.0,
                "temperature": 20,
                "hue": -15.5,
            }
        )
        assert config.brightness == 50.0
        assert config.contrast == -10.5
        assert config.saturation == 150.0
        assert config.temperature == 20.0
        assert config.hue == -15.5

    def test_invalid_float_values_return_none(self):
        config = ColorGradeConfig.from_dict(
            {
                "enabled": True,
                "brightness": "not_a_number",
                "contrast": None,
            }
        )
        assert config.brightness is None
        assert config.contrast is None

    def test_int_values_work(self):
        config = ColorGradeConfig.from_dict({"enabled": True, "brightness": 10})
        assert config.brightness == 10.0

    def test_string_float_values_work(self):
        config = ColorGradeConfig.from_dict({"enabled": True, "brightness": "15.5"})
        assert config.brightness == 15.5

    def test_partial_custom_params(self):
        config = ColorGradeConfig.from_dict(
            {
                "enabled": True,
                "preset": "cinema",
                "brightness": 5.0,
            }
        )
        assert config.preset == "cinema"
        assert config.brightness == 5.0
        assert config.contrast is None


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

    def test_valid_config(self):
        config = ColorGradeConfig(
            enabled=True,
            preset="fresh",
            brightness=50.0,
        )
        ok, msg = config.validate()
        assert ok is True

    def test_invalid_preset(self):
        config = ColorGradeConfig(enabled=True, preset="invalid")
        ok, msg = config.validate()
        assert ok is False
        assert "预设" in msg

    def test_brightness_above_max_invalid(self):
        config = ColorGradeConfig(enabled=True, brightness=150.0)
        ok, msg = config.validate()
        assert ok is False
        assert "brightness" in msg

    def test_brightness_below_min_invalid(self):
        config = ColorGradeConfig(enabled=True, brightness=-150.0)
        ok, msg = config.validate()
        assert ok is False
        assert "brightness" in msg

    def test_saturation_below_zero_invalid(self):
        config = ColorGradeConfig(enabled=True, saturation=-10.0)
        ok, msg = config.validate()
        assert ok is False
        assert "saturation" in msg

    def test_saturation_above_max_invalid(self):
        config = ColorGradeConfig(enabled=True, saturation=250.0)
        ok, msg = config.validate()
        assert ok is False
        assert "saturation" in msg

    def test_hue_above_max_invalid(self):
        config = ColorGradeConfig(enabled=True, hue=200.0)
        ok, msg = config.validate()
        assert ok is False
        assert "hue" in msg

    def test_hue_below_min_invalid(self):
        config = ColorGradeConfig(enabled=True, hue=-200.0)
        ok, msg = config.validate()
        assert ok is False
        assert "hue" in msg

    def test_boundary_values_valid(self):
        config = ColorGradeConfig(
            enabled=True,
            brightness=-100.0,
            contrast=100.0,
            saturation=0.0,
            temperature=-100.0,
            hue=-180.0,
        )
        ok, _ = config.validate()
        assert ok is True

    def test_none_values_valid(self):
        """None值不参与校验（视为未设置）."""
        config = ColorGradeConfig(enabled=True)
        ok, _ = config.validate()
        assert ok is True


class TestGetPresetNames:
    def test_returns_sorted_list(self):
        names = get_preset_names()
        assert isinstance(names, list)
        preset_keys = [n[0] for n in names]
        assert preset_keys == sorted(preset_keys)

    def test_count_matches_valid_presets(self):
        names = get_preset_names()
        assert len(names) == len(VALID_PRESETS)

    def test_each_entry_has_name_and_display(self):
        names = get_preset_names()
        for name, display in names:
            assert name in VALID_PRESETS
            assert display == PRESET_DISPLAY_NAMES[name]
            assert len(display) > 0


class TestGetPresetParams:
    def test_existing_preset(self):
        params = get_preset_params("fresh")
        assert params is not None
        assert params["brightness"] == 8

    def test_nonexistent_preset(self):
        params = get_preset_params("nonexistent")
        assert params is None

    def test_all_presets_have_all_params(self):
        for preset in VALID_PRESETS:
            params = get_preset_params(preset)
            assert params is not None
            for key in ALL_PARAM_KEYS:
                assert key in params

    def test_returns_dict_with_correct_values(self):
        """返回的字典包含所有预期参数."""
        params = get_preset_params("warm")
        assert params["brightness"] == 5
        assert params["temperature"] == 30
        assert len(params) == 5


class TestClampParam:
    def test_brightness_within_range(self):
        assert clamp_param("brightness", 50.0) == 50.0

    def test_brightness_above_max(self):
        assert clamp_param("brightness", 200.0) == 100.0

    def test_brightness_below_min(self):
        assert clamp_param("brightness", -200.0) == -100.0

    def test_saturation_within_range(self):
        assert clamp_param("saturation", 100.0) == 100.0

    def test_saturation_at_boundary(self):
        assert clamp_param("saturation", 0.0) == 0.0
        assert clamp_param("saturation", 200.0) == 200.0

    def test_hue_within_range(self):
        assert clamp_param("hue", 90.0) == 90.0

    def test_hue_above_max(self):
        assert clamp_param("hue", 200.0) == 180.0

    def test_unknown_param_passthrough(self):
        assert clamp_param("unknown", 999.0) == 999.0


class TestConstants:
    def test_all_presets_have_params(self):
        for preset in VALID_PRESETS:
            assert preset in PRESET_PARAMS

    def test_all_presets_have_display_names(self):
        for preset in VALID_PRESETS:
            assert preset in PRESET_DISPLAY_NAMES

    def test_param_ranges_has_all_keys(self):
        for key in ALL_PARAM_KEYS:
            assert key in PARAM_RANGES

    def test_default_params_has_all_keys(self):
        for key in ALL_PARAM_KEYS:
            assert key in DEFAULT_PARAMS

    def test_param_ranges_min_less_than_max(self):
        for key, (min_val, max_val) in PARAM_RANGES.items():
            assert min_val < max_val, f"{key}: min ({min_val}) should be < max ({max_val})"
