"""Integration tests for capture_handler.capture_deltas.

Exercises the full chain: _get_row_count → snapshot_table →
compute_table_delta using fake connection objects.  No patches on
inner functions — only the outermost DB boundary (the connection)
and executor_factory infrastructure are faked.
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from test_runner.common.config import ReplayConfiguration
from test_runner.replay.capture_handler import capture_deltas
from test_runner.replay.source_introspector import DefaultIntrospector


# ---------------------------------------------------------------------------
# Fake DB boundary objects
# ---------------------------------------------------------------------------

class _FakeCursor:
    """Minimal DB-API cursor returning canned data."""

    def __init__(
        self,
        *,
        description=None,
        rows=None,
        fetchone_result=None,
        execute_error=None,
    ):
        self.description = description
        self._rows = rows or []
        self._fetchone = fetchone_result
        self._execute_error = execute_error
        self.executed: list[str] = []
        self._closed = False

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

    def fetchall(self):
        return self._rows

    def fetchone(self):
        return self._fetchone

    def close(self) -> None:
        self._closed = True


def _count_cursor(row_count: int) -> _FakeCursor:
    """Cursor for _get_row_count: returns a single (count,) row."""
    return _FakeCursor(fetchone_result=(row_count,))


def _snapshot_cursor(description, rows) -> _FakeCursor:
    """Cursor for snapshot_table: column description + rows."""
    return _FakeCursor(description=description, rows=rows)


class _FakeConnection:
    """Returns pre-built cursors in FIFO order."""

    def __init__(self, cursors: list[_FakeCursor]):
        self._cursors = list(cursors)
        self._idx = 0

    def cursor(self) -> _FakeCursor:
        c = self._cursors[self._idx]
        self._idx += 1
        return c


def _mock_executor_factory():
    """Factory that returns DefaultIntrospector + no-op mock isolation.

    DefaultIntrospector uppercases identifiers and returns empty
    column metadata (no DB calls needed).  Isolation is an
    infrastructure boundary, so mocking it is justified.
    """
    factory = MagicMock()
    factory.create_introspector.return_value = DefaultIntrospector()
    factory.create_isolation.return_value = MagicMock()
    return factory


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestCaptureDeltasIntegration:

    def test_full_chain_produces_deltas(self):
        """Before=empty, after=1 row → delta with 1 insert."""
        count_c = _count_cursor(5)
        before_c = _snapshot_cursor([("color",)], [])
        after_c = _snapshot_cursor([("color",)], [("Blue",)])
        conn = _FakeConnection([count_c, before_c, after_c])

        result, deltas, col_meta = capture_deltas(
            connection=conn,
            affected_tables=["dbo.Colors"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: "exec_result",
            executor_factory=_mock_executor_factory(),
        )

        assert result == "exec_result"
        assert deltas["DBO.COLORS"]["inserts"] == [{"COLOR": "Blue"}]
        assert deltas["DBO.COLORS"]["deletes"] == []
        assert count_c.executed == ['SELECT COUNT(*) FROM "DBO"."COLORS"']
        assert before_c.executed == ['SELECT * FROM "DBO"."COLORS"']
        assert after_c.executed == ['SELECT * FROM "DBO"."COLORS"']

    def test_no_changes_produces_empty_deltas(self):
        """Same before and after → delta_total_changes == 0 → excluded."""
        conn = _FakeConnection([
            _count_cursor(1),
            _snapshot_cursor([("color",)], [("Blue",)]),
            _snapshot_cursor([("color",)], [("Blue",)]),
        ])

        result, deltas, _ = capture_deltas(
            connection=conn,
            affected_tables=["dbo.Colors"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: "ok",
            executor_factory=_mock_executor_factory(),
        )

        assert deltas == {}

    def test_empty_affected_tables_skips_capture(self):
        """No affected tables → execute_fn called directly, no snapshots."""
        conn = _FakeConnection([])

        result, deltas, col_meta = capture_deltas(
            connection=conn,
            affected_tables=[],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: "direct",
            executor_factory=_mock_executor_factory(),
        )

        assert result == "direct"
        assert deltas == {}
        assert col_meta == {}

    def test_large_table_skipped(self):
        """Row count exceeding large_table_threshold → table excluded.

        execute_fn must still be called even when all tables are skipped.
        """
        conn = _FakeConnection([
            _count_cursor(20_000_000),
        ])
        cfg = ReplayConfiguration()
        cfg.large_table_threshold = 10_000_000

        result, deltas, _ = capture_deltas(
            connection=conn,
            affected_tables=["dbo.BigTable"],
            replay_cfg=cfg,
            execute_fn=lambda: "ok",
            executor_factory=_mock_executor_factory(),
        )

        assert result == "ok"
        assert deltas == {}

    def test_multiple_tables_each_get_own_delta(self):
        """Two affected tables → both captured independently.

        Cursor order: count(Colors), count(Orders), before(Colors),
        before(Orders), after(Colors), after(Orders).  SQL assertions
        verify each cursor received the expected query.
        """
        cnt_colors = _count_cursor(2)
        cnt_orders = _count_cursor(1)
        bef_colors = _snapshot_cursor([("color",)], [])
        bef_orders = _snapshot_cursor([("id",)], [])
        aft_colors = _snapshot_cursor([("color",)], [("Blue",)])
        aft_orders = _snapshot_cursor([("id",)], [(1,)])
        conn = _FakeConnection([
            cnt_colors, cnt_orders,
            bef_colors, bef_orders,
            aft_colors, aft_orders,
        ])

        _, deltas, _ = capture_deltas(
            connection=conn,
            affected_tables=["dbo.Colors", "dbo.Orders"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: None,
            executor_factory=_mock_executor_factory(),
        )

        assert deltas["DBO.COLORS"]["inserts"] == [{"COLOR": "Blue"}]
        assert deltas["DBO.ORDERS"]["inserts"] == [{"ID": 1}]
        assert cnt_colors.executed == ['SELECT COUNT(*) FROM "DBO"."COLORS"']
        assert cnt_orders.executed == ['SELECT COUNT(*) FROM "DBO"."ORDERS"']
        assert bef_colors.executed == ['SELECT * FROM "DBO"."COLORS"']
        assert aft_orders.executed == ['SELECT * FROM "DBO"."ORDERS"']

    def test_get_row_count_failure_falls_back_to_zero(self):
        """Cursor error during COUNT(*) → fallback to 0, table still captured."""
        conn = _FakeConnection([
            _FakeCursor(execute_error=RuntimeError("connection reset")),
            _snapshot_cursor([("id",)], []),
            _snapshot_cursor([("id",)], [(1,)]),
        ])

        _, deltas, _ = capture_deltas(
            connection=conn,
            affected_tables=["dbo.T"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: None,
            executor_factory=_mock_executor_factory(),
        )

        assert "DBO.T" in deltas
        assert deltas["DBO.T"]["inserts"] == [{"ID": 1}]

    def test_4_part_table_name_raises_value_error(self):
        """Names with 4+ dot-parts indicate un-escaped identifiers → ValueError."""
        conn = _FakeConnection([])

        with pytest.raises(ValueError, match="4 dot-separated parts"):
            capture_deltas(
                connection=conn,
                affected_tables=["db.schema.table.extra"],
                replay_cfg=ReplayConfiguration(),
                execute_fn=lambda: None,
                executor_factory=_mock_executor_factory(),
            )

    def test_isolation_setup_and_teardown_bracket_execution(self):
        """Isolation provider's setup/teardown wrap the capture cycle.

        Isolation is an infrastructure boundary — mocking it is justified
        because we are testing the capture orchestration, not the isolation
        SQL.  We verify the call order (setup before snapshots, teardown
        after) via a recording list.
        """
        call_order: list[str] = []
        factory = _mock_executor_factory()
        mock_iso = factory.create_isolation.return_value
        mock_iso.setup.side_effect = lambda c: call_order.append("setup")
        mock_iso.teardown.side_effect = lambda c: call_order.append("teardown")

        conn = _FakeConnection([
            _count_cursor(1),
            _snapshot_cursor([("id",)], []),
            _snapshot_cursor([("id",)], []),
        ])

        capture_deltas(
            connection=conn,
            affected_tables=["dbo.T"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: call_order.append("execute"),
            executor_factory=factory,
        )

        assert call_order == ["setup", "execute", "teardown"]
