"""unit tests for :class:`RedshiftDriver` against a mocked ``redshift_connector``.

scope: driver construction + close-concurrency + cache routing +
SQL-constant + secret-resolution + cancellation-callback code paths
that don't need a real Redshift cluster.

live cancellation against a real cluster is covered by
``tests/integration/test_redshift_driver_live.py``. these unit tests
mock the connector module so cancellation paths can be exercised
deterministically.
"""

from __future__ import annotations

import asyncio
import socket
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from threetears.datasources.config import RedshiftConnectionConfig
from threetears.datasources.drivers.redshift_driver import (
    DriverConnectError,
    RedshiftDriver,
    _CANCEL_TIMEOUT_SECONDS,
    _PING_SQL,
    _REDSHIFT_COLUMNS_SQL_TEMPLATE,
    _REDSHIFT_TABLES_SQL_TEMPLATE,
    _REDSHIFT_TABLE_HASHES_SQL_TEMPLATE,
)
from threetears.datasources.entities import DataSourceType


def _is_set_stmt_timeout(sql: str) -> bool:
    """test helper: classify a SQL string as the SET statement_timeout call.

    Redshift rejects bind parameters in ``SET``, so the driver inlines
    the integer milliseconds value into the SQL text. tests filter
    on the canonical prefix rather than an exact-match constant.

    :param sql: SQL text from a ``cursor.execute`` call
    :ptype sql: str
    :return: True iff this is the SET statement_timeout call
    :rtype: bool
    """
    return sql.startswith("SET statement_timeout")


def _is_open_setup_stmt(sql: str) -> bool:
    """test helper: classify a SQL string as a connection-open setup call.

    the driver issues a handful of statements at connection-open time
    that are NOT the caller's query: ``SET statement_timeout``,
    ``SET search_path``, and ``SELECT pg_backend_pid()`` (the last
    captures the server-side backend pid so the cancel path can issue
    ``pg_terminate_backend``). tests that want only the caller's query
    filter these out.

    :param sql: SQL text from a ``cursor.execute`` call
    :ptype sql: str
    :return: True iff this is an open-time setup statement
    :rtype: bool
    """
    return sql.startswith("SET ") or sql.startswith("SELECT pg_backend_pid")


# ---------------------------------------------------------------------------
# Mock builder
# ---------------------------------------------------------------------------


def _build_mock_connection(
    *,
    fetchall_rows: list[tuple[Any, ...]] | None = None,
    description: list[tuple[str, Any]] | None = None,
    fetchone_row: tuple[Any, ...] | None = None,
    fetchmany_chunks: list[list[tuple[Any, ...]]] | None = None,
) -> MagicMock:
    """build a MagicMock that behaves like a ``redshift_connector.Connection``.

    cursor + connection cycles work as expected; ``execute`` / ``fetchall``
    / ``fetchmany`` / ``fetchone`` return the configured sequences.

    :param fetchall_rows: rows ``cursor.fetchall()`` returns
    :ptype fetchall_rows: list[tuple] | None
    :param description: ``cursor.description`` value (list of column
        descriptors; only the first element of each is used)
    :ptype description: list[tuple] | None
    :param fetchone_row: row ``cursor.fetchone()`` returns
    :ptype fetchone_row: tuple | None
    :param fetchmany_chunks: successive return values for fetchmany;
        the final element should be ``[]`` to terminate the loop
    :ptype fetchmany_chunks: list[list[tuple]] | None
    :return: connection mock with cursor/close/commit wired
    :rtype: MagicMock
    """
    conn = MagicMock(name="MockRedshiftConn")
    cursor = MagicMock(name="MockRedshiftCursor")
    cursor.description = description or []
    cursor.fetchall = MagicMock(return_value=fetchall_rows or [])
    cursor.fetchone = MagicMock(return_value=fetchone_row)
    if fetchmany_chunks is not None:
        cursor.fetchmany = MagicMock(side_effect=fetchmany_chunks)
    else:
        cursor.fetchmany = MagicMock(return_value=[])
    cursor.execute = MagicMock(return_value=None)
    cursor.close = MagicMock(return_value=None)
    conn.cursor = MagicMock(return_value=cursor)
    conn.commit = MagicMock(return_value=None)
    conn.close = MagicMock(return_value=None)
    # surface the cursor on the conn mock for assertions.
    conn._cursor = cursor  # noqa: SLF001 - test surface only
    return conn


@pytest.fixture
def redshift_config() -> RedshiftConnectionConfig:
    """default :class:`RedshiftConnectionConfig` for the unit tests.

    no ``password_ref`` -- the connect call gets ``password=None``,
    which is fine for the mocked path.
    """
    return RedshiftConnectionConfig(
        datasource_type=DataSourceType.REDSHIFT,
        host="rs.example.com",
        port=5439,
        database="analytics",
        username="rs_user",
        password_ref=None,
        executor_max_workers=2,
        connection_cache_size=2,
        query_timeout_seconds=60,
    )


# ---------------------------------------------------------------------------
# Construction + lifecycle
# ---------------------------------------------------------------------------


class TestConstruction:
    """``__init__`` stores config + bridge + cache; no I/O."""

    def test_init_does_not_open_connection(self, redshift_config: RedshiftConnectionConfig) -> None:
        """constructing the driver does NOT open a redshift connection."""
        with patch("threetears.datasources.drivers.redshift_driver.redshift_connector.connect") as connect_mock:
            driver = RedshiftDriver(redshift_config)
            assert driver._config is redshift_config  # noqa: SLF001
            assert driver._closed is False  # noqa: SLF001
            assert len(driver._cache) == 0  # noqa: SLF001
            connect_mock.assert_not_called()

    def test_init_bridge_sized_from_config(self, redshift_config: RedshiftConnectionConfig) -> None:
        """bridge's executor max_workers matches ``executor_max_workers``."""
        driver = RedshiftDriver(redshift_config)
        # public surface on AsyncSyncBridge
        assert driver._bridge.max_workers == 2  # noqa: SLF001

    def test_init_datasource_name_default_is_unknown(self, redshift_config: RedshiftConnectionConfig) -> None:
        """omitting ``datasource_name`` defaults to ``"unknown"``."""
        driver = RedshiftDriver(redshift_config)
        assert driver._datasource_name == "unknown"  # noqa: SLF001

    def test_init_datasource_name_captured(self, redshift_config: RedshiftConnectionConfig) -> None:
        """passing ``datasource_name`` is stored for metric tagging."""
        driver = RedshiftDriver(redshift_config, datasource_name="ots")
        assert driver._datasource_name == "ots"  # noqa: SLF001

    def test_init_registers_finalize(self, redshift_config: RedshiftConnectionConfig) -> None:
        """:func:`weakref.finalize` is registered for pod-crash mitigation."""
        driver = RedshiftDriver(redshift_config)
        # the finalize is alive until detach() / GC.
        assert driver._finalize.alive  # noqa: SLF001

    def test_finalize_drains_cache_added_after_init(
        self,
        redshift_config: RedshiftConnectionConfig,
    ) -> None:
        """finalize closes connections added to the cache POST-init.

        regression guard for the bug where ``weakref.finalize`` was
        registered with ``list(self._cache)`` -- a snapshot at init
        time, when the cache is empty due to lazy-fill. the finalize
        MUST see the live cache state at GC time so connections added
        between init and GC actually get closed.
        """
        driver = RedshiftDriver(redshift_config)
        c1 = _build_mock_connection()
        c2 = _build_mock_connection()
        driver._cache.append(c1)  # noqa: SLF001
        driver._cache.append(c2)  # noqa: SLF001
        # directly invoke the finalize (simulates GC-time invocation).
        # this exercises the ``self._cache`` reference the finalize
        # captured at __init__; a snapshot would close nothing.
        driver._finalize()  # noqa: SLF001
        c1.close.assert_called()
        c2.close.assert_called()


class TestClose:
    """close() concurrency contract per DS-09-12 / DS-11-10."""

    @pytest.mark.asyncio
    async def test_close_idempotent(self, redshift_config: RedshiftConnectionConfig) -> None:
        """second :meth:`close` is a no-op."""
        driver = RedshiftDriver(redshift_config)
        await driver.close()
        assert driver._closed is True  # noqa: SLF001
        # second call: no-op
        await driver.close()

    @pytest.mark.asyncio
    async def test_close_drains_cache(self, redshift_config: RedshiftConnectionConfig) -> None:
        """every cached connection has ``close`` called on close()."""
        driver = RedshiftDriver(redshift_config)
        # inject two mock connections into the cache
        c1 = _build_mock_connection()
        c2 = _build_mock_connection()
        driver._cache.append(c1)  # noqa: SLF001
        driver._cache.append(c2)  # noqa: SLF001
        await driver.close()
        c1.close.assert_called()
        c2.close.assert_called()
        assert len(driver._cache) == 0  # noqa: SLF001

    @pytest.mark.asyncio
    async def test_methods_reject_after_close(self, redshift_config: RedshiftConnectionConfig) -> None:
        """every public method raises :class:`RuntimeError` post-close."""
        driver = RedshiftDriver(redshift_config)
        await driver.close()
        with pytest.raises(RuntimeError, match="closed"):
            await driver.fetch("SELECT 1")
        with pytest.raises(RuntimeError, match="closed"):
            await driver.execute("SELECT 1")
        with pytest.raises(RuntimeError, match="closed"):
            await driver.list_tables(["s"])
        with pytest.raises(RuntimeError, match="closed"):
            await driver.list_columns(["s"])
        with pytest.raises(RuntimeError, match="closed"):
            await driver.table_hashes(["s"])
        with pytest.raises(RuntimeError, match="closed"):
            await driver.test_connection()

    @pytest.mark.asyncio
    async def test_fetch_iter_rejects_after_close(self, redshift_config: RedshiftConnectionConfig) -> None:
        """:meth:`fetch_iter` (async generator) also raises post-close."""
        driver = RedshiftDriver(redshift_config)
        await driver.close()
        with pytest.raises(RuntimeError, match="closed"):
            async for _row in driver.fetch_iter("SELECT 1"):
                pass  # pragma: no cover

    @pytest.mark.asyncio
    async def test_close_detaches_finalize(self, redshift_config: RedshiftConnectionConfig) -> None:
        """post-close, the weakref finalize is detached."""
        driver = RedshiftDriver(redshift_config)
        await driver.close()
        # detached finalize is no longer alive
        assert not driver._finalize.alive  # noqa: SLF001


# ---------------------------------------------------------------------------
# Connection caching
# ---------------------------------------------------------------------------


class TestConnectionCaching:
    """cache hit/miss + release routing."""

    @pytest.mark.asyncio
    async def test_first_fetch_misses_then_caches(self, redshift_config: RedshiftConnectionConfig) -> None:
        """initial fetch: opens a fresh connection + caches it on release."""
        conn = _build_mock_connection(
            fetchall_rows=[(1, "alpha")],
            description=[("a", None), ("b", None)],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(redshift_config)
            rows = await driver.fetch("SELECT a, b FROM t")
            assert rows == [{"a": 1, "b": "alpha"}]
            # connect called once on miss
            connect_mock.assert_called_once()
            # connection released back to cache
            assert len(driver._cache) == 1  # noqa: SLF001

    @pytest.mark.asyncio
    async def test_connect_passes_default_sslmode(self, redshift_config: RedshiftConnectionConfig) -> None:
        """the config's sslmode (default verify-ca) is passed to connect."""
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT 1")
            assert connect_mock.call_args.kwargs["sslmode"] == "verify-ca"

    @pytest.mark.asyncio
    async def test_connect_passes_aggressive_tcp_keepalive(self, redshift_config: RedshiftConnectionConfig) -> None:
        """the config's TCP keepalive tuning is applied so the OS detects a half-dead
        socket in ~1 min (not the ~2h system default). redshift_connector.connect (2.1.7)
        accepts ONLY the tcp_keepalive bool, so the granular idle/interval/count are set
        via setsockopt on the underlying socket -- passing them as connect kwargs raises
        TypeError (the regression that silently broke every datasource connection).
        """
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT 1")
            kwargs = connect_mock.call_args.kwargs
            # only the supported bool reaches connect; the granular knobs must NOT
            # (they raise TypeError against the real redshift_connector).
            assert kwargs["tcp_keepalive"] is True
            assert "tcp_keepalive_idle" not in kwargs
            assert "tcp_keepalive_interval" not in kwargs
            assert "tcp_keepalive_count" not in kwargs
            # the granular tuning is applied via setsockopt on the underlying socket.
            usock = getattr(conn, "_usock")
            opts = {(call.args[0], call.args[1]): call.args[2] for call in usock.setsockopt.call_args_list}
            # SO_KEEPALIVE is always enabled.
            assert opts[(socket.SOL_SOCKET, socket.SO_KEEPALIVE)] == 1
            # each granular option is applied when the platform exposes it -- Linux (hub
            # container) has all three; macOS lacks TCP_KEEPIDLE. the driver skips an
            # absent option (getattr-guarded), so the test mirrors that guard.
            expected_granular = {"TCP_KEEPIDLE": 30, "TCP_KEEPINTVL": 10, "TCP_KEEPCNT": 3}
            applied_any = False
            for opt_name, value in expected_granular.items():
                opt = getattr(socket, opt_name, None)
                if opt is not None:
                    assert opts[(socket.IPPROTO_TCP, opt)] == value
                    applied_any = True
            assert applied_any, "no granular keepalive option applied on this platform"

    @pytest.mark.asyncio
    async def test_connect_passes_verify_full_sslmode(self, redshift_config: RedshiftConnectionConfig) -> None:
        """verify-full (for a proxy-fronted cluster) reaches connect unchanged."""
        cfg = redshift_config.model_copy(update={"sslmode": "verify-full"})
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(cfg)
            await driver.fetch("SELECT 1")
            assert connect_mock.call_args.kwargs["sslmode"] == "verify-full"

    @pytest.mark.asyncio
    async def test_second_fetch_hits_cache(self, redshift_config: RedshiftConnectionConfig) -> None:
        """second fetch reuses the cached connection (no second connect)."""
        conn = _build_mock_connection(
            fetchall_rows=[],
            description=[],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT 1")
            await driver.fetch("SELECT 2")
            # connect called exactly once across both fetches
            connect_mock.assert_called_once()

    @pytest.mark.asyncio
    async def test_set_statement_timeout_applied_on_open(self, redshift_config: RedshiftConnectionConfig) -> None:
        """fresh connection has ``SET statement_timeout`` issued once."""
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT 1")
            # the cursor saw at least one execute call with the
            # statement_timeout SQL (plus the fetch's own execute).
            calls = conn._cursor.execute.call_args_list  # noqa: SLF001
            stmt_timeout_calls = [c for c in calls if c.args and _is_set_stmt_timeout(c.args[0])]
            assert len(stmt_timeout_calls) == 1
            # ms value is inlined into the SQL (Redshift rejects
            # parameter binding in SET statements). assert the value
            # is present in the SQL text.
            expected_ms = redshift_config.query_timeout_seconds * 1000
            assert str(expected_ms) in stmt_timeout_calls[0].args[0]


class TestSetSearchPathOnOpen:
    """``allowed_schemas`` config flows into a ``SET search_path`` at open.

    contract: a non-empty ``allowed_schemas`` causes the connection-open
    path to issue exactly one ``SET search_path TO "schema1", "schema2"``
    cursor.execute alongside the existing ``SET statement_timeout``. an
    empty list issues no search_path statement (the backend default
    applies). identifier quoting is the standard Postgres double-quote
    form (internal quotes doubled) so arbitrary schema names cannot
    smuggle SQL.
    """

    @pytest.mark.asyncio
    async def test_search_path_issued_when_allowed_schemas_non_empty(self) -> None:
        """non-empty allowed_schemas issues one SET search_path on open."""
        config = RedshiftConnectionConfig(
            datasource_type=DataSourceType.REDSHIFT,
            host="rs.example.com",
            port=5439,
            database="analytics",
            username="rs_user",
            password_ref=None,
            allowed_schemas=["reporting_prod", "audit"],
        )
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(config)
            await driver.fetch("SELECT 1")
            calls = conn._cursor.execute.call_args_list  # noqa: SLF001
            search_path_calls = [c for c in calls if c.args and c.args[0].startswith("SET search_path")]
            assert len(search_path_calls) == 1, (
                f"expected exactly one SET search_path on open, got {len(search_path_calls)}: "
                f"{[c.args[0] for c in search_path_calls]}"
            )
            sql = search_path_calls[0].args[0]
            # leftmost-wins schema ordering preserved; each name is
            # identifier-quoted (no naked names that could collide with
            # SQL keywords like 'user' / 'order')
            assert sql == 'SET search_path TO "reporting_prod", "audit"'

    @pytest.mark.asyncio
    async def test_search_path_reapplied_on_cache_hit(self) -> None:
        """a reused (cache-hit) connection re-applies search_path.

        a recycled redshift session can lose the SET issued at open, surfacing as
        intermittent "relation does not exist" on unqualified names. the driver
        re-applies search_path on every acquisition, so two fetches over one cached
        connection issue the SET twice (open + cache-hit), not once.
        """
        config = RedshiftConnectionConfig(
            datasource_type=DataSourceType.REDSHIFT,
            host="rs.example.com",
            port=5439,
            database="analytics",
            username="rs_user",
            password_ref=None,
            allowed_schemas=["reporting_prod"],
        )
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(config)
            await driver.fetch("SELECT 1")  # miss -> open (SET on open)
            await driver.fetch("SELECT 2")  # hit -> re-apply (SET on cache-hit)
            connect_mock.assert_called_once()  # second fetch reused the connection
            calls = conn._cursor.execute.call_args_list  # noqa: SLF001
            search_path_calls = [c for c in calls if c.args and c.args[0].startswith("SET search_path")]
            assert len(search_path_calls) == 2, (
                f"expected SET search_path on open AND cache-hit, got {len(search_path_calls)}"
            )

    @pytest.mark.asyncio
    async def test_no_search_path_when_allowed_schemas_empty(
        self,
        redshift_config: RedshiftConnectionConfig,
    ) -> None:
        """empty allowed_schemas issues no SET search_path; backend default applies."""
        # the default redshift_config fixture has allowed_schemas=[] -- no
        # explicit assignment needed
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT 1")
            calls = conn._cursor.execute.call_args_list  # noqa: SLF001
            search_path_calls = [c for c in calls if c.args and c.args[0].startswith("SET search_path")]
            assert search_path_calls == [], (
                f"expected zero SET search_path statements, got: {[c.args[0] for c in search_path_calls]}"
            )

    @pytest.mark.asyncio
    async def test_search_path_quotes_schema_names_safely(self) -> None:
        """schema names with double-quotes are SQL-injection-safe via doubled-quote escape."""
        config = RedshiftConnectionConfig(
            datasource_type=DataSourceType.REDSHIFT,
            host="rs.example.com",
            port=5439,
            database="analytics",
            username="rs_user",
            password_ref=None,
            # adversarial schema name carrying an embedded double quote
            allowed_schemas=['my"schema'],
        )
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(config)
            await driver.fetch("SELECT 1")
            calls = conn._cursor.execute.call_args_list  # noqa: SLF001
            search_path_calls = [c for c in calls if c.args and c.args[0].startswith("SET search_path")]
            assert len(search_path_calls) == 1
            # internal double-quote is doubled per SQL-standard quoting
            assert search_path_calls[0].args[0] == 'SET search_path TO "my""schema"'

    @pytest.mark.asyncio
    async def test_search_path_applied_after_statement_timeout(self) -> None:
        """on a fresh connection, SET statement_timeout fires before SET search_path.

        timeout must apply to the whole session including the search_path
        statement itself, so the order matters: timeout first, then any
        other session config.
        """
        config = RedshiftConnectionConfig(
            datasource_type=DataSourceType.REDSHIFT,
            host="rs.example.com",
            port=5439,
            database="analytics",
            username="rs_user",
            password_ref=None,
            allowed_schemas=["reporting_prod"],
        )
        conn = _build_mock_connection(fetchall_rows=[], description=[])
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(config)
            await driver.fetch("SELECT 1")
            calls = conn._cursor.execute.call_args_list  # noqa: SLF001
            set_calls = [c.args[0] for c in calls if c.args and c.args[0].startswith("SET ")]
            assert len(set_calls) >= 2
            timeout_idx = next(i for i, s in enumerate(set_calls) if _is_set_stmt_timeout(s))
            search_idx = next(i for i, s in enumerate(set_calls) if s.startswith("SET search_path"))
            assert timeout_idx < search_idx, (
                f"expected SET statement_timeout before SET search_path, got order: {set_calls}"
            )


# ---------------------------------------------------------------------------
# Query routing
# ---------------------------------------------------------------------------


class TestQueryRouting:
    """fetch / execute / list_* route through the mocked connection."""

    @pytest.mark.asyncio
    async def test_fetch_translates_placeholders_dollar_to_percent_s(
        self, redshift_config: RedshiftConnectionConfig
    ) -> None:
        """``$1, $2`` becomes ``%s, %s`` for DB-API."""
        conn = _build_mock_connection(
            fetchall_rows=[],
            description=[],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT $1, $2", 1, "x")
            # find the non-statement_timeout execute call
            calls = [
                c
                for c in conn._cursor.execute.call_args_list  # noqa: SLF001
                if c.args and not _is_open_setup_stmt(c.args[0])
            ]
            assert len(calls) == 1
            assert calls[0].args[0] == "SELECT %s, %s"
            assert calls[0].args[1] == (1, "x")

    @pytest.mark.asyncio
    async def test_list_tables_uses_tables_sql(self, redshift_config: RedshiftConnectionConfig) -> None:
        """:meth:`list_tables` issues the canonical tables SQL."""
        conn = _build_mock_connection(
            fetchall_rows=[("s1", "t1")],
            description=[("table_schema", None), ("table_name", None)],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            rows = await driver.list_tables(["s1"])
            assert rows == [{"table_schema": "s1", "table_name": "t1"}]
            calls = [
                c
                for c in conn._cursor.execute.call_args_list  # noqa: SLF001
                if c.args and not _is_open_setup_stmt(c.args[0])
            ]
            # SQL is built from template + IN-clause placeholder for one schema
            expected_sql = _REDSHIFT_TABLES_SQL_TEMPLATE.format(placeholders="%s")
            assert calls[0].args[0] == expected_sql
            assert calls[0].args[1] == ("s1",)

    @pytest.mark.asyncio
    async def test_list_columns_preserves_raw_is_nullable(self, redshift_config: RedshiftConnectionConfig) -> None:
        """``is_nullable`` is the raw warehouse string, NOT a bool."""
        conn = _build_mock_connection(
            fetchall_rows=[
                ("s1", "t1", "c1", "INT4", "NO", 1),
                ("s1", "t1", "c2", "VARCHAR", "YES", 2),
            ],
            description=[
                ("table_schema", None),
                ("table_name", None),
                ("column_name", None),
                ("data_type", None),
                ("is_nullable", None),
                ("ordinal_position", None),
            ],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            rows = await driver.list_columns(["s1"])
            assert rows[0]["is_nullable"] == "NO"
            assert rows[1]["is_nullable"] == "YES"
            assert isinstance(rows[0]["is_nullable"], str)
            calls = [
                c
                for c in conn._cursor.execute.call_args_list  # noqa: SLF001
                if c.args and not _is_open_setup_stmt(c.args[0])
            ]
            expected_sql = _REDSHIFT_COLUMNS_SQL_TEMPLATE.format(placeholders="%s")
            assert calls[0].args[0] == expected_sql

    @pytest.mark.asyncio
    async def test_table_hashes_returns_dict_keyed_by_schema_table(
        self, redshift_config: RedshiftConnectionConfig
    ) -> None:
        """:meth:`table_hashes` keys on ``(schema, table)``."""
        conn = _build_mock_connection(
            fetchall_rows=[("s1", "t1", "abc123")],
            description=[
                ("table_schema", None),
                ("table_name", None),
                ("column_hash", None),
            ],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            hashes = await driver.table_hashes(["s1"])
            assert hashes == {("s1", "t1"): "abc123"}
            calls = [
                c
                for c in conn._cursor.execute.call_args_list  # noqa: SLF001
                if c.args and not _is_open_setup_stmt(c.args[0])
            ]
            expected_sql = _REDSHIFT_TABLE_HASHES_SQL_TEMPLATE.format(placeholders="%s")
            assert calls[0].args[0] == expected_sql

    @pytest.mark.asyncio
    async def test_execute_commits(self, redshift_config: RedshiftConnectionConfig) -> None:
        """:meth:`execute` issues ``conn.commit()`` so DDL/DML lands."""
        conn = _build_mock_connection()
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            await driver.execute("INSERT INTO t VALUES ($1)", 1)
            conn.commit.assert_called()


# ---------------------------------------------------------------------------
# test_connection sanitization
# ---------------------------------------------------------------------------


class TestTestConnection:
    """:meth:`test_connection` issues PING + sanitizes failures."""

    @pytest.mark.asyncio
    async def test_test_connection_happy_path(self, redshift_config: RedshiftConnectionConfig) -> None:
        """successful round-trip returns None silently."""
        conn = _build_mock_connection(
            fetchone_row=(1,),
            description=[("?column?", None)],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            await driver.test_connection()
            # the PING SQL was issued
            calls = [
                c
                for c in conn._cursor.execute.call_args_list  # noqa: SLF001
                if c.args and not _is_open_setup_stmt(c.args[0])
            ]
            assert any(c.args[0] == _PING_SQL for c in calls)

    @pytest.mark.asyncio
    async def test_test_connection_sanitizes_connect_failure(self, redshift_config: RedshiftConnectionConfig) -> None:
        """``redshift_connector.connect`` failure wraps in :class:`DriverConnectError`."""
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            side_effect=RuntimeError("kapow"),
        ):
            driver = RedshiftDriver(redshift_config)
            with pytest.raises(DriverConnectError) as exc_info:
                await driver.test_connection()
            # ``from None`` clears __cause__
            assert exc_info.value.__cause__ is None
            assert "rs.example.com" in str(exc_info.value)
            # the original error MESSAGE stays sanitized (may carry sensitive detail)...
            assert "kapow" not in str(exc_info.value)
            # ...but the exception TYPE is surfaced (a class name, never sensitive) so a
            # config / library error is diagnosable, not masked as a bare "connection failed".
            assert "RuntimeError" in str(exc_info.value)


# ---------------------------------------------------------------------------
# fetch_iter streaming
# ---------------------------------------------------------------------------


class TestFetchIter:
    """:meth:`fetch_iter` streams via DB-API ``fetchmany``."""

    @pytest.mark.asyncio
    async def test_fetch_iter_yields_chunks_correctly(self, redshift_config: RedshiftConnectionConfig) -> None:
        """multiple fetchmany chunks are concatenated correctly."""
        conn = _build_mock_connection(
            description=[("a", None)],
            fetchmany_chunks=[
                [(1,), (2,)],
                [(3,)],
                [],  # end of stream
            ],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            rows: list[dict[str, Any]] = []
            async for row in driver.fetch_iter("SELECT a FROM t"):
                rows.append(row)
            assert rows == [{"a": 1}, {"a": 2}, {"a": 3}]

    @pytest.mark.asyncio
    async def test_fetch_iter_sets_arraysize(self, redshift_config: RedshiftConnectionConfig) -> None:
        """cursor.arraysize is set before iteration (server-side fetchmany)."""
        conn = _build_mock_connection(
            description=[("a", None)],
            fetchmany_chunks=[[]],
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            async for _row in driver.fetch_iter("SELECT a FROM t"):
                pass  # pragma: no cover
            # arraysize attribute was set on the cursor (>=1000 per
            # module constant; we don't import to avoid coupling, just
            # check it's a positive int)
            assert isinstance(conn._cursor.arraysize, int)  # noqa: SLF001
            assert conn._cursor.arraysize > 0  # noqa: SLF001


# ---------------------------------------------------------------------------
# Cancellation contract (DS-11-08)
# ---------------------------------------------------------------------------


class TestCancellation:
    """cancellation routes through wait_for + conn.close + observability."""

    @pytest.mark.asyncio
    async def test_cancellation_closes_connection_and_evicts(self, redshift_config: RedshiftConnectionConfig) -> None:
        """cancellation closes the connection + evicts from cache."""
        conn = _build_mock_connection()

        # make the cursor.execute block forever to simulate a slow query
        execute_blocked = asyncio.Event()

        def _blocking_execute(*args: Any, **kwargs: Any) -> None:
            if args and _is_open_setup_stmt(args[0]):
                return
            # signal that the slow path is engaged, then block.
            execute_blocked.set()
            # in the real thread this blocks indefinitely; mock by
            # spinning until the connection is closed (cancel cb).
            import time

            for _ in range(100):
                if conn.close.called:
                    return
                time.sleep(0.05)
            # safety bail
            return

        conn._cursor.execute.side_effect = _blocking_execute  # noqa: SLF001

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            task = asyncio.create_task(driver.fetch("SELECT pg_sleep(60)"))
            # let the task progress to the blocking execute
            await asyncio.wait_for(execute_blocked.wait(), timeout=2.0)
            task.cancel()
            with pytest.raises(asyncio.CancelledError):
                await task
            # the connection was closed via the cancel path
            conn.close.assert_called()
            # cache should not contain the poisoned connection
            assert conn not in driver._cache  # noqa: SLF001
            await driver.close()

    @pytest.mark.asyncio
    async def test_cancellation_callback_failure_observable(
        self,
        redshift_config: RedshiftConnectionConfig,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """if conn.close hangs past the timeout, WARNING is logged + cancellation.failed.

        we make conn.close raise after a delay simulating a hung
        terminate; the wait_for guard should fire and log a WARNING.
        """
        conn = _build_mock_connection()

        # make execute block so cancel actually has something to cancel
        execute_blocked = asyncio.Event()

        def _blocking_execute(*args: Any, **kwargs: Any) -> None:
            if args and _is_open_setup_stmt(args[0]):
                return
            execute_blocked.set()
            import time

            for _ in range(200):
                if close_started.is_set():
                    return
                time.sleep(0.05)

        close_started = asyncio.Event()
        loop = asyncio.get_event_loop()

        def _hanging_close() -> None:
            # signal that close started, then hang
            loop.call_soon_threadsafe(close_started.set)
            import time

            time.sleep(_CANCEL_TIMEOUT_SECONDS + 2)
            # release after the wait_for has already fired

        conn._cursor.execute.side_effect = _blocking_execute  # noqa: SLF001
        conn.close.side_effect = _hanging_close

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            task = asyncio.create_task(driver.fetch("SELECT slow"))
            await asyncio.wait_for(execute_blocked.wait(), timeout=2.0)
            task.cancel()
            with pytest.raises(asyncio.CancelledError):
                await task
            # the WARNING log carries the "cancel ... failed" marker.
            warnings = [r for r in caplog.records if r.levelname == "WARNING"]
            assert any("cancel" in r.getMessage().lower() for r in warnings)
            await driver.close()


# ---------------------------------------------------------------------------
# Server-side backend termination on cancel
# ---------------------------------------------------------------------------


class TestBackendPidCancel:
    """cancellation terminates the SERVER-SIDE backend via a fresh connection.

    closing the CLIENT socket (``conn.close``) does NOT kill a running
    Redshift query -- an abandoned query kept running for 7.4 hours in
    production, leaking a connection-pool slot. the driver captures each
    connection's backend pid at open (``SELECT pg_backend_pid()``) and,
    on cancel, opens a FRESH short-lived connection to issue
    ``SELECT pg_terminate_backend(<pid>)`` (our DB user is not a
    superuser so ``CANCEL`` does not work; ``pg_terminate_backend``
    does). these tests pin the pid-capture + fresh-connection-terminate
    contract.
    """

    @pytest.mark.asyncio
    async def test_backend_pid_captured_on_open(self, redshift_config: RedshiftConnectionConfig) -> None:
        """``pg_backend_pid()`` is read at open and stored keyed on the connection."""
        conn = _build_mock_connection(
            fetchall_rows=[],
            description=[],
            fetchone_row=(4242,),
        )
        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            await driver.fetch("SELECT 1")
            assert driver._backend_pids.get(conn) == 4242  # noqa: SLF001
            await driver.close()

    @pytest.mark.asyncio
    async def test_cancellation_terminates_backend_via_fresh_connection(
        self, redshift_config: RedshiftConnectionConfig
    ) -> None:
        """on cancel a FRESH connection issues ``pg_terminate_backend(<pid>)``."""
        # main_conn runs the (blocking) query; its pg_backend_pid() read
        # returns 4242. terminate_conn is the distinct fresh connection
        # opened by the cancel path to issue pg_terminate_backend.
        terminate_conn = _build_mock_connection()
        main_conn = _build_mock_connection(fetchone_row=(4242,))

        execute_blocked = asyncio.Event()

        def _blocking_execute(*args: Any, **kwargs: Any) -> None:
            # SET statement_timeout / SET search_path / pg_backend_pid
            # must return immediately; only the real query blocks.
            if args and _is_set_stmt_timeout(args[0]):
                return
            if args and "pg_backend_pid" in args[0]:
                return
            execute_blocked.set()
            import time

            for _ in range(100):
                if main_conn.close.called:
                    return
                time.sleep(0.05)
            return

        main_conn._cursor.execute.side_effect = _blocking_execute  # noqa: SLF001

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            side_effect=[main_conn, terminate_conn],
        ) as connect_mock:
            driver = RedshiftDriver(redshift_config)
            task = asyncio.create_task(driver.fetch("SELECT pg_sleep(60)"))
            await asyncio.wait_for(execute_blocked.wait(), timeout=2.0)
            task.cancel()
            with pytest.raises(asyncio.CancelledError):
                await task
            # a second connection was opened (the fresh terminate conn)
            assert connect_mock.call_count >= 2
            # the fresh connection executed pg_terminate_backend(4242)
            terminate_calls = [
                c.args[0]
                for c in terminate_conn._cursor.execute.call_args_list  # noqa: SLF001
                if c.args and "pg_terminate_backend" in c.args[0]
            ]
            assert len(terminate_calls) == 1
            assert "4242" in terminate_calls[0]
            # the client socket is still closed + evicted (unchanged path)
            main_conn.close.assert_called()
            assert main_conn not in driver._cache  # noqa: SLF001
            await driver.close()

    @pytest.mark.asyncio
    async def test_backend_terminate_failure_is_nonfatal(
        self,
        redshift_config: RedshiftConnectionConfig,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """fresh-connection terminate failure does not escape; WARNING logged."""
        main_conn = _build_mock_connection(fetchone_row=(4242,))

        execute_blocked = asyncio.Event()

        def _blocking_execute(*args: Any, **kwargs: Any) -> None:
            if args and _is_set_stmt_timeout(args[0]):
                return
            if args and "pg_backend_pid" in args[0]:
                return
            execute_blocked.set()
            import time

            for _ in range(100):
                if main_conn.close.called:
                    return
                time.sleep(0.05)
            return

        main_conn._cursor.execute.side_effect = _blocking_execute  # noqa: SLF001

        # first connect returns the main conn; the cancel path's fresh
        # connect raises -- the terminate must degrade non-fatally.
        def _connect_side_effect(*args: Any, **kwargs: Any) -> Any:
            if not _connect_state["opened_main"]:
                _connect_state["opened_main"] = True
                return main_conn
            raise RuntimeError("fresh connect for terminate failed")

        _connect_state = {"opened_main": False}

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            side_effect=_connect_side_effect,
        ):
            driver = RedshiftDriver(redshift_config)
            task = asyncio.create_task(driver.fetch("SELECT pg_sleep(60)"))
            await asyncio.wait_for(execute_blocked.wait(), timeout=2.0)
            task.cancel()
            # the task still cancels cleanly -- no exception escapes the
            # best-effort terminate.
            with pytest.raises(asyncio.CancelledError):
                await task
            warnings = [r for r in caplog.records if r.levelname == "WARNING"]
            assert any("terminate" in r.getMessage().lower() for r in warnings)
            # the client socket close + evict still ran.
            main_conn.close.assert_called()
            assert main_conn not in driver._cache  # noqa: SLF001
            await driver.close()

    @pytest.mark.asyncio
    async def test_backend_pid_read_failure_nonfatal_on_open(
        self,
        redshift_config: RedshiftConnectionConfig,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """a failed ``pg_backend_pid()`` read does not break open; pid simply absent."""
        conn = _build_mock_connection(
            fetchall_rows=[(1,)],
            description=[("ok", None)],
        )

        def _fetchone_side_effect(*args: Any, **kwargs: Any) -> Any:
            raise RuntimeError("pg_backend_pid read failed")

        conn._cursor.fetchone.side_effect = _fetchone_side_effect  # noqa: SLF001

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            # the connection still opens and the fetch succeeds despite
            # the pid read failing.
            rows = await driver.fetch("SELECT 1 AS ok")
            assert rows == [{"ok": 1}]
            # pid absent because the read failed
            assert driver._backend_pids.get(conn) is None  # noqa: SLF001
            warnings = [r for r in caplog.records if r.levelname == "WARNING"]
            assert any("pid" in r.getMessage().lower() for r in warnings)
            await driver.close()


# ---------------------------------------------------------------------------
# Rollback on query error
# ---------------------------------------------------------------------------


class TestRollbackOnError:
    """on query error the connection is rolled back before release.

    Redshift uses :mod:`redshift_connector` with the DB-API default of
    ``autocommit=False``: every statement implicitly opens a
    transaction and a failed statement leaves that transaction in
    ``aborted`` state. without an explicit ``ROLLBACK`` the server
    rejects every subsequent statement on the connection with
    ``25P02: current transaction is aborted, commands ignored until
    end of transaction block``. these tests pin the driver's
    rollback-before-release contract: a single bad query must not
    poison the cached connection for the next caller.
    """

    @pytest.mark.asyncio
    async def test_query_error_triggers_rollback_and_releases_connection(
        self, redshift_config: RedshiftConnectionConfig
    ) -> None:
        """failed SELECT runs ROLLBACK and returns conn to cache (not evicted)."""
        conn = _build_mock_connection()

        # build a fresh exception type so the assertion below can match
        # by identity instead of accidentally matching one raised
        # elsewhere in the driver.
        class _ProgrammingError(Exception):
            pass

        def _execute_side_effect(*args: Any, **kwargs: Any) -> None:
            if args and _is_open_setup_stmt(args[0]):
                return
            raise _ProgrammingError("relation does not exist")

        conn._cursor.execute.side_effect = _execute_side_effect  # noqa: SLF001

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            with pytest.raises(_ProgrammingError, match="relation does not exist"):
                await driver.fetch("SELECT * FROM missing")
            # rollback fired exactly once on the connection that ran
            # the bad query
            conn.rollback.assert_called_once()
            # connection survived: it's the one in the cache, ready
            # for the next caller (post-rollback the session is clean)
            assert list(driver._cache) == [conn]  # noqa: SLF001
            await driver.close()

    @pytest.mark.asyncio
    async def test_rollback_failure_evicts_connection_and_logs_warning(
        self,
        redshift_config: RedshiftConnectionConfig,
        caplog: pytest.LogCaptureFixture,
    ) -> None:
        """rollback raising drops the conn from cache + logs WARNING, original error still raised."""
        conn = _build_mock_connection()

        class _ProgrammingError(Exception):
            pass

        class _RollbackError(Exception):
            pass

        def _execute_side_effect(*args: Any, **kwargs: Any) -> None:
            if args and _is_open_setup_stmt(args[0]):
                return
            raise _ProgrammingError("relation does not exist")

        conn._cursor.execute.side_effect = _execute_side_effect  # noqa: SLF001
        conn.rollback.side_effect = _RollbackError("connection broken")

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ):
            driver = RedshiftDriver(redshift_config)
            # the caller sees the ORIGINAL query error, not the
            # rollback's secondary error -- callers act on the real
            # failure, the rollback failure is observability only.
            with pytest.raises(_ProgrammingError, match="relation does not exist"):
                await driver.fetch("SELECT * FROM missing")
            # the doubly-poisoned connection is NOT in the cache:
            # never hand a broken connection to the next caller.
            assert conn not in driver._cache  # noqa: SLF001
            # WARNING captured so the rollback failure is not silently
            # swallowed even though it does not replace the raised
            # exception. matches the project no-silent-swallow rule.
            warnings = [r for r in caplog.records if r.levelname == "WARNING"]
            assert any("rollback" in r.getMessage().lower() for r in warnings)
            await driver.close()

    @pytest.mark.asyncio
    async def test_second_fetch_after_first_failure_succeeds_on_cached_conn(
        self, redshift_config: RedshiftConnectionConfig
    ) -> None:
        """end-to-end shape: first fetch raises, second fetch succeeds on same cached conn.

        this is the user-observed shape of the bug -- a single bad
        SELECT in the agent's tool-loop used to poison every
        subsequent SELECT in the same conversation with ``25P02``.
        post-fix the cached connection is rolled back so the next
        ``fetch`` runs cleanly and ``redshift_connector.connect`` is
        called only once across both queries.
        """
        conn = _build_mock_connection(
            fetchall_rows=[(1,)],
            description=[("ok", None)],
        )

        class _ProgrammingError(Exception):
            pass

        # first non-SET execute raises; subsequent ones succeed.
        call_count = {"non_set": 0}

        def _execute_side_effect(*args: Any, **kwargs: Any) -> None:
            if args and _is_open_setup_stmt(args[0]):
                return
            call_count["non_set"] += 1
            if call_count["non_set"] == 1:
                raise _ProgrammingError("relation does not exist")
            return None

        conn._cursor.execute.side_effect = _execute_side_effect  # noqa: SLF001

        with patch(
            "threetears.datasources.drivers.redshift_driver.redshift_connector.connect",
            return_value=conn,
        ) as connect_mock:
            driver = RedshiftDriver(redshift_config)
            with pytest.raises(_ProgrammingError):
                await driver.fetch("SELECT * FROM missing")
            # second call must succeed on the cached (rolled-back)
            # connection; if rollback had not fired the connection
            # would be in the aborted-transaction state and this call
            # would never have reached the cached-hit path with
            # working semantics.
            rows = await driver.fetch("SELECT 1 AS ok")
            assert rows == [{"ok": 1}]
            # exactly one connect: cache hit on the second fetch
            # proves the same connection was reused without eviction
            connect_mock.assert_called_once()
            # rollback fired once (on the first error) and not on
            # the successful second fetch
            conn.rollback.assert_called_once()
            await driver.close()
