"""Shared test fixtures for the CMP API test suite.

Provides two modes:
  - Unit tests: use `app` and `client` fixtures (no database required)
  - Integration tests: use `db_client` fixture (requires PostgreSQL)

Integration tests are automatically skipped when no database is available.
"""

import os

# Disable rate limiting for the test suite. Many tests make dozens of
# requests from the same loopback address in rapid succession and the
# middleware would legitimately reject them as a DoS; the middleware
# has its own dedicated test module.
os.environ.setdefault("RATE_LIMIT_ENABLED", "false")
os.environ.setdefault("ENVIRONMENT", "test")

import uuid

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from src.main import create_app
from src.models.base import Base

# ── Detect whether a test database is available ──────────────────────

_TEST_DB_URL = os.environ.get(
    "TEST_DATABASE_URL",
    os.environ.get("DATABASE_URL", ""),
)

_HAS_DB = bool(_TEST_DB_URL) and "localhost" in _TEST_DB_URL


def _requires_db(fn):
    """Mark a test as requiring a live database.

    Also pins the event loop to session scope so that fixtures sharing the
    session-scoped engine don't get 'Future attached to a different loop'.
    """
    fn = pytest.mark.asyncio(loop_scope="session")(fn)
    fn = pytest.mark.skipif(not _HAS_DB, reason="No test database available")(fn)
    return fn


requires_db = _requires_db


# ── Unit test fixtures (no database) ─────────────────────────────────


@pytest.fixture
def app():
    """Create a fresh FastAPI application instance."""
    return create_app()


@pytest.fixture
async def client(app):
    """Async HTTP client for unit tests (no database)."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac


# ── Integration test fixtures (with database) ────────────────────────


@pytest.fixture(scope="session")
def _test_engine():
    """Create a test database engine (session-scoped)."""
    if not _HAS_DB:
        pytest.skip("No test database available")
    return create_async_engine(_TEST_DB_URL, echo=False)


@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def _setup_db(_test_engine):
    """Create all tables once per test session, then seed fixture data.

    Tests that depend on the cookie-category seed (normally applied by
    the ``0001_initial_schema`` alembic migration) get the same rows
    here so they can run without invoking alembic.
    """
    async with _test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
        await _seed_cookie_categories(conn)
    yield
    async with _test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)


async def _seed_cookie_categories(conn) -> None:
    """Insert the default cookie categories. Mirrors migration 0001."""
    import uuid as _uuid

    from sqlalchemy import text

    rows = [
        ("10000000-0000-0000-0000-000000000001", "Necessary", "necessary", True, 0),
        ("10000000-0000-0000-0000-000000000002", "Functional", "functional", False, 1),
        ("10000000-0000-0000-0000-000000000003", "Analytics", "analytics", False, 2),
        ("10000000-0000-0000-0000-000000000004", "Marketing", "marketing", False, 3),
        ("10000000-0000-0000-0000-000000000005", "Personalisation", "personalisation", False, 4),
    ]
    stmt = text(
        """
        INSERT INTO cookie_categories
            (id, name, slug, description, is_essential, display_order)
        VALUES (:id, :name, :slug, :description, :is_essential, :display_order)
        ON CONFLICT (slug) DO NOTHING
        """,
    )
    for row_id, name, slug, is_essential, order in rows:
        await conn.execute(
            stmt,
            {
                "id": _uuid.UUID(row_id),
                "name": name,
                "slug": slug,
                "description": f"{name} cookies",
                "is_essential": is_essential,
                "display_order": order,
            },
        )


@pytest_asyncio.fixture(loop_scope="session")
async def db_client(_test_engine, _setup_db):
    """Async HTTP client where each route handler gets its own DB session.

    Each request gets an independent session/connection so there are no
    'another operation is in progress' errors from asyncpg.
    """
    from src.db import get_db

    app = create_app()

    async def _override_get_db():
        async with AsyncSession(_test_engine, expire_on_commit=False) as session:
            try:
                yield session
                await session.commit()
            except Exception:
                await session.rollback()
                raise

    app.dependency_overrides[get_db] = _override_get_db

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


# ── Auth helper fixtures ─────────────────────────────────────────────


@pytest_asyncio.fixture(loop_scope="session")
async def test_org(_test_engine, _setup_db):
    """Create a test organisation in the database."""
    from src.models.organisation import Organisation

    async with AsyncSession(_test_engine, expire_on_commit=False) as session:
        org = Organisation(
            id=uuid.uuid4(),
            name="Test Organisation",
            slug=f"test-org-{uuid.uuid4().hex[:8]}",
        )
        session.add(org)
        await session.commit()
        return org


@pytest_asyncio.fixture(loop_scope="session")
async def test_user(_test_engine, _setup_db, test_org):
    """Create a test user (owner role) with a known password."""
    from src.models.user import User
    from src.services.auth import hash_password

    async with AsyncSession(_test_engine, expire_on_commit=False) as session:
        user = User(
            id=uuid.uuid4(),
            email=f"admin-{uuid.uuid4().hex[:8]}@test.com",
            password_hash=hash_password("TestPassword123"),
            full_name="Test Admin",
            role="owner",
            organisation_id=test_org.id,
        )
        session.add(user)
        await session.commit()
        return user


@pytest_asyncio.fixture(loop_scope="session")
async def auth_token(test_user):
    """Generate a valid JWT token for the test user."""
    from src.services.auth import create_access_token

    return create_access_token(
        user_id=str(test_user.id),
        organisation_id=str(test_user.organisation_id),
        role=test_user.role,
        email=test_user.email,
    )


@pytest_asyncio.fixture(loop_scope="session")
async def auth_headers(auth_token):
    """HTTP headers with a valid Bearer token."""
    return {"Authorization": f"Bearer {auth_token}"}


# ── Shared helper for creating sites in integration tests ────────────


async def create_test_site(
    client: AsyncClient,
    headers: dict,
    *,
    domain_prefix: str = "test",
    display_name: str = "Test Site",
) -> str:
    """Create a site via the API and return its ID.

    This is a helper function (not a fixture) so it can be called
    inline within each test, avoiding async fixture event-loop issues.
    """
    resp = await client.post(
        "/api/v1/sites/",
        json={
            "domain": f"{domain_prefix}-{uuid.uuid4().hex[:8]}.com",
            "display_name": display_name,
        },
        headers=headers,
    )
    assert resp.status_code == 201, f"Failed to create test site: {resp.text}"
    return resp.json()["id"]
