"""T5.5 POST /projects/:id/rules 端点（内存替身，无 DB/无网络）。

覆盖：
- 201 + 回显 level/content + 端点 commit；
- 非法 level → 422（Pydantic Literal 校验，FastAPI 422）；
- 空 content → 422；
- 项目不存在 → 404（QA H2 回归：此前 FK 违例逃逸成 500）。
"""

from __future__ import annotations

import uuid

import httpx
import pytest
from cryptography.fernet import Fernet
from fakes_projects import FakeProjectRepo, FakeSession
from ww_api.services.credentials import STUB_OWNER_ID
from ww_core.domain.project_repo import ProjectView
from ww_core.domain.rule_repo import RuleListItemView, RuleWriteView


class _FakeRuleWriteRepo:
    def __init__(self) -> None:
        self.rows: list[RuleWriteView] = []

    async def create(self, project_id: uuid.UUID, *, level: str, content: str) -> RuleWriteView:
        view = RuleWriteView(id=uuid.uuid4(), project_id=project_id, level=level, content=content)
        self.rows.append(view)
        return view

    async def list_for_project(self, project_id: uuid.UUID) -> list[RuleListItemView]:
        return [
            RuleListItemView(id=r.id, level=r.level, content=r.content)
            for r in self.rows
            if r.project_id in (project_id, None)
        ]

    async def delete(self, project_id: uuid.UUID, rule_id: uuid.UUID) -> bool:
        before = len(self.rows)
        self.rows = [r for r in self.rows if not (r.id == rule_id and r.project_id == project_id)]
        return len(self.rows) < before


def _make_client() -> tuple[
    httpx.AsyncClient, _FakeRuleWriteRepo, FakeSession, FakeProjectRepo, uuid.UUID
]:
    """构建测试 client，并 seed 一个属于 STUB_OWNER_ID 的项目；返回其 pid。

    create_rule 现在先校验项目存在（QA H2），故必须 override get_project_repo 并 seed，
    否则正常用例会 404。返回的 pid 是已存在项目；未 seed 的随机 pid 即"不存在"。
    """
    import os

    os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
    from ww_api.main import create_app
    from ww_api.services.project_deps import get_project_repo, get_rule_write_repo
    from ww_db import get_session

    repo = _FakeRuleWriteRepo()
    session = FakeSession()
    project_repo = FakeProjectRepo()
    pid = uuid.uuid4()
    project_repo.rows[pid] = (STUB_OWNER_ID, ProjectView(id=pid, title="作品"))

    app = create_app()
    app.dependency_overrides[get_rule_write_repo] = lambda: repo
    app.dependency_overrides[get_project_repo] = lambda: project_repo
    app.dependency_overrides[get_session] = lambda: session
    transport = httpx.ASGITransport(app=app)
    client = httpx.AsyncClient(transport=transport, base_url="http://test")
    return client, repo, session, project_repo, pid


@pytest.mark.asyncio
async def test_create_rule_returns_201_and_commits() -> None:
    client, repo, session, _project_repo, pid = _make_client()
    async with client:
        resp = await client.post(
            f"/projects/{pid}/rules",
            json={"level": "project", "content": "主角不许中途复活"},
        )
    assert resp.status_code == 201
    body = resp.json()
    assert body["level"] == "project"
    assert body["content"] == "主角不许中途复活"
    # 创建回显带 id（前端删除 handle）。
    assert body["id"] == str(repo.rows[0].id)
    assert session.commits == 1
    assert len(repo.rows) == 1
    assert repo.rows[0].project_id == pid


@pytest.mark.asyncio
async def test_create_rule_invalid_level_returns_422() -> None:
    client, _repo, _session, _project_repo, pid = _make_client()
    async with client:
        resp = await client.post(
            f"/projects/{pid}/rules",
            json={"level": "nonsense", "content": "x"},
        )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_rule_empty_content_returns_422() -> None:
    client, _repo, _session, _project_repo, pid = _make_client()
    async with client:
        resp = await client.post(
            f"/projects/{pid}/rules",
            json={"level": "global", "content": ""},
        )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_rule_whitespace_content_returns_422() -> None:
    # QA MEDIUM 回归：纯空白 content（strip 后为空）应 422，不可入库成垃圾行。
    client, repo, _session, _project_repo, pid = _make_client()
    async with client:
        resp = await client.post(
            f"/projects/{pid}/rules",
            json={"level": "project", "content": "   "},
        )
    assert resp.status_code == 422
    assert len(repo.rows) == 0


@pytest.mark.asyncio
async def test_create_rule_unknown_project_returns_404() -> None:
    # QA H2 回归：给不存在的 project 加规则应 404（不是 500 的 FK 违例逃逸）。
    client, repo, _session, _project_repo, _pid = _make_client()
    async with client:
        resp = await client.post(
            f"/projects/{uuid.uuid4()}/rules",
            json={"level": "project", "content": "x"},
        )
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "NOT_FOUND"
    assert len(repo.rows) == 0  # 未触达写库


@pytest.mark.asyncio
async def test_delete_rule_returns_204_and_commits() -> None:
    # 作者显式删一条本作品规则（不变量 #3）：204 + 端点提交 + 行被移除。
    client, repo, session, _project_repo, pid = _make_client()
    seeded = await repo.create(pid, level="project", content="待删规则")
    async with client:
        resp = await client.delete(f"/projects/{pid}/rules/{seeded.id}")
    assert resp.status_code == 204
    assert session.commits == 1
    assert len(repo.rows) == 0


@pytest.mark.asyncio
async def test_delete_rule_unknown_rule_returns_404() -> None:
    # 未知 rule_id → 404；不提交（删不到行）。
    client, repo, session, _project_repo, pid = _make_client()
    async with client:
        resp = await client.delete(f"/projects/{pid}/rules/{uuid.uuid4()}")
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "NOT_FOUND"
    assert session.commits == 0


@pytest.mark.asyncio
async def test_delete_rule_unknown_project_returns_404() -> None:
    # 项目不存在 → 404（先于 rule 查校验），不触达删除。
    client, repo, session, _project_repo, _pid = _make_client()
    async with client:
        resp = await client.delete(f"/projects/{uuid.uuid4()}/rules/{uuid.uuid4()}")
    assert resp.status_code == 404
    assert resp.json()["error"]["code"] == "NOT_FOUND"
    assert session.commits == 0
