"""#1749 EditPlanService 新增方法单元测试。

覆盖 get_asset_durations / apply_voice_duration_to_plan / ensure_variant_plans。
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

# ── helpers ───────────────────────────────────────────────────────────────────


def _make_svc():
    """构造 EditPlanService，mock 掉 DB/repos。"""
    from app.services.edit_plan_service import EditPlanService

    db = MagicMock()
    svc = EditPlanService.__new__(EditPlanService)
    svc._plan_repo = MagicMock()
    svc._clip_repo = MagicMock()
    svc._clip_repo.session = db
    svc._generation_task_repo = MagicMock()
    return svc, db


def _make_clip(
    order=0,
    asset_id="a1",
    start_time=0.0,
    duration=5.0,
    clip_type="main",
    transition_effect="cut",
    transition_duration=0.0,
    playback_speed=1.0,
    text_content="",
    config=None,
):
    c = MagicMock()
    c.order = order
    c.asset_id = asset_id
    c.start_time = start_time
    c.duration = duration
    c.clip_type = clip_type
    c.transition_effect = transition_effect
    c.transition_duration = transition_duration
    c.playback_speed = playback_speed
    c.text_content = text_content
    c.config = config or {}
    return c


# ═══════════════════════════════════════════════════════════════════════════════
# get_asset_durations
# ═══════════════════════════════════════════════════════════════════════════════


class TestGetAssetDurations:
    def test_empty_ids_returns_empty(self):
        svc, _db = _make_svc()
        assert svc.get_asset_durations([]) == {}
        assert svc.get_asset_durations(None) == {}

    def test_dedup_and_query(self):
        svc, db = _make_svc()
        m1 = MagicMock()
        m1.id = "a1"
        m1.duration = 10.5
        m2 = MagicMock()
        m2.id = "a2"
        m2.duration = None  # -> 0.0

        db.query.return_value.filter.return_value.all.return_value = [m1, m2]

        result = svc.get_asset_durations(["a1", "a2", "a1"])
        assert result == {"a1": 10.5, "a2": 0.0}

    def test_bad_duration_type_returns_zero(self):
        svc, db = _make_svc()
        m = MagicMock()
        m.id = "a1"
        m.duration = "not-a-number"
        db.query.return_value.filter.return_value.all.return_value = [m]
        result = svc.get_asset_durations(["a1"])
        assert result == {"a1": 0.0}


# ═══════════════════════════════════════════════════════════════════════════════
# apply_voice_duration_to_plan
# ═══════════════════════════════════════════════════════════════════════════════


class TestApplyVoiceDurationToPlan:
    def test_voice_le_zero_returns_none(self):
        svc, _ = _make_svc()
        assert svc.apply_voice_duration_to_plan("p1", 0) is None
        assert svc.apply_voice_duration_to_plan("p1", -5) is None

    def test_plan_not_found_returns_none(self):
        svc, _ = _make_svc()
        svc.get_plan = MagicMock(return_value=None)
        assert svc.apply_voice_duration_to_plan("p1", 30.0) is None

    def test_no_clips_returns_none(self):
        svc, _ = _make_svc()
        plan = MagicMock()
        svc.get_plan = MagicMock(return_value=plan)
        svc._clip_repo.list_by_plan.return_value = []
        assert svc.apply_voice_duration_to_plan("p1", 30.0) is None

    def test_happy_path_two_clips(self):
        svc, db = _make_svc()
        plan = MagicMock()
        plan.total_duration = 0.0
        svc.get_plan = MagicMock(return_value=plan)

        clips = [
            _make_clip(order=0, asset_id="a1", start_time=0.0),
            _make_clip(order=1, asset_id="a2", start_time=0.0),
        ]
        svc._clip_repo.list_by_plan.return_value = clips

        # get_asset_durations: a1=100s, a2=50s
        m1 = MagicMock(id="a1", duration=100.0)
        m2 = MagicMock(id="a2", duration=50.0)
        db.query.return_value.filter.return_value.all.return_value = [m1, m2]

        svc.replace_all_clips_transactional = MagicMock(return_value=2)

        result = svc.apply_voice_duration_to_plan("p1", 30.0)
        assert result is plan
        svc.replace_all_clips_transactional.assert_called_once()

    def test_invalid_voice_returns_none(self):
        svc, _ = _make_svc()
        assert svc.apply_voice_duration_to_plan("p1", "bad") is None


# ═══════════════════════════════════════════════════════════════════════════════
# ensure_variant_plans
# ═══════════════════════════════════════════════════════════════════════════════


class TestEnsureVariantPlans:
    def _setup_svc(self):
        svc, _ = _make_svc()
        plan0 = MagicMock()
        plan0.id = "plan_v0"
        svc.clone_plan_for_variant = MagicMock(return_value=plan0)

        plan1 = MagicMock()
        plan1.id = "plan_v1"
        svc.reselect_plan_for_variant = MagicMock(return_value=plan1)

        svc.apply_voice_duration_to_plan = MagicMock(return_value=plan0)
        return svc

    def test_count_1_no_voice(self):
        svc = self._setup_svc()
        result = svc.ensure_variant_plans("src", 1, ["a1", "a2"])
        assert result == ["plan_v0"]
        svc.clone_plan_for_variant.assert_called_once()
        svc.apply_voice_duration_to_plan.assert_not_called()

    def test_count_1_with_voice(self):
        svc = self._setup_svc()
        result = svc.ensure_variant_plans("src", 1, ["a1"], voice_durations=[30.0])
        assert result == ["plan_v0"]
        svc.apply_voice_duration_to_plan.assert_called_once_with("plan_v0", 30.0)

    def test_count_3_with_voice(self):
        svc = self._setup_svc()
        svc.reselect_plan_for_variant = MagicMock(
            side_effect=[
                MagicMock(id="plan_v1"),
                MagicMock(id="plan_v2"),
            ]
        )

        result = svc.ensure_variant_plans("src", 3, ["a1", "a2"], voice_durations=[30.0, 20.0, 15.0])
        assert len(result) == 3
        assert result[0] == "plan_v0"
        # v0 配音
        svc.apply_voice_duration_to_plan.assert_called_once_with("plan_v0", 30.0)
        # reselect 被调 2 次
        assert svc.reselect_plan_for_variant.call_count == 2

    def test_voice_apply_failure_not_blocking(self):
        svc = self._setup_svc()
        svc.apply_voice_duration_to_plan.side_effect = RuntimeError("boom")
        result = svc.ensure_variant_plans("src", 1, ["a1"], voice_durations=[30.0])
        assert result == ["plan_v0"]
