"""Tests for fetch_baseline_rows() in transient_table_manager."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, call

import pytest

from test_runner.validate.transient_table_manager import (
    fetch_baseline_rows,
    is_numeric_type,
    coerce_numeric,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_connector(raw_json: str | None) -> MagicMock:
    """Return a mock connector whose cursor.fetchone() yields *raw_json*."""
    cursor = MagicMock()
    cursor.fetchone.return_value = (raw_json,) if raw_json is not None else None
    connector = MagicMock()
    connector.connection.cursor.return_value = cursor
    return connector


def _build_baseline_json(col_types: dict, rows: list[list]) -> str:
    return json.dumps({
        "column_types": [col_types],
        "result_sets": [rows],
    })


# ---------------------------------------------------------------------------
# TestFetchBaselineRows
# ---------------------------------------------------------------------------

class TestFetchBaselineRows:
    def test_returns_empty_when_no_stage_file(self):
        connector = _make_connector(None)
        rows, col_types = fetch_baseline_rows(connector, "MYDB", "abc123")
        assert rows == []
        assert col_types == {}

    def test_returns_rows_from_list_format(self):
        """Rows stored as plain lists (legacy format) are zipped with col_types."""
        col_types = {"ProductName": "VARCHAR", "UnitsSold": "NUMBER"}
        raw_rows = [["Widget", 100], ["Gadget", 50]]
        connector = _make_connector(_build_baseline_json(col_types, raw_rows))

        rows, returned_col_types = fetch_baseline_rows(connector, "MYDB", "abc123")

        assert returned_col_types == col_types
        assert len(rows) == 2
        assert rows[0] == {"ProductName": "Widget", "UnitsSold": 100}
        assert rows[1] == {"ProductName": "Gadget", "UnitsSold": 50}

    def test_returns_rows_from_dict_format(self):
        """Rows stored as dicts (current capture format) are uppercased to match col_types."""
        col_types = {"PRODUCTNAME": "VARCHAR", "UNITSSOLD": "NUMBER"}
        raw_rows = [
            {"ProductName": "Widget", "UnitsSold": 100},
            {"ProductName": "Gadget", "UnitsSold": 50},
        ]
        connector = _make_connector(_build_baseline_json(col_types, raw_rows))

        rows, returned_col_types = fetch_baseline_rows(connector, "MYDB", "abc123")

        assert returned_col_types == col_types
        assert len(rows) == 2
        # Keys are normalised to uppercase
        assert rows[0] == {"PRODUCTNAME": "Widget", "UNITSSOLD": 100}
        assert rows[1] == {"PRODUCTNAME": "Gadget", "UNITSSOLD": 50}

    def test_empty_result_set(self):
        col_types = {"Col": "VARCHAR"}
        connector = _make_connector(_build_baseline_json(col_types, []))
        rows, _ = fetch_baseline_rows(connector, "MYDB", "abc123")
        assert rows == []

    def test_cursor_closed_on_success(self):
        col_types = {"A": "NUMBER"}
        connector = _make_connector(_build_baseline_json(col_types, [[1]]))
        fetch_baseline_rows(connector, "MYDB", "hash1")
        connector.connection.cursor.return_value.close.assert_called_once()

    def test_cursor_closed_on_no_row(self):
        connector = _make_connector(None)
        fetch_baseline_rows(connector, "MYDB", "hash1")
        connector.connection.cursor.return_value.close.assert_called_once()

    def test_raw_text_format_created(self):
        col_types = {"X": "VARCHAR"}
        connector = _make_connector(_build_baseline_json(col_types, [["a"]]))
        fetch_baseline_rows(connector, "TESTDB", "h1")
        cursor = connector.connection.cursor.return_value
        calls_text = " ".join(str(c) for c in cursor.execute.call_args_list)
        assert "RAW_TEXT_FORMAT" in calls_text

    def test_stage_query_filters_by_params_hash(self):
        col_types = {"N": "NUMBER"}
        connector = _make_connector(_build_baseline_json(col_types, [[42]]))
        fetch_baseline_rows(connector, "DB", "myhash")
        cursor = connector.connection.cursor.return_value
        # Find the SELECT query call
        select_call = next(
            c for c in cursor.execute.call_args_list
            if "SELECT" in str(c)
        )
        assert "myhash" in str(select_call)

    def test_missing_column_types_returns_empty_col_types(self):
        raw = json.dumps({"result_sets": [[[1, 2]]]})
        connector = _make_connector(raw)
        rows, col_types = fetch_baseline_rows(connector, "DB", "h")
        assert col_types == {}
        assert rows == []

    def test_numeric_string_values_coerced(self):
        """NUMBER columns with string values are converted to int/float."""
        col_types = {"STOCKGROUPID": "NUMBER", "TOTALVALUE": "NUMBER", "NAME": "VARCHAR"}
        raw_rows = [{"StockGroupID": 4, "TotalValue": "19303078.50", "Name": "Foo"}]
        connector = _make_connector(_build_baseline_json(col_types, raw_rows))
        rows, _ = fetch_baseline_rows(connector, "DB", "h")
        assert rows[0]["STOCKGROUPID"] == 4
        assert isinstance(rows[0]["STOCKGROUPID"], int)
        assert rows[0]["TOTALVALUE"] == 19303078.5
        assert isinstance(rows[0]["TOTALVALUE"], float)
        assert rows[0]["NAME"] == "Foo"   # VARCHAR left as-is


class TestIsNumericType:
    def test_number_is_numeric(self):
        assert is_numeric_type("NUMBER") is True

    def test_number_with_precision_is_numeric(self):
        assert is_numeric_type("NUMBER(38,10)") is True

    def test_integer_variants(self):
        for t in ("INT", "INTEGER", "BIGINT", "SMALLINT", "BYTEINT"):
            assert is_numeric_type(t) is True, t

    def test_float_variants(self):
        for t in ("FLOAT", "FLOAT4", "FLOAT8", "DOUBLE", "REAL"):
            assert is_numeric_type(t) is True, t

    def test_varchar_not_numeric(self):
        assert is_numeric_type("VARCHAR") is False

    def test_date_not_numeric(self):
        assert is_numeric_type("DATE") is False

    def test_boolean_not_numeric(self):
        assert is_numeric_type("BOOLEAN") is False


class TestCoerceNumeric:
    def test_int_string_becomes_int(self):
        assert coerce_numeric("4") == 4
        assert isinstance(coerce_numeric("4"), int)

    def test_decimal_string_becomes_float(self):
        assert coerce_numeric("19303078.50") == 19303078.5
        assert isinstance(coerce_numeric("19303078.50"), float)

    def test_none_stays_none(self):
        assert coerce_numeric(None) is None

    def test_int_unchanged(self):
        assert coerce_numeric(42) == 42
        assert isinstance(coerce_numeric(42), int)

    def test_float_unchanged(self):
        assert coerce_numeric(3.14) == 3.14

    def test_non_numeric_string_unchanged(self):
        assert coerce_numeric("hello") == "hello"
