"""Tests for replay.isolation — isolation providers."""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from test_runner.replay.isolation import (
    IsolationProvider,
    RedshiftTransactionIsolation,
    SnowflakeCloneIsolation,
    SqlServerTransactionIsolation,
)


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

def _mock_connection(autocommit: bool | None = None) -> MagicMock:
    conn = MagicMock()
    if autocommit is not None:
        conn.autocommit = autocommit
    else:
        del conn.autocommit
    return conn


# ---------------------------------------------------------------------------
# SqlServerTransactionIsolation
# ---------------------------------------------------------------------------

class TestSqlServerTransactionIsolation:

    def test_setup_begins_transaction_and_forces_autocommit_true(self):
        conn = _mock_connection(autocommit=False)
        iso = SqlServerTransactionIsolation()
        iso.setup(conn)

        assert conn.autocommit is True
        conn.cursor().execute.assert_called_once_with("BEGIN TRANSACTION")

    def test_teardown_issues_rollback_and_restores_autocommit(self):
        conn = _mock_connection(autocommit=False)
        iso = SqlServerTransactionIsolation()
        iso.setup(conn)

        iso.teardown(conn)

        conn.cursor().execute.assert_called_with("ROLLBACK")
        assert conn.autocommit is False

    def test_teardown_noop_when_not_active(self):
        conn = _mock_connection(autocommit=False)
        iso = SqlServerTransactionIsolation()

        iso.teardown(conn)
        conn.cursor().execute.assert_not_called()

    def test_cleanup_delegates_to_teardown(self):
        conn = _mock_connection(autocommit=False)
        iso = SqlServerTransactionIsolation()
        iso.setup(conn)

        iso.cleanup(conn)

        conn.cursor().execute.assert_called_with("ROLLBACK")

    def test_setup_without_autocommit_attr(self):
        conn = _mock_connection(autocommit=None)
        iso = SqlServerTransactionIsolation()
        iso.setup(conn)
        conn.cursor().execute.assert_called_once_with("BEGIN TRANSACTION")


# ---------------------------------------------------------------------------
# RedshiftTransactionIsolation
# ---------------------------------------------------------------------------

class TestRedshiftTransactionIsolation:

    def test_setup_begins_and_forces_autocommit_false(self):
        conn = _mock_connection(autocommit=True)
        iso = RedshiftTransactionIsolation()
        iso.setup(conn)

        assert conn.autocommit is False
        conn.cursor().execute.assert_called_once_with("BEGIN")

    def test_teardown_issues_rollback_and_restores_autocommit(self):
        conn = _mock_connection(autocommit=True)
        iso = RedshiftTransactionIsolation()
        iso.setup(conn)

        iso.teardown(conn)

        conn.cursor().execute.assert_called_with("ROLLBACK")
        assert conn.autocommit is True

    def test_teardown_noop_when_not_active(self):
        conn = _mock_connection(autocommit=True)
        iso = RedshiftTransactionIsolation()

        iso.teardown(conn)
        conn.cursor().execute.assert_not_called()

    def test_cleanup_delegates_to_teardown(self):
        conn = _mock_connection(autocommit=True)
        iso = RedshiftTransactionIsolation()
        iso.setup(conn)

        iso.cleanup(conn)

        conn.cursor().execute.assert_called_with("ROLLBACK")

    def test_setup_without_autocommit_attr_raises(self):
        conn = _mock_connection(autocommit=None)
        iso = RedshiftTransactionIsolation()
        with pytest.raises(RuntimeError, match="autocommit"):
            iso.setup(conn)


# ---------------------------------------------------------------------------
# SnowflakeCloneIsolation
# ---------------------------------------------------------------------------

class _RecordingCursor:
    """Cursor that records executed SQL into a shared log."""

    def __init__(self, sql_log: list[str]):
        self._log = sql_log
        self._error: Exception | None = None

    def execute(self, sql: str) -> None:
        if self._error:
            raise self._error
        self._log.append(sql)

    def close(self) -> None:
        pass


class _RecordingConnection:
    """Connection that tracks all SQL statements across cursors.

    Set *next_cursor_error* to make the next ``cursor()`` call return
    a cursor that raises on ``execute()``, without monkey-patching.
    """

    def __init__(self):
        self.executed_sql: list[str] = []
        self.next_cursor_error: Exception | None = None

    def cursor(self) -> _RecordingCursor:
        c = _RecordingCursor(self.executed_sql)
        if self.next_cursor_error is not None:
            c._error = self.next_cursor_error
            self.next_cursor_error = None
        return c


class TestSnowflakeCloneIsolation:

    @pytest.mark.parametrize("source, procedure, params_hash, expected", [
        ("MYDB", "dbo.MyProc", "abc123", "MYDB_REPLAY_DBO_MYPROC_ABC123"),
        ("MYDB", "", "", "MYDB_REPLAY"),
        ("DB", "dbo.MyProc", "abc", "DB_REPLAY_DBO_MYPROC_ABC"),
        ("DB", "schema with spaces.Proc", "", "DB_REPLAY_SCHEMA_WITH_SPACES_PROC"),
    ], ids=["full_format", "bare_no_proc", "dots_and_hash", "spaces_no_hash"])
    def test_clone_database_naming(self, source, procedure, params_hash, expected):
        iso = SnowflakeCloneIsolation(
            source_database=source,
            procedure=procedure,
            params_hash=params_hash,
        )

        assert iso.clone_database == expected

    def test_setup_creates_clone_and_restores_original_db(self):
        """setup() issues CREATE CLONE then USE to restore original DB."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(
            source_database="PROD", procedure="dbo.P", params_hash="x",
        )

        iso.setup(conn)

        assert conn.executed_sql == [
            f"CREATE OR REPLACE DATABASE {iso.clone_database} CLONE PROD",
            "USE DATABASE PROD",
        ]

    def test_teardown_drops_clone_after_setup(self):
        """teardown() drops the clone database."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")
        iso.setup(conn)
        conn.executed_sql.clear()

        iso.teardown(conn)

        assert conn.executed_sql == [
            f"DROP DATABASE IF EXISTS {iso.clone_database}",
        ]

    def test_teardown_noop_when_not_created(self):
        """teardown() before setup() issues no SQL."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")

        iso.teardown(conn)

        assert conn.executed_sql == []

    def test_revert_tables_clones_individual_tables(self):
        """revert_tables() re-clones each listed table from source."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")
        iso.setup(conn)
        conn.executed_sql.clear()

        iso.revert_tables(conn, ["WAREHOUSE.COLORS", "DBO.ORDERS"])

        clone = iso.clone_database
        assert conn.executed_sql == [
            f"CREATE OR REPLACE TABLE {clone}.WAREHOUSE.COLORS CLONE PROD.WAREHOUSE.COLORS",
            f"CREATE OR REPLACE TABLE {clone}.DBO.ORDERS CLONE PROD.DBO.ORDERS",
        ]

    def test_revert_tables_noop_when_empty_list(self):
        """revert_tables([]) issues no SQL."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")
        iso.setup(conn)
        conn.executed_sql.clear()

        iso.revert_tables(conn, [])

        assert conn.executed_sql == []

    def test_revert_tables_noop_when_not_created(self):
        """revert_tables() before setup() issues no SQL."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")

        iso.revert_tables(conn, ["WAREHOUSE.COLORS"])

        assert conn.executed_sql == []

    def test_revert_tables_failure_reraises(self):
        """Inner cursor error during revert is re-raised."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")
        iso.setup(conn)

        conn.next_cursor_error = RuntimeError("permission denied")

        with pytest.raises(RuntimeError, match="permission denied"):
            iso.revert_tables(conn, ["S.T"])

    def test_cleanup_always_drops_even_without_setup(self):
        """cleanup() drops the clone regardless of _created state."""
        conn = _RecordingConnection()
        iso = SnowflakeCloneIsolation(source_database="PROD")

        iso.cleanup(conn)

        assert conn.executed_sql == [
            f"DROP DATABASE IF EXISTS {iso.clone_database}",
        ]


# ---------------------------------------------------------------------------
# IsolationProvider contract
# ---------------------------------------------------------------------------

class TestIsolationProviderContract:

    @pytest.mark.parametrize("cls", [
        SqlServerTransactionIsolation,
        RedshiftTransactionIsolation,
        SnowflakeCloneIsolation,
    ])
    def test_is_isolation_provider(self, cls):
        assert issubclass(cls, IsolationProvider)
