"""color_grade_config 模块单测 — 纯逻辑，无 FFmpeg 依赖."""

from __future__ import annotations

import pytest

from packages.domain.color_grade_config import (
    ALL_PARAM_KEYS,
    DEFAULT_PARAMS,
    PARAM_RANGES,
    PRESET_BW,
    PRESET_CINEMA,
    PRESET_COOL,
    PRESET_DISPLAY_NAMES,
    PRESET_FILM,
    PRESET_FRESH,
    PRESET_JAPANESE,
    PRESET_PARAMS,
    PRESET_VINTAGE,
    PRESET_WARM,
    VALID_PRESETS,
    ColorGradeConfig,
    clamp_param,
    get_preset_names,
    get_preset_params,
)

# ── 常量 ──────────────────────────────────────────────────────────────────────


class TestConstants:
    def test_eight_presets(self):
        assert len(VALID_PRESETS) == 8

    def test_preset_display_names_match(self):
        assert set(PRESET_DISPLAY_NAMES.keys()) == VALID_PRESETS
        for name in VALID_PRESETS:
            assert len(PRESET_DISPLAY_NAMES[name]) > 0

    def test_preset_params_complete(self):
        assert set(PRESET_PARAMS.keys()) == VALID_PRESETS
        for _preset, params in PRESET_PARAMS.items():
            assert set(params.keys()) == set(ALL_PARAM_KEYS)

    def test_default_params_keys(self):
        assert set(DEFAULT_PARAMS.keys()) == set(ALL_PARAM_KEYS)

    def test_param_ranges_keys(self):
        assert set(PARAM_RANGES.keys()) == set(ALL_PARAM_KEYS)

    def test_default_within_ranges(self):
        for key in ALL_PARAM_KEYS:
            min_val, max_val = PARAM_RANGES[key]
            assert min_val <= DEFAULT_PARAMS[key] <= max_val

    def test_all_presets_within_ranges(self):
        for _preset, params in PRESET_PARAMS.items():
            for key in ALL_PARAM_KEYS:
                min_val, max_val = PARAM_RANGES[key]
                assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]} out of range"


# ── 默认值 ────────────────────────────────────────────────────────────────────


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

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

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


# ── from_dict ────────────────────────────────────────────────────────────────


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

    def test_empty_dict_disabled(self):
        cfg = ColorGradeConfig.from_dict({})
        assert cfg.enabled is False

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

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

    def test_with_valid_preset(self):
        cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "fresh"})
        assert cfg.enabled is True
        assert cfg.preset == "fresh"

    def test_with_invalid_preset_ignored(self):
        cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "unknown_preset"})
        assert cfg.enabled is True
        assert cfg.preset == ""  # 无效预设被忽略

    def test_with_custom_params(self):
        cfg = ColorGradeConfig.from_dict(
            {
                "enabled": True,
                "brightness": 10,
                "contrast": -5,
                "saturation": 150,
                "temperature": 20,
                "hue": 30,
            }
        )
        assert cfg.enabled is True
        assert cfg.brightness == 10.0
        assert cfg.contrast == -5.0
        assert cfg.saturation == 150.0
        assert cfg.temperature == 20.0
        assert cfg.hue == 30.0

    def test_with_preset_and_custom_override(self):
        cfg = ColorGradeConfig.from_dict(
            {
                "enabled": True,
                "preset": "fresh",
                "brightness": 50,  # 覆盖预设的 8
            }
        )
        assert cfg.preset == "fresh"
        assert cfg.brightness == 50.0

    def test_invalid_param_values_none(self):
        cfg = ColorGradeConfig.from_dict(
            {
                "enabled": True,
                "brightness": "not_a_number",
            }
        )
        assert cfg.enabled is True
        assert cfg.brightness is None  # 解析失败为 None

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

    def test_preset_bw(self):
        cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "black_white"})
        assert cfg.preset == "black_white"
        params = cfg.resolve_params()
        assert params["saturation"] == 0.0


# ── resolve_params ───────────────────────────────────────────────────────────


class TestResolveParams:
    def test_default_returns_defaults(self):
        cfg = ColorGradeConfig()
        params = cfg.resolve_params()
        assert params == DEFAULT_PARAMS

    def test_preset_fresh_params(self):
        cfg = ColorGradeConfig(preset="fresh")
        params = cfg.resolve_params()
        assert params["brightness"] == 8
        assert params["contrast"] == 10
        assert params["saturation"] == 120

    def test_preset_bw_saturation_zero(self):
        cfg = ColorGradeConfig(preset="black_white")
        params = cfg.resolve_params()
        assert params["saturation"] == 0
        assert params["contrast"] == 15

    def test_custom_override_preset(self):
        cfg = ColorGradeConfig(preset="fresh", brightness=50)
        params = cfg.resolve_params()
        assert params["brightness"] == 50  # 覆盖了预设
        assert params["saturation"] == 120  # 预设值保留

    def test_below_min_clamped(self):
        cfg = ColorGradeConfig(brightness=-200, saturation=-10)
        params = cfg.resolve_params()
        assert params["brightness"] == PARAM_RANGES["brightness"][0]
        assert params["saturation"] == PARAM_RANGES["saturation"][0]

    def test_above_max_clamped(self):
        cfg = ColorGradeConfig(brightness=200, saturation=300, hue=360)
        params = cfg.resolve_params()
        assert params["brightness"] == PARAM_RANGES["brightness"][1]
        assert params["saturation"] == PARAM_RANGES["saturation"][1]
        assert params["hue"] == PARAM_RANGES["hue"][1]

    def test_all_presets_resolve_within_ranges(self):
        for preset in VALID_PRESETS:
            cfg = ColorGradeConfig(enabled=True, preset=preset)
            params = cfg.resolve_params()
            for key in ALL_PARAM_KEYS:
                min_val, max_val = PARAM_RANGES[key]
                assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]}"

    def test_returns_new_dict_each_call(self):
        cfg = ColorGradeConfig(preset="warm")
        p1 = cfg.resolve_params()
        p2 = cfg.resolve_params()
        assert p1 is not p2
        p1["brightness"] = 999
        assert p2["brightness"] != 999


# ── has_effect ───────────────────────────────────────────────────────────────


class TestHasEffect:
    def test_disabled_no_effect(self):
        cfg = ColorGradeConfig(enabled=False)
        assert cfg.has_effect() is False

    def test_default_no_effect(self):
        cfg = ColorGradeConfig(enabled=True)
        assert cfg.has_effect() is False

    def test_preset_has_effect(self):
        for preset in VALID_PRESETS:
            cfg = ColorGradeConfig(enabled=True, preset=preset)
            # 大部分预设都有效果，除了默认值完全一致的（应该没有）
            if preset in ("black_white",):
                assert cfg.has_effect() is True  # BW 有 contrast=15
            else:
                assert cfg.has_effect() is True

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

    def test_tiny_change_no_effect(self):
        # 小于 0.001 的差异视为无效果
        cfg = ColorGradeConfig(enabled=True)
        # 直接通过默认值的话应该没有效果
        assert cfg.has_effect() is False

    def test_custom_saturation_changed_from_default(self):
        # 默认 saturation=100，改成 101 就有效果
        cfg = ColorGradeConfig(enabled=True, saturation=101)
        assert cfg.has_effect() is True


# ── validate ─────────────────────────────────────────────────────────────────


class TestValidate:
    def test_disabled_always_valid(self):
        cfg = ColorGradeConfig(enabled=False)
        ok, err = cfg.validate()
        assert ok is True
        assert err == ""

    def test_enabled_no_preset_valid(self):
        cfg = ColorGradeConfig(enabled=True)
        ok, err = cfg.validate()
        assert ok is True

    def test_valid_preset(self):
        cfg = ColorGradeConfig(enabled=True, preset="cinema")
        ok, _ = cfg.validate()
        assert ok is True

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

    def test_custom_param_out_of_range(self):
        cfg = ColorGradeConfig(enabled=True, brightness=500)
        ok, err = cfg.validate()
        assert ok is False
        assert "brightness" in err

    def test_custom_param_within_range(self):
        cfg = ColorGradeConfig(enabled=True, brightness=50, contrast=-50)
        ok, _ = cfg.validate()
        assert ok is True

    def test_saturation_negative_invalid(self):
        cfg = ColorGradeConfig(enabled=True, saturation=-1)
        ok, err = cfg.validate()
        assert ok is False
        assert "saturation" in err


# ── 工具函数 ─────────────────────────────────────────────────────────────────


class TestUtils:
    def test_get_preset_names_returns_all(self):
        names = get_preset_names()
        assert len(names) == len(VALID_PRESETS)
        preset_keys = [n[0] for n in names]
        assert set(preset_keys) == VALID_PRESETS

    def test_get_preset_names_sorted(self):
        names = get_preset_names()
        preset_keys = [n[0] for n in names]
        assert preset_keys == sorted(preset_keys)

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

    def test_get_preset_params_invalid(self):
        params = get_preset_params("unknown")
        assert params is None

    @pytest.mark.parametrize(
        "param,value,expected",
        [
            ("brightness", 50, 50),
            ("brightness", 200, 100),
            ("brightness", -200, -100),
            ("saturation", 50, 50),
            ("saturation", -10, 0),
            ("saturation", 300, 200),
            ("hue", 0, 0),
            ("hue", 200, 180),
            ("hue", -200, -180),
        ],
    )
    def test_clamp_param(self, param, value, expected):
        assert clamp_param(param, value) == expected

    def test_clamp_param_unknown_passthrough(self):
        assert clamp_param("unknown_param", 999) == 999
