"""AutoClipService 单元测试."""

from __future__ import annotations

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

from dataclasses import dataclass, field
from enum import Enum
from typing import Any

import pytest
from app.services.auto_clip_service import AutoClipService

# ── Stub 实体 ─────────────────────────────────────────────────────────────────


class _ClipStatus(str, Enum):
    PENDING = "pending"
    READY = "ready"


class _ClipType(str, Enum):
    INTRO = "intro"
    MAIN = "main"
    TRANSITION = "transition"
    OUTRO = "outro"


@dataclass
class _StubClip:
    id: str
    plan_id: str
    template_clip_config_id: str | None = None
    clip_type: _ClipType = _ClipType.MAIN
    order: int = 0
    asset_id: str | None = None
    status: _ClipStatus = _ClipStatus.PENDING

    @property
    def has_asset(self) -> bool:
        return self.asset_id is not None

    def assign_asset(self, asset_id: str) -> None:
        self.asset_id = asset_id

    def mark_ready(self) -> None:
        if self.status == _ClipStatus.PENDING:
            self.status = _ClipStatus.READY


@dataclass
class _StubAsset:
    id: str
    quality_score: float | None = 80.0
    duration: float | None = 10.0
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass
class _StubConfig:
    id: str
    template_id: str
    clip_type: _ClipType = _ClipType.MAIN
    order: int = 0
    min_duration: float | None = None
    max_duration: float | None = None
    material_requirements: dict[str, Any] = field(default_factory=dict)


@dataclass
class _StubPlan:
    id: str
    template_id: str = "tpl-001"


# ── Stub 仓储 ─────────────────────────────────────────────────────────────────


class _StubPlanRepo:
    def __init__(self, plan: _StubPlan | None = None) -> None:
        self._plan = plan

    def get(self, plan_id: str) -> _StubPlan | None:
        return self._plan if self._plan and self._plan.id == plan_id else None


class _StubClipRepo:
    def __init__(self, clips: list[_StubClip] | None = None) -> None:
        self._clips = clips or []
        self.updated: list[_StubClip] = []

    def get(self, clip_id: str) -> _StubClip | None:
        for c in self._clips:
            if c.id == clip_id:
                return c
        return None

    def list_by_plan(self, plan_id: str) -> list[_StubClip]:
        return [c for c in self._clips if c.plan_id == plan_id]

    def update(self, clip: _StubClip) -> _StubClip:
        self.updated.append(clip)
        return clip


class _StubConfigRepo:
    def __init__(self, configs: list[_StubConfig] | None = None) -> None:
        self._configs = configs or []

    def get(self, config_id: str) -> _StubConfig | None:
        for c in self._configs:
            if c.id == config_id:
                return c
        return None

    def list_by_template(self, template_id: str, **_: Any) -> list[_StubConfig]:
        return [c for c in self._configs if c.template_id == template_id]


class _StubAssetRepo:
    def __init__(self, candidates: list[_StubAsset] | None = None) -> None:
        self._candidates = candidates or []
        self.last_query: dict[str, Any] = {}

    def search_candidates(self, project_id: str, **kwargs: Any) -> list[_StubAsset]:
        self.last_query = {"project_id": project_id, **kwargs}
        return list(self._candidates)


# ── 构造 Service (注入 stub) ──────────────────────────────────────────────────


def _make_service(
    plan_repo: _StubPlanRepo,
    clip_repo: _StubClipRepo,
    config_repo: _StubConfigRepo,
    asset_repo: _StubAssetRepo,
) -> AutoClipService:
    """创建注入 stub 仓储的 AutoClipService（绕过 __init__）。"""
    svc = AutoClipService.__new__(AutoClipService)
    svc._plan_repo = plan_repo  # type: ignore[assignment]
    svc._clip_repo = clip_repo  # type: ignore[assignment]
    svc._config_repo = config_repo  # type: ignore[assignment]
    svc._asset_repo = asset_repo  # type: ignore[assignment]
    return svc


# ── 测试：auto_select_assets ──────────────────────────────────────────────────


class TestAutoSelectAssets:
    def test_plan_not_found_raises(self) -> None:
        svc = _make_service(
            _StubPlanRepo(None),
            _StubClipRepo(),
            _StubConfigRepo(),
            _StubAssetRepo(),
        )
        with pytest.raises(ValueError, match="剪辑计划不存在"):
            svc.auto_select_assets("bad-id", "proj-1")

    def test_no_clips_returns_empty(self) -> None:
        plan = _StubPlan(id="plan-1", template_id="tpl-1")
        svc = _make_service(
            _StubPlanRepo(plan),
            _StubClipRepo([]),
            _StubConfigRepo(),
            _StubAssetRepo(),
        )
        result = svc.auto_select_assets("plan-1", "proj-1")
        assert result.total_clips == 0
        assert result.assigned_clips == 0
        assert result.unassigned_clips == 0

    def test_assigns_best_candidate(self) -> None:
        plan = _StubPlan(id="plan-1", template_id="tpl-1")
        config = _StubConfig(
            id="cfg-1",
            template_id="tpl-1",
            clip_type=_ClipType.MAIN,
            min_duration=8.0,
            max_duration=12.0,
            material_requirements={"type": "video", "category": "scenic"},
        )
        clip = _StubClip(id="clip-1", plan_id="plan-1", template_clip_config_id="cfg-1")
        # 两个候选，第一个质量分更高
        assets = [
            _StubAsset(id="a1", quality_score=90.0, duration=10.0, metadata={"category": "scenic"}),
            _StubAsset(id="a2", quality_score=60.0, duration=10.0, metadata={"category": "scenic"}),
        ]
        svc = _make_service(
            _StubPlanRepo(plan),
            _StubClipRepo([clip]),
            _StubConfigRepo([config]),
            _StubAssetRepo(assets),
        )
        result = svc.auto_select_assets("plan-1", "proj-1")
        assert result.assigned_clips == 1
        assert result.details[0].assigned_asset_id == "a1"
        assert clip.asset_id == "a1"
        assert clip.status == _ClipStatus.READY

    def test_no_candidates_marks_unassigned(self) -> None:
        plan = _StubPlan(id="plan-1", template_id="tpl-1")
        config = _StubConfig(id="cfg-1", template_id="tpl-1")
        clip = _StubClip(id="clip-1", plan_id="plan-1", template_clip_config_id="cfg-1")
        svc = _make_service(
            _StubPlanRepo(plan),
            _StubClipRepo([clip]),
            _StubConfigRepo([config]),
            _StubAssetRepo([]),  # 无候选
        )
        result = svc.auto_select_assets("plan-1", "proj-1")
        assert result.assigned_clips == 0
        assert result.unassigned_clips == 1
        assert result.details[0].assigned_asset_id is None
        assert "无符合条件" in result.details[0].reason


# ── 测试：select_for_clip ─────────────────────────────────────────────────────


class TestSelectForClip:
    def test_clip_not_found_raises(self) -> None:
        svc = _make_service(
            _StubPlanRepo(None),
            _StubClipRepo(),
            _StubConfigRepo(),
            _StubAssetRepo(),
        )
        with pytest.raises(ValueError, match="片段不存在"):
            svc.select_for_clip("bad-id", "proj-1")

    def test_assigns_without_config(self) -> None:
        """片段没有关联 config 时仍可分配（无筛选条件）。"""
        clip = _StubClip(id="clip-1", plan_id="plan-1", template_clip_config_id=None)
        assets = [_StubAsset(id="a1", quality_score=70.0, duration=5.0)]
        svc = _make_service(
            _StubPlanRepo(None),
            _StubClipRepo([clip]),
            _StubConfigRepo(),
            _StubAssetRepo(assets),
        )
        detail = svc.select_for_clip("clip-1", "proj-1")
        assert detail.assigned_asset_id == "a1"


# ── 测试：评分逻辑 ────────────────────────────────────────────────────────────


class TestScoring:
    def test_high_quality_wins(self) -> None:
        a = _StubAsset(id="a", quality_score=95.0, duration=10.0, metadata={"category": "scenic"})
        b = _StubAsset(id="b", quality_score=50.0, duration=10.0, metadata={"category": "scenic"})
        sa = AutoClipService._score_candidate(a, target_duration=10.0, target_category="scenic")
        sb = AutoClipService._score_candidate(b, target_duration=10.0, target_category="scenic")
        assert sa > sb

    def test_duration_match_beats_mismatch(self) -> None:
        a = _StubAsset(id="a", quality_score=80.0, duration=10.0, metadata={})
        b = _StubAsset(id="b", quality_score=80.0, duration=30.0, metadata={})
        sa = AutoClipService._score_candidate(a, target_duration=10.0, target_category=None)
        sb = AutoClipService._score_candidate(b, target_duration=10.0, target_category=None)
        assert sa > sb

    def test_category_match_beats_mismatch(self) -> None:
        a = _StubAsset(id="a", quality_score=80.0, duration=10.0, metadata={"category": "scenic"})
        b = _StubAsset(id="b", quality_score=80.0, duration=10.0, metadata={"category": "tech"})
        sa = AutoClipService._score_candidate(a, target_duration=10.0, target_category="scenic")
        sb = AutoClipService._score_candidate(b, target_duration=10.0, target_category="scenic")
        assert sa > sb

    def test_no_target_category_all_get_full_classification(self) -> None:
        a = _StubAsset(id="a", quality_score=80.0, duration=10.0, metadata={})
        score = AutoClipService._score_candidate(a, target_duration=10.0, target_category=None)
        # classification_score = 1.0 when no target
        assert score == pytest.approx(0.5 * 0.8 + 0.3 * 1.0 + 0.2 * 1.0)

    def test_no_quality_defaults_to_half(self) -> None:
        a = _StubAsset(id="a", quality_score=None, duration=10.0, metadata={})
        score = AutoClipService._score_candidate(a, target_duration=None, target_category=None)
        assert score == pytest.approx(0.5 * 0.5 + 0.3 * 0.5 + 0.2 * 1.0)


# ── 测试：解析素材需求 ────────────────────────────────────────────────────────


class TestParseMaterialRequirements:
    def test_none_config_returns_empty(self) -> None:
        assert AutoClipService._parse_material_requirements(None) == {}

    def test_extracts_file_type(self) -> None:
        config = _StubConfig(
            id="c1",
            template_id="t1",
            material_requirements={"type": "video"},
        )
        result = AutoClipService._parse_material_requirements(config)
        assert result["file_type"] == "video"

    def test_extracts_min_quality(self) -> None:
        config = _StubConfig(
            id="c1",
            template_id="t1",
            material_requirements={"min_quality_score": 60},
        )
        result = AutoClipService._parse_material_requirements(config)
        assert result["min_quality_score"] == 60.0

    def test_extracts_category(self) -> None:
        config = _StubConfig(
            id="c1",
            template_id="t1",
            material_requirements={"category": "scenic"},
        )
        result = AutoClipService._parse_material_requirements(config)
        assert result["classification_category"] == "scenic"

    def test_invalid_category_ignored(self) -> None:
        config = _StubConfig(
            id="c1",
            template_id="t1",
            material_requirements={"category": "nonexistent"},
        )
        result = AutoClipService._parse_material_requirements(config)
        assert "classification_category" not in result

    def test_duration_range(self) -> None:
        config = _StubConfig(
            id="c1",
            template_id="t1",
            min_duration=5.0,
            max_duration=15.0,
            material_requirements={},
        )
        result = AutoClipService._parse_material_requirements(config)
        assert result["min_duration"] == 5.0
        assert result["max_duration"] == 15.0
        assert result["target_duration"] == 10.0

    def test_tags_extracted(self) -> None:
        config = _StubConfig(
            id="c1",
            template_id="t1",
            material_requirements={"tags": ["outdoor", "sunset"]},
        )
        result = AutoClipService._parse_material_requirements(config)
        assert result["tags"] == ["outdoor", "sunset"]
