"""Shared test fixtures.

The ASGI app is driven in-process via httpx + asgi-lifespan (no network, no
running server). DB/Redis-backed integration fixtures arrive with the data
layer (plan §11 step 2).

DB safety
---------
Integration fixtures call ``Base.metadata.drop_all`` / ``create_all`` on
``get_engine()``. That engine is built from ``DATABASE_URL``, which in normal
runs points at the *developer's* database — ``localhost:5432/mcma`` for host
``pytest`` and ``db:5432/mcma`` for ``make test-api`` (which execs ``pytest``
inside the api container). Running the suite there silently wipes real data:
``drop_all`` removes every ORM table while leaving Alembic's ``alembic_version``
(it lives outside ``Base.metadata``) — the exact "tables keep disappearing,
version survives" symptom.

To make that impossible, this module redirects every test to a dedicated
``<db>_test`` database *before settings/engine load*, and creates it on demand.
The real dev database is never opened by the test suite.
"""

import asyncio
import os
from collections.abc import AsyncIterator
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit

import pytest
from asgi_lifespan import LifespanManager
from httpx import ASGITransport, AsyncClient

# Force a test-safe environment before settings load.
os.environ.setdefault("ENVIRONMENT", "test")
os.environ.setdefault("JWT_SECRET", "test-secret")


def _base_database_url() -> str:
    """Resolve the DB URL the app *would* use, mirroring pydantic-settings
    precedence: real env var → ``.env`` file → the app's compiled-in default."""
    if env := os.environ.get("DATABASE_URL"):
        return env
    dotenv = Path(__file__).resolve().parents[1] / ".env"
    if dotenv.exists():
        for raw in dotenv.read_text().splitlines():
            line = raw.strip()
            if line.startswith("DATABASE_URL=") and not line.startswith("#"):
                return line.split("=", 1)[1].strip().strip("\"'")
    return "postgresql+asyncpg://mcma:mcma@localhost:5432/mcma"


def _with_database(url: str, name: str) -> str:
    """Return ``url`` with its database name swapped for ``name``."""
    return urlunsplit(urlsplit(url)._replace(path=f"/{name}"))


_BASE_DB_URL = _base_database_url()
_BASE_DB_NAME = urlsplit(_BASE_DB_URL).path.lstrip("/")
# Idempotent: if we're already pointed at a *_test DB, keep it as-is.
_TEST_DB_NAME = _BASE_DB_NAME if _BASE_DB_NAME.endswith("_test") else f"{_BASE_DB_NAME}_test"
_TEST_DB_URL = _with_database(_BASE_DB_URL, _TEST_DB_NAME)

# Redirect the whole suite to the test DB before anything reads settings.
os.environ["DATABASE_URL"] = _TEST_DB_URL


async def _create_test_db_if_missing() -> None:
    """Create ``<db>_test`` if the server is reachable. Best-effort: if Postgres
    is down the integration fixtures skip on their own reachability probe, so a
    failure here must stay silent rather than break unit-only runs."""
    import asyncpg  # type: ignore[import-untyped]  # driver behind postgresql+asyncpg

    # asyncpg wants a plain libpq DSN (no SQLAlchemy "+asyncpg" suffix), against
    # the always-present ``postgres`` maintenance database.
    dsn = _with_database(_TEST_DB_URL, "postgres").replace("+asyncpg", "")
    try:
        async with asyncio.timeout(5):
            conn = await asyncpg.connect(dsn)
    except Exception:
        return
    try:
        exists = await conn.fetchval(
            "SELECT 1 FROM pg_database WHERE datname = $1", _TEST_DB_NAME
        )
        if not exists:
            # CREATE DATABASE can't run inside a transaction; asyncpg's implicit
            # autocommit on a bare connection handles that.
            await conn.execute(f'CREATE DATABASE "{_TEST_DB_NAME}"')
    finally:
        await conn.close()


@pytest.fixture(scope="session", autouse=True)
def _ensure_test_database() -> None:
    """Guarantee the dedicated test database exists once per session."""
    asyncio.run(_create_test_db_if_missing())


@pytest.fixture
async def client() -> AsyncIterator[AsyncClient]:
    from app.main import create_app

    app = create_app()
    async with LifespanManager(app):
        transport = ASGITransport(app=app)
        async with AsyncClient(transport=transport, base_url="http://test") as ac:
            yield ac
