"""
Pytest configuration and shared fixtures.

Fixtures defined here are available to all tests without importing.
"""

import asyncio
import os
from collections.abc import AsyncGenerator
from urllib.parse import urlparse

import asyncpg
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool

os.environ["APP_ENV"] = "testing"
# Automated tests must never dispatch email using credentials from a local .env.
os.environ["SMTP_USER"] = ""
os.environ["SMTP_PASSWORD"] = ""
os.environ["SENTRY_DSN"] = ""

from app.config import settings
from app.core import state_store
from app.core.dependencies import get_db
from app.database import get_db as db_get_db
from app.main import app
from app.models.base import Base


# ── Test Database ──────────────────────────────────────────────────────────────
def _resolve_test_database_url() -> str:
    env_test_url = os.environ.get("TEST_DATABASE_URL")
    if env_test_url:
        if not urlparse(env_test_url).path.rstrip("/").endswith("_test"):
            raise ValueError("TEST_DATABASE_URL must point to a database ending in _test.")
        return env_test_url

    url = settings.DATABASE_URL
    if url.endswith("_test"):
        return url
    if url.endswith("/munal_lms"):
        return url[: -len("/munal_lms")] + "/munal_lms_test"

    parsed = urlparse(url)
    db_name = parsed.path.lstrip("/")
    if not db_name.endswith("_test"):
        return f"{url.rsplit('/', 1)[0]}/{db_name}_test"
    return url


TEST_DATABASE_URL = _resolve_test_database_url()


async def _clear_test_tables() -> None:
    """Clear the dedicated test database in one atomic round trip."""
    preparer = test_engine.dialect.identifier_preparer
    tables = ", ".join(preparer.format_table(table) for table in Base.metadata.sorted_tables)
    async with test_engine.begin() as conn:
        await conn.execute(text(f"TRUNCATE TABLE {tables} CASCADE"))


test_engine = create_async_engine(TEST_DATABASE_URL, poolclass=NullPool, echo=False)
TestSessionLocal = async_sessionmaker(
    bind=test_engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autoflush=False,
)


@pytest.fixture(autouse=True)
def fast_test_password_hashes(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -> None:
    """Keep real bcrypt verification while reducing cost only in tests."""
    if request.path.name == "test_security.py":
        return

    from app.core import security

    original_gensalt = security.bcrypt.gensalt
    monkeypatch.setattr(security.bcrypt, "gensalt", lambda: original_gensalt(rounds=4))


@pytest.fixture(autouse=True)
def use_test_state_database(monkeypatch: pytest.MonkeyPatch) -> None:
    """Session/OTP state must use the same isolated database as request data."""
    from app import database

    monkeypatch.setattr(state_store, "AsyncSessionLocal", TestSessionLocal)
    monkeypatch.setattr(database, "AsyncSessionLocal", TestSessionLocal)
    monkeypatch.setattr(settings, "DATABASE_URL", TEST_DATABASE_URL)


@pytest.fixture(scope="session", autouse=True)
def setup_test_database() -> None:
    """Ensure test database and all tables exist."""

    async def _init() -> None:
        parsed = urlparse(TEST_DATABASE_URL)
        test_db_name = parsed.path.lstrip("/")
        test_dsn = TEST_DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")

        try:
            conn = await asyncpg.connect(test_dsn)
            await conn.close()
        except asyncpg.InvalidCatalogNameError:
            admin_dsn = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
            if admin_dsn == test_dsn:
                admin_dsn = admin_dsn.rsplit("/", 1)[0] + "/postgres"
            conn = await asyncpg.connect(admin_dsn)
            try:
                exists = await conn.fetchval("SELECT 1 FROM pg_database WHERE datname = $1", test_db_name)
                if not exists:
                    await conn.execute(f'CREATE DATABASE "{test_db_name}"')
            finally:
                await conn.close()

        async with test_engine.begin() as conn_engine:
            await conn_engine.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm"))
            await conn_engine.run_sync(Base.metadata.create_all)
        await _clear_test_tables()

    asyncio.run(_init())


# ── Session & Cleanup Fixtures ────────────────────────────────────────────────
@pytest_asyncio.fixture(autouse=True)
async def clean_db() -> AsyncGenerator[None, None]:
    """Clean up table records between tests for test isolation."""
    yield
    await _clear_test_tables()


@pytest_asyncio.fixture
async def db_session() -> AsyncGenerator[AsyncSession, None]:
    """Provides a dedicated database session for creating test fixtures."""
    async with TestSessionLocal() as session:
        yield session
        await session.commit()


# ── HTTP Client Fixture ────────────────────────────────────────────────────────
@pytest_asyncio.fixture
async def client() -> AsyncGenerator[AsyncClient, None]:
    """
    Provides an async HTTP test client.

    Overrides the get_db dependency to yield sessions from the test engine.
    """

    async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
        async with TestSessionLocal() as session:
            try:
                yield session
                await session.commit()
            except Exception:
                await session.rollback()
                raise

    app.dependency_overrides[get_db] = override_get_db
    app.dependency_overrides[db_get_db] = override_get_db

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

    app.dependency_overrides.clear()
