"""转场配置领域模型单测.

纯逻辑模块，覆盖：TransitionType枚举、名称解析与别名、
TransitionConfig.parse解析/降级/边界、属性与校验。
"""

from __future__ import annotations

import logging

import pytest

from packages.domain.transition_config import (
    CUT_TRANSITION,
    DEFAULT_TRANSITION_DURATION,
    MAX_TRANSITION_DURATION,
    MIN_TRANSITION_DURATION,
    TransitionConfig,
    TransitionType,
)


class TestConstants:
    def test_min_duration(self):
        assert MIN_TRANSITION_DURATION == 0.3

    def test_max_duration(self):
        assert MAX_TRANSITION_DURATION == 2.0

    def test_default_duration(self):
        assert DEFAULT_TRANSITION_DURATION == 0.5

    def test_cut_transition(self):
        assert CUT_TRANSITION == "cut"


class TestTransitionTypeEnum:
    def test_cut_value(self):
        assert TransitionType.CUT.value == "cut"

    def test_fade_value(self):
        assert TransitionType.FADE.value == "fade"

    def test_dissolve_value(self):
        assert TransitionType.DISSOLVE.value == "dissolve"

    def test_slide_values(self):
        assert TransitionType.SLIDE_LEFT.value == "slideleft"
        assert TransitionType.SLIDE_RIGHT.value == "slideright"
        assert TransitionType.SLIDE_UP.value == "slideup"
        assert TransitionType.SLIDE_DOWN.value == "slidedown"

    def test_wipe_values(self):
        assert TransitionType.WIPE_LEFT.value == "wipeleft"
        assert TransitionType.WIPE_RIGHT.value == "wiperight"
        assert TransitionType.WIPE_UP.value == "wipeup"
        assert TransitionType.WIPE_DOWN.value == "wipedown"

    def test_zoom_value(self):
        assert TransitionType.ZOOM.value == "zoom"

    def test_circle_crop_value(self):
        assert TransitionType.CIRCLE_CROP.value == "circlecrop"

    def test_rect_crop_value(self):
        assert TransitionType.RECT_CROP.value == "rectcrop"

    def test_is_str_enum(self):
        assert isinstance(TransitionType.FADE, str)
        assert TransitionType.FADE == "fade"

    def test_all_supported_excludes_cut(self):
        supported = TransitionType.all_supported()
        assert "cut" not in supported
        assert "fade" in supported
        assert "dissolve" in supported
        assert len(supported) == len(TransitionType) - 1

    def test_all_supported_is_list_of_strings(self):
        supported = TransitionType.all_supported()
        assert isinstance(supported, list)
        assert all(isinstance(s, str) for s in supported)


class TestTransitionTypeIsSupported:
    def test_fade_supported(self):
        assert TransitionType.is_supported("fade") is True

    def test_cut_not_supported(self):
        """cut不算在supported里（is_supported只看效果类型）."""
        assert TransitionType.is_supported("cut") is True

    def test_case_insensitive(self):
        assert TransitionType.is_supported("FADE") is True
        assert TransitionType.is_supported("Fade") is True

    def test_underscore_normalized(self):
        assert TransitionType.is_supported("slide_left") is True
        assert TransitionType.is_supported("SLIDE_LEFT") is True

    def test_dash_normalized(self):
        assert TransitionType.is_supported("slide-left") is True

    def test_circle_crop_supported(self):
        assert TransitionType.is_supported("circle_crop") is True
        assert TransitionType.is_supported("circlecrop") is True

    def test_rect_crop_supported(self):
        assert TransitionType.is_supported("rect_crop") is True

    def test_invalid_not_supported(self):
        assert TransitionType.is_supported("nonexistent") is False
        assert TransitionType.is_supported("") is False

    def test_alias_dissolve(self):
        assert TransitionType.is_supported("crossfade") is True
        assert TransitionType.is_supported("crossdissolve") is True

    def test_alias_slide(self):
        assert TransitionType.is_supported("slide") is True

    def test_alias_wipe(self):
        assert TransitionType.is_supported("wipe") is True

    def test_alias_zoom(self):
        assert TransitionType.is_supported("zoomin") is True
        assert TransitionType.is_supported("zoomout") is True

    def test_alias_fade_variants(self):
        assert TransitionType.is_supported("fadein") is True
        assert TransitionType.is_supported("fadeout") is True
        assert TransitionType.is_supported("fadeblack") is True

    def test_alias_circle(self):
        assert TransitionType.is_supported("circle") is True

    def test_alias_rect(self):
        assert TransitionType.is_supported("rect") is True


class TestTransitionConfigDefaults:
    def test_default_config(self):
        cfg = TransitionConfig()
        assert cfg.effect == CUT_TRANSITION
        assert cfg.duration == DEFAULT_TRANSITION_DURATION

    def test_custom_config(self):
        cfg = TransitionConfig(effect="fade", duration=1.0)
        assert cfg.effect == "fade"
        assert cfg.duration == 1.0


class TestTransitionConfigParse:
    def test_parse_none_both(self):
        cfg = TransitionConfig.parse()
        assert cfg.effect == CUT_TRANSITION
        assert cfg.duration == DEFAULT_TRANSITION_DURATION

    def test_parse_none_effect(self):
        cfg = TransitionConfig.parse(duration=1.0)
        assert cfg.effect == CUT_TRANSITION
        assert cfg.duration == 1.0

    def test_parse_none_duration(self):
        cfg = TransitionConfig.parse(effect="fade")
        assert cfg.effect == "fade"
        assert cfg.duration == DEFAULT_TRANSITION_DURATION

    def test_parse_fade(self):
        cfg = TransitionConfig.parse(effect="fade", duration=0.8)
        assert cfg.effect == "fade"
        assert cfg.duration == 0.8

    def test_parse_dissolve(self):
        cfg = TransitionConfig.parse(effect="dissolve")
        assert cfg.effect == "dissolve"

    def test_parse_case_insensitive(self):
        cfg = TransitionConfig.parse(effect="FADE")
        assert cfg.effect == "fade"

    def test_parse_with_underscore(self):
        cfg = TransitionConfig.parse(effect="slide_left")
        assert cfg.effect == "slideleft"

    def test_parse_with_dash(self):
        cfg = TransitionConfig.parse(effect="slide-right")
        assert cfg.effect == "slideright"

    def test_parse_empty_effect(self):
        cfg = TransitionConfig.parse(effect="")
        assert cfg.effect == CUT_TRANSITION

    def test_parse_whitespace_effect(self):
        cfg = TransitionConfig.parse(effect="   ")
        assert cfg.effect == CUT_TRANSITION

    def test_parse_unsupported_effect_fallback_to_cut(self, caplog):
        with caplog.at_level(logging.WARNING):
            cfg = TransitionConfig.parse(effect="nonexistent_effect")
        assert cfg.effect == CUT_TRANSITION
        assert "不支持的转场效果" in caplog.text

    def test_parse_cut_effect(self):
        cfg = TransitionConfig.parse(effect="cut")
        assert cfg.effect == CUT_TRANSITION

    def test_parse_cut_uppercase(self):
        cfg = TransitionConfig.parse(effect="CUT")
        assert cfg.effect == CUT_TRANSITION

    def test_parse_duration_min_boundary(self):
        cfg = TransitionConfig.parse(duration=MIN_TRANSITION_DURATION)
        assert cfg.duration == MIN_TRANSITION_DURATION

    def test_parse_duration_max_boundary(self):
        cfg = TransitionConfig.parse(duration=MAX_TRANSITION_DURATION)
        assert cfg.duration == MAX_TRANSITION_DURATION

    def test_parse_duration_below_min_clamped(self, caplog):
        with caplog.at_level(logging.WARNING):
            cfg = TransitionConfig.parse(duration=0.1)
        assert cfg.duration == MIN_TRANSITION_DURATION
        assert "小于最小值" in caplog.text

    def test_parse_duration_above_max_clamped(self, caplog):
        with caplog.at_level(logging.WARNING):
            cfg = TransitionConfig.parse(duration=5.0)
        assert cfg.duration == MAX_TRANSITION_DURATION
        assert "大于最大值" in caplog.text

    def test_parse_duration_zero_clamped(self, caplog):
        with caplog.at_level(logging.WARNING):
            cfg = TransitionConfig.parse(duration=0.0)
        assert cfg.duration == MIN_TRANSITION_DURATION

    def test_parse_duration_negative_clamped(self, caplog):
        with caplog.at_level(logging.WARNING):
            cfg = TransitionConfig.parse(duration=-1.0)
        assert cfg.duration == MIN_TRANSITION_DURATION

    def test_parse_duration_string_valid(self):
        cfg = TransitionConfig.parse(duration="1.5")
        assert cfg.duration == 1.5

    def test_parse_duration_string_invalid_fallback(self, caplog):
        with caplog.at_level(logging.WARNING):
            cfg = TransitionConfig.parse(duration="abc")
        assert cfg.duration == DEFAULT_TRANSITION_DURATION
        assert "无效的转场时长" in caplog.text

    def test_parse_duration_none_fallback(self):
        cfg = TransitionConfig.parse(duration=None)
        assert cfg.duration == DEFAULT_TRANSITION_DURATION

    def test_parse_alias_crossfade(self):
        cfg = TransitionConfig.parse(effect="crossfade")
        assert cfg.effect == "dissolve"  # 别名→dissolve

    def test_parse_alias_slide(self):
        cfg = TransitionConfig.parse(effect="slide")
        assert cfg.effect == "slideleft"

    def test_parse_alias_wipe(self):
        cfg = TransitionConfig.parse(effect="wipe")
        assert cfg.effect == "wipeleft"

    def test_parse_alias_circle(self):
        cfg = TransitionConfig.parse(effect="circle")
        assert cfg.effect == "circlecrop"


class TestIsCutProperty:
    def test_cut_is_true(self):
        cfg = TransitionConfig(effect="cut")
        assert cfg.is_cut is True

    def test_fade_is_false(self):
        cfg = TransitionConfig(effect="fade")
        assert cfg.is_cut is False

    def test_default_is_cut(self):
        cfg = TransitionConfig()
        assert cfg.is_cut is True


class TestFFmpegTransitionProperty:
    def test_cut_returns_empty(self):
        cfg = TransitionConfig(effect="cut")
        assert cfg.ffmpeg_transition == ""

    def test_fade_returns_fade(self):
        cfg = TransitionConfig(effect="fade")
        assert cfg.ffmpeg_transition == "fade"

    def test_dissolve_returns_dissolve(self):
        cfg = TransitionConfig(effect="dissolve")
        assert cfg.ffmpeg_transition == "dissolve"

    def test_slide_left(self):
        cfg = TransitionConfig(effect="slideleft")
        assert cfg.ffmpeg_transition == "slideleft"

    def test_slide_right(self):
        cfg = TransitionConfig(effect="slideright")
        assert cfg.ffmpeg_transition == "slideright"

    def test_zoom_returns_zoomin(self):
        """transition type是zoom，ffmpeg映射到zoomin."""
        cfg = TransitionConfig(effect="zoom")
        assert cfg.ffmpeg_transition == "zoomin"

    def test_wipe_left(self):
        cfg = TransitionConfig(effect="wipeleft")
        assert cfg.ffmpeg_transition == "wipeleft"

    def test_circle_crop(self):
        cfg = TransitionConfig(effect="circlecrop")
        assert cfg.ffmpeg_transition == "circlecrop"

    def test_rect_crop(self):
        cfg = TransitionConfig(effect="rectcrop")
        assert cfg.ffmpeg_transition == "rectcrop"

    def test_unknown_effect_fallback_to_fade(self):
        """未知效果默认返回fade（安全降级）."""
        cfg = TransitionConfig(effect="unknown_effect")
        # _resolve_transition_enum找不到就返回FADE
        assert cfg.ffmpeg_transition == "fade"


class TestValidate:
    def test_valid_cut(self):
        cfg = TransitionConfig(effect="cut", duration=0.5)
        ok, msg = cfg.validate()
        assert ok is True
        assert msg == ""

    def test_valid_fade(self):
        cfg = TransitionConfig(effect="fade", duration=1.0)
        ok, msg = cfg.validate()
        assert ok is True

    def test_duration_too_low(self):
        cfg = TransitionConfig(effect="fade", duration=0.1)
        ok, msg = cfg.validate()
        assert ok is False
        assert "不能小于" in msg

    def test_duration_too_high(self):
        cfg = TransitionConfig(effect="fade", duration=5.0)
        ok, msg = cfg.validate()
        assert ok is False
        assert "不能大于" in msg

    def test_duration_at_min(self):
        cfg = TransitionConfig(effect="fade", duration=MIN_TRANSITION_DURATION)
        ok, _ = cfg.validate()
        assert ok is True

    def test_duration_at_max(self):
        cfg = TransitionConfig(effect="fade", duration=MAX_TRANSITION_DURATION)
        ok, _ = cfg.validate()
        assert ok is True

    def test_unsupported_effect_invalid(self):
        cfg = TransitionConfig(effect="unknown", duration=0.5)
        ok, msg = cfg.validate()
        assert ok is False
        assert "不支持的转场效果" in msg

    def test_cut_always_valid(self):
        """cut即使duration稍微异常也能通过？不，cut也校验duration."""
        cfg = TransitionConfig(effect="cut", duration=0.5)
        ok, _ = cfg.validate()
        assert ok is True
