"""确认生成 API 单元测试.

覆盖 POST /tasks/{task_id}/confirm 端点：
  - 正常确认流程
  - 预览任务不存在 → 404
  - 权限不足 → 403
  - is_preview=False 及分辨率正确
  - cover_url 和 custom_title 正确传递
"""

from __future__ import annotations

import os
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional
from unittest.mock import MagicMock, patch

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

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "apps", "api"))

from packages.domain.generation_task import GenerationTask, GenerationTaskStatus

# ── Stub Repository ──────────────────────────────────────────────────────────


class StubGenerationTaskRepository:
    """内存中模拟 GenerationTask 仓储"""

    def __init__(self) -> None:
        self._store: dict[str, Any] = {}

    def create(self, task: Any) -> Any:
        self._store[task.id] = task
        return task

    def get(self, task_id: str) -> Optional[Any]:
        return self._store.get(task_id)

    def update(self, task: Any) -> Any:
        if task.id not in self._store:
            raise ValueError(f"GenerationTask {task.id} not found")
        self._store[task.id] = task
        return task

    def list_by_project(self, project_id: str) -> list[Any]:
        return [t for t in self._store.values() if t.project_id == project_id]

    def list_by_user(self, user_id: str) -> list[Any]:
        return [t for t in self._store.values() if t.created_by_user_id == user_id]

    def count_by_user(self, user_id: str) -> int:
        return len([t for t in self._store.values() if t.created_by_user_id == user_id])

    def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[Any]:
        items = [t for t in self._store.values() if t.created_by_user_id == user_id]
        items.sort(key=lambda t: t.created_at, reverse=True)
        return items[:limit]

    def list_by_source_edit_plan(self, plan_id: str) -> list[Any]:
        return [t for t in self._store.values() if (t.source_edit_plan_id or "") == plan_id]


# ── Stub Project Repository ──────────────────────────────────────────────────


@dataclass
class FakeProject:
    id: str = "project-001"
    owner_user_id: str = "user-001"
    shared_users: list[str] = field(default_factory=list)
    name: str = "Test Project"

    def can_access(self, user_id: str) -> bool:
        return user_id == self.owner_user_id or user_id in self.shared_users


class StubProjectRepository:
    def __init__(self) -> None:
        self._projects: dict[str, FakeProject] = {}

    def add(self, project: FakeProject) -> None:
        self._projects[project.id] = project

    def find_by_id(self, project_id: str) -> Optional[FakeProject]:
        return self._projects.get(project_id)


# ── Fixtures ─────────────────────────────────────────────────────────────────


@dataclass
class FakeUser:
    id: str = "user-001"
    email: str = "test@example.com"


@dataclass
class FakeAuthenticatedUser:
    user: FakeUser = field(default_factory=FakeUser)
    session_id: str | None = None
    token_type: str | None = None


@pytest.fixture
def gen_task_repo() -> StubGenerationTaskRepository:
    return StubGenerationTaskRepository()


@pytest.fixture
def project_repo() -> StubProjectRepository:
    repo = StubProjectRepository()
    repo.add(FakeProject())
    return repo


@pytest.fixture
def app(
    gen_task_repo: StubGenerationTaskRepository,
    project_repo: StubProjectRepository,
) -> FastAPI:
    """构建测试 FastAPI 应用，注入 Stub Repository"""
    from app.api.routes.generation_tasks import router
    from app.auth import get_current_user
    from app.dependencies import (
        get_asset_library_repository,
        get_asset_repository,
        get_generated_video_repository,
        get_generation_task_repository,
        get_project_repository,
    )

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

    def override_get_current_user():
        return FakeAuthenticatedUser()

    def override_get_generation_task_repository():
        return gen_task_repo

    def override_get_project_repository():
        return project_repo

    test_app.dependency_overrides[get_current_user] = override_get_current_user
    test_app.dependency_overrides[get_generation_task_repository] = override_get_generation_task_repository
    test_app.dependency_overrides[get_project_repository] = override_get_project_repository
    # Stubs for repositories not used by confirm endpoint but required by router
    test_app.dependency_overrides[get_asset_library_repository] = lambda: MagicMock()
    test_app.dependency_overrides[get_asset_repository] = lambda: MagicMock()
    test_app.dependency_overrides[get_generated_video_repository] = lambda: MagicMock()

    yield test_app
    test_app.dependency_overrides.clear()


@pytest.fixture
def client(app: FastAPI) -> TestClient:
    return TestClient(app)


def _make_preview_task(**kwargs: Any) -> GenerationTask:
    """创建预览任务"""
    defaults = dict(
        id="preview-task-001",
        project_id="project-001",
        asset_library_id="library-001",
        strategy_id="one_take",
        voice_library_id="",
        template_id="",
        asset_ids=["asset-1"],
        title_ids=[],
        voice_ids=[],
        status=GenerationTaskStatus.COMPLETED,
        progress=100.0,
        result_count=1,
        error_message="",
        created_by_user_id="user-001",
        source_edit_plan_id="",
        asset_select_mode="all",
        is_preview=True,
        source_task_id="",
        output_width=1280,
        output_height=720,
        cover_url="",
        custom_title="",
    )
    defaults.update(kwargs)
    return GenerationTask(**defaults)


# ── Tests ────────────────────────────────────────────────────────────────────


class TestConfirmGeneration:
    def test_confirm_success(
        self,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ) -> None:
        """正常确认流程：预览任务存在、权限正确 → 创建正式任务"""
        preview = _make_preview_task()
        gen_task_repo.create(preview)

        with patch("app.api.routes.generation_tasks.celery_app") as mock_celery:
            mock_celery.send_task = MagicMock()
            resp = client.post(
                f"/api/v1/tasks/{preview.id}/confirm",
                json={
                    "output_width": 1080,
                    "output_height": 1920,
                    "cover_url": "https://example.com/cover.jpg",
                    "custom_title": "我的视频",
                },
            )

        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1
        item = data["items"][0]
        assert item["is_preview"] is False
        assert item["source_task_id"] == preview.id
        assert item["output_width"] == 1080
        assert item["output_height"] == 1920
        assert item["cover_url"] == "https://example.com/cover.jpg"
        assert item["custom_title"] == "我的视频"
        # 复制了预览任务的配置
        assert item["project_id"] == "project-001"
        assert item["asset_library_id"] == "library-001"
        assert item["strategy_id"] == "one_take"
        assert item["asset_ids"] == ["asset-1"]

        # 验证 celery 任务被调度
        mock_celery.send_task.assert_called_once()
        call_args = mock_celery.send_task.call_args
        assert call_args[0][0] == "worker.generate_video"

    def test_confirm_not_found(self, client: TestClient) -> None:
        """预览任务不存在 → 404"""
        resp = client.post(
            "/api/v1/tasks/nonexistent-task/confirm",
            json={"output_width": 1080, "output_height": 1920},
        )
        assert resp.status_code == 404
        assert "not found" in resp.json()["detail"]

    def test_confirm_access_denied(
        self,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ) -> None:
        """权限不足 → 403"""
        preview = _make_preview_task(created_by_user_id="other-user-999")
        gen_task_repo.create(preview)

        resp = client.post(
            f"/api/v1/tasks/{preview.id}/confirm",
            json={"output_width": 1080, "output_height": 1920},
        )
        assert resp.status_code == 403
        assert "denied" in resp.json()["detail"].lower() or "Access" in resp.json()["detail"]

    def test_confirm_preserves_config(
        self,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ) -> None:
        """确认后的任务 is_preview=False，分辨率已更新，其余配置从预览任务复制"""
        preview = _make_preview_task(
            voice_library_id="voice-001",
            template_id="tmpl-001",
            title_ids=["title-1", "title-2"],
            voice_ids=["voice-a"],
        )
        gen_task_repo.create(preview)

        with patch("app.api.routes.generation_tasks.celery_app") as mock_celery:
            mock_celery.send_task = MagicMock()
            resp = client.post(
                f"/api/v1/tasks/{preview.id}/confirm",
                json={"output_width": 1920, "output_height": 1080},
            )

        assert resp.status_code == 200
        item = resp.json()["items"][0]
        assert item["is_preview"] is False
        assert item["source_task_id"] == preview.id
        assert item["output_width"] == 1920
        assert item["output_height"] == 1080
        # 默认分辨率
        assert item["cover_url"] == ""
        assert item["custom_title"] == ""
        # 复制的配置
        assert item["voice_library_id"] == "voice-001"
        assert item["template_id"] == "tmpl-001"
        assert item["title_ids"] == ["title-1", "title-2"]
        assert item["voice_ids"] == ["voice-a"]

    def test_confirm_cover_and_title(
        self,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ) -> None:
        """cover_url 和 custom_title 正确传递"""
        preview = _make_preview_task()
        gen_task_repo.create(preview)

        with patch("app.api.routes.generation_tasks.celery_app") as mock_celery:
            mock_celery.send_task = MagicMock()
            resp = client.post(
                f"/api/v1/tasks/{preview.id}/confirm",
                json={
                    "output_width": 1080,
                    "output_height": 1920,
                    "cover_url": "https://cdn.example.com/my-cover.png",
                    "custom_title": "测试视频标题",
                },
            )

        assert resp.status_code == 200
        item = resp.json()["items"][0]
        assert item["cover_url"] == "https://cdn.example.com/my-cover.png"
        assert item["custom_title"] == "测试视频标题"

    def test_confirm_default_resolution(
        self,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ) -> None:
        """不传分辨率时使用默认值 1080x1920"""
        preview = _make_preview_task()
        gen_task_repo.create(preview)

        with patch("app.api.routes.generation_tasks.celery_app") as mock_celery:
            mock_celery.send_task = MagicMock()
            resp = client.post(
                f"/api/v1/tasks/{preview.id}/confirm",
                json={},
            )

        assert resp.status_code == 200
        item = resp.json()["items"][0]
        assert item["output_width"] == 1080
        assert item["output_height"] == 1920

    def test_confirm_creates_new_task_in_repo(
        self,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ) -> None:
        """确认生成的任务确实被存入 repository"""
        preview = _make_preview_task()
        gen_task_repo.create(preview)

        initial_count = len(gen_task_repo._store)

        with patch("app.api.routes.generation_tasks.celery_app") as mock_celery:
            mock_celery.send_task = MagicMock()
            resp = client.post(
                f"/api/v1/tasks/{preview.id}/confirm",
                json={},
            )

        assert resp.status_code == 200
        new_task_id = resp.json()["items"][0]["id"]
        assert new_task_id != preview.id
        assert len(gen_task_repo._store) == initial_count + 1

        new_task = gen_task_repo.get(new_task_id)
        assert new_task is not None
        assert new_task.is_preview is False
        assert new_task.source_task_id == preview.id
