"""sticker_engine_pure 单元测试."""

from apps.worker.video_processing.sticker_engine_pure import (
    build_drawtext_alpha_expr,
    build_enable_expr,
    build_image_fade_filters,
    build_opacity_filter,
    build_overlay_position,
    build_pre_filter_label,
    build_scale_filter,
    build_shadow_params,
    build_stroke_params,
    calculate_end_time,
    calculate_fade_out_start,
    count_sticker_types,
    escape_drawtext_text,
    estimate_sticker_size,
    estimate_text_size,
    filter_enabled_stickers,
    has_time_range,
    safe_bool,
    safe_float,
    safe_int,
    sort_stickers_by_z_index,
    validate_image_sticker,
    validate_text_sticker,
)

# ── safe_float ──────────────────────────────────────────────────────────────────


class TestSafeFloat:
    def test_none_returns_none(self):
        assert safe_float(None) is None

    def test_int_input(self):
        assert safe_float(42) == 42.0

    def test_float_input(self):
        assert safe_float(3.14) == 3.14

    def test_string_int_string(self):
        assert safe_float("3.14") == 3.14

    def test_string_integer_string(self):
        assert safe_float("100") == 100.0

    def test_invalid_string(self):
        assert safe_float("abc") is None

    def test_empty_string(self):
        assert safe_float("") is None

    def test_nan_returns_none(self):
        import math

        assert safe_float(float("nan")) is None
        assert math.isnan(float("nan"))  # 确认NaN判断生效

    def test_negative_number(self):
        assert safe_float(-5.5) == -5.5

    def test_zero(self):
        assert safe_float(0) == 0.0

    def test_boolean(self):
        assert safe_float(True) == 1.0
        assert safe_float(False) == 0.0


# ── safe_int ──────────────────────────────────────────────────────────────────


class TestSafeInt:
    def test_none_returns_default(self):
        assert safe_int(None) == 0
        assert safe_int(None, default=5) == 5

    def test_int_input(self):
        assert safe_int(42) == 42

    def test_float_input_truncates(self):
        assert safe_int(3.7) == 3
        assert safe_int(3.2) == 3

    def test_string_integer(self):
        assert safe_int("100") == 100

    def test_string_float(self):
        assert safe_int("3.9") == 3

    def test_invalid_string_returns_default(self):
        assert safe_int("abc") == 0
        assert safe_int("abc", default=-1) == -1

    def test_empty_string(self):
        assert safe_int("") == 0

    def test_negative_number(self):
        assert safe_int(-10) == -10

    def test_zero(self):
        assert safe_int(0) == 0

    def test_boolean(self):
        assert safe_int(True) == 1
        assert safe_int(False) == 0


# ── safe_bool ────────────────────────────────────────────────────────────────


class TestSafeBool:
    def test_boolean_passthrough(self):
        assert safe_bool(True) is True
        assert safe_bool(False) is False

    def test_none_returns_false(self):
        assert safe_bool(None) is False

    def test_string_true_variants(self):
        assert safe_bool("true") is True
        assert safe_bool("True") is True
        assert safe_bool("TRUE") is True
        assert safe_bool("1") is True
        assert safe_bool("yes") is True
        assert safe_bool("YES") is True
        assert safe_bool("on") is True
        assert safe_bool("On") is True

    def test_string_false_variants(self):
        assert safe_bool("false") is False
        assert safe_bool("0") is False
        assert safe_bool("no") is False
        assert safe_bool("off") is False

    def test_numeric_values(self):
        assert safe_bool(1) is True
        assert safe_bool(0) is False
        assert safe_bool(-1) is True

    def test_empty_string(self):
        assert safe_bool("") is False

    def test_list_truthy_falsy(self):
        assert safe_bool([1]) is True
        assert safe_bool([]) is False


# ── estimate_sticker_size ────────────────────────────────────────────────


class TestEstimateStickerSize:
    def test_default_scale(self):
        w, h = estimate_sticker_size(1000, 800)
        assert w == 300  # 1000 * 0.3
        assert h == 240  # 800 * 0.3

    def test_custom_scale(self):
        w, h = estimate_sticker_size(1000, 800, scale=2.0)
        assert w == 600
        assert h == 480

    def test_fixed_width_and_height(self):
        w, h = estimate_sticker_size(1000, 800, fixed_width=200, fixed_height=150)
        assert w == 200
        assert h == 150

    def test_fixed_width_only(self):
        w, h = estimate_sticker_size(1000, 800, fixed_width=500)
        assert w == 500
        assert h == 240  # 仍然按比例算高

    def test_fixed_height_only(self):
        w, h = estimate_sticker_size(1000, 800, fixed_height=400)
        assert w == 300
        assert h == 400

    def test_minimum_size(self):
        w, h = estimate_sticker_size(1, 1, scale=0.01)
        assert w >= 1
        assert h >= 1

    def test_zero_canvas(self):
        w, h = estimate_sticker_size(0, 0)
        assert w >= 1
        assert h >= 1

    def test_scale_zero(self):
        w, h = estimate_sticker_size(1000, 800, scale=0)
        assert w >= 1
        assert h >= 1


# ── estimate_text_size ──────────────────────────────────────────────


class TestEstimateTextSize:
    def test_normal_text(self):
        w, h = estimate_text_size("hello", 20)
        assert w == int(5 * 20 * 0.6)
        assert h == int(20 * 1.4)

    def test_empty_text(self):
        w, h = estimate_text_size("", 20)
        assert w == 0
        assert h == 0

    def test_chinese_text(self):
        w, h = estimate_text_size("你好世界", 30)
        assert w == int(4 * 30 * 0.6)
        assert h == int(30 * 1.4)

    def test_minimum_size(self):
        w, h = estimate_text_size("a", 1)
        assert w >= 1
        assert h >= 1

    def test_single_char(self):
        w, h = estimate_text_size("x", 100)
        assert w == int(1 * 100 * 0.6)
        assert h == int(100 * 1.4)


# ── calculate_fade_out_start ────────────────────────────────────────


class TestCalculateFadeOutStart:
    def test_normal_case(self):
        assert calculate_fade_out_start(10, 20, 3) == 27.0  # 10 + 20 - 3

    def test_zero_fade_out(self):
        assert calculate_fade_out_start(10, 20, 0) == 0.0

    def test_zero_duration(self):
        assert calculate_fade_out_start(10, 0, 3) == 0.0

    def test_negative_fade_out(self):
        assert calculate_fade_out_start(10, 20, -1) == 0.0

    def test_fade_longer_than_duration(self):
        result = calculate_fade_out_start(5, 3, 10)
        assert result == 0.0  # max(0, 5+3-10) = max(0, -2) = 0

    def test_start_at_zero(self):
        assert calculate_fade_out_start(0, 10, 2) == 8.0

    def test_float_values(self):
        assert calculate_fade_out_start(1.5, 5.5, 2.0) == 5.0


# ── calculate_end_time ────────────────────────────────────────────


class TestCalculateEndTime:
    def test_normal_case(self):
        assert calculate_end_time(10, 5) == 15.0

    def test_zero_duration(self):
        assert calculate_end_time(10, 0) == 10.0

    def test_negative_duration(self):
        assert calculate_end_time(10, -5) == 10.0

    def test_start_at_zero(self):
        assert calculate_end_time(0, 10) == 10.0

    def test_float_values(self):
        assert calculate_end_time(1.5, 2.5) == 4.0


# ── has_time_range ──────────────────────────────────────────────


class TestHasTimeRange:
    def test_positive_duration(self):
        assert has_time_range(10) is True
        assert has_time_range(0.1) is True

    def test_zero_duration(self):
        assert has_time_range(0) is False

    def test_negative_duration(self):
        assert has_time_range(-5) is False


# ── build_scale_filter ────────────────────────────────────────


class TestBuildScaleFilter:
    def test_fixed_width_height(self):
        assert build_scale_filter(width=100, height=200) == "scale=100:200"

    def test_scale_only(self):
        assert build_scale_filter(scale=0.5) == "scale=iw*0.5:ih*0.5"

    def test_default_no_scale(self):
        assert build_scale_filter() is None
        assert build_scale_filter(scale=1.0) is None

    def test_width_only_returns_none(self):
        # 只有width没有height，且scale=1.0，返回None
        assert build_scale_filter(width=100) is None

    def test_height_only_returns_none(self):
        assert build_scale_filter(height=200) is None

    def test_scale_with_width_height_overrides_scale(self):
        # width和height都有时优先
        assert build_scale_filter(width=100, height=200, scale=0.5) == "scale=100:200"


# ── build_opacity_filter ──────────────────────────────────────


class TestBuildOpacityFilter:
    def test_full_opacity(self):
        assert build_opacity_filter(1.0) is None
        assert build_opacity_filter(1.5) is None  # 大于1也返回None

    def test_partial_opacity(self):
        assert build_opacity_filter(0.5) == "colorchannelmixer=aa=0.5"

    def test_zero_opacity(self):
        assert build_opacity_filter(0.0) == "colorchannelmixer=aa=0.0"

    def test_negative_clamped(self):
        assert build_opacity_filter(-0.5) == "colorchannelmixer=aa=0.0"

    def test_above_one_clamped(self):
        # 大于1的情况：>=1.0返回None
        assert build_opacity_filter(2.0) is None


# ── build_image_fade_filters ────────────────────────────────────


class TestBuildImageFadeFilters:
    def test_no_fade(self):
        assert build_image_fade_filters(10, 20) == []

    def test_fade_in_only(self):
        result = build_image_fade_filters(10, 20, fade_in=2)
        assert len(result) == 1
        assert "fade=in:st=10:d=2:alpha=1" in result[0]

    def test_fade_out_only(self):
        result = build_image_fade_filters(10, 20, fade_out=3)
        assert len(result) == 1
        assert "fade=out:st=27:d=3:alpha=1" in result[0]

    def test_both_fades(self):
        result = build_image_fade_filters(10, 20, fade_in=2, fade_out=3)
        assert len(result) == 2
        assert "fade=in" in result[0]
        assert "fade=out" in result[1]

    def test_fade_out_zero_duration_skipped(self):
        result = build_image_fade_filters(10, 0, fade_out=3)
        assert result == []

    def test_fade_out_negative_duration(self):
        result = build_image_fade_filters(10, -5, fade_out=3)
        assert result == []


# ── build_enable_expr ────────────────────────────────────────


class TestBuildEnableExpr:
    def test_positive_duration(self):
        result = build_enable_expr(10, 5)
        assert result == ":enable='between(t,10,15)'"

    def test_zero_duration(self):
        assert build_enable_expr(10, 0) == ""

    def test_negative_duration(self):
        assert build_enable_expr(10, -1) == ""

    def test_start_at_zero(self):
        result = build_enable_expr(0, 10)
        assert result == ":enable='between(t,0,10)'"

    def test_float_values(self):
        result = build_enable_expr(1.5, 2.5)
        assert "between(t,1.5,4.0)" in result


# ── escape_drawtext_text ──────────────────────────────────────


class TestEscapeDrawtextText:
    def test_no_special_chars(self):
        assert escape_drawtext_text("hello") == "hello"

    def test_colon_escaped(self):
        assert escape_drawtext_text("a:b:c") == "a\\:b\\:c"

    def test_single_quote_escaped(self):
        assert escape_drawtext_text("a'b'c") == "a\\'b\\'c"

    def test_both_special_chars(self):
        result = escape_drawtext_text("it's: test")
        assert result == "it\\'s\\: test"

    def test_empty_string(self):
        assert escape_drawtext_text("") == ""

    def test_backslash_not_escaped(self):
        # 只转义冒号和单引号
        assert escape_drawtext_text("a\\b") == "a\\b"


# ── build_drawtext_alpha_expr ──────────────────────────────────


class TestBuildDrawtextAlphaExpr:
    def test_no_fade(self):
        assert build_drawtext_alpha_expr(10, 20) == "1"

    def test_fade_in_only(self):
        result = build_drawtext_alpha_expr(10, 20, fade_in=2)
        assert result == "if(lt(t,12),(t-10)/2,1)"

    def test_fade_out_only(self):
        result = build_drawtext_alpha_expr(10, 20, fade_out=3)
        assert result == "if(gt(t,27),(30-t)/3,1)"

    def test_both_fades(self):
        result = build_drawtext_alpha_expr(10, 20, fade_in=2, fade_out=3)
        assert "if(lt(t," in result
        assert "if(gt(t," in result
        assert result.count("*") == 1  # 两部分相乘

    def test_fade_out_zero_duration(self):
        result = build_drawtext_alpha_expr(10, 0, fade_out=3)
        assert result == "1"

    def test_zero_fade_in(self):
        result = build_drawtext_alpha_expr(10, 20, fade_in=0)
        assert result == "1"


# ── build_stroke_params ──────────────────────────────────────


class TestBuildStrokeParams:
    def test_no_stroke(self):
        assert build_stroke_params() == []
        assert build_stroke_params(stroke_width=0) == []
        assert build_stroke_params(stroke_width=-1) == []

    def test_default_color(self):
        result = build_stroke_params(stroke_width=2)
        assert len(result) == 2
        assert "borderw=2" in result
        assert "bordercolor=black" in result

    def test_custom_color(self):
        result = build_stroke_params(stroke_width=3, stroke_color="red")
        assert "borderw=3" in result
        assert "bordercolor=red" in result


# ── build_shadow_params ──────────────────────────────────────


class TestBuildShadowParams:
    def test_no_shadow(self):
        assert build_shadow_params() == []
        assert build_shadow_params(shadow_alpha=0) == []
        assert build_shadow_params(shadow_alpha=-1) == []

    def test_default_values(self):
        result = build_shadow_params(shadow_alpha=0.5)
        assert len(result) == 3
        assert "shadowx=2" in result
        assert "shadowy=2" in result
        assert "shadowcolor=black@0.5" in result

    def test_custom_offset(self):
        result = build_shadow_params(shadow_alpha=0.3, shadow_x=5, shadow_y=7, shadow_color="red")
        assert "shadowx=5" in result
        assert "shadowy=7" in result
        assert "shadowcolor=red@0.3" in result

    def test_alpha_clamped(self):
        result = build_shadow_params(shadow_alpha=1.5)
        assert "shadowcolor=black@1.0" in result

    def test_alpha_negative_clamped_to_zero(self):
        # 负数会触发<=0分支，返回空列表
        assert build_shadow_params(shadow_alpha=-0.5) == []


# ── sort_stickers_by_z_index ────────────────────────────────────


class TestSortStickersByZIndex:
    def test_sorted_by_z_index(self):
        stickers = [
            {"name": "c", "z_index": 3},
            {"name": "a", "z_index": 1},
            {"name": "b", "z_index": 2},
        ]
        result = sort_stickers_by_z_index(stickers)
        assert [s["name"] for s in result] == ["a", "b", "c"]

    def test_missing_z_index_defaults_to_10(self):
        stickers = [
            {"name": "low", "z_index": 5},
            {"name": "no_z"},  # 默认10
            {"name": "high", "z_index": 15},
        ]
        result = sort_stickers_by_z_index(stickers)
        assert [s["name"] for s in result] == ["low", "no_z", "high"]

    def test_same_z_index_stable(self):
        stickers = [
            {"name": "first", "z_index": 5},
            {"name": "second", "z_index": 5},
            {"name": "third", "z_index": 5},
        ]
        result = sort_stickers_by_z_index(stickers)
        assert [s["name"] for s in result] == ["first", "second", "third"]

    def test_empty_list(self):
        assert sort_stickers_by_z_index([]) == []

    def test_negative_z_index(self):
        stickers = [
            {"name": "neg", "z_index": -5},
            {"name": "zero", "z_index": 0},
            {"name": "pos", "z_index": 5},
        ]
        result = sort_stickers_by_z_index(stickers)
        assert [s["name"] for s in result] == ["neg", "zero", "pos"]

    def test_original_not_modified(self):
        stickers = [{"z_index": 3}, {"z_index": 1}]
        original = list(stickers)
        sort_stickers_by_z_index(stickers)
        assert stickers == original


# ── filter_enabled_stickers ────────────────────────────────────


class TestFilterEnabledStickers:
    def test_all_enabled(self):
        stickers = [
            {"name": "a", "enabled": True},
            {"name": "b"},  # 默认True
        ]
        result = filter_enabled_stickers(stickers)
        assert len(result) == 2

    def test_mixed_enabled(self):
        stickers = [
            {"name": "a", "enabled": True},
            {"name": "b", "enabled": False},
            {"name": "c"},
        ]
        result = filter_enabled_stickers(stickers)
        assert len(result) == 2
        assert [s["name"] for s in result] == ["a", "c"]

    def test_all_disabled(self):
        stickers = [
            {"name": "a", "enabled": False},
            {"name": "b", "enabled": "false"},
        ]
        result = filter_enabled_stickers(stickers)
        assert len(result) == 0

    def test_empty_list(self):
        assert filter_enabled_stickers([]) == []

    def test_string_enabled_values(self):
        stickers = [
            {"name": "a", "enabled": "true"},
            {"name": "b", "enabled": "yes"},
            {"name": "c", "enabled": "0"},
        ]
        result = filter_enabled_stickers(stickers)
        assert [s["name"] for s in result] == ["a", "b"]


# ── count_sticker_types ────────────────────────────────────────


class TestCountStickerTypes:
    def test_multiple_types(self):
        stickers = [
            {"type": "image"},
            {"type": "text"},
            {"type": "image"},
            {"type": "image"},
            {"type": "emoji"},
        ]
        result = count_sticker_types(stickers)
        assert result == {"image": 3, "text": 1, "emoji": 1}

    def test_default_type_image(self):
        stickers = [
            {"name": "a"},  # 无type字段
            {"type": "text"},
        ]
        result = count_sticker_types(stickers)
        assert result == {"image": 1, "text": 1}

    def test_empty_list(self):
        assert count_sticker_types([]) == {}

    def test_single_type(self):
        stickers = [{"type": "text"} for _ in range(5)]
        result = count_sticker_types(stickers)
        assert result == {"text": 5}


# ── build_overlay_position ──────────────────────────────────────


class TestBuildOverlayPosition:
    def test_integer_values(self):
        assert build_overlay_position(100, 200) == "100:200"

    def test_float_values_rounded(self):
        assert build_overlay_position(100.6, 200.3) == "101:200"

    def test_negative_values(self):
        assert build_overlay_position(-10, -20) == "-10:-20"

    def test_zero_values(self):
        assert build_overlay_position(0, 0) == "0:0"


# ── build_pre_filter_label ─────────────────────────────────────


class TestBuildPreFilterLabel:
    def test_index_zero(self):
        assert build_pre_filter_label(0) == "sticker_0_scaled"

    def test_positive_index(self):
        assert build_pre_filter_label(5) == "sticker_5_scaled"

    def test_large_index(self):
        assert build_pre_filter_label(999) == "sticker_999_scaled"


# ── validate_image_sticker ──────────────────────────────────


class TestValidateImageSticker:
    def test_valid_with_image_path(self):
        valid, errors = validate_image_sticker({"image_path": "/path/to/img.png"})
        assert valid is True
        assert errors == []

    def test_valid_with_asset_id(self):
        valid, errors = validate_image_sticker({"asset_id": "asset_123"})
        assert valid is True
        assert errors == []

    def test_missing_image_and_asset(self):
        valid, errors = validate_image_sticker({})
        assert valid is False
        assert "image_path 或 asset_id" in errors[0]

    def test_invalid_opacity_high(self):
        valid, errors = validate_image_sticker(
            {
                "image_path": "a.png",
                "opacity": 1.5,
            }
        )
        assert valid is False
        assert any("opacity" in e for e in errors)

    def test_invalid_opacity_low(self):
        valid, errors = validate_image_sticker(
            {
                "image_path": "a.png",
                "opacity": -0.5,
            }
        )
        assert valid is False
        assert any("opacity" in e for e in errors)

    def test_valid_opacity_boundary(self):
        valid, _ = validate_image_sticker({"image_path": "a.png", "opacity": 0})
        assert valid is True
        valid, _ = validate_image_sticker({"image_path": "a.png", "opacity": 1})
        assert valid is True

    def test_invalid_scale_zero(self):
        valid, errors = validate_image_sticker(
            {
                "image_path": "a.png",
                "scale": 0,
            }
        )
        assert valid is False
        assert any("scale" in e for e in errors)

    def test_invalid_scale_negative(self):
        valid, errors = validate_image_sticker(
            {
                "image_path": "a.png",
                "scale": -1,
            }
        )
        assert valid is False

    def test_invalid_duration_negative(self):
        valid, errors = validate_image_sticker(
            {
                "image_path": "a.png",
                "duration": -5,
            }
        )
        assert valid is False
        assert any("duration" in e for e in errors)

    def test_invalid_start_time_negative(self):
        valid, errors = validate_image_sticker(
            {
                "image_path": "a.png",
                "start_time": -1,
            }
        )
        assert valid is False
        assert any("start_time" in e for e in errors)

    def test_multiple_errors(self):
        valid, errors = validate_image_sticker(
            {
                "opacity": 2.0,
                "scale": -1,
                "duration": -5,
            }
        )
        assert valid is False
        assert len(errors) >= 3

    def test_valid_with_extra_fields(self):
        valid, _ = validate_image_sticker(
            {
                "image_path": "a.png",
                "extra_field": "ignored",
                "z_index": 5,
            }
        )
        assert valid is True


# ── validate_text_sticker ────────────────────────────────────


class TestValidateTextSticker:
    def test_valid_text(self):
        valid, errors = validate_text_sticker({"text": "hello"})
        assert valid is True
        assert errors == []

    def test_missing_text(self):
        valid, errors = validate_text_sticker({})
        assert valid is False
        assert any("text" in e for e in errors)

    def test_empty_text(self):
        valid, errors = validate_text_sticker({"text": ""})
        assert valid is False
        assert any("text" in e for e in errors)

    def test_invalid_font_size_zero(self):
        valid, errors = validate_text_sticker(
            {
                "text": "hello",
                "font_size": 0,
            }
        )
        assert valid is False
        assert any("font_size" in e for e in errors)

    def test_invalid_font_size_negative(self):
        valid, errors = validate_text_sticker(
            {
                "text": "hello",
                "font_size": -5,
            }
        )
        assert valid is False

    def test_missing_font_color(self):
        valid, errors = validate_text_sticker(
            {
                "text": "hello",
                "font_color": "",
            }
        )
        assert valid is False
        assert any("font_color" in e for e in errors)

    def test_invalid_duration_negative(self):
        valid, errors = validate_text_sticker(
            {
                "text": "hello",
                "duration": -3,
            }
        )
        assert valid is False
        assert any("duration" in e for e in errors)

    def test_default_font_size_valid(self):
        # 默认36，有效
        valid, _ = validate_text_sticker({"text": "hi"})
        assert valid is True

    def test_multiple_errors(self):
        valid, errors = validate_text_sticker(
            {
                "text": "",
                "font_size": -1,
                "font_color": "",
            }
        )
        assert valid is False
        assert len(errors) >= 3
