"""Nested update endpoint tests."""

from __future__ import annotations

from collections.abc import AsyncGenerator

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

from auen import CrudRouterBuilder
from auen.config import NestedRelationConfig, NestedWriteConfig
from auen.types import SelectQuery, User


class AuthorCreateIn(BaseModel):
    name: str
    bio: str | None = None


class AuthorReadOut(BaseModel):
    id: int
    name: str
    bio: str | None = None


class BookCreateIn(BaseModel):
    title: str
    isbn: str
    author_id: int | None = None


class BookReadOut(BaseModel):
    id: int
    title: str
    isbn: str
    author_id: int | None = None


class BookPatchIn(BaseModel):
    title: str | None = None
    isbn: str | None = None
    author_id: int | None = None


class NestedAuthorPatchIn(BaseModel):
    id: int | None = None
    name: str | None = None
    bio: str | None = None


class BookNestedPatchIn(BookPatchIn):
    author: NestedAuthorPatchIn | None = None


class BookNestedCreateIn(BookCreateIn):
    author: NestedAuthorPatchIn | None = None


class NestedAuthor(SQLModel, table=True):
    __tablename__ = "nested_author"
    id: int | None = Field(default=None, primary_key=True)
    name: str
    bio: str | None = None


class NestedBook(SQLModel, table=True):
    __tablename__ = "nested_book"
    id: int | None = Field(default=None, primary_key=True)
    title: str
    isbn: str
    author_id: int | None = Field(default=None, foreign_key="nested_author.id")

    author: NestedAuthor = Relationship()


class AllowAllPolicy:
    def can_create(self, user: User, obj_in: BaseModel) -> bool:
        return True

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

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

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

    def filter_list_query(
        self, user: User, query: SelectQuery[SQLModel]
    ) -> SelectQuery[SQLModel]:
        return query


class DenyAuthorUpdatePolicy(AllowAllPolicy):
    def can_update(self, user: User, db_obj: SQLModel, obj_in: BaseModel) -> bool:
        return False


@pytest.fixture
async def nested_app() -> AsyncGenerator[FastAPI, None]:
    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()
    nested_cfg = NestedWriteConfig(
        fields={
            "author": NestedRelationConfig(
                model=NestedAuthor,
                policy=AllowAllPolicy(),
            ),
        }
    )
    app.include_router(
        CrudRouterBuilder()
        .with_prefix("/books")
        .with_nested_writes(nested_cfg)
        .build_routers(models=[NestedBook], session_dep=get_session)[0]
    )
    app.include_router(
        CrudRouterBuilder()
        .with_prefix("/authors")
        .build_routers(models=[NestedAuthor], session_dep=get_session)[0]
    )

    yield app
    await engine.dispose()


@pytest.fixture
async def nested_client(nested_app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
    transport = ASGITransport(app=nested_app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


@pytest.fixture
async def nested_deny_author_policy_app() -> AsyncGenerator[FastAPI, None]:
    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()
    nested_cfg = NestedWriteConfig(
        fields={
            "author": NestedRelationConfig(
                model=NestedAuthor,
                policy=DenyAuthorUpdatePolicy(),
            ),
        }
    )
    app.include_router(
        CrudRouterBuilder()
        .with_prefix("/books")
        .with_nested_writes(nested_cfg)
        .build_routers(models=[NestedBook], session_dep=get_session)[0]
    )
    app.include_router(
        CrudRouterBuilder()
        .with_prefix("/authors")
        .build_routers(models=[NestedAuthor], session_dep=get_session)[0]
    )

    yield app
    await engine.dispose()


@pytest.fixture
async def nested_deny_author_policy_client(
    nested_deny_author_policy_app: FastAPI,
) -> AsyncGenerator[AsyncClient, None]:
    transport = ASGITransport(app=nested_deny_author_policy_app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


async def _create_author(client: AsyncClient, *, name: str, bio: str) -> AuthorReadOut:
    payload = AuthorCreateIn(name=name, bio=bio)
    resp = await client.post(
        "/authors/",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 201
    return AuthorReadOut.model_validate(resp.json())


async def _create_book(
    client: AsyncClient, *, title: str, isbn: str, author_id: int | None
) -> BookReadOut:
    payload = BookCreateIn(title=title, isbn=isbn, author_id=author_id)
    resp = await client.post(
        "/books/",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 201
    return BookReadOut.model_validate(resp.json())


async def test_regular_patch_rejects_nested_author_payload(
    nested_client: AsyncClient,
) -> None:
    author = await _create_author(nested_client, name="A", bio="old")
    book = await _create_book(
        nested_client,
        title="Book",
        isbn="111",
        author_id=author.id,
    )

    payload = BookNestedPatchIn(author=NestedAuthorPatchIn(id=author.id, name="A2"))
    resp = await nested_client.patch(
        f"/books/{book.id}",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 422


async def test_nested_patch_route_exists(nested_client: AsyncClient) -> None:
    author = await _create_author(nested_client, name="A", bio="B")
    book = await _create_book(
        nested_client,
        title="Book",
        isbn="111",
        author_id=author.id,
    )

    payload = BookPatchIn(title="Book2")
    resp = await nested_client.patch(
        f"/books/{book.id}/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 200
    updated_book = BookReadOut.model_validate(resp.json())
    assert updated_book.title == "Book2"


async def test_nested_post_route_exists(nested_client: AsyncClient) -> None:
    payload = BookNestedCreateIn(title="Book", isbn="111")
    resp = await nested_client.post(
        "/books/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 201
    created_book = BookReadOut.model_validate(resp.json())
    assert created_book.title == "Book"


async def test_nested_post_creates_and_updates_author(nested_client: AsyncClient) -> None:
    existing_author = await _create_author(nested_client, name="A", bio="old")

    connect_and_update = BookNestedCreateIn(
        title="Book",
        isbn="111",
        author=NestedAuthorPatchIn(id=existing_author.id, name="A2", bio="new"),
    )
    connect_resp = await nested_client.post(
        "/books/nested",
        json=connect_and_update.model_dump(mode="json", exclude_none=True),
    )
    assert connect_resp.status_code == 201
    connected_book = BookReadOut.model_validate(connect_resp.json())
    assert connected_book.author_id == existing_author.id

    author_resp = await nested_client.get(f"/authors/{existing_author.id}")
    assert author_resp.status_code == 200
    updated_author = AuthorReadOut.model_validate(author_resp.json())
    assert updated_author.name == "A2"
    assert updated_author.bio == "new"

    create_new = BookNestedCreateIn(
        title="Book2",
        isbn="222",
        author=NestedAuthorPatchIn(name="Created", bio="created"),
    )
    create_resp = await nested_client.post(
        "/books/nested",
        json=create_new.model_dump(mode="json", exclude_none=True),
    )
    assert create_resp.status_code == 201
    new_book = BookReadOut.model_validate(create_resp.json())
    assert new_book.author_id is not None
    assert new_book.author_id != existing_author.id

    new_author_resp = await nested_client.get(f"/authors/{new_book.author_id}")
    assert new_author_resp.status_code == 200
    created_author = AuthorReadOut.model_validate(new_author_resp.json())
    assert created_author.name == "Created"


async def test_nested_post_validation_and_policy(
    nested_client: AsyncClient,
    nested_deny_author_policy_client: AsyncClient,
) -> None:
    author = await _create_author(nested_client, name="A1", bio="b1")
    other_author = await _create_author(nested_client, name="A2", bio="b2")

    missing_required_fields = BookNestedCreateIn(
        title="Book",
        isbn="111",
        author=NestedAuthorPatchIn(),
    )
    missing_required_fields_resp = await nested_client.post(
        "/books/nested",
        json=missing_required_fields.model_dump(mode="json", exclude_none=True),
    )
    assert missing_required_fields_resp.status_code == 422

    conflict_payload = BookNestedCreateIn(
        title="Book",
        isbn="111",
        author_id=author.id,
        author=NestedAuthorPatchIn(id=other_author.id, name="X"),
    )
    conflict_resp = await nested_client.post(
        "/books/nested",
        json=conflict_payload.model_dump(mode="json", exclude_none=True),
    )
    assert conflict_resp.status_code == 422

    missing_related = BookNestedCreateIn(
        title="Book",
        isbn="111",
        author=NestedAuthorPatchIn(id=99999, name="Nope"),
    )
    missing_related_resp = await nested_client.post(
        "/books/nested",
        json=missing_related.model_dump(mode="json", exclude_none=True),
    )
    assert missing_related_resp.status_code == 404

    deny_author = await _create_author(
        nested_deny_author_policy_client,
        name="A",
        bio="old",
    )
    denied_payload = BookNestedCreateIn(
        title="Book",
        isbn="111",
        author=NestedAuthorPatchIn(id=deny_author.id, name="Blocked"),
    )
    denied_resp = await nested_deny_author_policy_client.post(
        "/books/nested",
        json=denied_payload.model_dump(mode="json", exclude_none=True),
    )
    assert denied_resp.status_code == 403


async def test_nested_patch_updates_existing_author(nested_client: AsyncClient) -> None:
    author = await _create_author(nested_client, name="A", bio="old")
    book = await _create_book(
        nested_client,
        title="Book",
        isbn="111",
        author_id=author.id,
    )

    payload = BookNestedPatchIn(
        title="Book2",
        author=NestedAuthorPatchIn(id=author.id, name="A2", bio="new"),
    )
    resp = await nested_client.patch(
        f"/books/{book.id}/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 200
    updated_book = BookReadOut.model_validate(resp.json())
    assert updated_book.title == "Book2"
    assert updated_book.author_id == author.id

    resp = await nested_client.get(f"/authors/{author.id}")
    assert resp.status_code == 200
    updated_author = AuthorReadOut.model_validate(resp.json())
    assert updated_author.name == "A2"
    assert updated_author.bio == "new"


async def test_nested_patch_upserts_new_author(nested_client: AsyncClient) -> None:
    author = await _create_author(nested_client, name="A", bio="old")
    book = await _create_book(
        nested_client,
        title="Book",
        isbn="111",
        author_id=author.id,
    )

    payload = BookNestedPatchIn(author=NestedAuthorPatchIn(name="Created", bio="created"))
    resp = await nested_client.patch(
        f"/books/{book.id}/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 200
    updated_book = BookReadOut.model_validate(resp.json())
    new_author_id = updated_book.author_id
    assert new_author_id is not None
    assert new_author_id != author.id

    resp = await nested_client.get(f"/authors/{new_author_id}")
    assert resp.status_code == 200
    created_author = AuthorReadOut.model_validate(resp.json())
    assert created_author.name == "Created"


async def test_nested_patch_rejects_fk_and_nested_id_conflict(
    nested_client: AsyncClient,
) -> None:
    author1 = await _create_author(nested_client, name="A1", bio="b1")
    author2 = await _create_author(nested_client, name="A2", bio="b2")
    book = await _create_book(
        nested_client,
        title="Book",
        isbn="111",
        author_id=author1.id,
    )

    payload = BookNestedPatchIn(
        author_id=author1.id,
        author=NestedAuthorPatchIn(id=author2.id, name="X"),
    )
    resp = await nested_client.patch(
        f"/books/{book.id}/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 422


async def test_nested_patch_missing_related_id_returns_404(
    nested_client: AsyncClient,
) -> None:
    book = await _create_book(
        nested_client,
        title="Book",
        isbn="111",
        author_id=None,
    )

    payload = BookNestedPatchIn(author=NestedAuthorPatchIn(id=99999, name="Nope"))
    resp = await nested_client.patch(
        f"/books/{book.id}/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 404


async def test_nested_patch_author_policy_forbidden(
    nested_deny_author_policy_client: AsyncClient,
) -> None:
    author = await _create_author(
        nested_deny_author_policy_client,
        name="A",
        bio="old",
    )
    book = await _create_book(
        nested_deny_author_policy_client,
        title="Book",
        isbn="111",
        author_id=author.id,
    )

    payload = BookNestedPatchIn(author=NestedAuthorPatchIn(id=author.id, name="Blocked"))
    resp = await nested_deny_author_policy_client.patch(
        f"/books/{book.id}/nested",
        json=payload.model_dump(mode="json", exclude_none=True),
    )
    assert resp.status_code == 403

    resp = await nested_deny_author_policy_client.get(f"/authors/{author.id}")
    assert resp.status_code == 200
    unchanged_author = AuthorReadOut.model_validate(resp.json())
    assert unchanged_author.name == "A"
