"""Tests for scene-change smart frame selection + random shuffle of segment processing."""

from __future__ import annotations

import random
from unittest.mock import MagicMock, patch

import pytest

from apps.api.app.api.routes.templates_editor.clips import (
    _build_scene_segments,
    _pick_start_in_scene_segment,
)
from packages.shared.mediakit_client import MediaKitClient

# ── Part 1: Random shuffle tests ──────────────────────────────────────────


class TestRandomShuffle:
    """验证 segments 处理顺序随机打乱逻辑."""

    def test_same_segments_produce_different_asset_orders(self):
        """同一批 segments 多次处理，asset 分配顺序有变化.

        模拟打乱后的处理顺序，验证多次运行中 asset_id 分配顺序
        存在差异（概率性验证，运行 50 次应该至少出现 2 种排列）。
        """
        segments = [(0, 3.0, 5.0), (1, 4.0, 6.0), (2, 3.0, 5.0), (3, 4.0, 6.0)]
        asset_ids = ["A", "B", "C", "D"]

        observed_orders: list[tuple] = set()

        for _ in range(50):
            shuffled_indices = list(range(len(segments)))
            random.shuffle(shuffled_indices)
            order_tuple = tuple(shuffled_indices)
            observed_orders.add(order_tuple)

        # 50 次打乱，4! = 24 种排列，应出现多种不同排列
        assert len(observed_orders) > 1, "打乱应该产生多种不同顺序"

    def test_clips_data_order_always_sorted(self):
        """clips_data 按 order 排序后始终有序.

        模拟打乱处理后 clips_data 按 order 排序，验证最终 order 为 [0,1,2,3]。
        """
        segments = [(0, 3.0, 5.0), (1, 4.0, 6.0), (2, 3.0, 5.0), (3, 4.0, 6.0)]

        for _ in range(20):
            shuffled_indices = list(range(len(segments)))
            random.shuffle(shuffled_indices)

            # 模拟构建 clips_data（用 _seg_order 作为 order）
            clips_data = []
            for idx in shuffled_indices:
                seg_order, _, _ = segments[idx]
                clips_data.append({"order": seg_order, "asset_id": f"asset_{idx}"})

            # 按 order 排序
            clips_data.sort(key=lambda c: c["order"])

            # 验证 order 始终有序
            orders = [c["order"] for c in clips_data]
            assert orders == [0, 1, 2, 3], f"排序后 order 应为 [0,1,2,3]，实际为 {orders}"


# ── Part 2: detect_scene_changes tests ────────────────────────────────────


class TestDetectSceneChanges:
    """验证 MediaKitClient.detect_scene_changes 方法."""

    def _make_client(self) -> MediaKitClient:
        """创建一个可用的 MediaKitClient（mock 配置）."""
        with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
            mock_settings.return_value.mediakit_api_key = "test-key"
            mock_settings.return_value.mediakit_base_url = "http://test"
            mock_settings.return_value.mediakit_timeout = 30
            client = MediaKitClient()
        return client

    def test_scene_change_success(self):
        """SceneChange 策略成功返回时间戳列表."""
        client = self._make_client()

        mock_frames = [
            {"image_url": "url1", "timestamp": 0.0},
            {"image_url": "url2", "timestamp": 3.2},
            {"image_url": "url3", "timestamp": 7.8},
            {"image_url": "url4", "timestamp": 12.5},
        ]

        with patch.object(client, "extract_frames", return_value=mock_frames):
            result = client.detect_scene_changes("https://example.com/video.mp4")

        assert result is not None
        assert result[0] == 0.0  # 始终以 0.0 开头
        assert 3.2 in result
        assert 7.8 in result
        assert 12.5 in result
        assert result == sorted(result)  # 应已排序

    def test_scene_change_fallback_to_time_interval(self):
        """SceneChange 失败降级到 TimeInterval 策略."""
        client = self._make_client()

        # 第一次调用（SceneChange）返回 None，第二次（TimeInterval）返回结果
        fallback_frames = [
            {"image_url": "url1", "timestamp": 0.0},
            {"image_url": "url2", "timestamp": 5.0},
            {"image_url": "url3", "timestamp": 10.0},
        ]

        call_count = 0

        def side_effect(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                # 第一次 SceneChange 失败
                return None
            else:
                # 第二次 TimeInterval 成功
                assert kwargs.get("strategy") == "TimeInterval"
                return fallback_frames

        with patch.object(client, "extract_frames", side_effect=side_effect):
            result = client.detect_scene_changes("https://example.com/video.mp4")

        assert result is not None
        assert result[0] == 0.0
        assert 5.0 in result
        assert 10.0 in result

    def test_mediakit_not_available_returns_none(self):
        """MediaKit 不可用时返回 None."""
        with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
            mock_settings.return_value.mediakit_api_key = ""  # 未配置
            mock_settings.return_value.mediakit_base_url = "http://test"
            mock_settings.return_value.mediakit_timeout = 30
            client = MediaKitClient()

        result = client.detect_scene_changes("https://example.com/video.mp4")
        assert result is None

    def test_both_strategies_fail_returns_none(self):
        """SceneChange 和 TimeInterval 都失败时返回 None."""
        client = self._make_client()

        with patch.object(client, "extract_frames", return_value=None):
            result = client.detect_scene_changes("https://example.com/video.mp4")

        assert result is None

    def test_prepends_zero_if_not_present(self):
        """若帧列表中不包含 0.0，自动在开头添加."""
        client = self._make_client()

        # 帧列表中没有 timestamp=0.0
        mock_frames = [
            {"image_url": "url1", "timestamp": 2.0},
            {"image_url": "url2", "timestamp": 5.5},
        ]

        with patch.object(client, "extract_frames", return_value=mock_frames):
            result = client.detect_scene_changes("https://example.com/video.mp4")

        assert result is not None
        assert result[0] == 0.0
        assert 2.0 in result
        assert 5.5 in result


# ── Part 2.2: Scene segment building and assignment ───────────────────────


class TestSceneSegments:
    """验证镜头段构建和分配逻辑."""

    def test_build_scene_segments(self):
        """从场景切换点正确构建镜头段."""
        scene_changes = [0.0, 3.2, 7.8, 12.5]
        asset_duration = 15.0

        segments = _build_scene_segments(scene_changes, asset_duration)

        assert len(segments) == 4
        assert segments[0] == (0.0, 3.2)
        assert segments[1] == (3.2, 7.8)
        assert segments[2] == (7.8, 12.5)
        assert segments[3] == (12.5, 15.0)

    def test_build_scene_segments_filters_short(self):
        """过滤掉过短的镜头段（< 0.5秒）."""
        scene_changes = [0.0, 0.1, 5.0, 5.3, 10.0]
        asset_duration = 12.0

        segments = _build_scene_segments(scene_changes, asset_duration)

        # (0.0, 0.1) 长度 0.1 < 0.5 → 过滤
        # (0.1, 5.0) → 保留
        # (5.0, 5.3) 长度 0.3 < 0.5 → 过滤
        # (5.3, 10.0) → 保留
        # (10.0, 12.0) → 保留
        assert len(segments) == 3
        assert segments[0] == (0.1, 5.0)
        assert segments[1] == (5.3, 10.0)
        assert segments[2] == (10.0, 12.0)

    def test_pick_start_in_segment(self):
        """在镜头段内随机选取起始时间."""
        seg_start = 3.0
        seg_end = 8.0
        clip_duration = 2.0

        starts = set()
        for _ in range(100):
            start = _pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
            assert start is not None
            assert seg_start <= start <= seg_end - clip_duration
            starts.add(round(start, 2))

        # 应该有多个不同的起始时间
        assert len(starts) > 1

    def test_pick_start_segment_too_short(self):
        """镜头段太短无法容纳片段时返回 None."""
        result = _pick_start_in_scene_segment(0.0, 1.0, 2.0)
        assert result is None

    def test_different_clips_from_different_scenes(self):
        """不同片段应来自不同的镜头段（模拟分配逻辑）."""
        scene_changes = [0.0, 5.0, 10.0, 15.0]
        asset_duration = 18.0
        clip_duration = 3.0

        segments = _build_scene_segments(scene_changes, asset_duration)
        assert len(segments) == 4  # (0,5), (5,10), (10,15), (15,18)

        # 模拟 3 个片段从不同镜头段取点
        scene_pool = list(segments)
        assigned_starts = []

        for _ in range(3):
            if not scene_pool:
                break
            seg_start, seg_end = scene_pool.pop(0)
            start = _pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
            assert start is not None
            assigned_starts.append(start)

        # 3 个片段分别从 3 个不同镜头段中选取
        assert len(assigned_starts) == 3
        # 第一个来自 [0, 2]，第二个来自 [5, 7]，第三个来自 [10, 12]
        assert 0.0 <= assigned_starts[0] <= 2.0
        assert 5.0 <= assigned_starts[1] <= 7.0
        assert 10.0 <= assigned_starts[2] <= 12.0
