"""Tests for replay.models — TableConfig, make_delta, delta_total_changes.

These are the foundational data types and factory functions used by
every strategy and handler in the replay stack.  Direct unit tests
ensure the canonical delta shape and counting logic are correct
independently of DuckDB or DB connections.
"""

from __future__ import annotations

import pytest

from test_runner.replay.models import (
    DeltaStrategy,
    TableConfig,
    delta_total_changes,
    make_delta,
)


# ---------------------------------------------------------------------------
# TableConfig
# ---------------------------------------------------------------------------

class TestTableConfig:

    def test_fqn_property(self):
        cfg = TableConfig(schema_name="WAREHOUSE", table_name="COLORS")
        assert cfg.fqn == "WAREHOUSE.COLORS"

    def test_fqn_with_empty_schema(self):
        cfg = TableConfig(schema_name="", table_name="COLORS")
        assert cfg.fqn == ".COLORS"

    def test_default_values(self):
        cfg = TableConfig(schema_name="S", table_name="T")
        assert cfg.primary_key_columns == []
        assert cfg.estimated_row_count == 0
        assert cfg.strategy == DeltaStrategy.FULL_SNAPSHOT
        assert cfg.ignore_columns == []

    def test_explicit_values(self):
        cfg = TableConfig(
            schema_name="DBO",
            table_name="ORDERS",
            primary_key_columns=["ORDER_ID"],
            estimated_row_count=5000,
            strategy=DeltaStrategy.PK_DIFF,
            ignore_columns=["VALIDFROM"],
        )
        assert cfg.schema_name == "DBO"
        assert cfg.table_name == "ORDERS"
        assert cfg.primary_key_columns == ["ORDER_ID"]
        assert cfg.estimated_row_count == 5000
        assert cfg.strategy == DeltaStrategy.PK_DIFF
        assert cfg.ignore_columns == ["VALIDFROM"]

    @pytest.mark.parametrize("field", ["primary_key_columns", "ignore_columns"])
    def test_none_list_field_becomes_empty_list(self, field):
        cfg = TableConfig(schema_name="S", table_name="T", **{field: None})
        assert getattr(cfg, field) == []


# ---------------------------------------------------------------------------
# make_delta
# ---------------------------------------------------------------------------

class TestMakeDelta:

    def test_canonical_shape(self):
        delta = make_delta("S.T", DeltaStrategy.FULL_SNAPSHOT)

        assert delta == {
            "table": "S.T",
            "strategy_used": "full_snapshot",
            "inserts": [],
            "deletes": [],
            "updates": [],
        }

    def test_with_inserts_and_deletes(self):
        inserts = [{"ID": 1}]
        deletes = [{"ID": 2}]

        delta = make_delta(
            "WAREHOUSE.COLORS",
            DeltaStrategy.FULL_SNAPSHOT,
            inserts=inserts,
            deletes=deletes,
        )

        assert delta["inserts"] == [{"ID": 1}]
        assert delta["deletes"] == [{"ID": 2}]
        assert delta["updates"] == []

    def test_with_updates(self):
        updates = [{"key": {"ID": 1}, "before": {"NAME": "old"}, "after": {"NAME": "new"}}]

        delta = make_delta(
            "S.T",
            DeltaStrategy.FULL_SNAPSHOT,
            updates=updates,
        )

        assert delta["updates"] == updates

    def test_strategy_serialized_as_string(self):
        delta = make_delta("S.T", DeltaStrategy.FULL_SNAPSHOT)
        assert delta["strategy_used"] == "full_snapshot"
        assert isinstance(delta["strategy_used"], str)

    def test_none_lists_become_empty(self):
        delta = make_delta("S.T", DeltaStrategy.FULL_SNAPSHOT, inserts=None, deletes=None, updates=None)
        assert delta["inserts"] == []
        assert delta["deletes"] == []
        assert delta["updates"] == []


# ---------------------------------------------------------------------------
# delta_total_changes
# ---------------------------------------------------------------------------

class TestDeltaTotalChanges:

    def test_empty_delta_returns_zero(self):
        delta = make_delta("S.T", DeltaStrategy.FULL_SNAPSHOT)
        assert delta_total_changes(delta) == 0

    @pytest.mark.parametrize("category, items, expected", [
        ("inserts", [{"ID": 1}, {"ID": 2}], 2),
        ("deletes", [{"ID": 1}], 1),
        ("updates", [{"key": {}, "before": {}, "after": {}}], 1),
    ], ids=["inserts", "deletes", "updates"])
    def test_counts_single_category(self, category, items, expected):
        delta = make_delta("S.T", DeltaStrategy.FULL_SNAPSHOT, **{category: items})
        assert delta_total_changes(delta) == expected

    def test_counts_all_categories(self):
        delta = make_delta(
            "S.T", DeltaStrategy.FULL_SNAPSHOT,
            inserts=[{"ID": 1}],
            deletes=[{"ID": 2}, {"ID": 3}],
            updates=[{"key": {}, "before": {}, "after": {}}],
        )
        assert delta_total_changes(delta) == 4

    def test_handles_missing_keys(self):
        assert delta_total_changes({}) == 0

    def test_handles_raw_dict(self):
        raw = {"inserts": [1, 2], "deletes": [3], "updates": []}
        assert delta_total_changes(raw) == 3


# ---------------------------------------------------------------------------
# DeltaStrategy enum
# ---------------------------------------------------------------------------

class TestDeltaStrategy:

    @pytest.mark.parametrize("member, expected_value", [
        (DeltaStrategy.FULL_SNAPSHOT, "full_snapshot"),
        (DeltaStrategy.PK_DIFF, "pk_diff"),
        (DeltaStrategy.SYNTHETIC_KEY, "synthetic_key"),
    ])
    def test_enum_value(self, member, expected_value):
        assert member.value == expected_value

    def test_enum_is_str(self):
        assert isinstance(DeltaStrategy.FULL_SNAPSHOT, str)
        assert DeltaStrategy.FULL_SNAPSHOT == "full_snapshot"
