"""
edit_plans.py 剪辑计划 API 端点单元测试

覆盖（25+ 测试用例）：
- 创建：正常创建、空名称 400、空 template_id 422
- 列表：默认分页、按状态筛选、按模板筛选、无效状态 400
- 详情：正常获取、不存在 404
- 更新：基础字段更新、状态机合法流转、状态机非法流转 400、不存在 404、无效状态值 400
- 删除：正常删除、不存在 404
"""

from __future__ import annotations

import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
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

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

from fastapi import FastAPI
from fastapi.testclient import TestClient

from packages.domain.config_schemas import normalize_plan_config
from packages.domain.edit_plan import EditPlan, EditPlanStatus

# ---------------------------------------------------------------------------
# Stub Repository
# ---------------------------------------------------------------------------


class StubEditPlanRepository:
    """内存中的 EditPlan 仓储 stub"""

    def __init__(self, plans: dict[str, EditPlan] | None = None):
        self._plans = plans or {}
        self._counter = 0

    def _next_id(self) -> str:
        self._counter += 1
        return f"plan-{self._counter:03d}"

    def list_all(
        self,
        *,
        status: Optional[EditPlanStatus] = None,
        skip: int = 0,
        limit: int = 50,
    ) -> list[EditPlan]:
        items = list(self._plans.values())
        if status is not None:
            items = [p for p in items if p.status == status]
        items.sort(key=lambda p: p.created_at, reverse=True)
        return items[skip : skip + limit]

    def list_by_template(
        self,
        template_id: str,
        *,
        status: Optional[EditPlanStatus] = None,
        skip: int = 0,
        limit: int = 50,
    ) -> list[EditPlan]:
        items = [p for p in self._plans.values() if p.template_id == template_id]
        if status is not None:
            items = [p for p in items if p.status == status]
        items.sort(key=lambda p: p.created_at, reverse=True)
        return items[skip : skip + limit]

    def get(self, plan_id: str) -> Optional[EditPlan]:
        return self._plans.get(plan_id)

    def create(self, plan: EditPlan) -> EditPlan:
        self._plans[plan.id] = plan
        return plan

    def update(self, plan: EditPlan) -> EditPlan:
        if plan.id not in self._plans:
            raise ValueError(f"EditPlan {plan.id} not found")
        self._plans[plan.id] = plan
        return plan

    def delete(self, plan_id: str) -> bool:
        if plan_id not in self._plans:
            return False
        del self._plans[plan_id]
        return True

    def delete_by_plan(self, plan_id: str) -> None:
        """按 plan_id 删除关联片段（stub 实现：无操作）。"""
        pass

    def count(
        self,
        *,
        template_id: Optional[str] = None,
        status: Optional[EditPlanStatus] = None,
    ) -> int:
        items = list(self._plans.values())
        if template_id:
            items = [p for p in items if p.template_id == template_id]
        if status is not None:
            items = [p for p in items if p.status == status]
        return len(items)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _make_auth_user():
    """构造 AuthenticatedUser mock"""
    from app.auth import AuthenticatedUser

    from packages.domain.entities import User

    user = User(
        id="user-001",
        email="test@example.com",
        display_name="测试用户",
    )
    return AuthenticatedUser(user=user)


def _create_test_app():
    """创建带 stub 注入的测试 FastAPI 应用"""
    import app.services.edit_plan_service as service_module
    from app.api.routes import edit_plans as edit_plans_module
    from app.api.routes.edit_plans import router

    stub_repo = StubEditPlanRepository()

    # 替换服务模块中的 Repository 类
    original_plan_repo_class = service_module.SQLAlchemyEditPlanRepository
    original_clip_repo_class = service_module.SQLAlchemyEditPlanClipRepository
    original_generation_task_repo_class = service_module.SQLAlchemyGenerationTaskRepository
    service_module.SQLAlchemyEditPlanRepository = lambda db: stub_repo
    service_module.SQLAlchemyEditPlanClipRepository = lambda db: stub_repo
    service_module.SQLAlchemyGenerationTaskRepository = lambda db: stub_repo

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

    # 覆盖认证依赖
    app.dependency_overrides[edit_plans_module.get_current_user] = _make_auth_user
    app.dependency_overrides[edit_plans_module.get_db_session] = lambda: MagicMock()

    def cleanup():
        service_module.SQLAlchemyEditPlanRepository = original_plan_repo_class
        service_module.SQLAlchemyEditPlanClipRepository = original_clip_repo_class
        service_module.SQLAlchemyGenerationTaskRepository = original_generation_task_repo_class

    return app, stub_repo, cleanup


@pytest.fixture
def client():
    app, stub_repo, cleanup = _create_test_app()
    yield TestClient(app), stub_repo
    cleanup()


# ---------------------------------------------------------------------------
# 创建测试
# ---------------------------------------------------------------------------


class TestCreatePlan:
    def test_create_success(self, client):
        c, repo = client
        resp = c.post(
            "/api/v1/edit-plans",
            json={
                "template_id": "tpl-001",
                "name": "我的剪辑计划",
                "config": {"bgm": "happy"},
                "total_duration": 60.0,
            },
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == "我的剪辑计划"
        assert data["template_id"] == "tpl-001"
        assert data["status"] == "draft"
        assert data["total_duration"] == 60.0
        assert data["config"] == normalize_plan_config({"bgm": "happy"})
        assert "id" in data
        assert "created_at" in data

    def test_create_minimal(self, client):
        c, repo = client
        resp = c.post(
            "/api/v1/edit-plans",
            json={"template_id": "tpl-001", "name": "最小计划"},
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["config"] == normalize_plan_config(None)
        assert data["total_duration"] == 0.0

    def test_create_empty_name_returns_422(self, client):
        c, repo = client
        resp = c.post(
            "/api/v1/edit-plans",
            json={"template_id": "tpl-001", "name": ""},
        )
        assert resp.status_code == 422

    def test_create_missing_template_id_returns_422(self, client):
        c, repo = client
        resp = c.post(
            "/api/v1/edit-plans",
            json={"name": "没有模板的计划"},
        )
        assert resp.status_code == 422

    def test_create_negative_duration_returns_422(self, client):
        c, repo = client
        resp = c.post(
            "/api/v1/edit-plans",
            json={"template_id": "tpl-001", "name": "test", "total_duration": -1.0},
        )
        assert resp.status_code == 422


# ---------------------------------------------------------------------------
# 列表测试
# ---------------------------------------------------------------------------


class TestListPlans:
    def _seed_plans(self, repo, count=3, template_id="tpl-001"):
        for i in range(count):
            plan = EditPlan.create(
                template_id=template_id,
                name=f"计划{i+1}",
                config={"index": i},
            )
            repo.create(plan)
        return plan

    def test_list_empty(self, client):
        c, repo = client
        resp = c.get("/api/v1/edit-plans")
        assert resp.status_code == 200
        data = resp.json()
        assert data["items"] == []
        assert data["total"] == 0
        assert data["page"] == 1
        assert data["page_size"] == 20

    def test_list_with_items(self, client):
        c, repo = client
        self._seed_plans(repo, count=3)
        resp = c.get("/api/v1/edit-plans")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["items"]) == 3
        assert data["total"] == 3

    def test_list_pagination(self, client):
        c, repo = client
        self._seed_plans(repo, count=5)
        resp = c.get("/api/v1/edit-plans?page=1&page_size=2")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["items"]) == 2
        assert data["total"] == 5
        assert data["page"] == 1

        resp2 = c.get("/api/v1/edit-plans?page=3&page_size=2")
        data2 = resp2.json()
        assert len(data2["items"]) == 1

    def test_list_filter_by_status(self, client):
        c, repo = client
        p1 = EditPlan.create("tpl-001", "计划A")
        repo.create(p1)
        p2 = EditPlan.create("tpl-001", "计划B")
        repo.create(p2)
        p2.start_editing()
        repo.update(p2)

        resp = c.get("/api/v1/edit-plans?status=draft")
        data = resp.json()
        assert data["total"] == 1
        assert data["items"][0]["name"] == "计划A"

        resp2 = c.get("/api/v1/edit-plans?status=editing")
        data2 = resp2.json()
        assert data2["total"] == 1
        assert data2["items"][0]["name"] == "计划B"

    def test_list_filter_by_template_id(self, client):
        c, repo = client
        p1 = EditPlan.create("tpl-001", "模板1计划")
        repo.create(p1)
        p2 = EditPlan.create("tpl-002", "模板2计划")
        repo.create(p2)

        resp = c.get("/api/v1/edit-plans?template_id=tpl-001")
        data = resp.json()
        assert data["total"] == 1
        assert data["items"][0]["name"] == "模板1计划"

    def test_list_filter_by_template_and_status(self, client):
        c, repo = client
        p1 = EditPlan.create("tpl-001", "模板1草稿")
        repo.create(p1)
        p2 = EditPlan.create("tpl-001", "模板1编辑中")
        repo.create(p2)
        p2.start_editing()
        repo.update(p2)
        p3 = EditPlan.create("tpl-002", "模板2草稿")
        repo.create(p3)

        resp = c.get("/api/v1/edit-plans?template_id=tpl-001&status=draft")
        data = resp.json()
        assert data["total"] == 1
        assert data["items"][0]["name"] == "模板1草稿"

    def test_list_invalid_status_returns_400(self, client):
        c, repo = client
        resp = c.get("/api/v1/edit-plans?status=invalid_status")
        assert resp.status_code == 400
        assert "无效的状态值" in resp.json()["detail"]


# ---------------------------------------------------------------------------
# 详情测试
# ---------------------------------------------------------------------------


class TestGetPlan:
    def test_get_success(self, client):
        c, repo = client
        plan = EditPlan.create("tpl-001", "测试计划", config={"key": "val"})
        repo.create(plan)

        resp = c.get(f"/api/v1/edit-plans/{plan.id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == plan.id
        assert data["name"] == "测试计划"
        assert data["config"] == {"key": "val"}

    def test_get_not_found_returns_404(self, client):
        c, repo = client
        resp = c.get("/api/v1/edit-plans/nonexistent-id")
        assert resp.status_code == 404
        assert "剪辑计划不存在" in resp.json()["detail"]


# ---------------------------------------------------------------------------
# 更新测试
# ---------------------------------------------------------------------------


class TestUpdatePlan:
    def _seed_plan(self, repo, name="原计划", template_id="tpl-001"):
        plan = EditPlan.create(template_id, name)
        repo.create(plan)
        return plan

    def test_update_name(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"name": "新名称"})
        assert resp.status_code == 200
        assert resp.json()["name"] == "新名称"

    def test_update_config(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        resp = c.put(
            f"/api/v1/edit-plans/{plan.id}",
            json={"config": {"bgm": "sad", "transition": "fade"}},
        )
        assert resp.status_code == 200
        assert resp.json()["config"] == normalize_plan_config({"bgm": "sad", "transition": "fade"})

    def test_update_total_duration(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"total_duration": 120.5})
        assert resp.status_code == 200
        assert resp.json()["total_duration"] == 120.5

    def test_update_status_draft_to_editing(self, client):
        c, repo = client
        plan = self._seed_plan(repo)
        assert plan.status == EditPlanStatus.DRAFT

        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "editing"})
        assert resp.status_code == 200
        assert resp.json()["status"] == "editing"

    def test_update_status_full_happy_path(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        # draft → editing
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "editing"})
        assert resp.json()["status"] == "editing"

        # editing → rendering
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "rendering"})
        assert resp.json()["status"] == "rendering"

        # rendering → completed
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "completed"})
        assert resp.json()["status"] == "completed"

    def test_update_status_failure_and_reset(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        # draft → editing → rendering → failed
        c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "editing"})
        c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "rendering"})
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "failed"})
        assert resp.json()["status"] == "failed"

        # failed → draft (reset)
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "draft"})
        assert resp.json()["status"] == "draft"

    def test_update_invalid_transition_returns_400(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        # draft → rendering 不合法
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "rendering"})
        assert resp.status_code == 400

    def test_update_draft_to_completed_returns_400(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        # draft → completed 不合法
        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "completed"})
        assert resp.status_code == 400

    def test_update_invalid_status_value_returns_400(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "bogus"})
        assert resp.status_code == 400
        assert "无效的状态值" in resp.json()["detail"]

    def test_update_same_status_is_noop(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        resp = c.put(f"/api/v1/edit-plans/{plan.id}", json={"status": "draft"})
        assert resp.status_code == 200
        assert resp.json()["status"] == "draft"

    def test_update_not_found_returns_404(self, client):
        c, repo = client
        resp = c.put("/api/v1/edit-plans/nonexistent", json={"name": "x"})
        assert resp.status_code == 404

    def test_update_combined_fields_and_status(self, client):
        c, repo = client
        plan = self._seed_plan(repo)

        resp = c.put(
            f"/api/v1/edit-plans/{plan.id}",
            json={"name": "新名称", "status": "editing", "total_duration": 90.0},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["name"] == "新名称"
        assert data["status"] == "editing"
        assert data["total_duration"] == 90.0


# ---------------------------------------------------------------------------
# 删除测试
# ---------------------------------------------------------------------------


class TestDeletePlan:
    def test_delete_success(self, client):
        c, repo = client
        plan = EditPlan.create("tpl-001", "待删除")
        repo.create(plan)

        resp = c.delete(f"/api/v1/edit-plans/{plan.id}")
        assert resp.status_code == 204
        assert repo.get(plan.id) is None

    def test_delete_not_found_returns_404(self, client):
        c, repo = client
        resp = c.delete("/api/v1/edit-plans/nonexistent")
        assert resp.status_code == 404
        assert "剪辑计划不存在" in resp.json()["detail"]
