"""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


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: object, obj_in: BaseModel) -> 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: BaseModel) -> 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 DenyAuthorUpdatePolicy(AllowAllPolicy):
    def can_update(self, user: object, 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.for_model(NestedBook, get_session)
        .with_prefix("/books")
        .with_nested_writes(nested_cfg)
        .build()
    )
    app.include_router(
        CrudRouterBuilder.for_model(NestedAuthor, get_session)
        .with_prefix("/authors")
        .build()
    )

    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.for_model(NestedBook, get_session)
        .with_prefix("/books")
        .with_nested_writes(nested_cfg)
        .build()
    )
    app.include_router(
        CrudRouterBuilder.for_model(NestedAuthor, get_session)
        .with_prefix("/authors")
        .build()
    )

    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"
