"""filter_presets 单元测试 - wave164

覆盖：
- FilterPreset 数据类（frozen/默认值/字段）
- FILTER_PRESET_LIBRARY 预设库（数量/分类/ID唯一性）
- get_filter_preset 按ID获取
- list_filter_presets 筛选列表（分类/关键词）
- build_ffmpeg_filter 滤镜生成（强度/参数插值/边界/空值）
"""

import dataclasses

import pytest

from packages.domain.filter_presets import (
    FILTER_PRESET_LIBRARY,
    FilterPreset,
    build_ffmpeg_filter,
    get_filter_preset,
    list_filter_presets,
)

# ============================================================
# FilterPreset 数据类
# ============================================================


class TestFilterPresetDataclass:
    def test_minimal_creation(self):
        p = FilterPreset(id="test", name="Test", category="basic")
        assert p.id == "test"
        assert p.name == "Test"
        assert p.category == "basic"

    def test_default_values(self):
        p = FilterPreset(id="t", name="T", category="basic")
        assert p.description == ""
        assert p.tags == []
        assert p.brightness == 0.0
        assert p.contrast == 1.0
        assert p.saturation == 1.0
        assert p.gamma == 1.0
        assert p.gamma_r == 1.0
        assert p.gamma_g == 1.0
        assert p.gamma_b == 1.0
        assert p.hue == 0.0
        assert p.lut_url == ""

    def test_full_creation(self):
        p = FilterPreset(
            id="full",
            name="Full",
            category="cinematic",
            description="test desc",
            tags=["tag1", "tag2"],
            brightness=0.5,
            contrast=1.5,
            saturation=2.0,
            gamma=1.2,
            gamma_r=1.3,
            gamma_g=0.9,
            gamma_b=0.8,
            hue=30.0,
            lut_url="http://lut.png",
        )
        assert p.description == "test desc"
        assert p.tags == ["tag1", "tag2"]
        assert p.brightness == 0.5
        assert p.contrast == 1.5
        assert p.gamma == 1.2
        assert p.hue == 30.0
        assert p.lut_url == "http://lut.png"

    def test_frozen_immutable(self):
        p = FilterPreset(id="t", name="T", category="basic")
        with pytest.raises((AttributeError, dataclasses.FrozenInstanceError)):
            p.brightness = 0.5  # frozen=True，不能修改

    def test_tags_default_new_list(self):
        # 每个实例有独立的列表
        p1 = FilterPreset(id="t1", name="T1", category="basic")
        p2 = FilterPreset(id="t2", name="T2", category="basic")
        assert p1.tags is not p2.tags


# ============================================================
# FILTER_PRESET_LIBRARY 预设库
# ============================================================


class TestFilterPresetLibrary:
    def test_library_not_empty(self):
        assert len(FILTER_PRESET_LIBRARY) > 0

    def test_all_ids_unique(self):
        ids = [p.id for p in FILTER_PRESET_LIBRARY]
        assert len(ids) == len(set(ids))

    def test_all_have_required_fields(self):
        for p in FILTER_PRESET_LIBRARY:
            assert p.id, f"预设缺少id: {p}"
            assert p.name, f"预设缺少name: {p.id}"
            assert p.category, f"预设缺少category: {p.id}"

    def test_categories_are_valid(self):
        valid_categories = {"basic", "cinematic", "vintage", "bw", "style"}
        for p in FILTER_PRESET_LIBRARY:
            assert p.category in valid_categories, f"无效分类: {p.id} -> {p.category}"

    def test_basic_category_exists(self):
        basics = [p for p in FILTER_PRESET_LIBRARY if p.category == "basic"]
        assert len(basics) >= 3

    def test_cinematic_category_exists(self):
        cinematic = [p for p in FILTER_PRESET_LIBRARY if p.category == "cinematic"]
        assert len(cinematic) >= 1

    def test_vintage_category_exists(self):
        vintage = [p for p in FILTER_PRESET_LIBRARY if p.category == "vintage"]
        assert len(vintage) >= 1

    def test_bw_category_exists(self):
        bw = [p for p in FILTER_PRESET_LIBRARY if p.category == "bw"]
        assert len(bw) >= 1
        # 所有黑白预设饱和度为0
        for p in bw:
            assert p.saturation == 0.0

    def test_style_category_exists(self):
        style = [p for p in FILTER_PRESET_LIBRARY if p.category == "style"]
        assert len(style) >= 1

    def test_none_filter_has_no_effect(self):
        none_preset = get_filter_preset("filter_none")
        assert none_preset is not None
        assert none_preset.brightness == 0.0
        assert none_preset.contrast == 1.0
        assert none_preset.saturation == 1.0
        assert none_preset.gamma == 1.0

    def test_total_count(self):
        # 至少有15个预设
        assert len(FILTER_PRESET_LIBRARY) >= 15


# ============================================================
# get_filter_preset
# ============================================================


class TestGetFilterPreset:
    def test_existing_id(self):
        p = get_filter_preset("filter_brighten")
        assert p is not None
        assert p.id == "filter_brighten"
        assert p.name == "明亮"

    def test_nonexistent_id(self):
        assert get_filter_preset("nonexistent") is None

    def test_empty_id(self):
        assert get_filter_preset("") is None

    def test_case_sensitive(self):
        # 大小写敏感
        assert get_filter_preset("Filter_Brighten") is None

    def test_returns_preset_object(self):
        p = get_filter_preset("filter_cinematic")
        assert isinstance(p, FilterPreset)


# ============================================================
# list_filter_presets
# ============================================================


class TestListFilterPresets:
    def test_no_filters_returns_all(self):
        result = list_filter_presets()
        assert len(result) == len(FILTER_PRESET_LIBRARY)

    def test_filter_by_category_basic(self):
        result = list_filter_presets(category="basic")
        assert len(result) > 0
        for p in result:
            assert p.category == "basic"

    def test_filter_by_category_cinematic(self):
        result = list_filter_presets(category="cinematic")
        for p in result:
            assert p.category == "cinematic"

    def test_filter_by_category_bw(self):
        result = list_filter_presets(category="bw")
        for p in result:
            assert p.category == "bw"

    def test_filter_by_category_vintage(self):
        result = list_filter_presets(category="vintage")
        for p in result:
            assert p.category == "vintage"

    def test_filter_by_category_style(self):
        result = list_filter_presets(category="style")
        for p in result:
            assert p.category == "style"

    def test_invalid_category_returns_empty(self):
        result = list_filter_presets(category="nonexistent")
        assert result == []

    def test_keyword_search_name(self):
        result = list_filter_presets(keyword="电影")
        assert len(result) >= 1
        names = [p.name for p in result]
        assert any("电影" in n for n in names)

    def test_keyword_search_tags(self):
        result = list_filter_presets(keyword="复古")
        assert len(result) >= 1
        # 至少有一个的标签或名称包含复古
        found = any(any("复古" in t for t in p.tags) or "复古" in p.name for p in result)
        assert found

    def test_keyword_search_description(self):
        result = list_filter_presets(keyword="青橙")
        assert len(result) >= 1

    def test_keyword_case_insensitive(self):
        # 中文不区分大小写，用英文标签/名称试
        result = list_filter_presets(keyword="FILTER")
        # 搜索 filter 应该能匹配到很多（id里有但搜索name/desc/tags）
        # 用确定的中文关键词更可靠
        assert isinstance(result, list)

    def test_keyword_nonexistent_returns_empty(self):
        result = list_filter_presets(keyword="zzz不存在的关键词zzz")
        assert result == []

    def test_category_and_keyword_combined(self):
        result = list_filter_presets(category="basic", keyword="亮")
        for p in result:
            assert p.category == "basic"
            assert "亮" in p.name or "亮" in p.description or any("亮" in t for t in p.tags)

    def test_keyword_empty_returns_all(self):
        result = list_filter_presets(keyword="")
        assert len(result) == len(FILTER_PRESET_LIBRARY)

    def test_returns_list_of_presets(self):
        result = list_filter_presets()
        for p in result:
            assert isinstance(p, FilterPreset)


# ============================================================
# build_ffmpeg_filter
# ============================================================


class TestBuildFfmpegFilter:
    def test_valid_preset_full_intensity(self):
        result = build_ffmpeg_filter("filter_brighten", 100)
        assert result.startswith("eq=")
        assert "brightness=" in result
        assert "contrast=" in result

    def test_invalid_preset_returns_empty(self):
        assert build_ffmpeg_filter("nonexistent", 100) == ""

    def test_zero_intensity_returns_empty(self):
        assert build_ffmpeg_filter("filter_brighten", 0) == ""

    def test_negative_intensity_returns_empty(self):
        assert build_ffmpeg_filter("filter_brighten", -10) == ""

    def test_intensity_over_100_clamped(self):
        # >100 按100算
        r100 = build_ffmpeg_filter("filter_saturate", 100)
        r200 = build_ffmpeg_filter("filter_saturate", 200)
        assert r100 == r200

    def test_half_intensity_half_effect(self):
        full = build_ffmpeg_filter("filter_brighten", 100)
        half = build_ffmpeg_filter("filter_brighten", 50)
        # 半强度的brightness应该小一些
        assert full != half
        # 半强度仍然有效果
        assert half.startswith("eq=")

    def test_none_filter_returns_empty(self):
        # filter_none所有参数都是默认值，应该返回空
        result = build_ffmpeg_filter("filter_none", 100)
        assert result == ""

    def test_format_has_eq_prefix(self):
        result = build_ffmpeg_filter("filter_saturate", 100)
        assert result.startswith("eq=")

    def test_parameters_separated_by_colon(self):
        result = build_ffmpeg_filter("filter_cinematic", 100)
        parts = result[len("eq=") :].split(":")
        assert len(parts) >= 3  # 至少有3个参数

    def test_brightness_value_format(self):
        result = build_ffmpeg_filter("filter_brighten", 100)
        # brightness参数应该存在且是数值
        assert "brightness=0." in result or "brightness=-0." in result

    def test_saturation_value_format(self):
        result = build_ffmpeg_filter("filter_saturate", 100)
        assert "saturation=" in result

    def test_contrast_value_format(self):
        result = build_ffmpeg_filter("filter_contrast", 100)
        assert "contrast=" in result

    def test_gamma_parameters_present(self):
        result = build_ffmpeg_filter("filter_warm", 100)
        # 暖色预设应该有gamma_r/gamma_b变化
        assert "gamma_r=" in result or "gamma_b=" in result

    def test_very_low_intensity_near_zero(self):
        # 强度1%，效果接近0，但因为有brightness等可能仍然在阈值以上
        result = build_ffmpeg_filter("filter_brighten", 1)
        # 至少应该有一个参数（brightness * 0.01 = 0.0012 > 0.001）
        assert isinstance(result, str)

    def test_all_presets_generate_valid_output(self):
        # 每个预设在100强度下都能生成合法结果
        for preset in FILTER_PRESET_LIBRARY:
            result = build_ffmpeg_filter(preset.id, 100)
            assert isinstance(result, str)
            if result:  # filter_none 可能为空
                assert result.startswith("eq=")

    def test_intensity_50_between_0_and_100(self):
        # 验证50%强度确实在0和100之间插值
        r0 = build_ffmpeg_filter("filter_saturate", 0)
        r50 = build_ffmpeg_filter("filter_saturate", 50)
        r100 = build_ffmpeg_filter("filter_saturate", 100)
        assert r0 == ""
        assert r50 != r100
        assert r50 != ""

    def test_hue_not_included_in_eq(self):
        # hue 不在 eq 滤镜参数中（当前实现没有hue）
        result = build_ffmpeg_filter("filter_cinematic", 100)
        assert "hue=" not in result
