"""
Integration test: PostgreSQL upgrade from revision 0005 to Phase 12 head (0006).

Prerequisite: a disposable PostgreSQL database accessible via
  INTEGRATION_DATABASE_URL  (psycopg sync DSN, e.g. postgresql://user:pw@host/db)
  or
  INTEGRATION=1 with DATABASE_MIGRATE_URL pointing at the test database.

If neither is set the tests are skipped with an explicit message.

The test uses an isolated Alembic migration run against this disposable DB — it
NEVER touches the normal development database and NEVER runs downgrade on it.

Requirements covered:
  CONN-04 — Connections endpoint requires display_name_override column
  CLOUD-01 — cloud_items/cloud_folder_states tables must exist
"""
from __future__ import annotations

import os
import subprocess
import sys

import pytest

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

REQUIRED_ENV = (
    "INTEGRATION_DATABASE_URL",
    "DATABASE_MIGRATE_URL",
)

PHASE_12_TABLES = {
    "cloud_items",
    "cloud_item_topics",
    "cloud_folder_states",
}

PHASE_12_UNIQUE_CONSTRAINTS = {
    "uq_cloud_items_connection_provider_item",
    "uq_cloud_folder_states_connection_parent",
}

PHASE_12_INDEXES = {
    "ix_cloud_items_user_id",
    "ix_cloud_items_connection_id",
    "ix_cloud_items_connection_parent",
    "ix_cloud_folder_states_connection",
}


def _get_migrate_url() -> str | None:
    """Return a disposable database URL for migration tests, or None to skip."""
    # Highest priority: dedicated integration URL
    url = os.environ.get("INTEGRATION_DATABASE_URL")
    if url:
        return url
    # Second: INTEGRATION=1 activates the normal DATABASE_MIGRATE_URL
    if os.environ.get("INTEGRATION") == "1":
        return os.environ.get("DATABASE_MIGRATE_URL")
    return None


def _psycopg_conn(dsn: str):
    """Return a psycopg3 (sync) connection or raise ImportError / OperationalError."""
    try:
        import psycopg  # psycopg v3 sync
    except ImportError as exc:
        pytest.skip(f"psycopg (v3) not installed — cannot run migration integration test: {exc}")
    return psycopg.connect(dsn, autocommit=True)


def _run_alembic(target: str, *, dsn: str) -> None:
    """Run alembic upgrade/downgrade to target using the given DSN."""
    env = {**os.environ, "DATABASE_MIGRATE_URL": dsn}
    result = subprocess.run(
        [sys.executable, "-m", "alembic", "upgrade", target],
        capture_output=True,
        text=True,
        env=env,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"alembic upgrade {target} failed (rc={result.returncode}):\n"
            f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}"
        )


def _run_alembic_downgrade(target: str, *, dsn: str) -> None:
    """Run alembic downgrade to target using the given DSN."""
    env = {**os.environ, "DATABASE_MIGRATE_URL": dsn}
    result = subprocess.run(
        [sys.executable, "-m", "alembic", "downgrade", target],
        capture_output=True,
        text=True,
        env=env,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"alembic downgrade {target} failed (rc={result.returncode}):\n"
            f"STDOUT: {result.stdout}\nSTDERR: {result.stderr}"
        )


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(scope="module")
def migrate_dsn() -> str:
    """Provide the integration database DSN, or skip the whole module."""
    dsn = _get_migrate_url()
    if not dsn:
        pytest.skip(
            "Migration integration tests skipped — set INTEGRATION_DATABASE_URL or "
            "INTEGRATION=1 (with DATABASE_MIGRATE_URL) to activate. "
            "These tests require a disposable PostgreSQL database and must never "
            "run against the normal development database."
        )
    return dsn


@pytest.fixture(scope="module")
def pg_conn(migrate_dsn):
    """Open a psycopg3 connection to the integration database."""
    conn = _psycopg_conn(migrate_dsn)
    yield conn
    conn.close()


# ---------------------------------------------------------------------------
# Test: upgrade 0005 → 0006/head
# ---------------------------------------------------------------------------

class TestMigration0005To0006:
    """Upgrade integration tests that prove the 0005->0006 transition."""

    def test_upgrade_to_0005(self, migrate_dsn):
        """Apply all migrations through 0005 (the pre-phase-12 baseline)."""
        _run_alembic("0005", dsn=migrate_dsn)

    def test_0005_lacks_display_name_override(self, pg_conn):
        """After 0005 only, display_name_override must NOT exist yet."""
        cur = pg_conn.execute(
            """
            SELECT column_name
            FROM information_schema.columns
            WHERE table_name = 'cloud_connections'
              AND column_name = 'display_name_override'
            """
        )
        row = cur.fetchone()
        assert row is None, (
            "display_name_override should not exist after revision 0005; "
            "migration 0006 has not run yet"
        )

    def test_0005_lacks_phase12_tables(self, pg_conn):
        """After 0005 only, cloud_items/cloud_item_topics/cloud_folder_states must be absent."""
        for table in PHASE_12_TABLES:
            cur = pg_conn.execute(
                "SELECT table_name FROM information_schema.tables "
                "WHERE table_schema='public' AND table_name=%s",
                (table,),
            )
            assert cur.fetchone() is None, (
                f"Table '{table}' must not exist after revision 0005"
            )

    def test_upgrade_to_head(self, migrate_dsn):
        """Upgrade from 0005 to head (0006)."""
        _run_alembic("head", dsn=migrate_dsn)

    def test_alembic_version_is_0006(self, pg_conn):
        """alembic_version must report '0006' after upgrade to head."""
        cur = pg_conn.execute("SELECT version_num FROM alembic_version")
        row = cur.fetchone()
        assert row is not None, "alembic_version table has no rows after migration"
        assert row[0] == "0006", (
            f"Expected alembic_version='0006', got {row[0]!r}"
        )

    def test_display_name_override_column_exists(self, pg_conn):
        """cloud_connections.display_name_override must exist after 0006."""
        cur = pg_conn.execute(
            """
            SELECT column_name, is_nullable, data_type
            FROM information_schema.columns
            WHERE table_name = 'cloud_connections'
              AND column_name = 'display_name_override'
            """
        )
        row = cur.fetchone()
        assert row is not None, (
            "cloud_connections.display_name_override column is missing after migration 0006"
        )
        col_name, is_nullable, data_type = row
        assert is_nullable == "YES", (
            "display_name_override must be nullable (optional user-defined override)"
        )

    def test_cloud_items_table_exists_with_key_columns(self, pg_conn):
        """cloud_items table must exist with essential columns."""
        expected_columns = {
            "id", "user_id", "connection_id", "provider_item_id", "name", "kind",
            "analysis_status", "semantic_index_status", "created_at", "updated_at",
        }
        cur = pg_conn.execute(
            "SELECT column_name FROM information_schema.columns "
            "WHERE table_name='cloud_items'"
        )
        actual = {row[0] for row in cur.fetchall()}
        missing = expected_columns - actual
        assert not missing, f"cloud_items is missing columns: {sorted(missing)}"

    def test_cloud_item_topics_table_exists(self, pg_conn):
        """cloud_item_topics association table must exist."""
        cur = pg_conn.execute(
            "SELECT table_name FROM information_schema.tables "
            "WHERE table_schema='public' AND table_name='cloud_item_topics'"
        )
        assert cur.fetchone() is not None, "cloud_item_topics table missing after migration 0006"

    def test_cloud_folder_states_table_exists_with_key_columns(self, pg_conn):
        """cloud_folder_states table must exist with essential columns."""
        expected_columns = {
            "id", "user_id", "connection_id", "parent_ref", "refresh_state",
            "last_refreshed_at", "error_code", "created_at", "updated_at",
        }
        cur = pg_conn.execute(
            "SELECT column_name FROM information_schema.columns "
            "WHERE table_name='cloud_folder_states'"
        )
        actual = {row[0] for row in cur.fetchall()}
        missing = expected_columns - actual
        assert not missing, f"cloud_folder_states is missing columns: {sorted(missing)}"

    def test_unique_constraints_exist(self, pg_conn):
        """Named unique constraints from 0006 must exist in the catalog."""
        for constraint in PHASE_12_UNIQUE_CONSTRAINTS:
            cur = pg_conn.execute(
                """
                SELECT constraint_name
                FROM information_schema.table_constraints
                WHERE constraint_type = 'UNIQUE'
                  AND constraint_name = %s
                """,
                (constraint,),
            )
            row = cur.fetchone()
            assert row is not None, (
                f"Unique constraint '{constraint}' is missing after migration 0006"
            )

    def test_indexes_exist(self, pg_conn):
        """Named indexes from 0006 must exist in pg_indexes."""
        for idx in PHASE_12_INDEXES:
            cur = pg_conn.execute(
                "SELECT indexname FROM pg_indexes WHERE indexname=%s",
                (idx,),
            )
            row = cur.fetchone()
            assert row is not None, (
                f"Index '{idx}' is missing after migration 0006"
            )

    def test_docuvault_app_cannot_create_table(self, migrate_dsn, pg_conn):
        """docuvault_app (DML-only role) must not be able to create tables (DDL forbidden)."""
        # Derive app DSN from migrate DSN by substituting the user
        # The test DB is shared; we test privilege by executing as the app role.
        # We check the privilege via information_schema — if the role has CREATE
        # privilege on the public schema, that is a misconfiguration.
        #
        # NOTE: In the Docker initdb.d setup, docuvault_app is granted USAGE but
        # not CREATE on the public schema. We assert this via has_schema_privilege.
        cur = pg_conn.execute(
            "SELECT has_schema_privilege('docuvault_app', 'public', 'CREATE')"
        )
        row = cur.fetchone()
        # Role absence means the DB is misconfigured — fail explicitly rather than silently skipping
        # the privilege-escalation gate. Set SKIP_ROLE_CHECK=1 to opt out intentionally.
        if row is None:
            import os
            if os.environ.get("SKIP_ROLE_CHECK") == "1":
                pytest.skip("docuvault_app role absent — SKIP_ROLE_CHECK=1 set")
            else:
                pytest.fail(
                    "docuvault_app role does not exist in the integration database. "
                    "The privilege boundary cannot be verified. "
                    "Set SKIP_ROLE_CHECK=1 to skip intentionally."
                )
        assert row[0] is False, (
            "docuvault_app must NOT have CREATE privilege on the public schema — "
            "only docuvault_migrate should be able to run DDL"
        )
