"""Extended tests for test_runner.validate.transient_table_manager.

Covers helpers and functions not tested in test_transient_table_manager.py:
  - _make_table_name
  - _safe_cast_type
  - execute_with_capture
  - drop_transient_tables
  - materialize_actual_transient_table
  - materialize_baseline_transient_table
"""

from __future__ import annotations

import json
from unittest.mock import MagicMock

import pytest

from test_runner.validate.transient_table_manager import (
    _make_table_name,
    _safe_cast_type,
    execute_with_capture,
    drop_transient_tables,
    materialize_actual_transient_table,
    materialize_baseline_transient_table,
    fetch_baseline_rows,
)


# ---------------------------------------------------------------------------
# _make_table_name
# ---------------------------------------------------------------------------

class TestMakeTableName:
    def test_basic_format(self):
        result = _make_table_name("MYDB", "dbo.GetProducts", "abc123", "SOURCE")
        assert result.startswith("MYDB.VALIDATION.TEMP_SOURCE_")
        assert "ABC123" in result
        assert "DBO_GETPRODUCTS" in result

    def test_dots_replaced_with_underscores(self):
        result = _make_table_name("DB", "master.dbo.Proc", "h1", "TARGET")
        assert "." not in result.split(".", 2)[-1].replace("TEMP_TARGET_", "")

    def test_spaces_replaced(self):
        result = _make_table_name("DB", "dbo.My Proc", "h1", "SOURCE")
        assert " " not in result

    def test_procedure_name_truncated_to_50(self):
        long_name = "a" * 100
        result = _make_table_name("DB", long_name, "h1", "SOURCE")
        # The safe name portion should be at most 50 chars
        parts = result.split("TEMP_SOURCE_")[1]
        safe_part = parts.rsplit("_", 1)[0]
        assert len(safe_part) <= 50

    def test_uppercased(self):
        result = _make_table_name("mydb", "dbo.proc", "abc", "source")
        assert result == result.upper()


# ---------------------------------------------------------------------------
# _safe_cast_type
# ---------------------------------------------------------------------------

class TestSafeCastType:
    def test_bare_number_becomes_double(self):
        assert _safe_cast_type("NUMBER") == "DOUBLE"

    def test_number_case_insensitive(self):
        assert _safe_cast_type("number") == "DOUBLE"
        assert _safe_cast_type("Number") == "DOUBLE"

    def test_number_with_precision_unchanged(self):
        assert _safe_cast_type("NUMBER(38,10)") == "NUMBER(38,10)"

    def test_varchar_unchanged(self):
        assert _safe_cast_type("VARCHAR") == "VARCHAR"

    def test_timestamp_unchanged(self):
        assert _safe_cast_type("TIMESTAMP_NTZ") == "TIMESTAMP_NTZ"

    def test_double_unchanged(self):
        assert _safe_cast_type("DOUBLE") == "DOUBLE"


# ---------------------------------------------------------------------------
# execute_with_capture
# ---------------------------------------------------------------------------

class TestExecuteWithCapture:
    def test_simple_execute_no_capture(self):
        cursor = MagicMock()
        execute_with_capture(cursor, "CALL proc()")
        cursor.execute.assert_called_once_with("CALL proc()")


# ---------------------------------------------------------------------------
# drop_transient_tables
# ---------------------------------------------------------------------------

class TestDropTransientTables:
    def test_drops_all_tables(self):
        connector = MagicMock()
        drop_transient_tables(connector, "DB.VALIDATION.T1", "DB.VALIDATION.T2")
        assert connector.execute_statement.call_count == 2

    def test_ignores_errors(self):
        connector = MagicMock()
        connector.execute_statement.side_effect = RuntimeError("table not found")
        drop_transient_tables(connector, "DB.VALIDATION.BAD")
        # Should not raise

    def test_no_tables_no_calls(self):
        connector = MagicMock()
        drop_transient_tables(connector)
        connector.execute_statement.assert_not_called()


# ---------------------------------------------------------------------------
# materialize_actual_transient_table
# ---------------------------------------------------------------------------

class TestMaterializeActualTransientTable:
    def test_basic_flow(self):
        cursor = MagicMock()
        cursor.sfqid = "query-123"
        connector = MagicMock()
        connector.connection.cursor.return_value = cursor

        table = materialize_actual_transient_table(
            connector, "MYDB", "CALL proc()", "dbo.Proc", "h1"
        )

        assert "MYDB" in table
        assert "VALIDATION" in table
        assert cursor.close.called

    def test_cursor_always_closed(self):
        cursor = MagicMock()
        cursor.execute.side_effect = RuntimeError("boom")
        connector = MagicMock()
        connector.connection.cursor.return_value = cursor

        with pytest.raises(RuntimeError):
            materialize_actual_transient_table(
                connector, "DB", "CALL bad()", "P", "h"
            )

        cursor.close.assert_called_once()


# ---------------------------------------------------------------------------
# materialize_baseline_transient_table
# ---------------------------------------------------------------------------

class TestMaterializeBaselineTransientTable:
    def _make_connector_with_baseline(self, baseline_json: str) -> MagicMock:
        cursor = MagicMock()
        cursor.fetchone.return_value = (baseline_json,)
        connector = MagicMock()
        connector.connection.cursor.return_value = cursor
        return connector

    def test_basic_flow(self):
        baseline = json.dumps({
            "params_hash": "h1",
            "result_sets": [[{"ID": 1, "NAME": "W"}]],
            "column_types": [{"ID": "NUMBER", "NAME": "VARCHAR"}],
        })
        connector = self._make_connector_with_baseline(baseline)

        table = materialize_baseline_transient_table(
            connector, "MYDB", "dbo.P", "h1"
        )

        assert "MYDB" in table
        assert "SOURCE" in table

    def test_no_baseline_creates_dummy_table(self):
        cursor = MagicMock()
        cursor.fetchone.return_value = None
        connector = MagicMock()
        connector.connection.cursor.return_value = cursor

        table = materialize_baseline_transient_table(
            connector, "DB", "dbo.P", "h1"
        )

        assert "DUMMY" in str(
            [c.args[0] for c in cursor.execute.call_args_list]
        ).upper()

    def test_target_col_types_override(self):
        baseline = json.dumps({
            "params_hash": "h1",
            "result_sets": [[{"ID": 1}]],
            "column_types": [{"ID": "NUMBER"}],
        })
        connector = self._make_connector_with_baseline(baseline)

        table = materialize_baseline_transient_table(
            connector, "DB", "dbo.P", "h1",
            target_col_types={"ID": "NUMBER(12,2)"},
        )

        create_sql = [
            c.args[0] for c in connector.connection.cursor.return_value.execute.call_args_list
            if "CREATE" in str(c.args[0]).upper() and "TRANSIENT" in str(c.args[0]).upper()
        ]
        if create_sql:
            assert "NUMBER(12,2)" in create_sql[0]

    def test_cursor_always_closed(self):
        cursor = MagicMock()
        cursor.fetchone.return_value = None
        connector = MagicMock()
        connector.connection.cursor.return_value = cursor

        materialize_baseline_transient_table(connector, "DB", "P", "h")
        cursor.close.assert_called()

    def test_uses_last_result_set_for_capture_baselines(self):
        """For capture baselines, the last result set (data) is used, not the first (OUT row)."""
        baseline = json.dumps({
            "params_hash": "h1",
            "result_sets": [
                [{"OUT_TABLE": "tbl"}],
                [{"ID": 1, "NAME": "Widget"}],
            ],
            "column_types": [
                {"OUT_TABLE": "VARCHAR"},
                {"ID": "NUMBER", "NAME": "VARCHAR"},
            ],
        })
        connector = self._make_connector_with_baseline(baseline)

        table = materialize_baseline_transient_table(
            connector, "DB", "dbo.P", "h1"
        )

        create_calls = [
            c.args[0] for c in connector.connection.cursor.return_value.execute.call_args_list
            if "CREATE" in str(c.args[0]).upper()
        ]
        # The transient table should reference result_sets[1], not result_sets[0]
        assert any("result_sets[1]" in c for c in create_calls)


# ---------------------------------------------------------------------------
# fetch_baseline_rows -- multi result set
# ---------------------------------------------------------------------------

class TestFetchBaselineRowsMultiResultSet:
    def _make_connector(self, raw_json: str | None) -> MagicMock:
        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 test_uses_last_result_set_for_capture(self):
        """For capture baselines with multiple result sets, the last is used."""
        raw = json.dumps({
            "result_sets": [
                [{"OUT_TABLE": "tbl"}],
                [{"ID": 1, "NAME": "Widget"}],
            ],
            "column_types": [
                {"OUT_TABLE": "VARCHAR"},
                {"ID": "NUMBER", "NAME": "VARCHAR"},
            ],
        })
        connector = self._make_connector(raw)
        rows, col_types = fetch_baseline_rows(connector, "DB", "h1")

        assert col_types == {"ID": "NUMBER", "NAME": "VARCHAR"}
        assert len(rows) == 1
        assert rows[0]["ID"] == 1

    def test_single_result_set_uses_index_0(self):
        """With a single result set, index 0 is used (non-capture case)."""
        raw = json.dumps({
            "result_sets": [[{"X": 42}]],
            "column_types": [{"X": "NUMBER"}],
        })
        connector = self._make_connector(raw)
        rows, col_types = fetch_baseline_rows(connector, "DB", "h1")

        assert col_types == {"X": "NUMBER"}
        assert rows[0]["X"] == 42
