"""Tests for Issue #1670 — 跨视频片段避让（生成前注入已用区间）."""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import pytest

from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import (
    SQLAlchemyEditPlanClipRepository,
)
from packages.domain.edit_plan_clip import EditPlanClip, EditPlanClipStatus
from packages.domain.plan_generator_utils import (
    _distribute_one_take,
    distribute_assets,
)

# ── Repository 层测试 ─────────────────────────────────────────────────────────


class TestListUsedSegmentsByUser:
    """测试 list_used_segments_by_user 方法."""

    def _make_repo(self, session_mock):
        return SQLAlchemyEditPlanClipRepository(session_mock)

    def test_empty_user_id_returns_empty_dict(self):
        """空 user_id 直接返回空 dict，不查 DB."""
        session = MagicMock()
        repo = self._make_repo(session)
        result = repo.list_used_segments_by_user("")
        assert result == {}
        session.query.assert_not_called()

    def test_no_completed_plans_returns_empty_dict(self):
        """用户没有已完成的 plan 时返回空 dict."""
        session = MagicMock()
        # Mock plan query returns empty
        plan_query = MagicMock()
        plan_query.filter.return_value = plan_query
        plan_query.order_by.return_value = plan_query
        plan_query.limit.return_value = plan_query
        plan_query.all.return_value = []
        session.query.return_value = plan_query

        repo = self._make_repo(session)
        result = repo.list_used_segments_by_user("user_123")
        assert result == {}

    def test_aggregates_clips_from_multiple_plans(self):
        """从多个已完成 plan 的 clips 聚合已用区间."""
        session = MagicMock()

        # Mock plan query: 2 completed plans
        plan_query = MagicMock()
        plan_query.filter.return_value = plan_query
        plan_query.order_by.return_value = plan_query
        plan_query.limit.return_value = plan_query
        plan_query.all.return_value = [("plan_1",), ("plan_2",)]
        session.query.return_value = plan_query

        # Mock clip query: clips from both plans
        clip_query = MagicMock()
        clip_query.filter.return_value = clip_query
        clip_query.all.return_value = [
            ("asset_A", 0.0, 5.0),  # plan_1, asset A: 0~5s
            ("asset_A", 10.0, 3.0),  # plan_1, asset A: 10~13s
            ("asset_B", 2.0, 4.0),  # plan_2, asset B: 2~6s
        ]
        # Second session.query call is for clips
        session.query.side_effect = [plan_query, clip_query]

        repo = self._make_repo(session)
        result = repo.list_used_segments_by_user("user_123")

        assert "asset_A" in result
        assert len(result["asset_A"]) == 2
        assert result["asset_A"][0] == (0.0, 5.0)
        assert result["asset_A"][1] == (10.0, 13.0)
        assert "asset_B" in result
        assert result["asset_B"][0] == (2.0, 6.0)

    def test_respects_limit_recent_parameter(self):
        """limit_recent 参数限制查询的 plan 数量."""
        session = MagicMock()

        plan_query = MagicMock()
        plan_query.filter.return_value = plan_query
        plan_query.order_by.return_value = plan_query
        plan_query.limit.return_value = plan_query
        plan_query.all.return_value = [("plan_1",)]
        session.query.return_value = plan_query

        clip_query = MagicMock()
        clip_query.filter.return_value = clip_query
        clip_query.all.return_value = [("asset_X", 1.0, 2.0)]
        session.query.side_effect = [plan_query, clip_query]

        repo = self._make_repo(session)
        result = repo.list_used_segments_by_user("user_123", limit_recent=10)

        # Verify limit was called with the parameter
        plan_query.limit.assert_called_once_with(10)
        assert "asset_X" in result


# ── Domain 层测试 ─────────────────────────────────────────────────────────────


class TestDistributeAssetsWithExternalSegments:
    """测试 distribute_assets 传入 external_used_segments 的行为."""

    def _make_clips(self, count: int, duration: float = 3.0) -> list[EditPlanClip]:
        """创建指定数量的 MAIN 类型 clips."""
        return [
            EditPlanClip(
                id=f"clip_{i}",
                plan_id="plan_1",
                clip_type="main",
                order=i,
                template_clip_config_id="",
                asset_id="",
                text_content="",
                start_time=0.0,
                duration=duration,
                status=EditPlanClipStatus.PENDING,
            )
            for i in range(count)
        ]

    def test_external_used_segments_none_backward_compatible(self):
        """external_used_segments=None 时行为不变（向后兼容）."""
        clips = self._make_clips(3)
        asset_ids = ["asset_1", "asset_2", "asset_3"]
        asset_durations = {aid: 30.0 for aid in asset_ids}

        # Should not raise
        distribute_assets(
            clips,
            asset_ids,
            "one_take",
            asset_durations=asset_durations,
            external_used_segments=None,
        )

        # All clips should have assets assigned
        for clip in clips:
            assert clip.asset_id != ""

    def test_external_used_segments_avoids_existing_ranges(self):
        """传入 external_used_segments 后，新分配的 start_time 避开已有区间."""
        clips = self._make_clips(2, duration=3.0)
        asset_ids = ["asset_1"]
        asset_durations = {"asset_1": 30.0}

        # Pretend asset_1 0~10s is already used by another video
        external = {"asset_1": [(0.0, 10.0)]}

        # Run multiple times to check that start_time always avoids 0~10s
        # (with some randomness, but the avoidance should be consistent)
        for _ in range(10):
            test_clips = self._make_clips(1, duration=3.0)
            distribute_assets(
                test_clips,
                asset_ids,
                "one_take",
                asset_durations=asset_durations,
                external_used_segments=external,
            )
            start = test_clips[0].start_time
            # Start time + duration (3s) should not overlap with 0~10
            # i.e., start >= 10.0 or start + 3 <= 0.0 (impossible since start >= 0)
            assert (
                start >= 10.0 or start + 3.0 <= 0.0 or start >= 10.0
            ), f"start_time {start} overlaps with existing segment 0~10"

    def test_external_used_segments_deep_copy(self):
        """external_used_segments 会被深拷贝，不会修改外部数据."""
        external = {"asset_1": [(0.0, 5.0)]}
        original = {"asset_1": [(0.0, 5.0)]}

        clips = self._make_clips(1, duration=2.0)
        asset_ids = ["asset_1"]
        asset_durations = {"asset_1": 20.0}

        distribute_assets(
            clips,
            asset_ids,
            "one_take",
            asset_durations=asset_durations,
            external_used_segments=external,
        )

        # External dict should be unchanged
        assert external == original

    def test_empty_external_used_segments_same_as_none(self):
        """空 dict 的 external_used_segments 行为与 None 相同."""
        clips = self._make_clips(2, duration=3.0)
        asset_ids = ["asset_1", "asset_2"]
        asset_durations = {aid: 30.0 for aid in asset_ids}

        # Should not raise and should assign assets normally
        distribute_assets(
            clips,
            asset_ids,
            "one_take",
            asset_durations=asset_durations,
            external_used_segments={},
        )
        for clip in clips:
            assert clip.asset_id != ""


# ── Service 层测试 ────────────────────────────────────────────────────────────


class TestServiceLayerIntegration:
    """测试 _distribute_assets 在 service 层的查询逻辑."""

    def _make_service(self, clip_repo_mock, asset_repo_mock=None):
        """创建 PlanGeneratorService 并注入 mock repos."""

        from apps.api.app.services.plan_generator_service import PlanGeneratorService

        with (
            patch("apps.api.app.services.plan_generator_service.SQLAlchemyEditPlanRepository"),
            patch(
                "apps.api.app.services.plan_generator_service.SQLAlchemyEditPlanClipRepository",
                return_value=clip_repo_mock,
            ),
        ):
            db = MagicMock()
            svc = PlanGeneratorService(db, asset_repo=asset_repo_mock)
            svc._clip_repo = clip_repo_mock
        return svc

    def _make_clip(self):
        return EditPlanClip(
            id="clip_1",
            plan_id="plan_1",
            clip_type="main",
            order=0,
            template_clip_config_id="",
            asset_id="",
            text_content="",
            start_time=0.0,
            duration=3.0,
            status=EditPlanClipStatus.PENDING,
        )

    def test_query_called_with_user_id(self):
        """有 user_id 时调用 list_used_segments_by_user."""
        clip_repo = MagicMock()
        clip_repo.list_used_segments_by_user.return_value = {"asset_A": [(0.0, 5.0)]}
        asset_repo = MagicMock()
        asset_repo.get.return_value = None  # smart_match fallback

        svc = self._make_service(clip_repo, asset_repo)
        clips = [self._make_clip()]

        svc._distribute_assets(
            clips,
            ["asset_A"],
            "one_take",
            asset_durations={"asset_A": 30.0},
            user_id="user_123",
        )

        clip_repo.list_used_segments_by_user.assert_called_once_with("user_123", limit_recent=50)

    def test_query_not_called_without_user_id(self):
        """无 user_id 时不调用查询."""
        clip_repo = MagicMock()
        asset_repo = MagicMock()
        asset_repo.get.return_value = None

        svc = self._make_service(clip_repo, asset_repo)
        clips = [self._make_clip()]

        svc._distribute_assets(
            clips,
            ["asset_A"],
            "one_take",
            asset_durations={"asset_A": 30.0},
            user_id="",
        )

        clip_repo.list_used_segments_by_user.assert_not_called()

    def test_query_failure_does_not_block_generation(self):
        """查询失败时不阻塞生成，回退到纯随机."""
        clip_repo = MagicMock()
        clip_repo.list_used_segments_by_user.side_effect = Exception("DB error")
        asset_repo = MagicMock()
        asset_repo.get.return_value = None

        svc = self._make_service(clip_repo, asset_repo)
        clips = [self._make_clip()]

        # Should not raise
        svc._distribute_assets(
            clips,
            ["asset_A"],
            "one_take",
            asset_durations={"asset_A": 30.0},
            user_id="user_123",
        )

        # Clip should still get an asset assigned (fallback to random)
        assert clips[0].asset_id == "asset_A"

    def test_preview_and_final_both_query(self):
        """预览和正式生成都触发查询."""
        for random_selection in [True, False]:
            clip_repo = MagicMock()
            clip_repo.list_used_segments_by_user.return_value = {}
            asset_repo = MagicMock()
            asset_repo.get.return_value = None

            svc = self._make_service(clip_repo, asset_repo)
            clips = [self._make_clip()]

            svc._distribute_assets(
                clips,
                ["asset_A"],
                "one_take",
                random_selection=random_selection,
                asset_durations={"asset_A": 30.0},
                user_id="user_123",
            )

            clip_repo.list_used_segments_by_user.assert_called_once()
