# -*- coding: utf-8 -*-
"""测试预览任务自动关联 edit_plan（generation_preview.py 增量覆盖率补充）。

覆盖 generation_preview.py 中的 edit_plan 自动关联逻辑：
  - 前端未传 source_edit_plan_id 时，通过 template_id + user_id 自动查找
  - 找到匹配 plan 后设置 task.source_edit_plan_id 并持久化
  - 查找失败时不影响主流程
  - 前端已传 source_edit_plan_id 时跳过自动关联
"""

from __future__ import annotations

import os
import sys
from dataclasses import dataclass, field
from types import SimpleNamespace
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 count_pending_by_user(self, user_id: str) -> int:
        return 0

    def count_pending_total(self) -> int:
        return 0


# ── Fake Edit Plan ──────────────────────────────────────────────────────────


@dataclass
class FakeEditPlan:
    id: str = "plan-001"
    created_by_user_id: str = "user-001"
    template_id: str = "tpl-001"


class FakeEditPlanRepository:
    def __init__(self, plans: list[FakeEditPlan] | None = None):
        self._plans = plans or []

    def list_by_template(self, template_id: str, limit: int = 20) -> list:
        return [p for p in self._plans if p.template_id == template_id]


# ── Auth Fakes ──────────────────────────────────────────────────────────────


@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


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


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


@pytest.fixture
def mock_db() -> MagicMock:
    return MagicMock()


@pytest.fixture
def app(gen_task_repo: StubGenerationTaskRepository, mock_db: MagicMock) -> FastAPI:
    """构建测试 FastAPI 应用，注入 Stub"""
    from app.api.routes.generation_preview import router
    from app.auth import get_current_user
    from app.dependencies import (
        get_asset_repository,
        get_db_session,
        get_generated_video_repository,
        get_generation_task_repository,
    )

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

    test_app.dependency_overrides[get_current_user] = lambda: FakeAuthenticatedUser()
    test_app.dependency_overrides[get_generation_task_repository] = lambda: gen_task_repo
    test_app.dependency_overrides[get_db_session] = lambda: mock_db
    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_request_body(**kwargs: Any) -> dict:
    defaults = dict(
        template_id="tpl-001",
        asset_ids=["asset-1"],
        title_ids=[],
        voice_ids=[],
        preview_count=1,
        video_ratio="",
        source_edit_plan_id="",
        video_title="",
        bgm_config={},
    )
    defaults.update(kwargs)
    return defaults


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


class TestPreviewEditPlanAutoAssociation:
    """预览任务创建后自动关联 edit_plan"""

    @patch(
        "app.api.routes.generation_preview._resolve_strategy_id_from_template",
        return_value="one_take",
    )
    @patch(
        "app.api.routes.generation_preview._infer_video_ratio_from_template",
        return_value="9:16",
    )
    @patch("app.api.routes.generation_preview.safe_enqueue_generation_task", return_value=True)
    def test_auto_associate_when_source_plan_empty(
        self,
        mock_enqueue,
        mock_ratio,
        mock_strategy,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ):
        """前端未传 source_edit_plan_id 时，应通过 template_id+user_id 自动查找并关联"""
        fake_plan = FakeEditPlan(id="plan-auto-001", created_by_user_id="user-001", template_id="tpl-001")
        fake_plan_repo = FakeEditPlanRepository(plans=[fake_plan])

        with patch(
            "packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository",
            return_value=fake_plan_repo,
        ):
            with patch("app.services.edit_plan_service.EditPlanService") as MockPlanSvc:
                MockPlanSvc.return_value.clone_plan_for_variant.return_value = SimpleNamespace(id="plan-clone-001")
                resp = client.post(
                    "/api/v1/generation/preview",
                    json=_make_request_body(source_edit_plan_id=""),
                )

        assert resp.status_code == 201
        # 找到 store 中的 task 并验证 source_edit_plan_id 被设置（自动关联后再克隆为独立 plan）
        tasks = list(gen_task_repo._store.values())
        assert len(tasks) == 1
        task = tasks[0]
        assert task.source_edit_plan_id == "plan-clone-001"

    @patch(
        "app.api.routes.generation_preview._resolve_strategy_id_from_template",
        return_value="one_take",
    )
    @patch(
        "app.api.routes.generation_preview._infer_video_ratio_from_template",
        return_value="9:16",
    )
    @patch("app.api.routes.generation_preview.safe_enqueue_generation_task", return_value=True)
    def test_skip_associate_when_source_plan_provided(
        self,
        mock_enqueue,
        mock_ratio,
        mock_strategy,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ):
        """前端已传 source_edit_plan_id 时，不触发自动关联，但仍克隆独立变体 plan"""
        with patch("app.services.edit_plan_service.EditPlanService") as MockPlanSvc:
            MockPlanSvc.return_value.clone_plan_for_variant.return_value = SimpleNamespace(id="plan-clone-explicit")
            resp = client.post(
                "/api/v1/generation/preview",
                json=_make_request_body(source_edit_plan_id="plan-explicit-001"),
            )

        assert resp.status_code == 201
        tasks = list(gen_task_repo._store.values())
        assert len(tasks) == 1
        assert tasks[0].source_edit_plan_id == "plan-clone-explicit"

    @patch(
        "app.api.routes.generation_preview._resolve_strategy_id_from_template",
        return_value="one_take",
    )
    @patch(
        "app.api.routes.generation_preview._infer_video_ratio_from_template",
        return_value="9:16",
    )
    @patch("app.api.routes.generation_preview.safe_enqueue_generation_task", return_value=True)
    def test_association_failure_does_not_break_main_flow(
        self,
        mock_enqueue,
        mock_ratio,
        mock_strategy,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ):
        """edit_plan 查找异常时不影响任务创建和入队"""
        with patch(
            "packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository",
            side_effect=RuntimeError("DB connection lost"),
        ):
            resp = client.post(
                "/api/v1/generation/preview",
                json=_make_request_body(source_edit_plan_id=""),
            )

        # 任务仍然创建成功
        assert resp.status_code == 201
        tasks = list(gen_task_repo._store.values())
        assert len(tasks) == 1
        # source_edit_plan_id 保持为空（关联失败）
        assert tasks[0].source_edit_plan_id == ""

    @patch(
        "app.api.routes.generation_preview._resolve_strategy_id_from_template",
        return_value="one_take",
    )
    @patch(
        "app.api.routes.generation_preview._infer_video_ratio_from_template",
        return_value="9:16",
    )
    @patch("app.api.routes.generation_preview.safe_enqueue_generation_task", return_value=True)
    def test_auto_associate_skips_when_no_matching_user(
        self,
        mock_enqueue,
        mock_ratio,
        mock_strategy,
        client: TestClient,
        gen_task_repo: StubGenerationTaskRepository,
    ):
        """模板下有 plan 但 created_by_user_id 不匹配时，不关联"""
        fake_plan = FakeEditPlan(id="plan-other-user", created_by_user_id="user-999", template_id="tpl-001")
        fake_plan_repo = FakeEditPlanRepository(plans=[fake_plan])

        with patch(
            "packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository",
            return_value=fake_plan_repo,
        ):
            resp = client.post(
                "/api/v1/generation/preview",
                json=_make_request_body(source_edit_plan_id=""),
            )

        assert resp.status_code == 201
        tasks = list(gen_task_repo._store.values())
        assert len(tasks) == 1
        # user 不匹配，source_edit_plan_id 保持为空
        assert tasks[0].source_edit_plan_id == ""
