"""Tests for ``mgf.fastapi.db`` — get_session + setup_db.

Carried over from mgf-common's tests/unit/db/test_session.py and
test_db_lifespan.py at the v0.30 federation paired-extraction.

The non-FastAPI portions of those test files moved to
mgf-sqlalchemy/tests/unit/. Here we pin the FastAPI-Depends and
FastAPI-lifespan integration shapes.
"""

from __future__ import annotations

from contextlib import asynccontextmanager
from typing import Any

import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from mgf.common.exceptions import AppConfigError
from mgf.sqlalchemy import create_engine, create_sessionmaker
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker

from mgf.fastapi.db import get_session, setup_db

# ---------------------------------------------------------------------------
# get_session — FastAPI Depends generator (direct calls)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
class TestGetSessionDepDirect:
    """Direct calls to get_session as an async generator."""

    async def test_yields_session_when_sessionmaker_present(self) -> None:
        engine = create_engine("sqlite+aiosqlite:///:memory:")
        sm = create_sessionmaker(engine)

        # Mock a Request with the right state.
        class _FakeApp:
            class state:  # noqa: N801 — must match FastAPI's request.app.state name
                mgf_db_sessionmaker = sm

        class _FakeRequest:
            app = _FakeApp()

        try:
            gen = get_session(_FakeRequest())  # type: ignore[arg-type]
            session = await gen.__anext__()
            try:
                assert isinstance(session, AsyncSession)
                # Session is usable.
                row = (await session.execute(text("SELECT 1"))).scalar_one()
                assert row == 1
            finally:
                # Close the generator (drains the finally in get_session).
                with pytest.raises(StopAsyncIteration):
                    await gen.__anext__()
        finally:
            await engine.dispose()

    async def test_raises_appconfigerror_when_sessionmaker_missing(self) -> None:
        class _FakeApp:
            class state:  # noqa: N801 — must match FastAPI's request.app.state name
                pass  # no mgf_db_sessionmaker attribute

        class _FakeRequest:
            app = _FakeApp()

        gen = get_session(_FakeRequest())  # type: ignore[arg-type]
        with pytest.raises(AppConfigError, match="setup_db"):
            await gen.__anext__()


# ---------------------------------------------------------------------------
# get_session — end-to-end with FastAPI + setup_db
# ---------------------------------------------------------------------------


class TestGetSessionDepViaFastAPI:
    """get_session integrated end-to-end with FastAPI + setup_db."""

    def test_fastapi_app_with_setup_db(self) -> None:
        @asynccontextmanager
        async def _lifespan(app: FastAPI):
            async with setup_db(app, database_url="sqlite+aiosqlite:///:memory:"):
                yield

        app = FastAPI(lifespan=_lifespan)

        @app.get("/check")
        async def _check(session: Any = Depends(get_session)) -> dict[str, int]:
            row = (await session.execute(text("SELECT 42"))).scalar_one()
            return {"answer": row}

        with TestClient(app) as client:
            response = client.get("/check")
        assert response.status_code == 200
        assert response.json() == {"answer": 42}


# ---------------------------------------------------------------------------
# setup_db lifespan
# ---------------------------------------------------------------------------


class TestSetupDb:
    def test_stashes_engine_and_sessionmaker_on_app_state(self) -> None:
        @asynccontextmanager
        async def _lifespan(app: FastAPI):
            async with setup_db(app, database_url="sqlite+aiosqlite:///:memory:"):
                yield

        app = FastAPI(lifespan=_lifespan)
        with TestClient(app):
            assert isinstance(app.state.mgf_db_engine, AsyncEngine)
            assert isinstance(app.state.mgf_db_sessionmaker, async_sessionmaker)

    def test_engine_disposed_on_lifespan_exit(self) -> None:
        captured_engine: AsyncEngine | None = None

        @asynccontextmanager
        async def _lifespan(app: FastAPI):
            async with setup_db(app, database_url="sqlite+aiosqlite:///:memory:"):
                nonlocal captured_engine
                captured_engine = app.state.mgf_db_engine
                yield

        app = FastAPI(lifespan=_lifespan)
        with TestClient(app):
            pass

        # After lifespan exits, the engine has been disposed.
        # (SQLAlchemy doesn't expose a public "is disposed" boolean,
        # but the engine should be present in our captured ref.)
        assert captured_engine is not None

    def test_extra_engine_kwargs_forwarded(self) -> None:
        @asynccontextmanager
        async def _lifespan(app: FastAPI):
            async with setup_db(
                app,
                database_url="sqlite+aiosqlite:///:memory:",
                echo=True,
            ):
                yield

        app = FastAPI(lifespan=_lifespan)
        with TestClient(app):
            assert app.state.mgf_db_engine.echo is True
