"""Tests for test_runner.capture.sources.sqlserver (mocked connector)."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from test_runner.capture.sources.sqlserver import (
    SqlServerExecutor,
    SqlServerExecutorFactory,
)
from test_runner.common.database_dialect import DatabaseDialect
from test_runner.common.models import ValidationSteps
from snowflake.snowflake_data_validation.utils.constants import Platform


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

_YAML_MAPPING = {
    "INT": "NUMBER",
    "BIGINT": "NUMBER",
    "NVARCHAR": "VARCHAR",
    "VARCHAR": "VARCHAR",
    "DATETIME2": "TIMESTAMP_NTZ",
    "DATE": "DATE",
    "BIT": "BOOLEAN",
    "DECIMAL": "NUMBER",
    "MONEY": "NUMBER",
    "FLOAT": "FLOAT",
}


def _make_sp_describe_row(name: str, system_type_name: str) -> tuple:
    """Build a mock sp_describe_first_result_set row.

    Layout: (is_hidden, column_ordinal, name, is_nullable, system_type_id,
             system_type_name, ...)
    """
    return (0, 1, name, 1, 56, system_type_name)


def _make_mock_connector(
    query_description: list[tuple],
    query_rows: list[tuple],
    sp_describe_rows: list[tuple],
) -> MagicMock:
    """Create a mock connector with two cursors: one for the query, one for sp_describe."""
    query_cursor = MagicMock()
    query_cursor.description = query_description
    query_cursor.fetchall.return_value = query_rows
    query_cursor.__enter__ = MagicMock(return_value=query_cursor)
    query_cursor.__exit__ = MagicMock(return_value=False)

    meta_cursor = MagicMock()
    meta_cursor.fetchall.return_value = sp_describe_rows
    meta_cursor.__enter__ = MagicMock(return_value=meta_cursor)
    meta_cursor.__exit__ = MagicMock(return_value=False)

    mock_connection = MagicMock()
    mock_connection.cursor.side_effect = [query_cursor, meta_cursor]

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


# ---------------------------------------------------------------------------
# SqlServerExecutor
# ---------------------------------------------------------------------------

class TestSqlServerExecutor:
    def test_execute_returns_rows_with_real_types(self):
        desc = [("Id", int, None, None, 10, 0, True), ("Name", str, None, None, None, None, True)]
        rows = [(1, "Widget"), (2, "Gadget")]
        sp_rows = [
            _make_sp_describe_row("Id", "int"),
            _make_sp_describe_row("Name", "nvarchar(100)"),
        ]
        mock = _make_mock_connector(desc, rows, sp_rows)

        executor = SqlServerExecutor(mock, _YAML_MAPPING)
        result = executor.execute("EXEC dbo.GetProducts")

        assert result.success is True
        assert result.result_sets[0] == [
            {"Id": 1, "Name": "Widget"},
            {"Id": 2, "Name": "Gadget"},
        ]
        assert result.row_counts == [2]
        assert result.column_types[0] == {"ID": "NUMBER", "NAME": "VARCHAR"}

    def test_execute_maps_all_common_types(self):
        desc = [("a", int, None, None, None, None, True)]
        sp_rows = [
            _make_sp_describe_row("id", "int"),
            _make_sp_describe_row("big_id", "bigint"),
            _make_sp_describe_row("name", "nvarchar(200)"),
            _make_sp_describe_row("created", "datetime2(7)"),
            _make_sp_describe_row("active", "bit"),
            _make_sp_describe_row("price", "decimal(18,2)"),
            _make_sp_describe_row("amount", "money"),
            _make_sp_describe_row("ratio", "float"),
            _make_sp_describe_row("bday", "date"),
        ]
        mock = _make_mock_connector(desc, [], sp_rows)

        executor = SqlServerExecutor(mock, _YAML_MAPPING)
        result = executor.execute("EXEC dbo.Proc")

        types = result.column_types[0]
        assert types["ID"] == "NUMBER"
        assert types["BIG_ID"] == "NUMBER"
        assert types["NAME"] == "VARCHAR"
        assert types["CREATED"] == "TIMESTAMP_NTZ"
        assert types["ACTIVE"] == "BOOLEAN"
        assert types["PRICE"] == "NUMBER"
        assert types["AMOUNT"] == "NUMBER"
        assert types["RATIO"] == "FLOAT"
        assert types["BDAY"] == "DATE"

    def test_execute_handles_error(self):
        connector = MagicMock()
        connector.connection.cursor.side_effect = Exception("Connection lost")

        executor = SqlServerExecutor(connector, _YAML_MAPPING)
        result = executor.execute("EXEC dbo.NonExistent")

        assert result.success is False
        assert "Connection lost" in (result.error or "")

    def test_execute_empty_result_set_still_captures_types(self):
        desc = [("Status", str, None, None, None, None, True)]
        sp_rows = [_make_sp_describe_row("Status", "varchar(50)")]
        mock = _make_mock_connector(desc, [], sp_rows)

        executor = SqlServerExecutor(mock, _YAML_MAPPING)
        result = executor.execute("EXEC dbo.EmptyProc")

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

    def test_sp_describe_failure_returns_empty_types(self):
        query_cursor = MagicMock()
        query_cursor.description = [("Id", int, None, None, 10, 0, True)]
        query_cursor.fetchall.return_value = [(1,)]
        query_cursor.__enter__ = MagicMock(return_value=query_cursor)
        query_cursor.__exit__ = MagicMock(return_value=False)

        meta_cursor = MagicMock()
        meta_cursor.__enter__ = MagicMock(return_value=meta_cursor)
        meta_cursor.__exit__ = MagicMock(return_value=False)
        meta_cursor.execute.side_effect = Exception("sp_describe failed")

        mock_connection = MagicMock()
        mock_connection.cursor.side_effect = [query_cursor, meta_cursor]
        connector = MagicMock()
        connector.connection = mock_connection

        executor = SqlServerExecutor(connector, _YAML_MAPPING)
        result = executor.execute("EXEC dbo.P")

        assert result.success is True
        assert result.column_types == [{}]

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


# ---------------------------------------------------------------------------
# SqlServerExecutor -- output parameters
# ---------------------------------------------------------------------------


class TestSqlServerExecutorOutputParams:
    """Tests for output parameter capture (the last result set in the batch)."""

    def test_captures_output_params_and_types(self):
        """When output_parameters is set, the last result set is stored as output_params."""
        import decimal

        cursor = MagicMock()
        cursor.description = [
            ("productName", str, None, None, None, None, True),
            ("unitPrice", decimal.Decimal, None, None, None, None, True),
        ]
        cursor.fetchall.return_value = [("Gizmo", decimal.Decimal("14.75"))]
        cursor.nextset.return_value = False
        cursor.__enter__ = MagicMock(return_value=cursor)
        cursor.__exit__ = MagicMock(return_value=False)

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

        connector = MagicMock()
        connector.connection = mock_connection

        steps = ValidationSteps(
            run="EXEC dbo.P @id = {0}, @name = @name OUTPUT",
            output_parameters=["@productName", "@unitPrice"],
        )

        executor = SqlServerExecutor(connector, _YAML_MAPPING)
        result = executor.execute("EXEC ...", steps=steps)

        assert result.success is True
        assert result.output_params == {
            "productName": "Gizmo",
            "unitPrice": decimal.Decimal("14.75"),
        }
        assert result.output_param_types is not None
        assert "PRODUCTNAME" in result.output_param_types
        assert "UNITPRICE" in result.output_param_types

    def test_no_proc_result_set_only_output_params(self):
        """When the procedure has no SELECT, only the output params are captured."""
        cursor = MagicMock()
        cursor.description = [
            ("name", str, None, None, None, None, True),
        ]
        cursor.fetchall.return_value = [("Widget",)]
        cursor.nextset.return_value = False
        cursor.__enter__ = MagicMock(return_value=cursor)
        cursor.__exit__ = MagicMock(return_value=False)

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

        connector = MagicMock()
        connector.connection = mock_connection

        steps = ValidationSteps(
            run="EXEC dbo.P @id = {0}",
            output_parameters=["@name"],
        )

        executor = SqlServerExecutor(connector, _YAML_MAPPING)
        result = executor.execute("EXEC ...", steps=steps)

        assert result.success is True
        assert result.result_sets == []
        assert result.output_params == {"name": "Widget"}

    def test_without_output_parameters_no_output_params(self):
        """When output_parameters is not set, output_params stays None."""
        desc = [("Id", int, None, None, 10, 0, True)]
        rows = [(1,)]
        sp_rows = [_make_sp_describe_row("Id", "int")]
        mock = _make_mock_connector(desc, rows, sp_rows)

        executor = SqlServerExecutor(mock, _YAML_MAPPING)
        result = executor.execute("EXEC dbo.P")

        assert result.output_params is None
        assert result.output_param_types is None


# ---------------------------------------------------------------------------
# SqlServerExecutor -- fetch_stmts (output.tables)
# ---------------------------------------------------------------------------


class TestSqlServerExecutorFetchStmts:
    """Tests for fetch_stmts support (output.tables / output.cursors)."""

    def test_fetch_stmts_appends_result_sets(self):
        """fetch_stmts results are appended after the main CALL execution."""
        # Main CALL returns no result set (description=None after EXEC)
        cursor = MagicMock()
        cursor.description = None

        # After the CALL, cursor.execute is called with each fetch_stmt.
        # We need description and fetchall to return data for each fetch.
        fetch_desc = [
            ("product_name", str, None, None, None, None, True),
            ("total_qty", int, None, None, None, None, True),
        ]
        fetch_rows = [("Wireless Mouse", 80), ("Mechanical Keyboard", 25)]

        call_count = [0]

        def _execute_side_effect(sql):
            call_count[0] += 1
            if call_count[0] > 1:
                # Only set description for fetch_stmts, not the main CALL
                cursor.description = fetch_desc
                cursor.fetchall.return_value = fetch_rows

        cursor.execute.side_effect = _execute_side_effect
        cursor.__enter__ = MagicMock(return_value=cursor)
        cursor.__exit__ = MagicMock(return_value=False)

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

        connector = MagicMock()
        connector.connection = mock_connection

        executor = SqlServerExecutor(connector)
        result = executor.execute(
            "EXEC test.get_top_products @p_category = 'Electronics'",
            fetch_stmts=["SELECT * FROM temp_top_electronics"],
        )

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == [
            {"product_name": "Wireless Mouse", "total_qty": 80},
            {"product_name": "Mechanical Keyboard", "total_qty": 25},
        ]
        assert result.row_counts == [2]

    def test_fetch_stmts_with_output_params(self):
        """fetch_stmts work alongside output_parameters."""
        # Main CALL produces one result set (the output params SELECT).
        cursor = MagicMock()
        out_desc = [("p_result_table", str, None, None, None, None, True)]
        cursor.description = out_desc
        cursor.fetchall.return_value = [("temp_top_electronics",)]
        cursor.nextset.return_value = False
        cursor.__enter__ = MagicMock(return_value=cursor)
        cursor.__exit__ = MagicMock(return_value=False)

        # After output params are extracted, fetch_stmts run.
        fetch_desc = [
            ("product_name", str, None, None, None, None, True),
            ("total_qty", int, None, None, None, None, True),
        ]
        fetch_rows = [("Widget", 50)]

        call_count = [0]

        def _execute_side_effect(sql):
            call_count[0] += 1
            if call_count[0] > 1:
                # This is a fetch_stmt execution
                cursor.description = fetch_desc
                cursor.fetchall.return_value = fetch_rows

        cursor.execute.side_effect = _execute_side_effect

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

        connector = MagicMock()
        connector.connection = mock_connection

        steps = ValidationSteps(
            run="EXEC test.get_top_products",
            output_parameters=["@p_result_table"],
        )

        executor = SqlServerExecutor(connector)
        result = executor.execute(
            "EXEC ...",
            steps=steps,
            fetch_stmts=["SELECT * FROM temp_top_electronics"],
        )

        assert result.success is True
        assert result.output_params == {"p_result_table": "temp_top_electronics"}
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == [{"product_name": "Widget", "total_qty": 50}]

    def test_fetch_stmts_none_description(self):
        """When a fetch_stmt returns no rows, an empty result set is appended."""
        cursor = MagicMock()
        cursor.description = None
        cursor.__enter__ = MagicMock(return_value=cursor)
        cursor.__exit__ = MagicMock(return_value=False)

        def _execute_side_effect(sql):
            cursor.description = None

        cursor.execute.side_effect = _execute_side_effect

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

        connector = MagicMock()
        connector.connection = mock_connection

        executor = SqlServerExecutor(connector)
        result = executor.execute(
            "EXEC ...",
            fetch_stmts=["SELECT * FROM nonexistent_temp"],
        )

        assert result.success is True
        assert result.result_sets == [[]]
        assert result.row_counts == [0]


# ---------------------------------------------------------------------------
# SqlServerExecutorFactory
# ---------------------------------------------------------------------------

class TestSqlServerExecutorFactory:
    def test_dialect(self):
        factory = SqlServerExecutorFactory()
        assert factory.dialect == DatabaseDialect.SQL_SERVER

    def test_build_config(self):
        factory = SqlServerExecutorFactory()
        cfg = factory.build_config({
            "host": "myhost",
            "port": 2433,
            "database": "MyDB",
            "username": "admin",
            "password": "pass",
        })
        assert cfg.host == "myhost"
        assert cfg.port == 2433

    def test_build_config_scai_keys(self):
        """SCAI writes 'user', 'server_url', and 'auth_method' — build_config must accept them."""
        factory = SqlServerExecutorFactory()
        cfg = factory.build_config({
            "server_url": "myhost",
            "port": 1433,
            "database": "MyDB",
            "user": "sa",
            "password": "pass",
            "auth_method": "standard",
        })
        assert cfg.host == "myhost"
        assert cfg.username == "sa"
        assert cfg.mode == "sql_auth"

    def test_build_config_canonical_keys_take_precedence(self):
        """When both SCAI and canonical keys are present, canonical wins."""
        factory = SqlServerExecutorFactory()
        cfg = factory.build_config({
            "host": "canonical-host",
            "server_url": "scai-host",
            "username": "canonical-user",
            "user": "scai-user",
            "password": "pass",
        })
        assert cfg.host == "canonical-host"
        assert cfg.username == "canonical-user"

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

    def test_format_literal_bool(self):
        fmt = SqlServerExecutorFactory().create_literal_formatter()
        assert fmt.format_literal(True) == "1"
        assert fmt.format_literal(False) == "0"

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

    def test_create_executor_uses_sqlserver_platform_mapping(self):
        """create_executor must call load_datatypes_mapping(Platform.SQLSERVER)."""
        fake_mapping = {"INT": "NUMBER", "NVARCHAR": "VARCHAR"}
        mock_connector = MagicMock()

        with patch(
            "test_runner.capture.sources.sqlserver.load_datatypes_mapping",
            return_value=fake_mapping,
        ) as mock_load, patch(
            "test_runner.capture.sources.sqlserver.SqlServerConnectorFactory.create_connector",
            return_value=mock_connector,
        ):
            factory = SqlServerExecutorFactory()
            executor = factory.create_executor(MagicMock())

        mock_load.assert_called_once_with(Platform.SQLSERVER)
        assert executor._datatypes_mappings is fake_mapping
