"""Tests for replay.delta_capture — snapshot_table and compute_table_delta.

Uses fake connection objects (not patches) to exercise the real
cursor-handling logic in snapshot_table, including column uppercasing,
hex encoding of bytes, and database-qualified SQL generation.

compute_table_delta is tested through the full chain:
snapshot data -> select_strategy -> FullSnapshotStrategy.compute_delta.
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from test_runner.common.config import ReplayConfiguration
from test_runner.replay.delta_capture import (
    compute_table_delta,
    select_strategy,
    snapshot_table,
)
from test_runner.replay.models import DeltaStrategy, TableConfig


# ---------------------------------------------------------------------------
# Helpers — fake connection that returns canned cursor data
# ---------------------------------------------------------------------------

def _fake_connection(description, rows):
    """Build a mock connection whose cursor returns *description* and *rows*.

    *description* is a list of (col_name, ...) tuples matching DB-API spec.
    *rows* is a list of tuples matching the column order.
    """
    conn = MagicMock()
    cursor = MagicMock()
    cursor.description = description
    cursor.fetchall.return_value = rows
    conn.cursor.return_value = cursor
    return conn, cursor


# ---------------------------------------------------------------------------
# snapshot_table
# ---------------------------------------------------------------------------

class TestSnapshotTable:

    def test_returns_rows_with_uppercased_column_names(self):
        conn, cursor = _fake_connection(
            description=[("color",), ("id",)],
            rows=[("Blue", 1), ("Red", 2)],
        )

        result = snapshot_table(conn, "dbo", "Colors")

        assert len(result) == 2
        assert result[0] == {"COLOR": "Blue", "ID": 1}
        assert result[1] == {"COLOR": "Red", "ID": 2}

    @pytest.mark.parametrize("database, expected_fragment, min_quotes", [
        (None, '"Warehouse"."Colors"', 4),
        ("CLONE_DB", '"CLONE_DB"."Warehouse"."Colors"', 6),
    ], ids=["two_part", "three_part"])
    def test_generates_qualified_sql(self, database, expected_fragment, min_quotes):
        conn, cursor = _fake_connection(
            description=[("id",)],
            rows=[(1,)],
        )

        snapshot_table(conn, "Warehouse", "Colors", database=database)

        sql = cursor.execute.call_args[0][0]
        assert expected_fragment in sql
        assert sql.count('"') >= min_quotes

    @pytest.mark.parametrize("binary_val, expected_hex", [
        (b"\x01\x02\x03", "010203"),
        (bytearray(b"\xff\x00"), "ff00"),
    ], ids=["bytes", "bytearray"])
    def test_hex_encodes_binary_columns(self, binary_val, expected_hex):
        conn, _ = _fake_connection(
            description=[("data",)],
            rows=[(binary_val,)],
        )

        result = snapshot_table(conn, "dbo", "T")

        assert result[0]["DATA"] == expected_hex

    def test_empty_table_returns_empty_list(self):
        conn, _ = _fake_connection(
            description=[("id",)],
            rows=[],
        )

        result = snapshot_table(conn, "dbo", "EmptyTable")

        assert result == []

    def test_cursor_is_always_closed(self):
        conn, cursor = _fake_connection(
            description=[("id",)],
            rows=[(1,)],
        )

        snapshot_table(conn, "dbo", "T")

        cursor.close.assert_called_once()

    def test_cursor_closed_even_on_error(self):
        conn = MagicMock()
        cursor = MagicMock()
        cursor.execute.side_effect = RuntimeError("connection lost")
        conn.cursor.return_value = cursor

        with pytest.raises(RuntimeError, match="connection lost"):
            snapshot_table(conn, "dbo", "T")

        cursor.close.assert_called_once()

    def test_preserves_none_values(self):
        conn, _ = _fake_connection(
            description=[("name",), ("age",)],
            rows=[("Alice", None)],
        )

        result = snapshot_table(conn, "dbo", "People")

        assert result[0]["NAME"] == "Alice"
        assert result[0]["AGE"] is None


# ---------------------------------------------------------------------------
# compute_table_delta — full chain through FullSnapshotStrategy
# ---------------------------------------------------------------------------

class TestComputeTableDelta:
    def _make_cfg(self, schema="S", table="T", ignore=None):
        return TableConfig(
            schema_name=schema,
            table_name=table,
            ignore_columns=ignore or [],
        )

    def test_detects_insert(self):
        before: list[dict] = []
        after = [{"COLOR": "Blue", "ID": 1}]
        cfg = self._make_cfg()
        replay_cfg = ReplayConfiguration()

        delta = compute_table_delta(before, after, cfg, replay_cfg)

        assert delta["inserts"] == [{"COLOR": "Blue", "ID": 1}]
        assert delta["deletes"] == []
        assert cfg.strategy == DeltaStrategy.FULL_SNAPSHOT

    def test_detects_delete(self):
        before = [{"COLOR": "Blue", "ID": 1}]
        after: list[dict] = []
        cfg = self._make_cfg()
        replay_cfg = ReplayConfiguration()

        delta = compute_table_delta(before, after, cfg, replay_cfg)

        assert delta["deletes"] == [{"COLOR": "Blue", "ID": 1}]
        assert delta["inserts"] == []

    def test_no_changes_returns_empty(self):
        rows = [{"COLOR": "Blue", "ID": 1}]
        cfg = self._make_cfg()
        replay_cfg = ReplayConfiguration()

        delta = compute_table_delta(rows, list(rows), cfg, replay_cfg)

        assert delta["inserts"] == []
        assert delta["deletes"] == []

    def test_sets_strategy_on_table_config(self):
        cfg = self._make_cfg()
        replay_cfg = ReplayConfiguration()

        compute_table_delta([], [], cfg, replay_cfg)

        assert cfg.strategy == DeltaStrategy.FULL_SNAPSHOT

    def test_ignore_columns_flow_through(self):
        before = [{"COLOR": "Blue", "VALIDFROM": "old"}]
        after = [{"COLOR": "Blue", "VALIDFROM": "new"}]
        cfg = self._make_cfg(ignore=["VALIDFROM"])
        replay_cfg = ReplayConfiguration()

        delta = compute_table_delta(before, after, cfg, replay_cfg)

        assert delta["inserts"] == []
        assert delta["deletes"] == []

    def test_delta_has_canonical_shape(self):
        cfg = self._make_cfg()
        replay_cfg = ReplayConfiguration()

        delta = compute_table_delta([], [], cfg, replay_cfg)

        assert "table" in delta
        assert "strategy_used" in delta
        assert "inserts" in delta
        assert "deletes" in delta
        assert "updates" in delta
        assert delta["strategy_used"] == "full_snapshot"
