"""Tests for Redshift-specific side-effects capture workflow.

Verifies that ``capture_deltas`` works correctly with Redshift dialect:
- Uses ``RedshiftIntrospector`` for column introspection and identifier casing
- Lowercases table names (Redshift convention)
- Captures deltas via before/after snapshots within TransactionIsolation
"""

from __future__ import annotations

from unittest.mock import MagicMock, call, patch

from test_runner.common.config import ReplayConfiguration
from test_runner.replay.capture_handler import (
    _resolve_table_configs,
    capture_deltas,
)
from test_runner.replay.source_introspector import (
    RedshiftIntrospector,
    SqlServerIntrospector,
)


class TestResolveTableConfigsRedshift:
    """Verify identifier casing with RedshiftIntrospector."""

    def test_two_part_names_lowercased(self):
        introspector = RedshiftIntrospector()
        configs = _resolve_table_configs(["Public.Users"], introspector)

        assert configs[0].schema_name == "public"
        assert configs[0].table_name == "users"

    def test_three_part_names_lowercased(self):
        introspector = RedshiftIntrospector()
        configs = _resolve_table_configs(
            ["ecommerce_db.Public.Customer_Reviews"], introspector,
        )

        assert configs[0].schema_name == "public"
        assert configs[0].table_name == "customer_reviews"

    def test_sqlserver_preserves_case(self):
        introspector = SqlServerIntrospector()
        configs = _resolve_table_configs(["Warehouse.Colors"], introspector)

        assert configs[0].schema_name == "Warehouse"
        assert configs[0].table_name == "Colors"


class TestCaptureDeltasRedshift:
    """Integration test for capture_deltas with Redshift dialect."""

    def _make_connection(self, before_rows, after_rows, count=None):
        conn = MagicMock()
        cursors = []

        # Cursor for TransactionIsolation setup (BEGIN TRANSACTION)
        iso_cursor = MagicMock()
        cursors.append(iso_cursor)

        # Cursor for row count
        count_cursor = MagicMock()
        count_cursor.fetchone.return_value = (count or len(before_rows),)
        cursors.append(count_cursor)

        # Cursor for before snapshot
        before_cursor = MagicMock()
        before_cursor.description = [(k,) for k in before_rows[0]] if before_rows else []
        before_cursor.fetchall.return_value = [
            tuple(r.values()) for r in before_rows
        ] if before_rows else []
        cursors.append(before_cursor)

        # Cursor for after snapshot
        after_cursor = MagicMock()
        after_cursor.description = [(k,) for k in after_rows[0]] if after_rows else []
        after_cursor.fetchall.return_value = [
            tuple(r.values()) for r in after_rows
        ] if after_rows else []
        cursors.append(after_cursor)

        # Cursor for TransactionIsolation teardown (ROLLBACK)
        teardown_cursor = MagicMock()
        cursors.append(teardown_cursor)

        conn.cursor.side_effect = cursors
        conn.autocommit = True
        return conn

    @patch("test_runner.replay.capture_handler.snapshot_table")
    @patch("test_runner.replay.capture_handler.compute_table_delta")
    def test_redshift_uses_lowercase_table_names(
        self, mock_compute, mock_snapshot, 
    ):
        """When dialect is 'redshift', table names must be lowercased."""
        mock_snapshot.return_value = []
        mock_compute.return_value = {
            "table": "", "strategy_used": "full_snapshot",
            "inserts": [], "deletes": [], "updates": [],
        }

        conn = MagicMock()
        conn.autocommit = True
        conn.cursor.return_value = MagicMock()

        from test_runner.capture.sources.redshift import RedshiftExecutorFactory
        factory = RedshiftExecutorFactory()

        capture_deltas(
            connection=conn,
            affected_tables=["ecommerce_db.Public.Customer_Reviews"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: MagicMock(),
            executor_factory=factory,
            code_unit_name="test_proc",
        )

        snapshot_calls = mock_snapshot.call_args_list
        assert len(snapshot_calls) == 2
        assert snapshot_calls[0].args[1] == "public"
        assert snapshot_calls[0].args[2] == "customer_reviews"

    @patch("test_runner.replay.capture_handler.snapshot_table")
    @patch("test_runner.replay.capture_handler.compute_table_delta")
    def test_redshift_introspects_columns(
        self, mock_compute, mock_snapshot,
    ):
        """Redshift dialect should call introspect_columns on the connection."""
        mock_snapshot.return_value = []
        mock_compute.return_value = {
            "table": "", "strategy_used": "full_snapshot",
            "inserts": [], "deletes": [], "updates": [],
        }

        conn = MagicMock()
        conn.autocommit = True
        main_cursor = MagicMock()
        main_cursor.fetchall.return_value = [
            ("public", "customer_reviews", "review_id", '"identity"(1,0)')
        ]
        meta_cursor = MagicMock()
        meta_cursor.fetchone.return_value = (0,)
        conn.cursor.side_effect = [main_cursor, meta_cursor, MagicMock(), MagicMock(), MagicMock()]

        from test_runner.capture.sources.redshift import RedshiftExecutorFactory
        factory = RedshiftExecutorFactory()

        _, _, col_metadata = capture_deltas(
            connection=conn,
            affected_tables=["public.customer_reviews"],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: MagicMock(),
            executor_factory=factory,
            code_unit_name="test_proc",
        )

        assert "PUBLIC.CUSTOMER_REVIEWS" in col_metadata
        assert "REVIEW_ID" in col_metadata["PUBLIC.CUSTOMER_REVIEWS"]["identity_columns"]

    def test_empty_affected_tables_skips_delta_capture(self):
        """With no affected_tables, capture_deltas should just call execute_fn."""
        conn = MagicMock()
        exec_result = MagicMock()
        
        from test_runner.capture.sources.redshift import RedshiftExecutorFactory
        factory = RedshiftExecutorFactory()

        result, deltas, col_meta = capture_deltas(
            connection=conn,
            affected_tables=[],
            replay_cfg=ReplayConfiguration(),
            execute_fn=lambda: exec_result,
            executor_factory=factory,
        )

        assert result is exec_result
        assert deltas == {}
        assert col_meta == {}


class TestNoBaselineResultSetWithEmptyRows:
    """Verify that baselines with result_sets=[[]] and row_counts=[0]
    are treated as 'no result sets' (the Redshift DML false-positive fix)."""

    def test_empty_result_set_is_no_baseline(self):
        """[[]] and [0] should be treated as no baseline result sets."""
        result_sets = [[]]
        row_counts = [0]

        has_nonempty_results = (
            result_sets and any(rs for rs in result_sets)
        )
        has_nonzero_counts = (
            row_counts and any(c > 0 for c in row_counts)
        )
        no_baseline = not has_nonempty_results and not has_nonzero_counts

        assert no_baseline is True

    def test_populated_result_set_is_not_empty(self):
        """[[{"Id": 1}]] and [1] should NOT be treated as no baseline."""
        result_sets = [[{"Id": 1}]]
        row_counts = [1]

        has_nonempty_results = (
            result_sets and any(rs for rs in result_sets)
        )
        has_nonzero_counts = (
            row_counts and any(c > 0 for c in row_counts)
        )
        no_baseline = not has_nonempty_results and not has_nonzero_counts

        assert no_baseline is False

    def test_absent_result_sets_is_empty(self):
        """None/[] should be treated as no baseline."""
        result_sets = []
        row_counts = []

        has_nonempty_results = (
            result_sets and any(rs for rs in result_sets)
        )
        has_nonzero_counts = (
            row_counts and any(c > 0 for c in row_counts)
        )
        no_baseline = not has_nonempty_results and not has_nonzero_counts

        assert no_baseline is True


class TestTargetDatabaseFromCUR:
    """Verify that _validate_case uses test_file.target_database
    for SQL rewriting instead of config.validation_database."""

    def test_procedure_db_uses_target_database(self):
        """procedure_db should come from test_file.target_database."""
        from test_runner.common.models import TestFile, ValidationSteps

        tf = TestFile(
            file_path="/test.yml",
            source=ValidationSteps(run="CALL proc()"),
            test_cases=[[1]],
            target_database="ECOMMERCE_DB",
        )

        procedure_db = (tf.target_database or "FALLBACK").upper()
        assert procedure_db == "ECOMMERCE_DB"

    def test_procedure_db_falls_back_to_validation_database(self):
        """When target_database is None, fall back to validation_database."""
        from test_runner.common.models import TestFile, ValidationSteps

        tf = TestFile(
            file_path="/test.yml",
            source=ValidationSteps(run="CALL proc()"),
            test_cases=[[1]],
            target_database=None,
        )

        validation_database = "TESTINGDEMO"
        procedure_db = (tf.target_database or validation_database or "").upper()
        assert procedure_db == "TESTINGDEMO"
