"""Integration tests — ``configure_env`` against a live Postgres.

Tier-2 (``@pytest.mark.integration``). These exercise the SYNC online
path of :func:`mgf.alembic.configure_env` against a real Postgres so
the migration round-trip is verified end-to-end against a real DDL
engine — not just sqlite.

Postgres is expected at ``localhost:5433`` (the federation dev stack).
If it's unreachable, every test in this module ``pytest.skip``s so the
hermetic suite stays green without a stack.

Isolation: each test uses a UNIQUE table name (uuid suffix) so
concurrent runs / a shared dev DB are never polluted. Cleanup drops
the test table + its ``alembic_version`` row tracker in a ``finally``.

Driver: the sync path uses ``postgresql+psycopg2``. The async path is
already covered against ``sqlite+aiosqlite`` in ``tests/unit`` — here
we want the real-Postgres DDL semantics (transactional DDL, real
``information_schema``), which the sync path delivers without the
event-loop-per-call cost.
"""

from __future__ import annotations

import textwrap
import uuid
from typing import TYPE_CHECKING

import pytest
from alembic import command
from alembic.config import Config

if TYPE_CHECKING:
    from pathlib import Path

# Sync Postgres URL for the federation dev stack.
_PG_URL = "postgresql+psycopg2://plasma_app:plasma_app@localhost:5433/plasmamapper"


def _pg_available() -> bool:
    """True if a real Postgres answers at the dev-stack URL.

    Returns False for BOTH "driver missing" and "DB unreachable" — in
    either case the live test cannot run and must skip rather than
    error, keeping the hermetic suite green.
    """
    try:
        import sqlalchemy as sa
    except ImportError:
        return False
    try:
        engine = sa.create_engine(_PG_URL)
    except Exception:
        return False
    try:
        with engine.connect() as conn:
            conn.execute(sa.text("SELECT 1"))
    except Exception:
        return False
    finally:
        engine.dispose()
    return True


pytestmark = [
    pytest.mark.integration,
    pytest.mark.skipif(
        not _pg_available(),
        reason="Postgres@localhost:5433 unreachable (or psycopg2 missing)",
    ),
]


# ---------------------------------------------------------------------------
# Project scaffolding — a 1-revision migration whose table name is
# injected so each run is isolated.
# ---------------------------------------------------------------------------


_ENV_PY = textwrap.dedent(
    """
    from mgf.alembic import configure_env
    from sqlalchemy import Column, Integer, MetaData, String, Table

    metadata = MetaData()
    Table(
        {table!r},
        metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(64), nullable=False),
    )

    configure_env(
        database_url={url!r},
        target_metadata=metadata,
    )
    """
).strip()


_MIGRATION = textwrap.dedent(
    '''
    """initial — create {table} table

    Revision ID: 0001
    Revises:
    Create Date: 2026-05-28 00:00:00

    """
    from alembic import op
    import sqlalchemy as sa

    revision = "0001"
    down_revision = None
    branch_labels = None
    depends_on = None


    def upgrade() -> None:
        op.create_table(
            {table!r},
            sa.Column("id", sa.Integer(), primary_key=True),
            sa.Column("name", sa.String(64), nullable=False),
        )


    def downgrade() -> None:
        op.drop_table({table!r})
    '''
).strip()


_ALEMBIC_INI = textwrap.dedent(
    """
    [alembic]
    script_location = {script_location}
    sqlalchemy.url = {url}

    [loggers]
    keys = root

    [handlers]
    keys = console

    [formatters]
    keys = generic

    [logger_root]
    level = WARN
    handlers = console
    qualname =

    [handler_console]
    class = StreamHandler
    args = (sys.stderr,)
    level = NOTSET
    formatter = generic

    [formatter_generic]
    format = %(levelname)-5.5s [%(name)s] %(message)s
    """
).strip()


def _build_project(tmp_path: Path, *, url: str, table: str) -> Config:
    """Lay out a minimal alembic project; return a Config bound to it."""
    project_root = tmp_path / "alembic_pg"
    project_root.mkdir()
    versions = project_root / "versions"
    versions.mkdir()

    (project_root / "env.py").write_text(_ENV_PY.format(url=url, table=table))
    (project_root / "script.py.mako").write_text(
        '"""${message}"""\n'
        "from alembic import op\n"
        "import sqlalchemy as sa\n"
        "revision = ${repr(up_revision)}\n"
        "down_revision = ${repr(down_revision)}\n"
        "branch_labels = ${repr(branch_labels)}\n"
        "depends_on = ${repr(depends_on)}\n"
        "\n"
        "def upgrade() -> None:\n"
        '    ${upgrades if upgrades else "pass"}\n'
        "\n"
        "def downgrade() -> None:\n"
        '    ${downgrades if downgrades else "pass"}\n'
    )
    (versions / "0001_initial.py").write_text(_MIGRATION.format(table=table))

    ini_path = tmp_path / "alembic.ini"
    ini_path.write_text(_ALEMBIC_INI.format(script_location=str(project_root), url=url))
    return Config(str(ini_path))


def _table_exists(table: str) -> bool:
    import sqlalchemy as sa

    engine = sa.create_engine(_PG_URL)
    try:
        with engine.connect() as conn:
            row = conn.execute(
                sa.text("SELECT to_regclass(:qualified)"),
                {"qualified": f"public.{table}"},
            ).scalar()
        return row is not None
    finally:
        engine.dispose()


def _drop_table_and_version(table: str) -> None:
    """Best-effort cleanup — drop the test table + alembic_version.

    ``alembic_version`` is shared in the ``public`` schema; we drop it
    too so a stale tracker row from a failed run never confuses the
    next test. (Each test brings its own single-revision tree.)
    """
    import sqlalchemy as sa

    engine = sa.create_engine(_PG_URL)
    try:
        with engine.begin() as conn:
            conn.execute(sa.text(f'DROP TABLE IF EXISTS "{table}"'))
            conn.execute(sa.text("DROP TABLE IF EXISTS alembic_version"))
    finally:
        engine.dispose()


@pytest.fixture
def unique_table() -> str:
    # Postgres identifiers cap at 63 chars; this is well under.
    return f"mgf_it_{uuid.uuid4().hex[:12]}"


class TestPostgresDowngradeRoundTrip:
    """RELIABILITY: upgrade head -> downgrade base returns the schema
    to its original state (table created, then fully removed) against a
    REAL Postgres engine — verifying the sync online path + the
    migration's ``downgrade()`` actually reverse the ``upgrade()``."""

    def test_upgrade_then_downgrade_returns_to_original(
        self, tmp_path: Path, unique_table: str
    ) -> None:
        config = _build_project(tmp_path, url=_PG_URL, table=unique_table)
        try:
            # Precondition: table absent.
            assert not _table_exists(unique_table)

            command.upgrade(config, "head")
            assert _table_exists(unique_table), "upgrade head did not create the table on Postgres"

            command.downgrade(config, "base")
            assert not _table_exists(unique_table), (
                "downgrade base did not drop the table — round-trip "
                "did not restore the original schema"
            )
        finally:
            _drop_table_and_version(unique_table)


class TestPostgresOfflineMode:
    """Offline mode (``--sql``) emits DDL WITHOUT touching the DB.

    Pins that ``configure_env`` routes through its offline branch and
    that the generated SQL contains the ``CREATE TABLE`` — and that no
    table is actually created (offline never connects)."""

    def test_offline_emits_ddl_without_connecting(
        self, tmp_path: Path, unique_table: str, capsys: pytest.CaptureFixture[str]
    ) -> None:
        config = _build_project(tmp_path, url=_PG_URL, table=unique_table)
        try:
            command.upgrade(config, "head", sql=True)
            out = capsys.readouterr()

            # DDL emitted to stdout.
            assert f"CREATE TABLE {unique_table}" in out.out, out.out
            assert "alembic_version" in out.out
            # SH-02 stderr warning fired (offline-mode credential note).
            assert "offline mode" in out.err

            # Offline must NOT have created the table for real.
            assert not _table_exists(unique_table), (
                "offline mode connected to the DB and created the table — it must only emit SQL"
            )
        finally:
            _drop_table_and_version(unique_table)


@pytest.mark.skip(
    reason=(
        "Concurrent-migration safety can't be pinned deterministically "
        "here. With a fresh version table and no lock_timeout, Postgres "
        "BLOCKS the losing transaction on the alembic_version row lock "
        "until the winner commits — it does not error, it waits. The "
        "loser's outcome then depends on whether it re-reads the version "
        "row before or after the winner's commit (a timing race), so the "
        "post-condition oscillates between 'already-applied no-op' and a "
        "'DuplicateTable' on the second CREATE TABLE. An empirical run of "
        "the two-thread version deadlocked/blocked past the 30s suite "
        "timeout. Per the task brief: SKIP a flaky concurrency test "
        "rather than commit it. The serialisation guarantee belongs to "
        "alembic + Postgres' row lock, not to mgf-alembic's wrapper "
        "(configure_env just hands the connection to alembic.context), so "
        "there is no mgf-alembic-owned behaviour this would pin anyway. "
        "A deterministic version would need an advisory-lock + "
        "lock_timeout protocol that alembic does not expose through this "
        "wrapper."
    )
)
class TestPostgresConcurrentMigration:
    """RELIABILITY (skipped — see module note): alembic serialises
    concurrent upgrades via the ``alembic_version`` row lock. Pinning
    that deterministically requires a lock-timeout protocol the wrapper
    doesn't expose; without it the loser BLOCKS indefinitely, so a
    timed test is inherently flaky. Kept as a documented placeholder so
    the gap is visible rather than silently absent."""

    def test_two_concurrent_upgrades_do_not_double_apply(
        self, tmp_path: Path, unique_table: str
    ) -> None:  # pragma: no cover - skipped
        pytest.skip("see class skip reason")
