"""Tests for soft delete support."""

from __future__ import annotations

from collections.abc import AsyncGenerator, Callable
from datetime import datetime

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

from auen import CrudRouterBuilder, Operation, SoftDeleteConfig


class Article(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    deleted_at: datetime | None = None


@pytest.fixture
async def sd_engine() -> AsyncGenerator[AsyncEngine, None]:
    eng = create_async_engine(
        "sqlite+aiosqlite://",
        echo=False,
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    async with eng.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    yield eng
    await eng.dispose()


@pytest.fixture
async def sd_session(
    sd_engine: AsyncEngine,
) -> Callable[[], AsyncGenerator[AsyncSession, None]]:
    async def _get() -> AsyncGenerator[AsyncSession, None]:
        async with AsyncSession(sd_engine) as session:
            yield session

    return _get


@pytest.fixture
def sd_app(
    sd_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> FastAPI:
    application = FastAPI()
    application.include_router(
        CrudRouterBuilder.for_model(Article, sd_session).with_soft_delete().build()
    )
    return application


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


async def test_soft_delete_hides_from_list(sd_client: AsyncClient) -> None:
    """Soft-deleted items should not appear in LIST."""
    resp = await sd_client.post("/articles/", json={"title": "Keep"})
    assert resp.status_code == 201
    resp = await sd_client.post("/articles/", json={"title": "Remove"})
    assert resp.status_code == 201
    remove_id = resp.json()["id"]

    # Delete one
    resp = await sd_client.delete(f"/articles/{remove_id}")
    assert resp.status_code == 204

    # List should only show "Keep"
    resp = await sd_client.get("/articles/")
    titles = [a["title"] for a in resp.json()]
    assert "Keep" in titles
    assert "Remove" not in titles


async def test_soft_delete_hides_from_read(sd_client: AsyncClient) -> None:
    """Soft-deleted items should return 404 on GET."""
    resp = await sd_client.post("/articles/", json={"title": "Gone"})
    article_id = resp.json()["id"]

    await sd_client.delete(f"/articles/{article_id}")

    resp = await sd_client.get(f"/articles/{article_id}")
    assert resp.status_code == 404


async def test_soft_delete_hides_from_update(sd_client: AsyncClient) -> None:
    """Soft-deleted items should return 404 on PATCH."""
    resp = await sd_client.post("/articles/", json={"title": "Gone"})
    article_id = resp.json()["id"]

    await sd_client.delete(f"/articles/{article_id}")

    resp = await sd_client.patch(f"/articles/{article_id}", json={"title": "New"})
    assert resp.status_code == 404


class SoftBook(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    deleted_at: str | None = None


async def test_soft_delete_read_returns_404(
    get_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> None:
    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(SoftBook, get_session)
        .with_soft_delete(SoftDeleteConfig())
        .with_operations({Operation.CREATE, Operation.READ, Operation.DELETE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/softbooks/", json={"title": "T"})
        book_id = resp.json()["id"]
        await c.delete(f"/softbooks/{book_id}")
        resp = await c.get(f"/softbooks/{book_id}")
        assert resp.status_code == 404


async def test_soft_delete_update_returns_404(
    get_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> None:
    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(SoftBook, get_session)
        .with_soft_delete(SoftDeleteConfig())
        .with_operations({Operation.CREATE, Operation.UPDATE, Operation.DELETE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/softbooks/", json={"title": "T"})
        book_id = resp.json()["id"]
        await c.delete(f"/softbooks/{book_id}")
        resp = await c.patch(f"/softbooks/{book_id}", json={"title": "Updated"})
        assert resp.status_code == 404


async def test_soft_delete_list_excludes(
    get_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> None:
    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(SoftBook, get_session)
        .with_soft_delete(SoftDeleteConfig())
        .with_operations({Operation.CREATE, Operation.LIST, Operation.DELETE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/softbooks/", json={"title": "T"})
        book_id = resp.json()["id"]
        await c.delete(f"/softbooks/{book_id}")
        resp = await c.get("/softbooks/")
        assert resp.status_code == 200
        assert resp.json() == []


async def test_bulk_delete_soft(
    get_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> None:
    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(SoftBook, get_session)
        .with_soft_delete(SoftDeleteConfig())
        .with_operations(
            {
                Operation.CREATE,
                Operation.READ,
                Operation.BULK_DELETE,
            }
        )
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/softbooks/", json={"title": "T"})
        book_id = resp.json()["id"]
        resp = await c.request("DELETE", "/softbooks/bulk", json=[book_id])
        assert resp.status_code == 204
        resp = await c.get(f"/softbooks/{book_id}")
        assert resp.status_code == 404


async def test_bulk_update_soft_deleted(
    get_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> None:
    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(SoftBook, get_session)
        .with_soft_delete(SoftDeleteConfig())
        .with_operations(
            {
                Operation.CREATE,
                Operation.DELETE,
                Operation.BULK_UPDATE,
            }
        )
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.post("/softbooks/", json={"title": "T"})
        book_id = resp.json()["id"]
        await c.delete(f"/softbooks/{book_id}")
        resp = await c.patch("/softbooks/bulk", json=[{"id": book_id, "title": "New"}])
        assert resp.status_code == 404


async def test_bulk_delete_not_found_soft(
    get_session: Callable[[], AsyncGenerator[AsyncSession, None]],
) -> None:
    app = FastAPI()
    app.include_router(
        CrudRouterBuilder.for_model(SoftBook, get_session)
        .with_soft_delete(SoftDeleteConfig())
        .with_operations({Operation.BULK_DELETE})
        .build()
    )
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        resp = await c.request("DELETE", "/softbooks/bulk", json=[9999])
        assert resp.status_code == 404
