"""Tests for replay.strategies — unmocked, using real DuckDB.

Exercises FullSnapshotStrategy.compute_delta with representative row
data to verify that inserts, deletes, ignore_columns, type promotion,
and edge cases produce correct results through the actual DuckDB EXCEPT
pipeline.

Also covers select_strategy which currently always returns FULL_SNAPSHOT.
"""

from __future__ import annotations

import pytest

from test_runner.common.config import ReplayConfiguration
from test_runner.replay.delta_capture import select_strategy
from test_runner.replay.models import DeltaStrategy, TableConfig, make_delta
from test_runner.replay.strategies.full_snapshot import (
    FullSnapshotStrategy,
    _register,
    _resolve_columns,
)


# ---------------------------------------------------------------------------
# FullSnapshotStrategy.compute_delta — real DuckDB
# ---------------------------------------------------------------------------

class TestFullSnapshotStrategy:
    """Tests run real DuckDB EXCEPT queries — no mocks."""

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

    def _strategy(self):
        return FullSnapshotStrategy()

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

        delta = self._strategy().compute_delta(before, after, cfg)

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

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

        delta = self._strategy().compute_delta(before, after, cfg)

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

    def test_no_changes_produces_empty_delta(self):
        rows = [{"COLOR": "Blue", "ID": 1}, {"COLOR": "Red", "ID": 2}]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(rows, list(rows), cfg)

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

    def test_mixed_mutations(self):
        before = [{"COLOR": "Blue", "ID": 1}, {"COLOR": "Red", "ID": 2}]
        after = [{"COLOR": "Blue", "ID": 1}, {"COLOR": "Green", "ID": 3}]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(before, after, cfg)

        assert len(delta["inserts"]) == 1
        assert delta["inserts"][0]["COLOR"] == "Green"
        assert len(delta["deletes"]) == 1
        assert delta["deletes"][0]["COLOR"] == "Red"

    def test_ignore_columns_excluded_from_diff(self):
        before = [{"COLOR": "Blue", "VALIDFROM": "2024-01-01"}]
        after = [{"COLOR": "Blue", "VALIDFROM": "2026-03-11"}]
        cfg = self._make_cfg(ignore=["VALIDFROM"])

        delta = self._strategy().compute_delta(before, after, cfg)

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

    def test_ignore_columns_case_insensitive(self):
        before = [{"Color": "Blue", "ValidFrom": "old"}]
        after = [{"Color": "Blue", "ValidFrom": "new"}]
        cfg = self._make_cfg(ignore=["VALIDFROM"])

        delta = self._strategy().compute_delta(before, after, cfg)

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

    def test_float_to_int64_promotion(self):
        """Float columns that contain only integer values should be
        promoted to Int64 so that DuckDB comparisons are exact."""
        before = [{"ID": 1.0, "QTY": 10.0}]
        after = [{"ID": 1.0, "QTY": 10.0}]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(before, after, cfg)

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

    def test_float_with_fractional_not_promoted(self):
        """Float columns with fractional values stay as float64."""
        before = [{"PRICE": 10.5}]
        after = [{"PRICE": 10.7}]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(before, after, cfg)

        assert len(delta["inserts"]) == 1
        assert len(delta["deletes"]) == 1

    def test_bytes_values_hex_encoded(self):
        """Binary data should be hex-encoded in _register before DuckDB."""
        before = [{"DATA": b"\x01\x02", "ID": 1}]
        after = [{"DATA": b"\x03\x04", "ID": 1}]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(before, after, cfg)

        assert len(delta["inserts"]) == 1
        assert delta["inserts"][0]["DATA"] == "0304"
        assert len(delta["deletes"]) == 1
        assert delta["deletes"][0]["DATA"] == "0102"

    def test_empty_both_sides(self):
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta([], [], cfg)

        assert delta["inserts"] == []
        assert delta["deletes"] == []
        assert delta["table"] == "S.T"

    def test_large_row_set(self):
        """Verify correctness at 1000+ rows."""
        before = [{"ID": i, "VAL": f"v{i}"} for i in range(1000)]
        after = [{"ID": i, "VAL": f"v{i}"} for i in range(1, 1001)]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(before, after, cfg)

        assert len(delta["inserts"]) == 1
        assert delta["inserts"][0]["ID"] == 1000
        assert len(delta["deletes"]) == 1
        assert delta["deletes"][0]["ID"] == 0

    def test_none_values_handled(self):
        before = [{"ID": 1, "NAME": None}]
        after = [{"ID": 1, "NAME": "Alice"}]
        cfg = self._make_cfg()

        delta = self._strategy().compute_delta(before, after, cfg)

        assert len(delta["inserts"]) == 1
        assert len(delta["deletes"]) == 1

    def test_delta_table_field_matches_fqn(self):
        cfg = self._make_cfg(schema="WAREHOUSE", table="COLORS")

        delta = self._strategy().compute_delta([], [], cfg)

        assert delta["table"] == "WAREHOUSE.COLORS"

    def test_strategy_name_is_full_snapshot(self):
        assert self._strategy().name == DeltaStrategy.FULL_SNAPSHOT


# ---------------------------------------------------------------------------
# _resolve_columns
# ---------------------------------------------------------------------------

class TestResolveColumns:
    def test_returns_sorted_column_names(self):
        rows = [{"C": 3, "A": 1, "B": 2}]
        cfg = TableConfig(schema_name="S", table_name="T")
        assert _resolve_columns(rows, cfg) == ["A", "B", "C"]

    def test_excludes_ignored_columns(self):
        rows = [{"A": 1, "B": 2, "C": 3}]
        cfg = TableConfig(schema_name="S", table_name="T", ignore_columns=["B"])
        assert _resolve_columns(rows, cfg) == ["A", "C"]

    def test_empty_rows_returns_empty(self):
        cfg = TableConfig(schema_name="S", table_name="T")
        assert _resolve_columns([], cfg) == []

    def test_ignore_is_case_insensitive(self):
        rows = [{"ValidFrom": "x", "Name": "y"}]
        cfg = TableConfig(schema_name="S", table_name="T", ignore_columns=["VALIDFROM"])
        assert _resolve_columns(rows, cfg) == ["Name"]


# ---------------------------------------------------------------------------
# _register — DuckDB DataFrame registration
# ---------------------------------------------------------------------------

class TestRegister:
    def test_registers_dataframe_readable_by_duckdb(self):
        import duckdb
        conn = duckdb.connect()
        try:
            _register(conn, "test_tbl", [{"A": 1, "B": "x"}], ["A", "B"])
            result = conn.execute("SELECT * FROM test_tbl").fetchdf()
            assert len(result) == 1
            assert list(result.columns) == ["A", "B"]
        finally:
            conn.close()

    def test_empty_rows_creates_empty_frame(self):
        import duckdb
        conn = duckdb.connect()
        try:
            _register(conn, "empty_tbl", [], ["A", "B"])
            result = conn.execute("SELECT * FROM empty_tbl").fetchdf()
            assert len(result) == 0
            assert list(result.columns) == ["A", "B"]
        finally:
            conn.close()

    def test_hex_encodes_bytes(self):
        import duckdb
        conn = duckdb.connect()
        try:
            _register(conn, "bin_tbl", [{"DATA": b"\xff\x00"}], ["DATA"])
            result = conn.execute("SELECT * FROM bin_tbl").fetchdf()
            assert result["DATA"][0] == "ff00"
        finally:
            conn.close()

    def test_float_integer_promoted_to_int64(self):
        import duckdb
        conn = duckdb.connect()
        try:
            _register(conn, "num_tbl", [{"ID": 1.0, "QTY": 10.0}], ["ID", "QTY"])
            result = conn.execute("SELECT typeof(\"ID\") FROM num_tbl").fetchone()
            assert "INT" in result[0].upper() or "BIGINT" in result[0].upper()
        finally:
            conn.close()


# ---------------------------------------------------------------------------
# select_strategy
# ---------------------------------------------------------------------------

class TestSelectStrategy:
    """Currently only FULL_SNAPSHOT is implemented; all configs must
    resolve to it regardless of row count or PK availability."""

    @pytest.mark.parametrize("row_count, pk_cols", [
        (0, []),
        (500_000, []),
        (500_000, ["ID"]),
    ], ids=["small_no_pk", "large_no_pk", "large_with_pk"])
    def test_always_returns_full_snapshot(self, row_count, pk_cols):
        cfg = TableConfig(
            schema_name="S", table_name="T",
            estimated_row_count=row_count,
            primary_key_columns=pk_cols,
        )

        strategy = select_strategy(cfg, ReplayConfiguration())

        assert strategy.name == DeltaStrategy.FULL_SNAPSHOT
