"""Nested relation graph mutation tests."""

from collections.abc import AsyncGenerator
from typing import cast

import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlalchemy.pool import StaticPool
from sqlmodel import Field, Relationship, SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession

from auen import CrudPolicy, CrudRouterBuilder


class GraphAuthor(SQLModel, table=True):
    __tablename__ = "graph_author"
    id: int | None = Field(default=None, primary_key=True)
    name: str
    books: list["GraphBook"] = Relationship(back_populates="author")


class GraphBookTag(SQLModel, table=True):
    __tablename__ = "graph_book_tag"
    book_id: int | None = Field(
        default=None,
        foreign_key="graph_book.id",
        primary_key=True,
    )
    tag_id: int | None = Field(
        default=None,
        foreign_key="graph_tag.id",
        primary_key=True,
    )
    weight: int | None = None


class GraphTag(SQLModel, table=True):
    __tablename__ = "graph_tag"
    id: int | None = Field(default=None, primary_key=True)
    name: str
    books: list["GraphBook"] = Relationship(
        back_populates="tags",
        link_model=GraphBookTag,
    )


class GraphChapter(SQLModel, table=True):
    __tablename__ = "graph_chapter"
    id: int | None = Field(default=None, primary_key=True)
    title: str
    book_id: int | None = Field(default=None, foreign_key="graph_book.id")
    book: "GraphBook" = Relationship(back_populates="chapters")


class GraphBookMeta(SQLModel, table=True):
    __tablename__ = "graph_book_meta"
    id: int | None = Field(default=None, primary_key=True)
    book_id: int | None = Field(default=None, foreign_key="graph_book.id", unique=True)
    summary: str | None = None
    book: "GraphBook" = Relationship(back_populates="meta")


class GraphBook(SQLModel, table=True):
    __tablename__ = "graph_book"
    id: int | None = Field(default=None, primary_key=True)
    title: str
    isbn: str
    author_id: int | None = Field(default=None, foreign_key="graph_author.id")
    author: GraphAuthor | None = Relationship(back_populates="books")
    chapters: list[GraphChapter] = Relationship(back_populates="book")
    tags: list[GraphTag] = Relationship(back_populates="books", link_model=GraphBookTag)
    meta: GraphBookMeta | None = Relationship(back_populates="book")


class DenyTagCreatePolicy:
    def can_create(self, user: object, obj_in: object) -> bool:
        return False

    def can_read(self, user: object, db_obj: SQLModel) -> bool:
        return True

    def can_update(self, user: object, db_obj: SQLModel, obj_in: object) -> bool:
        return True

    def can_delete(self, user: object, db_obj: SQLModel) -> bool:
        return True

    def filter_list_query(self, user: object, query: object) -> object:
        return query


class DenyLinkUpdatePolicy:
    def can_create(self, user: object, obj_in: object) -> bool:
        return True

    def can_read(self, user: object, db_obj: SQLModel) -> bool:
        return True

    def can_update(self, user: object, db_obj: SQLModel, obj_in: object) -> bool:
        return False

    def can_delete(self, user: object, db_obj: SQLModel) -> bool:
        return True

    def filter_list_query(self, user: object, query: object) -> object:
        return query


async def _build_client(
    *,
    nested_policy_registry: dict[type[SQLModel], CrudPolicy] | None = None,
) -> tuple[AsyncClient, AsyncEngine]:
    engine = create_async_engine(
        "sqlite+aiosqlite://",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)

    async def get_session() -> AsyncGenerator[AsyncSession, None]:
        async with AsyncSession(engine) as session:
            yield session

    app = FastAPI()
    book_builder = CrudRouterBuilder.for_model(GraphBook, get_session).with_prefix(
        "/books"
    )
    if nested_policy_registry:
        book_builder = book_builder.with_nested_policy_registry(nested_policy_registry)
    app.include_router(book_builder.build())
    app.include_router(
        CrudRouterBuilder.for_model(GraphAuthor, get_session)
        .with_prefix("/authors")
        .build()
    )
    app.include_router(
        CrudRouterBuilder.for_model(GraphTag, get_session).with_prefix("/tags").build()
    )
    app.include_router(
        CrudRouterBuilder.for_model(GraphChapter, get_session)
        .with_prefix("/chapters")
        .build()
    )
    app.include_router(
        CrudRouterBuilder.for_model(GraphBookMeta, get_session)
        .with_prefix("/meta")
        .build()
    )

    transport = ASGITransport(app=app)
    client = AsyncClient(transport=transport, base_url="http://test")
    return client, engine


@pytest.fixture
async def graph_client() -> AsyncGenerator[tuple[AsyncClient, AsyncEngine], None]:
    client, engine = await _build_client()
    try:
        yield client, engine
    finally:
        await client.aclose()
        await engine.dispose()


@pytest.fixture
async def graph_policy_client() -> AsyncGenerator[tuple[AsyncClient, AsyncEngine], None]:
    client, engine = await _build_client(
        nested_policy_registry=cast(
            dict[type[SQLModel], CrudPolicy],
            {
                GraphTag: DenyTagCreatePolicy(),
                GraphBookTag: DenyLinkUpdatePolicy(),
            },
        )
    )
    try:
        yield client, engine
    finally:
        await client.aclose()
        await engine.dispose()


async def _create_author(client: AsyncClient, name: str) -> int:
    response = await client.post("/authors/", json={"name": name})
    assert response.status_code == 201
    return response.json()["id"]


async def _create_book(client: AsyncClient, title: str, author_id: int) -> int:
    response = await client.post(
        "/books/",
        json={"title": title, "isbn": "isbn-1", "author_id": author_id},
    )
    assert response.status_code == 201
    return response.json()["id"]


async def test_nested_patch_updates_graph_relations(
    graph_client: tuple[AsyncClient, AsyncEngine],
) -> None:
    client, engine = graph_client
    author_id = await _create_author(client, "A1")
    book_id = await _create_book(client, "B1", author_id)

    payload = {
        "title": "B2",
        "author": {"id": author_id, "name": "A2"},
        "chapters": [{"title": "C1"}, {"title": "C2"}],
        "tags": [{"entity": {"name": "T1"}, "link": {"weight": 5}}],
        "meta": {"summary": "M1"},
    }
    response = await client.patch(f"/books/{book_id}/nested", json=payload)
    assert response.status_code == 200
    assert response.json()["title"] == "B2"

    author_response = await client.get(f"/authors/{author_id}")
    assert author_response.status_code == 200
    assert author_response.json()["name"] == "A2"

    chapters_response = await client.get("/chapters/")
    assert chapters_response.status_code == 200
    chapters = [c for c in chapters_response.json() if c["book_id"] == book_id]
    assert len(chapters) == 2

    tags_response = await client.get("/tags/")
    assert tags_response.status_code == 200
    created_tag = tags_response.json()[0]
    assert created_tag["name"] == "T1"

    async with AsyncSession(engine) as session:
        book_tag = (
            await session.exec(
                select(GraphBookTag).where(
                    GraphBookTag.book_id == book_id,
                    GraphBookTag.tag_id == created_tag["id"],
                )
            )
        ).one()
        assert book_tag.weight == 5
        meta = (
            await session.exec(
                select(GraphBookMeta).where(GraphBookMeta.book_id == book_id)
            )
        ).one()
        assert meta.summary == "M1"


async def test_nested_patch_many_to_many_merge_updates_entity_and_link(
    graph_client: tuple[AsyncClient, AsyncEngine],
) -> None:
    client, engine = graph_client
    author_id = await _create_author(client, "A1")
    book_id = await _create_book(client, "B1", author_id)

    create_link = await client.patch(
        f"/books/{book_id}/nested",
        json={"tags": [{"entity": {"name": "T1"}, "link": {"weight": 1}}]},
    )
    assert create_link.status_code == 200

    tags_response = await client.get("/tags/")
    tag_id = tags_response.json()[0]["id"]

    update_link = await client.patch(
        f"/books/{book_id}/nested",
        json={
            "tags": [
                {
                    "entity": {"id": tag_id, "name": "T1-updated"},
                    "link": {"weight": 9},
                }
            ]
        },
    )
    assert update_link.status_code == 200

    updated_tag = await client.get(f"/tags/{tag_id}")
    assert updated_tag.status_code == 200
    assert updated_tag.json()["name"] == "T1-updated"

    async with AsyncSession(engine) as session:
        book_tag = (
            await session.exec(
                select(GraphBookTag).where(
                    GraphBookTag.book_id == book_id,
                    GraphBookTag.tag_id == tag_id,
                )
            )
        ).one()
        assert book_tag.weight == 9


async def test_nested_patch_validation_and_relation_conflicts(
    graph_client: tuple[AsyncClient, AsyncEngine],
) -> None:
    client, _engine = graph_client
    author_id = await _create_author(client, "A1")
    other_author_id = await _create_author(client, "A2")
    book_id = await _create_book(client, "B1", author_id)
    other_book_id = await _create_book(client, "B2", other_author_id)

    chapter_resp = await client.post(
        "/chapters/",
        json={"title": "Other", "book_id": other_book_id},
    )
    chapter_id = chapter_resp.json()["id"]

    bad_author = await client.patch(f"/books/{book_id}/nested", json={"author": None})
    assert bad_author.status_code == 422

    bad_chapters = await client.patch(
        f"/books/{book_id}/nested",
        json={"chapters": {"title": "no-list"}},
    )
    assert bad_chapters.status_code == 422

    bad_tags = await client.patch(
        f"/books/{book_id}/nested",
        json={"tags": [{"link": {"weight": 1}}]},
    )
    assert bad_tags.status_code == 422

    conflict_fk = await client.patch(
        f"/books/{book_id}/nested",
        json={"author_id": author_id, "author": {"name": "MissingPk"}},
    )
    assert conflict_fk.status_code == 422

    chapter_conflict = await client.patch(
        f"/books/{book_id}/nested",
        json={"chapters": [{"id": chapter_id, "title": "Hijack"}]},
    )
    assert chapter_conflict.status_code == 422


async def test_nested_patch_enforces_nested_policy_registry(
    graph_policy_client: tuple[AsyncClient, AsyncEngine],
) -> None:
    client, _engine = graph_policy_client
    author_id = await _create_author(client, "A1")
    book_id = await _create_book(client, "B1", author_id)

    deny_tag_create = await client.patch(
        f"/books/{book_id}/nested",
        json={"tags": [{"entity": {"name": "blocked"}, "link": {"weight": 1}}]},
    )
    assert deny_tag_create.status_code == 403

    create_tag = await client.post("/tags/", json={"name": "T1"})
    tag_id = create_tag.json()["id"]
    create_link = await client.patch(
        f"/books/{book_id}/nested",
        json={"tags": [{"entity": {"id": tag_id}}]},
    )
    assert create_link.status_code == 200

    deny_link_update = await client.patch(
        f"/books/{book_id}/nested",
        json={"tags": [{"entity": {"id": tag_id}, "link": {"weight": 7}}]},
    )
    assert deny_link_update.status_code == 403
