"""第78波：Duplication 查重领域模型纯逻辑单测。"""

import pytest

from packages.domain.duplication import DuplicateSegment, DuplicationRecord

# ============================================================
# DuplicateSegment.create 测试
# ============================================================


class TestDuplicateSegmentCreate:
    def test_create_basic(self):
        seg = DuplicateSegment.create(
            source_start=10.0,
            source_end=20.0,
            matched_video_id="mv1",
            matched_video_name="匹配视频",
            matched_start=5.0,
            matched_end=15.0,
            similarity=85.5,
        )
        assert seg.id
        assert seg.source_start == 10.0
        assert seg.source_end == 20.0
        assert seg.matched_video_id == "mv1"
        assert seg.matched_video_name == "匹配视频"
        assert seg.matched_start == 5.0
        assert seg.matched_end == 15.0
        assert seg.similarity == 85.5

    def test_create_invalid_source_start_negative(self):
        with pytest.raises(ValueError, match="invalid source segment range"):
            DuplicateSegment.create(
                source_start=-1.0,
                source_end=10.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=0.0,
                matched_end=10.0,
                similarity=50.0,
            )

    def test_create_invalid_source_end_lte_start(self):
        with pytest.raises(ValueError, match="invalid source segment range"):
            DuplicateSegment.create(
                source_start=20.0,
                source_end=10.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=0.0,
                matched_end=10.0,
                similarity=50.0,
            )

    def test_create_source_end_equal_start_invalid(self):
        with pytest.raises(ValueError, match="invalid source segment range"):
            DuplicateSegment.create(
                source_start=10.0,
                source_end=10.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=0.0,
                matched_end=10.0,
                similarity=50.0,
            )

    def test_create_invalid_matched_start_negative(self):
        with pytest.raises(ValueError, match="invalid matched segment range"):
            DuplicateSegment.create(
                source_start=0.0,
                source_end=10.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=-1.0,
                matched_end=10.0,
                similarity=50.0,
            )

    def test_create_invalid_matched_end_lte_start(self):
        with pytest.raises(ValueError, match="invalid matched segment range"):
            DuplicateSegment.create(
                source_start=0.0,
                source_end=10.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=15.0,
                matched_end=5.0,
                similarity=50.0,
            )

    def test_create_similarity_zero(self):
        seg = DuplicateSegment.create(
            source_start=0.0,
            source_end=5.0,
            matched_video_id="mv1",
            matched_video_name="n",
            matched_start=0.0,
            matched_end=5.0,
            similarity=0.0,
        )
        assert seg.similarity == 0.0

    def test_create_similarity_hundred(self):
        seg = DuplicateSegment.create(
            source_start=0.0,
            source_end=5.0,
            matched_video_id="mv1",
            matched_video_name="n",
            matched_start=0.0,
            matched_end=5.0,
            similarity=100.0,
        )
        assert seg.similarity == 100.0

    def test_create_similarity_negative_raises(self):
        with pytest.raises(ValueError, match="similarity must be between 0 and 100"):
            DuplicateSegment.create(
                source_start=0.0,
                source_end=5.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=0.0,
                matched_end=5.0,
                similarity=-1.0,
            )

    def test_create_similarity_over_100_raises(self):
        with pytest.raises(ValueError, match="similarity must be between 0 and 100"):
            DuplicateSegment.create(
                source_start=0.0,
                source_end=5.0,
                matched_video_id="mv1",
                matched_video_name="n",
                matched_start=0.0,
                matched_end=5.0,
                similarity=101.0,
            )

    def test_create_id_unique(self):
        s1 = DuplicateSegment.create(0, 5, "m", "n", 0, 5, 50.0)
        s2 = DuplicateSegment.create(0, 5, "m", "n", 0, 5, 50.0)
        assert s1.id != s2.id


# ============================================================
# DuplicationRecord.create 测试
# ============================================================


class TestDuplicationRecordCreate:
    def test_create_minimal(self):
        rec = DuplicationRecord.create(
            user_id="u1",
            filename="test.mp4",
            file_size=102400,
            storage_key="oss://key",
        )
        assert rec.id
        assert rec.user_id == "u1"
        assert rec.filename == "test.mp4"
        assert rec.file_size == 102400
        assert rec.storage_key == "oss://key"
        assert rec.duration_seconds == 0.0
        assert rec.status == "pending"
        assert rec.duplicate_rate is None
        assert rec.duplicate_count == 0
        assert rec.segments == []
        assert rec.error_message == ""

    def test_create_with_duration(self):
        rec = DuplicationRecord.create(
            user_id="u1",
            filename="test.mp4",
            file_size=1024,
            storage_key="oss://k",
            duration_seconds=120.5,
        )
        assert rec.duration_seconds == 120.5

    def test_create_empty_user_id_raises(self):
        with pytest.raises(ValueError, match="user_id cannot be empty"):
            DuplicationRecord.create(
                user_id="  ",
                filename="t.mp4",
                file_size=100,
                storage_key="k",
            )

    def test_create_empty_filename_raises(self):
        with pytest.raises(ValueError, match="filename cannot be empty"):
            DuplicationRecord.create(
                user_id="u1",
                filename="",
                file_size=100,
                storage_key="k",
            )

    def test_create_zero_file_size_raises(self):
        with pytest.raises(ValueError, match="file_size must be positive"):
            DuplicationRecord.create(
                user_id="u1",
                filename="t.mp4",
                file_size=0,
                storage_key="k",
            )

    def test_create_negative_file_size_raises(self):
        with pytest.raises(ValueError, match="file_size must be positive"):
            DuplicationRecord.create(
                user_id="u1",
                filename="t.mp4",
                file_size=-100,
                storage_key="k",
            )

    def test_create_strips_whitespace(self):
        rec = DuplicationRecord.create(
            user_id=" u1 ",
            filename=" test.mp4 ",
            file_size=100,
            storage_key=" oss://k ",
        )
        assert rec.user_id == "u1"
        assert rec.filename == "test.mp4"
        # storage_key 不确定有没有 strip，看源码是直接赋值
        assert rec.storage_key == " oss://k "

    def test_create_id_unique(self):
        r1 = DuplicationRecord.create("u1", "a.mp4", 100, "k1")
        r2 = DuplicationRecord.create("u1", "b.mp4", 200, "k2")
        assert r1.id != r2.id

    def test_create_default_segments_empty_list(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        assert rec.segments == []
        assert isinstance(rec.segments, list)


# ============================================================
# DuplicationRecord 状态流转测试
# ============================================================


class TestDuplicationRecordStatusFlow:
    def test_mark_processing(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        before = rec.updated_at
        rec.mark_processing()
        assert rec.status == "processing"
        assert rec.updated_at >= before

    def test_mark_completed(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        seg = DuplicateSegment.create(0, 5, "m", "n", 0, 5, 90.0)
        rec.mark_completed(
            duplicate_rate=35.5,
            duplicate_count=1,
            segments=[seg],
        )
        assert rec.status == "completed"
        assert rec.duplicate_rate == 35.5
        assert rec.duplicate_count == 1
        assert len(rec.segments) == 1
        assert rec.segments[0].id == seg.id

    def test_mark_completed_zero_rate(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.mark_completed(0.0, 0, [])
        assert rec.status == "completed"
        assert rec.duplicate_rate == 0.0
        assert rec.duplicate_count == 0
        assert rec.segments == []

    def test_mark_completed_full_rate(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        seg = DuplicateSegment.create(0, 10, "m", "n", 0, 10, 100.0)
        rec.mark_completed(100.0, 1, [seg])
        assert rec.duplicate_rate == 100.0

    def test_mark_completed_invalid_rate_negative(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        with pytest.raises(ValueError, match="duplicate_rate must be between 0 and 100"):
            rec.mark_completed(-1.0, 0, [])

    def test_mark_completed_invalid_rate_over_100(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        with pytest.raises(ValueError, match="duplicate_rate must be between 0 and 100"):
            rec.mark_completed(100.1, 0, [])

    def test_mark_failed(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.mark_failed("网络超时")
        assert rec.status == "failed"
        assert rec.error_message == "网络超时"

    def test_mark_failed_from_processing(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.mark_processing()
        rec.mark_failed("解析失败")
        assert rec.status == "failed"
        assert rec.error_message == "解析失败"


# ============================================================
# DuplicationRecord 重试机制测试
# ============================================================


class TestDuplicationRecordRetry:
    def test_can_retry_failed_true(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.mark_failed("err")
        assert rec.can_retry() is True

    def test_can_retry_pending_false(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        assert rec.can_retry() is False

    def test_can_retry_processing_false(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.mark_processing()
        assert rec.can_retry() is False

    def test_can_retry_completed_false(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.mark_completed(10.0, 0, [])
        assert rec.can_retry() is False

    def test_reset_for_retry_clears_all(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        seg = DuplicateSegment.create(0, 5, "m", "n", 0, 5, 80.0)
        rec.mark_completed(30.0, 1, [seg])
        # 先设为 failed 再 reset
        rec.status = "failed"
        rec.reset_for_retry()
        assert rec.status == "pending"
        assert rec.duplicate_rate is None
        assert rec.duplicate_count == 0
        assert rec.error_message == ""
        assert rec.segments == []
        assert rec.video_fingerprint is None

    def test_reset_for_retry_updates_timestamp(self):
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        rec.status = "failed"
        before = rec.updated_at
        rec.reset_for_retry()
        assert rec.updated_at >= before

    def test_full_retry_flow(self):
        """完整的 创建→失败→重置→再处理→完成 流程。"""
        rec = DuplicationRecord.create("u1", "a.mp4", 100, "k")
        assert rec.status == "pending"

        rec.mark_processing()
        assert rec.status == "processing"

        rec.mark_failed("超时")
        assert rec.can_retry()

        rec.reset_for_retry()
        assert rec.status == "pending"
        assert rec.error_message == ""

        rec.mark_processing()
        rec.mark_completed(0.0, 0, [])
        assert rec.status == "completed"
        assert not rec.can_retry()


# ============================================================
# QuotaTier 测试
# ============================================================


class TestQuotaTier:
    def test_get_limit_defined(self):
        from packages.domain.quota import QUOTA_TIERS, QuotaTier

        free = QUOTA_TIERS["free"]
        assert free.get_limit("storage_gb") == 2
        assert free.get_limit("videos_per_month") == 5

    def test_get_limit_undefined_returns_zero(self):
        from packages.domain.quota import QuotaTier

        tier = QuotaTier(name="test", limits={"a": 10})
        assert tier.get_limit("nonexistent") == 0

    def test_is_unlimited_inf(self):
        from packages.domain.quota import QUOTA_TIERS

        premium = QUOTA_TIERS["premium"]
        assert premium.is_unlimited("max_templates") is True

    def test_is_unlimited_finite(self):
        from packages.domain.quota import QUOTA_TIERS

        free = QUOTA_TIERS["free"]
        assert free.is_unlimited("storage_gb") is False

    def test_is_unlimited_undefined_returns_true(self):
        """未定义的维度 limits.get 默认 inf，is_unlimited 返回 True。"""
        from packages.domain.quota import QuotaTier

        tier = QuotaTier(name="test", limits={})
        assert tier.is_unlimited("unknown") is True


# ============================================================
# QUOTA_TIERS 三档套餐验证
# ============================================================


class TestQuotaTiers:
    def test_free_tier_storage(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["free"].get_limit("storage_gb") == 2

    def test_free_tier_no_ai_voice(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["free"].get_limit("ai_voice_enabled") == 0

    def test_basic_tier_storage(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["basic"].get_limit("storage_gb") == 20

    def test_basic_tier_has_ai_voice(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["basic"].get_limit("ai_voice_enabled") == 1

    def test_basic_tier_ai_voice_credits(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["basic"].get_limit("ai_voice_credits") == 100

    def test_premium_tier_storage(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["premium"].get_limit("storage_gb") == 100

    def test_premium_tier_unlimited_templates(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["premium"].is_unlimited("max_templates")

    def test_premium_tier_multi_platform(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["premium"].get_limit("multi_platform_enabled") == 1

    def test_basic_no_multi_platform(self):
        from packages.domain.quota import QUOTA_TIERS

        assert QUOTA_TIERS["basic"].get_limit("multi_platform_enabled") == 0

    def test_all_tiers_exist(self):
        from packages.domain.quota import QUOTA_TIERS

        assert set(QUOTA_TIERS.keys()) == {"free", "basic", "premium", "pro"}


# ============================================================
# QuotaCheckResult 测试
# ============================================================


class TestQuotaCheckResult:
    def test_usage_percent_normal(self):
        from packages.domain.quota import QuotaCheckResult, QuotaWarningLevel

        r = QuotaCheckResult(
            allowed=True,
            dimension="storage_gb",
            limit=100,
            used=50,
            remaining=50,
            warning_level=QuotaWarningLevel.NORMAL,
        )
        assert r.usage_percent == 50.0

    def test_usage_percent_capped_at_100(self):
        from packages.domain.quota import QuotaCheckResult, QuotaWarningLevel

        r = QuotaCheckResult(
            allowed=False,
            dimension="x",
            limit=10,
            used=15,
            remaining=0,
            warning_level=QuotaWarningLevel.EXCEEDED,
        )
        assert r.usage_percent == 100.0

    def test_usage_percent_unlimited(self):
        from packages.domain.quota import QuotaCheckResult, QuotaWarningLevel

        r = QuotaCheckResult(
            allowed=True,
            dimension="x",
            limit=float("inf"),
            used=999,
            remaining=float("inf"),
            warning_level=QuotaWarningLevel.NORMAL,
        )
        assert r.usage_percent == 0.0

    def test_usage_percent_zero_limit_with_usage(self):
        from packages.domain.quota import QuotaCheckResult, QuotaWarningLevel

        r = QuotaCheckResult(
            allowed=False,
            dimension="x",
            limit=0,
            used=5,
            remaining=0,
            warning_level=QuotaWarningLevel.EXCEEDED,
        )
        assert r.usage_percent == 100.0

    def test_usage_percent_zero_limit_no_usage(self):
        from packages.domain.quota import QuotaCheckResult, QuotaWarningLevel

        r = QuotaCheckResult(
            allowed=True,
            dimension="x",
            limit=0,
            used=0,
            remaining=0,
            warning_level=QuotaWarningLevel.NORMAL,
        )
        assert r.usage_percent == 0.0


# ============================================================
# QuotaWarningLevel 计算测试
# ============================================================


class TestWarningLevel:
    def test_normal_below_80(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(50, 100) == QuotaWarningLevel.NORMAL

    def test_warning_at_80(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(80, 100) == QuotaWarningLevel.WARNING

    def test_warning_below_95(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(90, 100) == QuotaWarningLevel.WARNING

    def test_critical_at_95(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(95, 100) == QuotaWarningLevel.CRITICAL

    def test_critical_below_100(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(99, 100) == QuotaWarningLevel.CRITICAL

    def test_exceeded_at_100(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(100, 100) == QuotaWarningLevel.EXCEEDED

    def test_exceeded_over_100(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(150, 100) == QuotaWarningLevel.EXCEEDED

    def test_unlimited_always_normal(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(99999, float("inf")) == QuotaWarningLevel.NORMAL

    def test_zero_limit_with_usage_is_exceeded(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(1, 0) == QuotaWarningLevel.EXCEEDED

    def test_zero_limit_no_usage_is_normal(self):
        from packages.domain.quota import QuotaWarningLevel, get_warning_level

        assert get_warning_level(0, 0) == QuotaWarningLevel.NORMAL


# ============================================================
# QuotaChecker 测试
# ============================================================


class TestQuotaChecker:
    def test_check_allowed(self):
        from packages.domain.quota import QuotaChecker, QuotaWarningLevel

        checker = QuotaChecker()
        r = checker.check("free", "storage_gb", 1)
        assert r.allowed is True
        assert r.limit == 2
        assert r.used == 1
        assert r.remaining == 1
        assert r.warning_level == QuotaWarningLevel.NORMAL

    def test_check_exceeded(self):
        from packages.domain.quota import QuotaChecker, QuotaWarningLevel

        checker = QuotaChecker()
        r = checker.check("free", "storage_gb", 5)
        assert r.allowed is False
        assert r.remaining == 0
        assert r.warning_level == QuotaWarningLevel.EXCEEDED

    def test_check_at_limit_not_allowed(self):
        """used == limit 时 allowed 为 False（严格小于才算允许）。"""
        from packages.domain.quota import QuotaChecker

        checker = QuotaChecker()
        r = checker.check("free", "storage_gb", 2)
        assert r.allowed is False

    def test_check_unlimited_always_allowed(self):
        from packages.domain.quota import QuotaChecker, QuotaWarningLevel

        checker = QuotaChecker()
        r = checker.check("premium", "max_templates", 9999)
        assert r.allowed is True
        assert r.remaining == float("inf")
        assert r.warning_level == QuotaWarningLevel.NORMAL

    def test_check_unknown_plan_returns_zero(self):
        from packages.domain.quota import QuotaChecker, QuotaWarningLevel

        checker = QuotaChecker()
        r = checker.check("unknown_plan", "storage_gb", 1)
        assert r.allowed is False
        assert r.limit == 0
        assert r.warning_level == QuotaWarningLevel.EXCEEDED

    def test_check_warning_level_80_percent(self):
        from packages.domain.quota import QuotaChecker, QuotaWarningLevel

        checker = QuotaChecker()
        r = checker.check("basic", "storage_gb", 16)  # 20 * 0.8 = 16
        assert r.warning_level == QuotaWarningLevel.WARNING

    def test_check_multiple(self):
        from packages.domain.quota import QuotaChecker

        checker = QuotaChecker()
        results = checker.check_multiple(
            "free",
            {"storage_gb": 1, "videos_per_month": 10, "max_templates": 2},
        )
        assert len(results) == 3
        dims = {r.dimension: r for r in results}
        assert dims["storage_gb"].allowed is True
        assert dims["videos_per_month"].allowed is False
        assert dims["max_templates"].allowed is True


# ============================================================
# QuotaRegistry 测试
# ============================================================


class TestQuotaRegistry:
    def test_list_dimensions_includes_builtin(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        dims = reg.list_dimensions()
        assert "storage_gb" in dims
        assert "videos_per_month" in dims
        assert "ai_voice_enabled" in dims

    def test_list_tiers(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        tiers = reg.list_tiers()
        assert set(tiers) == {"free", "basic", "premium", "pro"}

    def test_register_new_dimension(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        reg.register_dimension("custom_dim", "自定义维度", {"free": 5, "basic": 20})
        dims = reg.list_dimensions()
        assert "custom_dim" in dims
        assert dims["custom_dim"] == "自定义维度"
        assert reg.get_limit("free", "custom_dim") == 5
        assert reg.get_limit("basic", "custom_dim") == 20

    def test_register_dimension_idempotent(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        reg.register_dimension("custom_dim", "v1", {"free": 5})
        reg.register_dimension("custom_dim", "v2", {"free": 10})
        # 第二次应该被忽略（幂等），描述和限制都保持第一次
        assert reg.list_dimensions()["custom_dim"] == "v1"
        assert reg.get_limit("free", "custom_dim") == 5

    def test_register_without_defaults_defaults_to_zero(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        reg.register_dimension("new_dim", "新维度")
        assert reg.get_limit("free", "new_dim") == 0
        assert reg.get_limit("basic", "new_dim") == 0
        assert reg.get_limit("premium", "new_dim") == 0

    def test_get_tier_exists(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        tier = reg.get_tier("free")
        assert tier is not None
        assert tier.name == "free"

    def test_get_tier_not_exists(self):
        from packages.domain.quota import QuotaRegistry

        reg = QuotaRegistry()
        assert reg.get_tier("nonexistent") is None

    def test_global_registry_instance(self):
        from packages.domain.quota import quota_checker, quota_registry

        assert quota_registry is not None
        assert quota_checker is not None
        assert quota_registry.get_limit("free", "storage_gb") == 2
