import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path

import httpx
import pytest
from alembic import command as alembic_command
from alembic.config import Config as AlembicConfig
from fastapi import FastAPI
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.core.config import Settings
from app.core.db import Base
from app.integrations.registry import AdapterRegistry
from app.main import create_app
from tests.fakes import FakeHelixtaHR, FakeLiveKitAdapter, make_helixtahr_adapter
from tests.tokens import meeting_token

ROOT = Path(__file__).resolve().parent.parent

SESSION_SECRET = "test-session-secret-" + "0123456789abcdef" * 3

# The local test database from README (never the dev one); override with TEST_DATABASE_URL.
TEST_DATABASE_URL = os.environ.get(
    "TEST_DATABASE_URL",
    "postgresql+asyncpg://livekit_meetings:livekit_meetings@localhost:5432/livekit_meetings_test",
)


def make_settings(**overrides: object) -> Settings:
    values: dict[str, object] = {
        "environment": "local",
        "database_url": TEST_DATABASE_URL,
        "livekit_url": "ws://livekit.test",
        "livekit_api_url": "http://livekit.test",
        "livekit_api_key": "test-key",
        "livekit_api_secret": "test-secret",
        "enabled_products": ["helixtahr"],
        "session_secret": SESSION_SECRET,
    }
    values.update(overrides)
    # _env_file=None: tests never read the developer's .env.
    return Settings(_env_file=None, **values)


def make_app(
    settings: Settings | None = None,
    livekit: FakeLiveKitAdapter | None = None,
    helixtahr: FakeHelixtaHR | None = None,
) -> FastAPI:
    """The app with test settings, a fake LiveKit and a fake HelixtaHR (test secrets)."""
    products = AdapterRegistry([make_helixtahr_adapter(helixtahr)])
    return create_app(
        settings or make_settings(),
        livekit_adapter=livekit or FakeLiveKitAdapter(),
        products=products,
    )


@asynccontextmanager
async def running(app: FastAPI) -> AsyncIterator[httpx.AsyncClient]:
    """Run the app's lifespan and give a client that talks to it in-process."""
    async with app.router.lifespan_context(app):
        transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            yield client


@pytest.fixture(scope="session")
def migrated_db() -> None:
    """Bring the test database to the latest migration once per run (down first, so
    every run also proves the migrations can be undone)."""
    config = AlembicConfig()
    config.set_main_option("script_location", str(ROOT / "migrations"))
    with pytest.MonkeyPatch.context() as patch:
        # migrations/env.py reads DATABASE_URL.
        patch.setenv("DATABASE_URL", TEST_DATABASE_URL)
        alembic_command.downgrade(config, "base")
        alembic_command.upgrade(config, "head")


@pytest.fixture
async def db(migrated_db: None) -> AsyncIterator[AsyncSession]:
    """A session on the test database; all tables are emptied afterwards."""
    engine = create_async_engine(TEST_DATABASE_URL)
    try:
        async with async_sessionmaker(engine, expire_on_commit=False)() as session:
            yield session
        async with engine.begin() as conn:
            tables = ", ".join(table.name for table in Base.metadata.sorted_tables)
            await conn.execute(text(f"TRUNCATE {tables} CASCADE"))
    finally:
        await engine.dispose()


async def session_headers(client: httpx.AsyncClient, **claims: object) -> dict[str, str]:
    """Open Meetings as a test user (claims override the test token) and return auth headers."""
    response = await client.post(
        "/v1/session", headers={"Authorization": f"Bearer {meeting_token(**claims)}"}
    )
    assert response.status_code == 200, response.text
    return {"Authorization": f"Bearer {response.json()['access_token']}"}


@pytest.fixture
def fake_livekit() -> FakeLiveKitAdapter:
    return FakeLiveKitAdapter()


@pytest.fixture
def fake_helixtahr() -> FakeHelixtaHR:
    return FakeHelixtaHR()


@pytest.fixture
async def client(
    fake_livekit: FakeLiveKitAdapter, fake_helixtahr: FakeHelixtaHR
) -> AsyncIterator[httpx.AsyncClient]:
    app = make_app(livekit=fake_livekit, helixtahr=fake_helixtahr)
    async with running(app) as c:
        yield c
