"""smart-match 过滤回退策略测试：余量过滤/高频排除导致结果集为空时必须回退。

线上事故：路由旧实现先 smart_select_assets(limit=N) 截取，再对这 N 条做
usable / 高频过滤，过滤后不回补——排名靠前素材全部被排除时返回空 items，
前端回退全选。修复后过滤全部前置，且每级过滤后为空/不足时回退保留。
"""

import json
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock

os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

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

from app.api.routes.assets import router
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import get_storage_service
from app.dependencies import (
    get_asset_library_repository,
    get_asset_repository,
    get_project_repository,
)

from packages.domain import (
    Asset,
    AssetLibrary,
    AssetLibraryKind,
    AssetStatus,
    Project,
    User,
)


class _StubProjectRepo:
    def __init__(self, projects):
        self._projects = projects

    def find_by_id(self, pid):
        return self._projects.get(pid)


class _StubAssetLibraryRepo:
    def __init__(self, libraries):
        self._libraries = libraries

    def get(self, lid):
        return self._libraries.get(lid)


class _StubAssetRepo:
    """模拟仓储；session 属性供 get_asset_recent_use_counts 使用（测试中会被 patch）。"""

    def __init__(self, assets):
        self._assets = assets
        self.session = MagicMock(name="stub-session")

    def find_by_library(self, lid, skip=0, limit=100, status=None):
        result = [a for a in self._assets if a.library_id == lid]
        if status:
            result = [a for a in result if (a.status.value if hasattr(a.status, "value") else a.status) in status]
        return result[skip : skip + limit]

    def find_by_library_and_file_type(self, lid, file_type, skip=0, limit=100, status=None):
        result = [a for a in self._assets if a.library_id == lid and a.file_type == file_type]
        if status:
            result = [a for a in result if (a.status.value if hasattr(a.status, "value") else a.status) in status]
        return result[skip : skip + limit]


def _make_app(asset_repo, lib_repo, proj_repo):
    app = FastAPI()
    app.include_router(router, prefix="/assets")
    fake_user = MagicMock()
    fake_user.user = User(id="user-1", email="test@test.com", display_name="Test")
    app.dependency_overrides[get_current_user] = lambda: AuthenticatedUser(user=fake_user.user)
    app.dependency_overrides[get_asset_repository] = lambda: asset_repo
    app.dependency_overrides[get_asset_library_repository] = lambda: lib_repo
    app.dependency_overrides[get_project_repository] = lambda: proj_repo
    app.dependency_overrides[get_storage_service] = lambda: MagicMock()
    return app


def _library():
    return AssetLibrary(id="lib-1", project_id="proj-1", name="Videos", kind=AssetLibraryKind.VIDEO)


def _exhausted_ranges(duration=15.0):
    """构造 used_time_ranges：整片覆盖 + 每区间 use_count 达上限 3 → usable=False。"""
    return [
        {"start": 0.0, "end": duration, "use_count": 3, "plan_id": "p1"},
    ]


def _video_asset(name, duration=15.0, quality=90, used_ranges=None):
    meta = {"used_time_ranges": used_ranges} if used_ranges is not None else {}
    return Asset.create(
        project_id="proj-1",
        library_id="lib-1",
        name=name,
        storage_key=f"key-{name}",
        mime_type="video/mp4",
        metadata=meta,
        quality_score=quality,
        duration=duration,
        status=AssetStatus.READY,
    )


class TestSmartMatchAvailabilityFallback:
    """余量过滤回退：全部素材 usable=False 时不返回空。"""

    def test_all_exhausted_returns_assets_instead_of_empty(self):
        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [
            _video_asset("v1.mp4", used_ranges=_exhausted_ranges(15)),
            _video_asset("v2.mp4", used_ranges=_exhausted_ranges(25)),
        ]
        repo = _StubAssetRepo(assets)
        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1"})
        assert resp.status_code == 200, resp.text
        data = resp.json()
        # 回退：保留全部候选，不返回空
        assert len(data["items"]) == 2
        assert data["total_candidates"] == 2

    def test_mixed_exhausted_and_fresh_excludes_exhausted(self):
        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [
            _video_asset("exhausted.mp4", quality=99, used_ranges=_exhausted_ranges(15)),
            _video_asset("fresh.mp4", quality=50, used_ranges=None),
        ]
        repo = _StubAssetRepo(assets)
        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1"})
        assert resp.status_code == 200, resp.text
        names = [item["name"] for item in resp.json()["items"]]
        assert "fresh.mp4" in names
        assert "exhausted.mp4" not in names

    def test_limit_backfills_from_lower_ranked_when_top_exhausted(self):
        """limit=1 且排名第一的素材耗尽时，必须回补排名靠后的可用素材，不返回空。"""
        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [
            _video_asset("top-exhausted.mp4", quality=100, used_ranges=_exhausted_ranges(15)),
            # second 质量分显著高于 third（质量项差 (90-30)*0.4=24 > 噪声上限 20），
            # 排除耗尽素材后 second 稳定排首位回补（噪声不影响大分差排名）
            _video_asset("second-fresh.mp4", quality=90, used_ranges=None),
            _video_asset("third-fresh.mp4", quality=30, used_ranges=None),
        ]
        repo = _StubAssetRepo(assets)
        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1", "limit": 1})
        assert resp.status_code == 200, resp.text
        data = resp.json()
        assert len(data["items"]) == 1
        # 不能是空，也不能是耗尽的高分素材
        assert data["items"][0]["name"] == "second-fresh.mp4"


class TestSmartMatchHighFreqFallback:
    """高频排除回退：排除后为空/不足 limit 时保留全部可用素材。"""

    def test_all_high_freq_keeps_all(self, monkeypatch):
        import app.api.routes.assets as routes_mod

        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [_video_asset("v1.mp4", quality=90), _video_asset("v2.mp4", quality=80)]
        repo = _StubAssetRepo(assets)

        # 全部素材在最近 5 个视频中使用 5 次（> 3）
        fake_counts = {assets[0].id: 5, assets[1].id: 5}
        monkeypatch.setattr(
            routes_mod, "get_asset_recent_use_counts", lambda db, asset_ids, recent_video_count=5: fake_counts
        )

        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1"})
        assert resp.status_code == 200, resp.text
        data = resp.json()
        # 全部高频 → 回退保留全部
        assert len(data["items"]) == 2

    def test_high_freq_partial_exclusion_with_enough_remaining(self, monkeypatch):
        import app.api.routes.assets as routes_mod

        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [
            _video_asset("hot.mp4", quality=99),
            _video_asset("cool1.mp4", quality=80),
            _video_asset("cool2.mp4", quality=70),
        ]
        repo = _StubAssetRepo(assets)
        fake_counts = {assets[0].id: 9, assets[1].id: 1, assets[2].id: 0}
        monkeypatch.setattr(
            routes_mod, "get_asset_recent_use_counts", lambda db, asset_ids, recent_video_count=5: fake_counts
        )

        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1"})
        assert resp.status_code == 200, resp.text
        names = [item["name"] for item in resp.json()["items"]]
        assert "hot.mp4" not in names
        assert set(names) == {"cool1.mp4", "cool2.mp4"}

    def test_high_freq_insufficient_for_limit_keeps_all(self, monkeypatch):
        """3 个素材、limit=5、2 个高频 → 剩余 1 < limit → 保留全部。"""
        import app.api.routes.assets as routes_mod

        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [
            _video_asset("hot1.mp4", quality=99),
            _video_asset("hot2.mp4", quality=98),
            _video_asset("cool.mp4", quality=50),
        ]
        repo = _StubAssetRepo(assets)
        fake_counts = {assets[0].id: 8, assets[1].id: 7, assets[2].id: 0}
        monkeypatch.setattr(
            routes_mod, "get_asset_recent_use_counts", lambda db, asset_ids, recent_video_count=5: fake_counts
        )

        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1", "limit": 5})
        assert resp.status_code == 200, resp.text
        names = [item["name"] for item in resp.json()["items"]]
        # 剩余 1 < limit=5 → 回退保留全部 3 条
        assert set(names) == {"hot1.mp4", "hot2.mp4", "cool.mp4"}

    def test_high_freq_query_failure_skips_exclusion(self, monkeypatch):
        import app.api.routes.assets as routes_mod

        def _boom(db, asset_ids, recent_video_count=5):
            raise RuntimeError("DB down")

        project = Project(id="proj-1", name="Test", owner_user_id="user-1")
        assets = [_video_asset("v1.mp4", quality=90), _video_asset("v2.mp4", quality=80)]
        repo = _StubAssetRepo(assets)
        monkeypatch.setattr(routes_mod, "get_asset_recent_use_counts", _boom)

        app = _make_app(repo, _StubAssetLibraryRepo({"lib-1": _library()}), _StubProjectRepo({"proj-1": project}))
        with TestClient(app) as client:
            resp = client.post("/assets/smart-match", json={"library_id": "lib-1"})
        assert resp.status_code == 200, resp.text
        assert len(resp.json()["items"]) == 2
