"""
upload.py 路由单元测试

覆盖：
- _require_project_and_library 中 find_by_project 调用正确性
- OSS 凭证校验（未配置时返回 503）
- prepare_direct_upload 正常路径
- 文件类型校验
- 异常处理路径
"""

from __future__ import annotations

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

# 设置必要环境变量（必须在导入 app 模块之前）
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")


# 确保 app 模块可导入
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

from fastapi import FastAPI
from fastapi.testclient import TestClient

from packages.domain import AssetLibrary, AssetLibraryKind, Project

# ---------------------------------------------------------------------------
# 测试用 Stub（不继承 Port ABC，因为 Port 定义 async 方法，路由实际使用同步 duck-type）
# ---------------------------------------------------------------------------


class StubProjectRepository:
    def __init__(self, projects: dict[str, Project] | None = None):
        self._projects = projects or {}

    def get(self, project_id: str) -> Project | None:
        return self._projects.get(project_id)

    def find_by_id(self, project_id: str) -> Project | None:
        return self._projects.get(project_id)


class StubAssetLibraryRepository:
    def __init__(self, libraries: dict[str, AssetLibrary] | None = None):
        self._libraries = libraries or {}

    def find_by_project(self, project_id: str, kind=None) -> list[AssetLibrary]:
        items = [lib for lib in self._libraries.values() if lib.project_id == project_id]
        if kind is not None:
            items = [lib for lib in items if lib.kind == kind]
        return items

    def list_by_project(self, project_id: str) -> list[AssetLibrary]:
        """故意保留旧方法名，验证路由不会调用它。"""
        raise AssertionError("路由不应调用 list_by_project，应调用 find_by_project")


class StubIngestJobRepository:
    def add(self, job) -> None:
        pass

    def get(self, job_id: str):
        return None

    def update_status(self, job_id, status, **kwargs):
        pass

    def list_by_library(self, library_id: str, skip: int = 0, limit: int = 50):
        return []

    def count_by_library(self, library_id: str) -> int:
        return 0


# ---------------------------------------------------------------------------
# 测试 Fixtures
# ---------------------------------------------------------------------------


def _make_project(id: str = "proj-1", owner_user_id: str = "user-1") -> Project:
    return Project(id=id, name="Test Project", owner_user_id=owner_user_id)


def _make_library(
    id: str = "lib-1",
    project_id: str = "proj-1",
    kind: AssetLibraryKind = AssetLibraryKind.VIDEO,
) -> AssetLibrary:
    return AssetLibrary(id=id, name="Test Library", project_id=project_id, kind=kind)


def _build_app(
    project_repo: StubProjectRepository | None = None,
    library_repo: StubAssetLibraryRepository | None = None,
    storage: MagicMock | None = None,
    ingest_repo: StubIngestJobRepository | None = None,
) -> FastAPI:
    """构建一个最小化的 FastAPI app，只注册 upload 路由。"""
    from app.api.routes.upload 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_ingest_job_repository,
        get_project_repository,
    )

    app = FastAPI()
    app.include_router(router, prefix="/api/v1")

    project_repo = project_repo or StubProjectRepository()
    library_repo = library_repo or StubAssetLibraryRepository()
    storage = storage or MagicMock()
    storage.is_configured = True
    storage.create_direct_upload_post.return_value = {
        "url": "https://bucket.oss.example.com",
        "method": "POST",
        "storage_key": "uploads/abc/test.mp4",
        "expires_at": "2026-01-01T00:00:00Z",
        "fields": {"key": "uploads/abc/test.mp4"},
    }
    ingest_repo = ingest_repo or StubIngestJobRepository()

    # Mock auth
    mock_user = MagicMock(spec=AuthenticatedUser)
    mock_user.id = "user-1"
    mock_user.email = "test@example.com"

    app.dependency_overrides[get_current_user] = lambda: mock_user
    app.dependency_overrides[get_project_repository] = lambda: project_repo
    app.dependency_overrides[get_asset_library_repository] = lambda: library_repo
    app.dependency_overrides[get_storage_service] = lambda: storage
    app.dependency_overrides[get_ingest_job_repository] = lambda: ingest_repo

    return app


def _client(**kwargs) -> TestClient:
    app = _build_app(**kwargs)
    return TestClient(app)


# ---------------------------------------------------------------------------
# 测试用例
# ---------------------------------------------------------------------------


class TestRequireProjectAndLibrary:
    """_require_project_and_library 辅助函数测试。"""

    def test_returns_200_when_project_and_library_exist(self):
        """项目和素材库都存在时，正常返回。"""
        project = _make_project()
        library = _make_library()
        project_repo = StubProjectRepository({project.id: project})
        library_repo = StubAssetLibraryRepository({library.id: library})

        client = _client(project_repo=project_repo, library_repo=library_repo)

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": library.id,
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 200

    def test_returns_404_when_project_not_found(self):
        """项目不存在时返回 404。"""
        library = _make_library()
        library_repo = StubAssetLibraryRepository({library.id: library})

        client = _client(
            project_repo=StubProjectRepository(),
            library_repo=library_repo,
        )

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": "nonexistent",
                "library_id": library.id,
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 404
        assert "Project not found" in resp.json()["detail"]

    def test_returns_404_when_library_not_found(self):
        """素材库不属于该项目时返回 404。"""
        project = _make_project()
        project_repo = StubProjectRepository({project.id: project})

        other_library = _make_library(project_id="other-project")
        library_repo = StubAssetLibraryRepository({other_library.id: other_library})

        client = _client(project_repo=project_repo, library_repo=library_repo)

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": "nonexistent-lib",
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 404
        assert "Asset library not found" in resp.json()["detail"]

    def test_find_by_project_is_called_not_list_by_project(self):
        """
        验证路由调用的是 find_by_project 而不是 list_by_project。
        StubAssetLibraryRepository.list_by_project 会抛出 AssertionError。
        """
        project = _make_project()
        library = _make_library()
        project_repo = StubProjectRepository({project.id: project})
        library_repo = StubAssetLibraryRepository({library.id: library})

        client = _client(project_repo=project_repo, library_repo=library_repo)

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": library.id,
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        # 如果调用了 list_by_project，会抛 AssertionError 导致 500
        assert resp.status_code == 200


class TestPrepareDirectUpload:
    """prepare_direct_upload 端点测试。"""

    def test_returns_upload_credentials_when_configured(self):
        """OSS 已配置时，返回上传凭证。"""
        project = _make_project()
        library = _make_library()
        project_repo = StubProjectRepository({project.id: project})
        library_repo = StubAssetLibraryRepository({library.id: library})

        storage = MagicMock()
        storage.is_configured = True
        storage.create_direct_upload_post.return_value = {
            "url": "https://bucket.oss.example.com",
            "method": "POST",
            "storage_key": "uploads/abc/test-video.mp4",
            "expires_at": "2026-01-01T00:00:00Z",
            "fields": {"key": "uploads/abc/test-video.mp4"},
        }

        client = _client(
            project_repo=project_repo,
            library_repo=library_repo,
            storage=storage,
        )

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": library.id,
                "filename": "test-video.mp4",
                "content_type": "video/mp4",
                "file_size": 1024 * 1024,
            },
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "upload_url" in data
        assert "storage_key" in data

    def test_returns_503_when_oss_not_configured(self):
        """OSS 未配置时，返回 503。"""
        project = _make_project()
        library = _make_library()
        project_repo = StubProjectRepository({project.id: project})
        library_repo = StubAssetLibraryRepository({library.id: library})

        storage = MagicMock()
        storage.create_direct_upload_post.side_effect = RuntimeError("OSS 未配置")

        client = _client(
            project_repo=project_repo,
            library_repo=library_repo,
            storage=storage,
        )

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": library.id,
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 503


class TestCompleteDirectUpload:
    """complete_direct_upload 端点测试。"""

    def test_returns_404_when_project_not_found(self):
        """项目不存在时返回 404。"""
        library = _make_library()
        library_repo = StubAssetLibraryRepository({library.id: library})

        client = _client(
            project_repo=StubProjectRepository(),
            library_repo=library_repo,
        )

        resp = client.post(
            "/api/v1/direct/complete",
            json={
                "project_id": "nonexistent",
                "library_id": library.id,
                "storage_key": "uploads/abc/test.mp4",
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 404


class TestMimeTypeValidation:
    """文件类型校验测试。"""

    def test_accepts_video_mp4(self):
        """video/mp4 是合法类型。"""
        project = _make_project()
        library = _make_library()
        project_repo = StubProjectRepository({project.id: project})
        library_repo = StubAssetLibraryRepository({library.id: library})

        client = _client(project_repo=project_repo, library_repo=library_repo)

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": library.id,
                "filename": "test.mp4",
                "content_type": "video/mp4",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 200

    def test_rejects_invalid_mime_type(self):
        """非法文件类型被拒绝。"""
        project = _make_project()
        library = _make_library()
        project_repo = StubProjectRepository({project.id: project})
        library_repo = StubAssetLibraryRepository({library.id: library})

        client = _client(project_repo=project_repo, library_repo=library_repo)

        resp = client.post(
            "/api/v1/direct/prepare",
            json={
                "project_id": project.id,
                "library_id": library.id,
                "filename": "malware.exe",
                "content_type": "application/x-executable",
                "file_size": 1024,
            },
        )
        assert resp.status_code == 415
