"""The RLS-bite test: prove ``tenant_session`` ACTUALLY isolates tenants
against a real Postgres, executed as a **NOSUPERUSER** role.

Why this lives here (and not only in a consumer)
------------------------------------------------
``mgf.sqlalchemy.tenant_session`` IS the RLS-scoping mechanic — it emits
``SET LOCAL app.current_tenant = '<uuid>'`` so a Postgres row-level-
security policy keyed on ``current_setting('app.current_tenant')`` filters
every query to the bound tenant. The library ships that mechanic, so the
library must own the proof that it bites. Until now the only behavioural
proof lived in a consumer (PlasmaMapper's
``tests/integration/test_rls_bite.py``); the unit suite only mocks the
session and asserts the *SQL string*, never that a real policy filters
real rows.

The NOSUPERUSER requirement is load-bearing
--------------------------------------------
Per PostgreSQL RLS semantics a **SUPERUSER** — and the **table owner**,
unless ``FORCE ROW LEVEL SECURITY`` is set — BYPASSES row-level security
entirely. The dev ``plasma_app`` role on the :5433 stack is a superuser,
so a proof run as ``plasma_app`` would pass even if ``tenant_session``
were a no-op. This test therefore:

  * creates a dedicated **NOSUPERUSER** login role,
  * connects a *separate engine as that role*, and
  * runs the bite assertions through ``tenant_session`` on that engine.

It also sets ``FORCE ROW LEVEL SECURITY`` so even owner-side reads obey
the policy (belt-and-braces; the NOSUPERUSER role is not the owner here
anyway).

Hermeticity
-----------
Everything is created inside a uniquely-named schema and a uniquely-named
role, both torn down in a ``finally``. Nothing touches shared tables. If
asyncpg is absent or Postgres@5433 is unreachable, the module
``pytest.skip``s so the default (hermetic) suite stays green without the
dev stack. The policy DDL mirrors PlasmaMapper migration 0001/0015:
``tenant_id = current_setting('app.current_tenant', true)::uuid``.
"""

from __future__ import annotations

import os
import uuid
from typing import TYPE_CHECKING

import pytest
import pytest_asyncio

from mgf.sqlalchemy import create_sessionmaker, tenant_session

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

pytestmark = pytest.mark.integration

# asyncpg is an integration-only dependency (the library's test extras
# ship aiosqlite for the hermetic suite). Skip the whole module if the
# driver isn't installed rather than erroring at collection.
pytest.importorskip(
    "sqlalchemy.ext.asyncio",
    reason="SQLAlchemy asyncio extras required for the Postgres RLS test",
)
pytest.importorskip(
    "asyncpg",
    reason="asyncpg required for the Postgres RLS integration test",
)

from sqlalchemy import text  # noqa: E402  (after importorskip guard)
from sqlalchemy.engine.url import URL, make_url  # noqa: E402
from sqlalchemy.ext.asyncio import create_async_engine  # noqa: E402

# Superuser/admin URL used only for schema + role provisioning and seed.
# Mirrors PlasmaMapper's dev-stack convention; override via env.
_ADMIN_URL = os.environ.get(
    "MGF_SQLALCHEMY_TEST_DATABASE_URL",
    os.environ.get(
        "PLASMA_TEST_DATABASE_URL",
        "postgresql+asyncpg://plasma_app:plasma_app@localhost:5433/plasmamapper",
    ),
)

# Per-run-unique identifiers so concurrent runs / leftover state never
# collide and teardown never touches anything it didn't create. SQL
# identifiers can't be parameterised, but these are hex-only constants
# we generate ourselves — no injection surface.
_RUN = uuid.uuid4().hex[:12]
_SCHEMA = f"mgf_rls_test_{_RUN}"
_TABLE = "tenant_data"
_QUALIFIED = f"{_SCHEMA}.{_TABLE}"
_ROLE = f"mgf_rls_role_{_RUN}"
_ROLE_PW = "mgf_rls_test_pw"  # ephemeral local-only test role


def _nosuper_url() -> URL:
    """Build the NOSUPERUSER login URL from the admin URL's host/port/db.

    Returns a SQLAlchemy ``URL`` object (NOT a string): ``str(url)`` masks
    the password as ``***``, and feeding that masked string back into
    ``create_async_engine`` would authenticate with the literal ``***``.
    Passing the ``URL`` object preserves the real password without ever
    rendering it.
    """
    admin = make_url(_ADMIN_URL)
    return admin.set(username=_ROLE, password=_ROLE_PW).set(drivername="postgresql+asyncpg")


async def _can_reach_admin() -> bool:
    eng = create_async_engine(_ADMIN_URL)
    try:
        async with eng.connect() as conn:
            await conn.execute(text("SELECT 1"))
        return True
    except Exception:
        return False
    finally:
        await eng.dispose()


@pytest_asyncio.fixture
async def rls_fixture() -> AsyncIterator[tuple[URL, uuid.UUID, uuid.UUID]]:
    """Provision an isolated schema + RLS-policied table + NOSUPERUSER
    role; seed two tenants' rows; yield ``(nosuper_url, tenant_a, tenant_b)``.

    Skips if the admin DB is unreachable. Tears down the schema + role
    unconditionally so the run leaves zero residue.
    """
    if not await _can_reach_admin():
        pytest.skip(f"Postgres unreachable at {_ADMIN_URL!r}; skipping RLS bite test")

    tenant_a = uuid.uuid4()
    tenant_b = uuid.uuid4()
    admin = create_async_engine(_ADMIN_URL)
    try:
        async with admin.begin() as conn:
            # NOSUPERUSER is the load-bearing attribute: without it the
            # role would BYPASS RLS and the proof would be vacuous.
            await conn.execute(
                text(
                    f"CREATE ROLE {_ROLE} LOGIN PASSWORD '{_ROLE_PW}' "
                    f"NOSUPERUSER NOINHERIT NOCREATEDB NOCREATEROLE"
                )
            )
            await conn.execute(text(f"CREATE SCHEMA {_SCHEMA}"))
            await conn.execute(
                text(
                    f"CREATE TABLE {_QUALIFIED} ("
                    f"  id uuid PRIMARY KEY,"
                    f"  tenant_id uuid NOT NULL,"
                    f"  label text NOT NULL"
                    f")"
                )
            )
            # Mirror PlasmaMapper migration 0001 / 0015 policy shape.
            await conn.execute(text(f"ALTER TABLE {_QUALIFIED} ENABLE ROW LEVEL SECURITY"))
            await conn.execute(text(f"ALTER TABLE {_QUALIFIED} FORCE ROW LEVEL SECURITY"))
            await conn.execute(
                text(
                    f"CREATE POLICY tenant_isolation ON {_QUALIFIED} "
                    f"  FOR ALL "
                    f"  USING (tenant_id = current_setting('app.current_tenant', true)::uuid) "
                    f"  WITH CHECK (tenant_id = current_setting('app.current_tenant', true)::uuid)"
                )
            )
            # The NOSUPERUSER role needs schema USAGE + table privileges.
            # It is NOT the table owner (admin role owns it), so RLS
            # applies to it without relying on FORCE — but we set FORCE
            # anyway for defence in depth.
            await conn.execute(text(f"GRANT USAGE ON SCHEMA {_SCHEMA} TO {_ROLE}"))
            await conn.execute(text(f"GRANT SELECT, INSERT ON {_QUALIFIED} TO {_ROLE}"))
            # Seed two rows per tenant as the admin (RLS-bypassing) role.
            for t_id, n in ((tenant_a, 2), (tenant_b, 3)):
                for i in range(n):
                    await conn.execute(
                        text(
                            f"INSERT INTO {_QUALIFIED} (id, tenant_id, label) "
                            f"VALUES (:id, :t, :label)"
                        ),
                        {"id": uuid.uuid4(), "t": t_id, "label": f"row-{i}"},
                    )

        yield _nosuper_url(), tenant_a, tenant_b
    finally:
        async with admin.begin() as conn:
            await conn.execute(text(f"DROP SCHEMA IF EXISTS {_SCHEMA} CASCADE"))
            await conn.execute(text(f"DROP ROLE IF EXISTS {_ROLE}"))
        await admin.dispose()


def _count_visible(rows: list[tuple[object, ...]], tenant: uuid.UUID) -> int:
    return sum(1 for r in rows if r[1] == tenant)


@pytest.mark.asyncio
async def test_tenant_session_isolates_as_nosuperuser(
    rls_fixture: tuple[URL, uuid.UUID, uuid.UUID],
) -> None:
    """THE BITE TEST. As a NOSUPERUSER role, ``tenant_session`` scoped to
    tenant A must surface ONLY tenant A's rows and ZERO of tenant B's,
    and vice-versa. A leak here is a real security bug in the library."""
    nosuper_url, tenant_a, tenant_b = rls_fixture

    engine = create_async_engine(nosuper_url)
    sessionmaker = create_sessionmaker(engine)
    try:
        # Sanity: confirm the connection really is NOT a superuser, so a
        # green result can't be a false pass from RLS bypass.
        async with sessionmaker() as session:
            is_super = (
                await session.execute(
                    text("SELECT rolsuper FROM pg_roles WHERE rolname = current_user")
                )
            ).scalar()
        assert is_super is False, (
            "test role is a superuser — RLS would be bypassed and the "
            "proof would be vacuous. Fix the fixture's NOSUPERUSER role."
        )

        # Scoped to tenant A.
        async with (
            sessionmaker() as session,
            tenant_session(session, tenant_a) as scoped,
        ):
            result = await scoped.execute(
                text(f"SELECT id, tenant_id FROM {_QUALIFIED} ORDER BY id")
            )
            rows_a = [tuple(r) for r in result.fetchall()]
        visible_a = {r[1] for r in rows_a}
        assert tenant_a in visible_a, "tenant A must see its own rows"
        assert tenant_b not in visible_a, (
            f"LEAK: tenant A saw tenant B's rows — tenant_session does NOT "
            f"isolate. visible tenant_ids={visible_a}"
        )
        assert _count_visible(rows_a, tenant_a) == 2
        assert _count_visible(rows_a, tenant_b) == 0

        # Scoped to tenant B — the reverse.
        async with (
            sessionmaker() as session,
            tenant_session(session, tenant_b) as scoped,
        ):
            result = await scoped.execute(
                text(f"SELECT id, tenant_id FROM {_QUALIFIED} ORDER BY id")
            )
            rows_b = [tuple(r) for r in result.fetchall()]
        visible_b = {r[1] for r in rows_b}
        assert tenant_b in visible_b, "tenant B must see its own rows"
        assert tenant_a not in visible_b, (
            f"LEAK: tenant B saw tenant A's rows — tenant_session does NOT "
            f"isolate. visible tenant_ids={visible_b}"
        )
        assert _count_visible(rows_b, tenant_b) == 3
        assert _count_visible(rows_b, tenant_a) == 0
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_cross_tenant_direct_lookup_returns_empty_as_nosuperuser(
    rls_fixture: tuple[URL, uuid.UUID, uuid.UUID],
) -> None:
    """Scoped to tenant A, a direct lookup of a KNOWN tenant-B row id
    returns empty (silent filter, not an error) — proves the policy
    filters per-row, not just via the ORDER BY scan above."""
    nosuper_url, tenant_a, tenant_b = rls_fixture

    # Find a tenant-B id via the RLS-bypassing admin connection.
    admin = create_async_engine(_ADMIN_URL)
    try:
        async with admin.connect() as conn:
            b_id = (
                await conn.execute(
                    text(f"SELECT id FROM {_QUALIFIED} WHERE tenant_id = :t LIMIT 1"),
                    {"t": tenant_b},
                )
            ).scalar()
    finally:
        await admin.dispose()
    assert b_id is not None, "fixture did not seed a tenant-B row"

    engine = create_async_engine(nosuper_url)
    sessionmaker = create_sessionmaker(engine)
    try:
        async with (
            sessionmaker() as session,
            tenant_session(session, tenant_a) as scoped,
        ):
            result = await scoped.execute(
                text(f"SELECT id FROM {_QUALIFIED} WHERE id = :id"),
                {"id": b_id},
            )
            rows = result.fetchall()
        assert rows == [], (
            f"LEAK: tenant A's direct lookup of tenant B's row id returned "
            f"rows — tenant_session does NOT isolate. got {rows!r}"
        )
    finally:
        await engine.dispose()
