"""模板管理 API 单元测试 — Phase 8 任务 2.03.

覆盖 5 个端点：
  GET    /api/v1/edit-templates      — 列表（分页 + 筛选）
  GET    /api/v1/edit-templates/{id} — 详情
  POST   /api/v1/edit-templates      — 创建
  PUT    /api/v1/edit-templates/{id} — 更新
  DELETE /api/v1/edit-templates/{id} — 软删除

使用 FastAPI TestClient + Stub Repository + dependency_overrides.
"""

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

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.config_schemas import normalize_template_config
from packages.domain.edit_template import EditTemplate, EditTemplateStatus

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


class StubEditTemplateRepository:
    """内存中模拟 EditTemplate 仓储"""

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

    def list_all(
        self,
        *,
        template_type: Optional[str] = None,
        status: Optional[EditTemplateStatus] = None,
        skip: int = 0,
        limit: int = 50,
    ) -> list[EditTemplate]:
        items = list(self._store.values())
        if template_type:
            items = [t for t in items if t.template_type == template_type]
        if status:
            items = [t for t in items if t.status == status]
        items.sort(key=lambda t: t.created_at, reverse=True)
        return items[skip : skip + limit]

    def list_active(
        self,
        *,
        template_type: Optional[str] = None,
        skip: int = 0,
        limit: int = 50,
    ) -> list[EditTemplate]:
        return self.list_all(template_type=template_type, status=EditTemplateStatus.ACTIVE, skip=skip, limit=limit)

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

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

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

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

    def count(
        self,
        *,
        template_type: Optional[str] = None,
        status: Optional[EditTemplateStatus] = None,
    ) -> int:
        items = list(self._store.values())
        if template_type:
            items = [t for t in items if t.template_type == template_type]
        if status:
            items = [t for t in items if t.status == status]
        return len(items)


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


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


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


@pytest.fixture
def stub_repo() -> StubEditTemplateRepository:
    return StubEditTemplateRepository()


@pytest.fixture
def app(stub_repo: StubEditTemplateRepository) -> FastAPI:
    """构建测试 FastAPI 应用，注入 Stub Repository"""
    import app.services.edit_template_service as service_module
    from app.api.routes.edit_templates import router
    from app.auth import get_current_user
    from app.dependencies import get_db_session

    # 替换服务模块中的 Repository 类
    original_template_repo_cls = service_module.SQLAlchemyEditTemplateRepository
    original_clip_config_repo_cls = service_module.SQLAlchemyTemplateClipConfigRepository
    service_module.SQLAlchemyEditTemplateRepository = lambda session: stub_repo
    service_module.SQLAlchemyTemplateClipConfigRepository = lambda session: stub_repo

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

    # 覆盖依赖
    def override_get_db_session():
        yield MagicMock()

    def override_get_current_user():
        return FakeAuthenticatedUser()

    test_app.dependency_overrides[get_db_session] = override_get_db_session
    test_app.dependency_overrides[get_current_user] = override_get_current_user

    yield test_app

    # 恢复
    service_module.SQLAlchemyEditTemplateRepository = original_template_repo_cls
    service_module.SQLAlchemyTemplateClipConfigRepository = original_clip_config_repo_cls
    test_app.dependency_overrides.clear()


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


def _make_template(name: str = "测试模板", **kwargs: Any) -> EditTemplate:
    return EditTemplate.create(name=name, **kwargs)


# ── GET /api/v1/edit-templates (列表) ─────────────────────────────────────────


class TestListTemplates:
    def test_empty_list(self, client: TestClient) -> None:
        resp = client.get("/api/v1/edit-templates")
        assert resp.status_code == 200
        data = resp.json()
        assert data["items"] == []
        assert data["total"] == 0
        assert data["page"] == 1

    def test_list_with_items(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        for i in range(3):
            stub_repo.create(_make_template(f"模板{i}"))
        resp = client.get("/api/v1/edit-templates")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 3
        assert len(data["items"]) == 3

    def test_pagination(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        for i in range(5):
            stub_repo.create(_make_template(f"模板{i}"))
        resp = client.get("/api/v1/edit-templates?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
        assert data["page_size"] == 2

    def test_filter_by_type(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        stub_repo.create(_make_template("Vlog模板", template_type="vlog"))
        stub_repo.create(_make_template("短视频模板", template_type="short"))
        stub_repo.create(_make_template("另一个Vlog", template_type="vlog"))
        resp = client.get("/api/v1/edit-templates?template_type=vlog")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 2
        assert all(item["template_type"] == "vlog" for item in data["items"])

    def test_filter_by_status(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t1 = _make_template("活跃模板")
        stub_repo.create(t1)
        t2 = _make_template("停用模板", status=EditTemplateStatus.INACTIVE)
        stub_repo.create(t2)
        resp = client.get("/api/v1/edit-templates?status=active")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1
        assert data["items"][0]["name"] == "活跃模板"

    def test_invalid_status_filter(self, client: TestClient) -> None:
        resp = client.get("/api/v1/edit-templates?status=invalid")
        assert resp.status_code == 400

    def test_invalid_page(self, client: TestClient) -> None:
        resp = client.get("/api/v1/edit-templates?page=0")
        assert resp.status_code == 422


# ── GET /api/v1/edit-templates/{id} (详情) ────────────────────────────────────


class TestGetTemplate:
    def test_get_existing(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("详情模板", description="这是描述", template_type="vlog")
        stub_repo.create(t)
        resp = client.get(f"/api/v1/edit-templates/{t.id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == t.id
        assert data["name"] == "详情模板"
        assert data["description"] == "这是描述"
        assert data["template_type"] == "vlog"
        assert data["status"] == "active"

    def test_get_not_found(self, client: TestClient) -> None:
        resp = client.get("/api/v1/edit-templates/nonexistent-id")
        assert resp.status_code == 404
        assert "不存在" in resp.json()["detail"]


# ── POST /api/v1/edit-templates (创建) ────────────────────────────────────────


class TestCreateTemplate:
    def test_create_basic(self, client: TestClient) -> None:
        resp = client.post("/api/v1/edit-templates", json={"name": "新模板"})
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == "新模板"
        assert data["description"] == ""
        assert data["template_type"] == "default"
        assert data["status"] == "active"
        assert data["sort_weight"] == 0
        assert "id" in data

    def test_create_with_all_fields(self, client: TestClient) -> None:
        body = {
            "name": "完整模板",
            "description": "完整描述",
            "template_type": "vlog",
            "config": {"key": "value"},
            "preview_url": "https://example.com/preview.mp4",
            "sort_weight": 10,
        }
        resp = client.post("/api/v1/edit-templates", json=body)
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == "完整模板"
        assert data["description"] == "完整描述"
        assert data["template_type"] == "vlog"
        assert data["config"] == normalize_template_config({"key": "value"})
        assert data["preview_url"] == "https://example.com/preview.mp4"
        assert data["sort_weight"] == 10

    def test_create_empty_name(self, client: TestClient) -> None:
        resp = client.post("/api/v1/edit-templates", json={"name": ""})
        assert resp.status_code == 422  # Pydantic min_length=1

    def test_create_whitespace_name(self, client: TestClient) -> None:
        resp = client.post("/api/v1/edit-templates", json={"name": "   "})
        assert resp.status_code == 400  # domain validation

    def test_create_missing_name(self, client: TestClient) -> None:
        resp = client.post("/api/v1/edit-templates", json={})
        assert resp.status_code == 422

    def test_create_negative_sort_weight(self, client: TestClient) -> None:
        resp = client.post("/api/v1/edit-templates", json={"name": "模板", "sort_weight": -1})
        assert resp.status_code == 422


# ── PUT /api/v1/edit-templates/{id} (更新) ────────────────────────────────────


class TestUpdateTemplate:
    def test_update_name(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("旧名称")
        stub_repo.create(t)
        resp = client.put(f"/api/v1/edit-templates/{t.id}", json={"name": "新名称"})
        assert resp.status_code == 200
        assert resp.json()["name"] == "新名称"

    def test_update_multiple_fields(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板")
        stub_repo.create(t)
        body = {"name": "更新后", "description": "新描述", "sort_weight": 5}
        resp = client.put(f"/api/v1/edit-templates/{t.id}", json=body)
        assert resp.status_code == 200
        data = resp.json()
        assert data["name"] == "更新后"
        assert data["description"] == "新描述"
        assert data["sort_weight"] == 5

    def test_update_status(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板")
        stub_repo.create(t)
        resp = client.put(f"/api/v1/edit-templates/{t.id}", json={"status": "inactive"})
        assert resp.status_code == 200
        assert resp.json()["status"] == "inactive"

    def test_update_invalid_status(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板")
        stub_repo.create(t)
        resp = client.put(f"/api/v1/edit-templates/{t.id}", json={"status": "bogus"})
        assert resp.status_code == 400

    def test_update_not_found(self, client: TestClient) -> None:
        resp = client.put("/api/v1/edit-templates/nonexistent", json={"name": "x"})
        assert resp.status_code == 404

    def test_partial_update_preserves_others(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("原名", description="原描述", template_type="vlog")
        stub_repo.create(t)
        resp = client.put(f"/api/v1/edit-templates/{t.id}", json={"name": "新名"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["name"] == "新名"
        assert data["description"] == "原描述"
        assert data["template_type"] == "vlog"

    def test_update_empty_body(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板")
        stub_repo.create(t)
        resp = client.put(f"/api/v1/edit-templates/{t.id}", json={})
        assert resp.status_code == 200
        assert resp.json()["name"] == "模板"


# ── DELETE /api/v1/edit-templates/{id} (软删除) ───────────────────────────────


class TestDeleteTemplate:
    def test_soft_delete(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("待删除")
        stub_repo.create(t)
        resp = client.delete(f"/api/v1/edit-templates/{t.id}")
        assert resp.status_code == 204
        # 软删除后仍存在，但状态为 inactive
        updated = stub_repo.get(t.id)
        assert updated is not None
        assert updated.status == EditTemplateStatus.INACTIVE

    def test_soft_delete_not_found(self, client: TestClient) -> None:
        resp = client.delete("/api/v1/edit-templates/nonexistent")
        assert resp.status_code == 404

    def test_soft_delete_idempotent(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板")
        stub_repo.create(t)
        # 第一次删除
        resp1 = client.delete(f"/api/v1/edit-templates/{t.id}")
        assert resp1.status_code == 204
        # 第二次删除（已经是 inactive，但仍可再次设为 inactive）
        resp2 = client.delete(f"/api/v1/edit-templates/{t.id}")
        assert resp2.status_code == 204

    def test_deleted_not_in_active_list(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板")
        stub_repo.create(t)
        client.delete(f"/api/v1/edit-templates/{t.id}")
        resp = client.get("/api/v1/edit-templates?status=active")
        data = resp.json()
        assert data["total"] == 0


# ── Response Schema 验证 ──────────────────────────────────────────────────────


class TestResponseSchema:
    def test_response_has_all_fields(self, client: TestClient, stub_repo: StubEditTemplateRepository) -> None:
        t = _make_template("模板", description="描述", template_type="vlog")
        stub_repo.create(t)
        resp = client.get(f"/api/v1/edit-templates/{t.id}")
        data = resp.json()
        expected_keys = {
            "id",
            "name",
            "description",
            "template_type",
            "config",
            "preview_url",
            "sort_weight",
            "status",
            "created_at",
            "updated_at",
        }
        assert set(data.keys()) == expected_keys

    def test_list_response_structure(self, client: TestClient) -> None:
        resp = client.get("/api/v1/edit-templates")
        data = resp.json()
        assert "items" in data
        assert "total" in data
        assert "page" in data
        assert "page_size" in data
        assert isinstance(data["items"], list)
