"""#1743 批量变体独立选片纯核心测试（packages/domain/variant_plan_selector.py）。

覆盖：
- 固定种子下 N 次独立选片：素材组合/片段顺序/起点显著不同（降重核心）
- 批次内同素材区间重叠 >20% 触发避让重选
- 异常输入：空源片段/空素材池/时长全 0 → ValueError（严禁退回同源）
- 非 main 片段（intro/outro/overlay）保留源骨架素材，仅重算起点
- 跨变体 batch_segments 就地累加（串行选片天然避让）
- 文案/转场/速度等骨架字段透传
"""

from __future__ import annotations

import random
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
for sub in ("packages", ""):
    p = str(REPO_ROOT / sub) if sub else str(REPO_ROOT)
    if p not in sys.path:
        sys.path.insert(0, p)

from packages.domain import variant_plan_selector as vps  # noqa: E402
from packages.domain.variant_plan_selector import (  # noqa: E402
    BATCH_CLIP_OVERLAP_LIMIT,
    _clip_overlap_ratio,
    reselect_clips_for_variant,
)


def _source_clips(assets=("a1", "a2", "a3"), dur=5.0, with_non_main=False):
    """构造源片段骨架：main 片段若干，可选 intro/outro 固定角色片段。"""
    clips = []
    order = 0
    if with_non_main:
        clips.append(
            {
                "order": order,
                "asset_id": "intro_asset",
                "start_time": 0.0,
                "duration": 3.0,
                "clip_type": "intro",
                "playback_speed": 1.0,
                "transition_effect": "fade",
                "transition_duration": 0.5,
                "text_content": "片头",
                "config": {"role": "intro"},
            }
        )
        order += 1
    for i, aid in enumerate(assets):
        clips.append(
            {
                "order": order,
                "asset_id": aid,
                "start_time": float(i * 10),
                "duration": dur,
                "clip_type": "main",
                "playback_speed": 1.2,
                "transition_effect": "cut",
                "transition_duration": 0.0,
                "text_content": f"文案{i}",
                "config": {},
            }
        )
        order += 1
    if with_non_main:
        clips.append(
            {
                "order": order,
                "asset_id": "outro_asset",
                "start_time": 0.0,
                "duration": 2.0,
                "clip_type": "outro",
                "playback_speed": 1.0,
                "transition_effect": "fade",
                "transition_duration": 0.5,
                "text_content": "片尾",
                "config": {"role": "outro"},
            }
        )
    return clips


def _durations(asset_ids, total=120.0, extra=None):
    d = {a: total for a in asset_ids}
    if extra:
        d.update(extra)
    return d


class TestReselectClipsValidation:
    def test_empty_source_clips_raises(self):
        with pytest.raises(ValueError, match="源 plan 无片段"):
            reselect_clips_for_variant(
                [],
                ["a1"],
                asset_durations={"a1": 60.0},
                rng=random.Random(1),
            )

    def test_empty_asset_pool_raises(self):
        with pytest.raises(ValueError, match="素材池为空"):
            reselect_clips_for_variant(
                _source_clips(),
                [],
                asset_durations={},
                rng=random.Random(1),
            )

    def test_all_zero_duration_raises(self):
        with pytest.raises(ValueError, match="时长全部未知"):
            reselect_clips_for_variant(
                _source_clips(),
                ["a1", "a2"],
                asset_durations={"a1": 0.0, "a2": 0.0},
                rng=random.Random(1),
            )


class TestReselectClipsDifferentiation:
    def test_three_variants_differ_in_assets_order_and_starts(self):
        """核心验收：固定种子连续 3 次独立选片，素材组合/顺序/起点显著不同。"""
        source = _source_clips(assets=("a1", "a2", "a3", "a4"))
        pool = ["a1", "a2", "a3", "a4", "a5", "a6"]
        durations = _durations(pool, total=300.0)

        batch_segments: dict = {}
        variants = []
        for seed in range(3):
            clips = reselect_clips_for_variant(
                source,
                pool,
                asset_durations=durations,
                batch_segments=batch_segments,  # 串行调用：上一变体区间参与避让
                rng=random.Random(100 + seed),
            )
            variants.append(clips)

        main_sequences = []
        for clips in variants:
            main = [c for c in clips if c["clip_type"] == "main"]
            main_sequences.append([(c["asset_id"], round(c["start_time"], 2)) for c in main])

        # 1) 每个变体片段数与源骨架一致
        for clips in variants:
            main_clips = [c for c in clips if c["clip_type"] == "main"]
            assert len(main_clips) == 4

        # 2) 三个变体的素材序列不全相同（素材洗牌 + main 顺序洗牌生效）
        seq_sets = {tuple(a for a, _ in seq) for seq in main_sequences}
        assert len(seq_sets) >= 2, f"变体素材序列应存在差异，实际全部相同: {seq_sets}"

        # 3) 起点组合不全相同（起点重选生效）
        start_sets = {tuple(s for _, s in seq) for seq in main_sequences}
        assert len(start_sets) >= 2, f"变体起点组合应存在差异，实际全部相同: {start_sets}"

        # 4) batch_segments 跨变体累加（串行避让链路存在）
        total_segments = sum(len(v) for v in batch_segments.values())
        assert total_segments >= 12, f"3 变体 × 4 main 片段应累加 >=12 区间，实际 {total_segments}"

    def test_skeleton_fields_preserved(self):
        """文案/转场/速度等骨架字段随片段透传（只换素材与起点）。"""
        source = _source_clips(assets=("a1", "a2"), with_non_main=True)
        pool = ["a1", "a2", "a3"]
        durations = _durations(pool, total=120.0, extra={"intro_asset": 30.0, "outro_asset": 30.0})

        clips = reselect_clips_for_variant(
            source,
            pool,
            asset_durations=durations,
            rng=random.Random(7),
        )
        by_order = {c["order"]: c for c in clips}

        # intro/outro 骨架字段保留
        intro = next(c for c in clips if c["clip_type"] == "intro")
        outro = next(c for c in clips if c["clip_type"] == "outro")
        assert intro["asset_id"] == "intro_asset"
        assert intro["text_content"] == "片头"
        assert intro["transition_effect"] == "fade"
        assert intro["transition_duration"] == 0.5
        assert outro["asset_id"] == "outro_asset"
        assert outro["text_content"] == "片尾"

        # main 片段文案/速度随骨架 order 保留
        for c in clips:
            if c["clip_type"] == "main":
                assert c["playback_speed"] == 1.2
                assert c["text_content"].startswith("文案")


class TestBatchOverlapAvoidance:
    def test_overlap_ratio_calculation(self):
        segs = {"a1": [(10.0, 20.0)]}  # 已占 10s 区间
        # 新区间 [10,20) 完全重叠 → 1.0
        assert _clip_overlap_ratio("a1", 10.0, 10.0, segs) == pytest.approx(1.0)
        # 新区间 [20,30) 零重叠 → 0.0
        assert _clip_overlap_ratio("a1", 20.0, 10.0, segs) == pytest.approx(0.0)
        # 新区间 [15,25) 重叠 5s / 10s → 0.5
        assert _clip_overlap_ratio("a1", 15.0, 10.0, segs) == pytest.approx(0.5)
        # 空 asset / 零时长 → 0
        assert _clip_overlap_ratio("", 0.0, 10.0, segs) == 0.0
        assert _clip_overlap_ratio("a1", 10.0, 0.0, segs) == 0.0

    def test_over_limit_triggers_reselect_to_non_overlapping(self):
        """批次已占满素材前段时，避让重选应把起点挪到重叠 ≤20% 的位置。"""
        source = _source_clips(assets=("a1",), dur=10.0)
        durations = {"a1": 120.0}
        # 批次已选区间：a1 [0, 100) 几乎占满前段
        batch_segments = {"a1": [(0.0, 100.0)]}

        # _resolve_start_time 第一次返回高重叠起点（2.0），之后 rng 抖动应找到低重叠位置
        call_count = {"n": 0}

        def fake_resolve(asset_id, clip_duration, asset_durations, used_segments, scene_points=None, on_exhausted=None):
            call_count["n"] += 1
            return 2.0 if call_count["n"] == 1 else None  # 后续回退 rng.uniform

        with patch.object(vps, "_resolve_start_time", side_effect=fake_resolve):
            # rng.uniform 返回 105.0（与 [0,100) 零重叠）；rng 是 random.Random 实例，
            # 需 patch 类方法 uniform 才能生效
            with patch.object(random.Random, "uniform", return_value=105.0):
                clips = reselect_clips_for_variant(
                    source,
                    ["a1"],
                    asset_durations=durations,
                    batch_segments=batch_segments,
                    rng=random.Random(3),
                )

        main = [c for c in clips if c["clip_type"] == "main"]
        assert len(main) == 1
        start = main[0]["start_time"]
        ratio = _clip_overlap_ratio("a1", start, 10.0, {"a1": [(0.0, 100.0)]})
        assert ratio <= BATCH_CLIP_OVERLAP_LIMIT, f"避让后重叠应 ≤20%，实际 {ratio:.2%}"
        # #1749：选片改 0.25s 窗口扫描找最优错开起点（不再依赖 rng.uniform 单点），
        # 落点只需满足语义：与已用区间 [0,100) 不完全重叠且重叠 ≤20%
        assert start > 0.0
        # start+10 区间与 [0,100) 的重叠段（若有）≤ 2s
        overlap = max(0.0, min(start + 10.0, 100.0) - max(start, 0.0))
        assert overlap <= 2.0 + 1e-6, f"重叠时长 {overlap}s 应 ≤2s（10s 段的 20%）"

    def test_first_variant_segments_become_avoidance_target(self):
        """变体 0 选定区间后，变体 1 选同素材时批次区间生效（不与源区间完全重合）。"""
        source = _source_clips(assets=("a1", "a2"), dur=8.0)
        pool = ["a1", "a2", "a3"]
        durations = _durations(pool, total=600.0)

        batch_segments: dict = {}
        v1 = reselect_clips_for_variant(
            source, pool, asset_durations=durations, batch_segments=batch_segments, rng=random.Random(11)
        )
        v1_main = [(c["asset_id"], c["start_time"], c["duration"]) for c in v1 if c["clip_type"] == "main"]

        # 快照 v1 之后的批次避让集（v2 调用会就地追加 v2 自身区间，断言必须用调用前快照）
        import copy

        batch_snapshot = copy.deepcopy(batch_segments)

        v2 = reselect_clips_for_variant(
            source, pool, asset_durations=durations, batch_segments=batch_segments, rng=random.Random(12)
        )
        # v2 与 v1（快照）同素材片段的区间重叠占比均 ≤20%
        for c in v2:
            if c["clip_type"] != "main" or not c["asset_id"]:
                continue
            ratio = _clip_overlap_ratio(c["asset_id"], c["start_time"], c["duration"], batch_snapshot)
            assert (
                ratio <= BATCH_CLIP_OVERLAP_LIMIT + 1e-6
            ), f"变体间片段重叠超限: asset={c['asset_id']} start={c['start_time']} ratio={ratio:.2%}"
        # v1 片段确实进入了批次避让集
        assert any(a in batch_segments for a, _, _ in v1_main)
