"""Tests for lifecycle hooks."""

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 sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from tests.conftest import Book, SessionFactory

from auen import CrudRouterBuilder, HooksConfig, Operation
from auen.types import User


@pytest.fixture
def hook_log() -> list[tuple[str, int | str | None]]:
    return []


@pytest.fixture
def hooks_app(
    get_session: SessionFactory, hook_log: list[tuple[str, int | str | None]]
) -> FastAPI:
    async def before_create(session: AsyncSession, obj_in: BaseModel, user: User) -> None:
        hook_log.append(("before_create", type(obj_in).__name__))

    async def after_create(session: AsyncSession, db_obj: Book, user: User) -> None:
        hook_log.append(("after_create", db_obj.id))

    async def before_update(session: AsyncSession, db_obj: Book, user: User) -> None:
        hook_log.append(("before_update", db_obj.id))

    async def after_update(session: AsyncSession, db_obj: Book, user: User) -> None:
        hook_log.append(("after_update", db_obj.title))

    async def before_delete(session: AsyncSession, db_obj: Book, user: User) -> None:
        hook_log.append(("before_delete", db_obj.id))

    async def after_delete(session: AsyncSession, db_obj: Book, user: User) -> None:
        hook_log.append(("after_delete", db_obj.id))

    from tests.conftest import Book

    application = FastAPI()
    application.include_router(
        CrudRouterBuilder.for_model(Book, get_session)
        .with_hooks(
            HooksConfig(
                before_create=before_create,
                after_create=after_create,
                before_update=before_update,
                after_update=after_update,
                before_delete=before_delete,
                after_delete=after_delete,
            )
        )
        .build()
    )
    return application


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


async def test_create_hooks_fire(
    hooks_client: AsyncClient, hook_log: list[tuple[str, int | str | None]]
) -> None:
    resp = await hooks_client.post("/books/", json={"title": "Test", "isbn": "123"})
    assert resp.status_code == 201
    assert ("before_create", "BookCreate") in hook_log
    assert ("after_create", resp.json()["id"]) in hook_log


async def test_update_hooks_fire(
    hooks_client: AsyncClient, hook_log: list[tuple[str, int | str | None]]
) -> None:
    resp = await hooks_client.post("/books/", json={"title": "Old", "isbn": "456"})
    book_id = resp.json()["id"]
    resp = await hooks_client.patch(f"/books/{book_id}", json={"title": "New"})
    assert resp.status_code == 200
    assert ("before_update", book_id) in hook_log
    assert ("after_update", "New") in hook_log


async def test_delete_hooks_fire(
    hooks_client: AsyncClient, hook_log: list[tuple[str, int | str | None]]
) -> None:
    resp = await hooks_client.post("/books/", json={"title": "Bye", "isbn": "789"})
    book_id = resp.json()["id"]
    resp = await hooks_client.delete(f"/books/{book_id}")
    assert resp.status_code == 200
    assert ("before_delete", book_id) in hook_log
    assert ("after_delete", book_id) in hook_log


async def test_hooks_before_after_create(get_session: SessionFactory) -> None:
    called = []

    async def before_create(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("before_create")

    async def after_create(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("after_create")

    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(Book, get_session)
        .with_hooks(HooksConfig(before_create=before_create, after_create=after_create))
        .with_operations({Operation.CREATE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/books/", json={"title": "T", "isbn": "I"})
        assert resp.status_code == 201
    assert called == ["before_create", "after_create"]


async def test_hooks_before_after_update(get_session: SessionFactory) -> None:
    called = []

    async def before_update(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("before_update")

    async def after_update(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("after_update")

    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(Book, get_session)
        .with_hooks(HooksConfig(before_update=before_update, after_update=after_update))
        .with_operations({Operation.CREATE, Operation.UPDATE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/books/", json={"title": "T", "isbn": "I"})
        book_id = resp.json()["id"]
        resp = await c.patch(f"/books/{book_id}", json={"title": "Updated"})
        assert resp.status_code == 200
    assert called == ["before_update", "after_update"]


async def test_hooks_before_after_delete(get_session: SessionFactory) -> None:
    called = []

    async def before_delete(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("before_delete")

    async def after_delete(session: AsyncSession, obj: SQLModel, user: User) -> None:
        called.append("after_delete")

    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(Book, get_session)
        .with_hooks(HooksConfig(before_delete=before_delete, after_delete=after_delete))
        .with_operations({Operation.CREATE, Operation.DELETE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/books/", json={"title": "T", "isbn": "I"})
        book_id = resp.json()["id"]
        resp = await c.delete(f"/books/{book_id}")
        assert resp.status_code == 200
    assert called == ["before_delete", "after_delete"]
