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

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, PropertyMock, call, patch

import pytest

from test_runner.capture.sources.redshift import (
    RedshiftExecutor,
    RedshiftExecutorFactory,
    RedshiftLiteralFormatter,
)
from test_runner.common.database_dialect import DatabaseDialect
from test_runner.common.models import ValidationSteps


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

_YAML_MAPPING = {
    "INT4": "NUMBER",
    "INT8": "NUMBER",
    "VARCHAR": "VARCHAR",
    "NUMERIC": "NUMBER",
    "FLOAT8": "FLOAT",
    "BOOL": "BOOLEAN",
    "DATE": "DATE",
    "TIMESTAMP": "TIMESTAMP_NTZ",
    "BPCHAR": "VARCHAR",
}


def _make_mock_connector(
    query_description: list[tuple] | None,
    query_rows: list[tuple],
    pg_type_rows: list[tuple] | None = None,
    *,
    fetchone_value: tuple | None = None,
) -> MagicMock:
    """Create a mock Redshift connector.

    The main cursor handles the CALL + result reads.
    A second cursor handles pg_catalog.pg_type lookups.
    """
    main_cursor = MagicMock()
    main_cursor.description = query_description
    main_cursor.fetchall.return_value = query_rows
    main_cursor.fetchone.return_value = fetchone_value

    meta_cursor = MagicMock()
    if pg_type_rows is not None:
        meta_cursor.fetchall.return_value = pg_type_rows
    else:
        meta_cursor.fetchall.return_value = []

    connection = MagicMock()
    connection.cursor.side_effect = [main_cursor, meta_cursor]
    connection.autocommit = True

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


def _make_multi_step_connector(
    steps: list[dict],
    pg_type_rows: list[tuple] | None = None,
) -> tuple[MagicMock, MagicMock]:
    """Create a mock connector where sequential cursor.execute calls
    change description/rows per *steps*.  Also provides a meta cursor
    for pg_type lookups.

    The first ``connection.cursor()`` call returns the main cursor;
    subsequent calls return a separate meta cursor for
    ``_describe_types`` pg_catalog lookups.
    """
    call_idx = {"i": 0}
    main_cursor = MagicMock()

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

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

    meta_cursor = MagicMock()
    meta_cursor.fetchall.return_value = pg_type_rows or []

    cursor_call_idx = {"i": 0}

    def _cursor_side_effect():
        idx = cursor_call_idx["i"]
        cursor_call_idx["i"] += 1
        return main_cursor if idx == 0 else meta_cursor

    connection = MagicMock()
    connection.cursor = MagicMock(side_effect=_cursor_side_effect)
    connection.autocommit = True

    connector = MagicMock()
    connector.connection = connection
    return connector, main_cursor


# ---------------------------------------------------------------------------
# RedshiftLiteralFormatter
# ---------------------------------------------------------------------------

class TestRedshiftLiteralFormatter:
    def test_null(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal(None) == "NULL"

    def test_bool_true(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal(True) == "TRUE"

    def test_bool_false(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal(False) == "FALSE"

    def test_integer(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal(42) == "42"

    def test_float(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal(3.14) == "3.14"

    def test_string(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal("hello") == "'hello'"

    def test_string_with_single_quotes(self):
        fmt = RedshiftLiteralFormatter()
        assert fmt.format_literal("it's") == "'it''s'"

    def test_non_string_non_numeric_value(self):
        fmt = RedshiftLiteralFormatter()
        from datetime import date
        result = fmt.format_literal(date(2026, 1, 1))
        assert result == "'2026-01-01'"


# ---------------------------------------------------------------------------
# RedshiftExecutorFactory
# ---------------------------------------------------------------------------

class TestRedshiftExecutorFactory:
    def test_dialect(self):
        factory = RedshiftExecutorFactory()
        assert factory.dialect == DatabaseDialect.REDSHIFT

    def test_build_config_defaults(self):
        factory = RedshiftExecutorFactory()
        cfg = factory.build_config({})
        assert cfg.host == "localhost"
        assert cfg.port == 5439

    def test_build_config_custom(self):
        factory = RedshiftExecutorFactory()
        cfg = factory.build_config({
            "host": "my-cluster.redshift.amazonaws.com",
            "port": "5440",
            "database": "mydb",
            "username": "admin",
            "password": "s3cret",
        })
        assert cfg.host == "my-cluster.redshift.amazonaws.com"
        assert cfg.port == 5440
        assert cfg.database == "mydb"
        assert cfg.username == "admin"
        assert cfg.password == "s3cret"

    def test_build_config_scai_keys(self):
        """SCAI writes 'user', 'auth_method', and 'server_url' — build_config must accept them."""
        factory = RedshiftExecutorFactory()
        cfg = factory.build_config({
            "server_url": "my-cluster.redshift.amazonaws.com",
            "port": 5439,
            "database": "mydb",
            "user": "admin",
            "password": "s3cret",
            "auth_method": "standard",
        })
        assert cfg.host == "my-cluster.redshift.amazonaws.com"
        assert cfg.username == "admin"
        assert cfg.mode == "credentials"

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

    def test_create_literal_formatter_returns_redshift_formatter(self):
        factory = RedshiftExecutorFactory()
        fmt = factory.create_literal_formatter()
        assert isinstance(fmt, RedshiftLiteralFormatter)


# ---------------------------------------------------------------------------
# RedshiftExecutor - basic execution
# ---------------------------------------------------------------------------

class TestRedshiftExecutor:
    def test_execute_returns_rows(self):
        desc = [("id", 23, None, None, None, None), ("name", 1043, None, None, None, None)]
        rows = [(1, "Widget"), (2, "Gadget")]
        pg_types = [(23, "int4"), (1043, "varchar")]
        connector = _make_mock_connector(desc, rows, pg_types)

        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute("CALL dbo.GetProducts()")

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == [
            {"id": 1, "name": "Widget"},
            {"id": 2, "name": "Gadget"},
        ]
        assert result.row_counts == [2]

    def test_execute_returns_column_types_via_pg_type(self):
        desc = [("id", 23, None, None, None, None), ("name", 1043, None, None, None, None)]
        rows = [(1, "Widget")]
        pg_types = [(23, "int4"), (1043, "varchar")]
        connector = _make_mock_connector(desc, rows, pg_types)

        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute("SELECT 1")

        assert result.column_types[0]["ID"] == "NUMBER"
        assert result.column_types[0]["NAME"] == "VARCHAR"

    def test_execute_empty_result(self):
        desc = [("status", 1043, None, None, None, None)]
        pg_types = [(1043, "varchar")]
        connector = _make_mock_connector(desc, [], pg_types)

        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute("CALL dbo.EmptyProc()")

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

    def test_execute_handles_error(self):
        connector = MagicMock()
        connection = MagicMock()
        connection.autocommit = True
        cursor = MagicMock()
        cursor.execute.side_effect = RuntimeError("Connection refused")
        connection.cursor.return_value = cursor
        connector.connection = connection

        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute("CALL boom()")

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

    def test_execute_no_description_returns_empty(self):
        connector, main_cursor = _make_multi_step_connector([
            {"description": None, "rows": []},  # CALL (no result)
        ])

        executor = RedshiftExecutor(connector)
        result = executor.execute("CALL dml_proc()")

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

    def test_close_delegates_to_connector(self):
        connector = MagicMock()
        connector.connection.autocommit = True
        executor = RedshiftExecutor(connector)
        executor.close()
        connector.close.assert_called_once()

    def test_connector_property(self):
        connector = MagicMock()
        connector.connection.autocommit = True
        executor = RedshiftExecutor(connector)
        assert executor.connector is connector

    def test_autocommit_set_on_init(self):
        connector = MagicMock()
        connector.connection.autocommit = False
        RedshiftExecutor(connector)
        assert connector.connection.autocommit is True

    def test_autocommit_failure_is_silent(self):
        connector = MagicMock()
        type(connector.connection).autocommit = PropertyMock(side_effect=Exception("nope"))
        # Should not raise
        RedshiftExecutor(connector)

    def test_unmapped_types_use_base_type_name(self):
        desc = [("col", 1700, None, None, None, None)]
        rows = [(42,)]
        pg_types = [(1700, "numeric")]
        connector = _make_mock_connector(desc, rows, pg_types)

        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute("SELECT 42")
        assert result.column_types[0]["COL"] == "NUMBER"

    def test_no_datatypes_mapping_uses_raw_base_type(self):
        desc = [("col", 23, None, None, None, None)]
        rows = [(1,)]
        pg_types = [(23, "int4")]
        connector = _make_mock_connector(desc, rows, pg_types)

        executor = RedshiftExecutor(connector, datatypes_mappings=None)
        result = executor.execute("SELECT 1")
        assert result.column_types[0]["COL"] == "INT4"



# ---------------------------------------------------------------------------
# RedshiftExecutor - template-driven output capture
# ---------------------------------------------------------------------------

class TestRedshiftExecutorOutputParams:
    """Scalar OUT/INOUT parameters via output_parameters + fetch_stmts."""

    def test_scalar_output_params_extracted(self):
        """output_parameters extracts named columns from CALL result into output_params."""
        connector, main_cursor = _make_multi_step_connector(
            [
                {  # CALL: returns OUT values as result set
                    "description": [
                        ("result", 23, None, None, None, None),
                    ],
                    "rows": [(21,)],
                },
            ],
            pg_type_rows=[(23, "int4")],
        )

        steps = ValidationSteps(
            run="CALL fibonacci(8)",
            output_parameters=["result"],
        )
        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute(
            "CALL fibonacci(8)", steps=steps, fetch_stmts=[],
        )

        assert result.success is True
        assert result.output_params == {"result": 21}
        assert result.output_param_types["result"] == "NUMBER"
        assert result.result_sets == []

    def test_scalar_params_case_insensitive(self):
        """Column name matching is case-insensitive."""
        connector, main_cursor = _make_multi_step_connector(
            [
                {
                    "description": [("RESULT", 23, None, None, None, None)],
                    "rows": [(42,)],
                },
            ],
            pg_type_rows=[(23, "int4")],
        )

        steps = ValidationSteps(
            run="CALL fib(5, 0)",
            output_parameters=["result"],
        )
        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute("CALL fib(5, 0)", steps=steps, fetch_stmts=[])

        assert result.output_params == {"result": 42}


class TestRedshiftExecutorFetchStmts:
    """Template-driven fetch statements (output.tables / output.cursors)."""

    def test_cursor_fetch_captured(self):
        """A single FETCH ALL statement captures the cursor result set."""
        connector, main_cursor = _make_multi_step_connector(
            [
                {"description": None, "rows": []},  # CALL (no scalar result)
                {  # FETCH ALL result
                    "description": [
                        ("sellerid", 23, None, None, None, None),
                        ("amount", 1700, None, None, None, None),
                    ],
                    "rows": [(1, 500), (2, 300)],
                },
            ],
            pg_type_rows=[(23, "int4"), (1700, "numeric")],
        )

        steps = ValidationSteps(
            run="CALL get_sales_by_region('North', 'mycursor')",
            output_cursors=["{1}"],
        )
        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute(
            "CALL get_sales_by_region('North', 'mycursor')",
            steps=steps,
            fetch_stmts=["FETCH ALL FROM mycursor"],
        )

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == [
            {"sellerid": 1, "amount": 500},
            {"sellerid": 2, "amount": 300},
        ]
        assert result.row_counts == [2]

    def test_table_select_captured(self):
        """A SELECT * FROM table statement captures the table result set."""
        connector, main_cursor = _make_multi_step_connector(
            [
                {"description": None, "rows": []},  # CALL
                {  # SELECT * FROM #temp
                    "description": [("id", 23, None, None, None, None)],
                    "rows": [(10,), (20,)],
                },
            ],
            pg_type_rows=[(23, "int4")],
        )

        steps = ValidationSteps(
            run="CALL proc_with_temp()",
            output_tables=["#temp_results"],
        )
        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute(
            "CALL proc_with_temp()",
            steps=steps,
            fetch_stmts=["SELECT * FROM #temp_results"],
        )

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.row_counts == [2]

    def test_scalar_params_plus_cursor(self):
        """Combined: scalar output params + cursor fetch in one call."""
        connector, main_cursor = _make_multi_step_connector(
            [
                {  # CALL: returns scalar OUT values
                    "description": [
                        ("total_sales", 23, None, None, None, None),
                        ("rs_out", 1043, None, None, None, None),
                    ],
                    "rows": [(42000, "sales_cursor")],
                },
                {  # FETCH ALL FROM cursor
                    "description": [
                        ("sellerid", 23, None, None, None, None),
                        ("amount", 1700, None, None, None, None),
                    ],
                    "rows": [(1, 500)],
                },
            ],
            pg_type_rows=[(23, "int4"), (1043, "varchar"), (1700, "numeric")],
        )

        steps = ValidationSteps(
            run="CALL get_sales_summary('North', 0, 'sales_cursor')",
            output_parameters=["total_sales", "rs_out"],
            output_cursors=["{2}"],
        )
        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute(
            "CALL get_sales_summary('North', 0, 'sales_cursor')",
            steps=steps,
            fetch_stmts=["FETCH ALL FROM sales_cursor"],
        )

        assert result.success is True
        assert result.output_params == {"total_sales": 42000, "rs_out": "sales_cursor"}
        assert result.output_param_types["total_sales"] == "NUMBER"
        assert len(result.result_sets) == 1
        assert result.result_sets[0] == [{"sellerid": 1, "amount": 500}]

    def test_no_fetch_stmts_no_output_params_unchanged(self):
        """Without fetch_stmts or output_parameters, behavior is unchanged."""
        connector, _ = _make_multi_step_connector([
            {
                "description": [("total", 23, None, None, None, None)],
                "rows": [(100,)],
            },
        ])

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

        assert result.success is True
        assert len(result.result_sets) == 1
        assert result.output_params is None

    def test_empty_cursor_result(self):
        """A cursor with no rows still produces an empty result set."""
        connector, main_cursor = _make_multi_step_connector(
            [
                {"description": None, "rows": []},  # CALL
                {  # FETCH ALL: empty
                    "description": [("id", 23, None, None, None, None)],
                    "rows": [],
                },
            ],
            pg_type_rows=[(23, "int4")],
        )

        steps = ValidationSteps(
            run="CALL proc()",
            output_cursors=["mycursor"],
        )
        executor = RedshiftExecutor(connector, _YAML_MAPPING)
        result = executor.execute(
            "CALL proc()", steps=steps,
            fetch_stmts=["FETCH ALL FROM mycursor"],
        )

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


# ---------------------------------------------------------------------------
# render_output_fetch_statements
# ---------------------------------------------------------------------------

class TestRenderOutputFetchStatements:
    """Tests for template-driven rendering of fetch statements."""

    def test_redshift_cursor(self):
        from test_runner.common.template import render_output_fetch_statements
        steps = ValidationSteps(
            run="CALL proc({0}, {1})",
            output_cursors=["{1}"],
        )
        stmts = render_output_fetch_statements(steps, ["North", "mycursor"], "redshift")
        assert stmts == ["FETCH ALL FROM mycursor"]

    def test_redshift_table(self):
        from test_runner.common.template import render_output_fetch_statements
        steps = ValidationSteps(
            run="CALL proc()",
            output_tables=["#temp_results"],
        )
        stmts = render_output_fetch_statements(steps, [], "redshift")
        assert stmts == ["SELECT * FROM #temp_results"]

    def test_snowflake_table(self):
        from test_runner.common.template import render_output_fetch_statements
        steps = ValidationSteps(
            run="CALL proc({0}, {1})",
            output_tables=["{1}"],
        )
        stmts = render_output_fetch_statements(steps, ["North", "mycursor"], "snowflake")
        assert stmts == ["SELECT * FROM IDENTIFIER('mycursor')"]

    def test_combined_tables_and_cursors(self):
        from test_runner.common.template import render_output_fetch_statements
        steps = ValidationSteps(
            run="CALL proc()",
            output_tables=["#temp"],
            output_cursors=["cur1"],
        )
        stmts = render_output_fetch_statements(steps, [], "redshift")
        assert stmts == ["SELECT * FROM #temp", "FETCH ALL FROM cur1"]

    def test_no_outputs_returns_empty(self):
        from test_runner.common.template import render_output_fetch_statements
        steps = ValidationSteps(run="CALL proc()")
        stmts = render_output_fetch_statements(steps, [], "redshift")
        assert stmts == []
