"""Tests for test_runner.validate.targets.snowflake (mocked connector)."""

from __future__ import annotations

from unittest.mock import MagicMock, call, patch

import pytest

from test_runner.validate.targets.snowflake import (
    SnowflakeExecutor,
    SnowflakeExecutorFactory,
)
from test_runner.common.database_dialect import DatabaseDialect
from test_runner.common.models import ValidationSteps


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

def _sf_desc(name: str, type_code: int):
    """Build a single Snowflake cursor.description tuple."""
    return (name, type_code, None, None, None, None, True)


def _make_sf_mock_connector(
    description: list[tuple],
    rows: list[dict],
) -> MagicMock:
    """Create a mock Snowflake connector with cursor-based execution."""
    mock_cursor = MagicMock()
    mock_cursor.description = description
    mock_cursor.fetchall.return_value = rows
    mock_cursor.close = MagicMock()

    mock_connection = MagicMock()
    mock_connection.cursor.return_value = mock_cursor

    connector = MagicMock()
    connector.connection = mock_connection
    return connector


# ---------------------------------------------------------------------------
# SnowflakeExecutorFactory
# ---------------------------------------------------------------------------

class TestSnowflakeExecutorFactory:
    def test_dialect(self):
        factory = SnowflakeExecutorFactory()
        assert factory.dialect == DatabaseDialect.SNOWFLAKE

    def test_build_config(self):
        factory = SnowflakeExecutorFactory()
        cfg = factory.build_config({"name": "my-conn", "mode": "name"})
        assert cfg.name == "my-conn"

    def test_format_literal_null(self):
        fmt = SnowflakeExecutorFactory().create_literal_formatter()
        assert fmt.format_literal(None) == "NULL"

    def test_format_literal_bool_true(self):
        fmt = SnowflakeExecutorFactory().create_literal_formatter()
        assert fmt.format_literal(True) == "TRUE"

    def test_format_literal_bool_false(self):
        fmt = SnowflakeExecutorFactory().create_literal_formatter()
        assert fmt.format_literal(False) == "FALSE"

    def test_format_literal_int(self):
        fmt = SnowflakeExecutorFactory().create_literal_formatter()
        assert fmt.format_literal(42) == "42"

    def test_format_literal_string(self):
        fmt = SnowflakeExecutorFactory().create_literal_formatter()
        assert fmt.format_literal("hello") == "'hello'"

    def test_format_literal_string_with_quotes(self):
        fmt = SnowflakeExecutorFactory().create_literal_formatter()
        assert fmt.format_literal("it's") == "'it''s'"


# ---------------------------------------------------------------------------
# SnowflakeExecutor
# ---------------------------------------------------------------------------

class TestSnowflakeExecutor:
    @patch("test_runner.validate.targets.snowflake.sf_connector", create=True)
    def test_execute_returns_rows_with_types(self, _mock_sf):
        desc = [
            _sf_desc("Id", 0),     # FIXED -> NUMBER(38,18)
            _sf_desc("Name", 2),   # TEXT
        ]
        rows = [{"Id": 1, "Name": "Widget"}, {"Id": 2, "Name": "Gadget"}]
        mock = _make_sf_mock_connector(desc, rows)

        executor = SnowflakeExecutor(mock)
        result = executor.execute("CALL dbo.GetProducts()")

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == rows
        assert result.column_types[0] == {"ID": "FIXED", "NAME": "TEXT"}

    @patch("test_runner.validate.targets.snowflake.sf_connector", create=True)
    def test_execute_captures_all_common_types(self, _mock_sf):
        desc = [
            _sf_desc("amount", 0),    # FIXED -> NUMBER(38,18)
            _sf_desc("label", 2),     # TEXT
            _sf_desc("active", 13),   # BOOLEAN
            _sf_desc("created", 8),   # TIMESTAMP_NTZ
            _sf_desc("bday", 3),      # DATE
            _sf_desc("data", 11),     # BINARY
            _sf_desc("ratio", 1),     # REAL
            _sf_desc("ts_tz", 7),     # TIMESTAMP_TZ
            _sf_desc("clock", 12),    # TIME
            _sf_desc("payload", 5),   # VARIANT
        ]
        mock = _make_sf_mock_connector(desc, [])

        executor = SnowflakeExecutor(mock)
        result = executor.execute("SELECT 1")

        types = result.column_types[0]
        assert types == {
            "AMOUNT": "FIXED",
            "LABEL": "TEXT",
            "ACTIVE": "BOOLEAN",
            "CREATED": "TIMESTAMP_NTZ",
            "BDAY": "DATE",
            "DATA": "BINARY",
            "RATIO": "REAL",
            "TS_TZ": "TIMESTAMP_TZ",
            "CLOCK": "TIME",
            "PAYLOAD": "VARIANT",
        }

    def test_execute_handles_error(self):
        connector = MagicMock()
        connector.connection.cursor.side_effect = RuntimeError("Connection failed")

        executor = SnowflakeExecutor(connector)
        result = executor.execute("CALL dbo.P()")

        assert result.success is False
        assert "Connection failed" in result.error

    @patch("test_runner.validate.targets.snowflake.sf_connector", create=True)
    def test_execute_empty_result(self, _mock_sf):
        desc = [_sf_desc("Status", 2)]
        mock = _make_sf_mock_connector(desc, [])

        executor = SnowflakeExecutor(mock)
        result = executor.execute("CALL dbo.P()")

        assert result.success is True
        assert result.result_sets == [[]]
        assert result.column_types[0] == {"STATUS": "TEXT"}

    def test_close_delegates(self):
        mock = MagicMock()
        executor = SnowflakeExecutor(mock)
        executor.close()
        mock.close.assert_called_once()


# ---------------------------------------------------------------------------
# SnowflakeExecutor -- capture support
# ---------------------------------------------------------------------------

def _make_multi_step_mock_connector(
    steps: list[dict],
) -> MagicMock:
    """Create a mock connector where each ``cursor.execute`` call returns
    different description/rows based on call order.

    *steps* is a list of dicts with keys ``description`` and ``rows``.
    The first call to ``execute`` uses ``steps[0]``, the second uses
    ``steps[1]``, etc.
    """
    call_idx = {"i": 0}
    mock_cursor = MagicMock()

    def _execute_side_effect(sql):
        idx = call_idx["i"]
        if idx < len(steps):
            step = steps[idx]
            mock_cursor.description = step["description"]
            mock_cursor.fetchall.return_value = step["rows"]
            mock_cursor.fetchone.return_value = (
                step["rows"][0] if step["rows"] else None
            )
        else:
            mock_cursor.description = None
            mock_cursor.fetchall.return_value = []
            mock_cursor.fetchone.return_value = None
        call_idx["i"] += 1

    mock_cursor.execute = MagicMock(side_effect=_execute_side_effect)
    mock_cursor.close = MagicMock()

    mock_connection = MagicMock()
    mock_connection.cursor.return_value = mock_cursor

    connector = MagicMock()
    connector.connection = mock_connection
    return connector, mock_cursor


class TestSnowflakeExecutorBasic:
    """Basic execution tests (no capture)."""

    @patch("test_runner.validate.targets.snowflake.sf_connector", create=True)
    def test_simple_query(self, _mock_sf):
        """Without any output spec, behavior returns the CALL result."""
        desc = [_sf_desc("total", 0)]
        rows = [{"total": 100}]
        connector, _ = _make_multi_step_mock_connector([
            {"description": desc, "rows": rows},
        ])

        executor = SnowflakeExecutor(connector)
        result = executor.execute("SELECT COUNT(*) AS total")

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == rows
