"""Tests for test_runner.common.step_parser."""

from __future__ import annotations

import pytest

from test_runner.common.models.step_types import (
    VALIDATE_SKIP,
    CursorStep,
    ParamStep,
    QueryStep,
    StepBasedValidation,
    TableStep,
    ValidateQuery,
    ValidateResult,
    ValidateTable,
)
from test_runner.common.step_parser import (
    extract_step_parameter_names,
    parse_step,
    parse_step_validation,
)


# ---------------------------------------------------------------------------
# parse_step — QueryStep
# ---------------------------------------------------------------------------


class TestParseQueryStep:
    def test_both_shorthand(self):
        step = parse_step({"both": "SELECT 1"}, 0)
        assert isinstance(step, QueryStep)
        assert step.source_query == "SELECT 1"
        assert step.target_query == "SELECT 1"
        assert step.validate is True

    def test_source_and_target(self):
        step = parse_step({
            "source_query": "EXEC dbo.P @a = {0}",
            "target_query": "CALL P({0})",
        }, 0)
        assert isinstance(step, QueryStep)
        assert step.source_query == "EXEC dbo.P @a = {0}"
        assert step.target_query == "CALL P({0})"

    def test_source_only(self):
        step = parse_step({"source_query": "SELECT 1"}, 0)
        assert isinstance(step, QueryStep)
        assert step.source_query == "SELECT 1"
        assert step.target_query is None

    def test_target_only(self):
        step = parse_step({"target_query": "SELECT 1"}, 0)
        assert isinstance(step, QueryStep)
        assert step.source_query is None
        assert step.target_query == "SELECT 1"

    def test_validate_false(self):
        step = parse_step({"both": "SELECT 1", "validate": False}, 0)
        assert step.validate is False

    def test_validate_list(self):
        step = parse_step({
            "source_query": "EXEC dbo.Dashboard @region = {0}",
            "target_query": "CALL DASHBOARD({0})",
            "validate": [{"table": "#Orders"}, False, {"table": "#Summary"}],
        }, 0)
        assert isinstance(step, QueryStep)
        assert isinstance(step.validate, list)
        assert len(step.validate) == 3
        assert isinstance(step.validate[0], ValidateTable)
        assert step.validate[0].table == "#Orders"
        assert step.validate[1] is VALIDATE_SKIP
        assert isinstance(step.validate[2], ValidateTable)
        assert step.validate[2].table == "#Summary"


# ---------------------------------------------------------------------------
# parse_step — ParamStep
# ---------------------------------------------------------------------------


class TestParseParamStep:
    def test_both_sides(self):
        step = parse_step({
            "source_params": ["@out1", "@out2"],
            "target_params": ["OUT1", "OUT2"],
        }, 0)
        assert isinstance(step, ParamStep)
        assert step.source_params == ["@out1", "@out2"]
        assert step.target_params == ["OUT1", "OUT2"]

    def test_source_only(self):
        step = parse_step({"source_params": ["@x"]}, 0)
        assert isinstance(step, ParamStep)
        assert step.source_params == ["@x"]
        assert step.target_params == []

    def test_target_only(self):
        step = parse_step({"target_params": ["X"]}, 0)
        assert isinstance(step, ParamStep)
        assert step.source_params == []
        assert step.target_params == ["X"]


# ---------------------------------------------------------------------------
# parse_step — TableStep
# ---------------------------------------------------------------------------


class TestParseTableStep:
    def test_shared_table(self):
        step = parse_step({"table": "#Results"}, 0)
        assert isinstance(step, TableStep)
        assert step.source_table == "#Results"
        assert step.target_table == "#Results"

    def test_per_side_tables(self):
        step = parse_step({
            "source_table": "#TempResults",
            "target_table": "TEMPRESULTS",
        }, 0)
        assert isinstance(step, TableStep)
        assert step.source_table == "#TempResults"
        assert step.target_table == "TEMPRESULTS"

    def test_source_table_only(self):
        step = parse_step({"source_table": "#Temp"}, 0)
        assert isinstance(step, TableStep)
        assert step.source_table == "#Temp"
        assert step.target_table is None


# ---------------------------------------------------------------------------
# parse_step — CursorStep
# ---------------------------------------------------------------------------


class TestParseCursorStep:
    def test_shared_cursor(self):
        step = parse_step({"cursor": "mycursor"}, 0)
        assert isinstance(step, CursorStep)
        assert step.source_cursor == "mycursor"
        assert step.target_cursor == "mycursor"

    def test_per_side_cursors(self):
        step = parse_step({
            "source_cursor": "src_cursor",
            "target_cursor": "tgt_cursor",
        }, 0)
        assert isinstance(step, CursorStep)
        assert step.source_cursor == "src_cursor"
        assert step.target_cursor == "tgt_cursor"

    def test_cursor_with_placeholder(self):
        step = parse_step({"cursor": "{1}"}, 0)
        assert isinstance(step, CursorStep)
        assert step.source_cursor == "{1}"
        assert step.target_cursor == "{1}"


# ---------------------------------------------------------------------------
# parse_step — error cases
# ---------------------------------------------------------------------------


class TestParseStepErrors:
    def test_unknown_keys(self):
        with pytest.raises(ValueError, match="Step 0.*unknown key"):
            parse_step({"foo": "bar"}, 0)

    def test_mixed_step_types(self):
        with pytest.raises(ValueError, match="Step 2.*mixed step-type"):
            parse_step({"both": "SELECT 1", "table": "#Temp"}, 2)

    def test_no_step_keys(self):
        with pytest.raises(ValueError, match="Step 1.*no recognised"):
            parse_step({"validate": False}, 1)

    def test_error_includes_step_index(self):
        with pytest.raises(ValueError, match="Step 5"):
            parse_step({"unknown_key": "value"}, 5)

    def test_invalid_validate_type(self):
        with pytest.raises(ValueError, match="Step 0.*must be true, false, or a list"):
            parse_step({"both": "SELECT 1", "validate": "yes"}, 0)

    def test_empty_dict(self):
        with pytest.raises(ValueError, match="Step 0.*no recognised"):
            parse_step({}, 0)

    def test_both_none_value(self):
        with pytest.raises(ValueError, match="Step 0"):
            parse_step({"both": None}, 0)

    def test_source_query_none_value(self):
        with pytest.raises(ValueError, match="Step 0"):
            parse_step({"source_query": None}, 0)


# ---------------------------------------------------------------------------
# parse_step — validate field defaults
# ---------------------------------------------------------------------------


class TestParseStepValidateDefaults:
    def test_default_true(self):
        step = parse_step({"both": "SELECT 1"}, 0)
        assert step.validate is True

    def test_explicit_true(self):
        step = parse_step({"both": "SELECT 1", "validate": True}, 0)
        assert step.validate is True

    def test_explicit_false(self):
        step = parse_step({"both": "SELECT 1", "validate": False}, 0)
        assert step.validate is False

    def test_list_validate(self):
        step = parse_step({
            "source_query": "EXEC P",
            "validate": ["result", False, {"table": "#T"}, {"target_query": "SELECT 1"}],
        }, 0)
        assert isinstance(step.validate, list)
        assert len(step.validate) == 4
        assert isinstance(step.validate[0], ValidateResult)
        assert step.validate[1] is VALIDATE_SKIP
        assert isinstance(step.validate[2], ValidateTable)
        assert step.validate[2].table == "#T"
        assert isinstance(step.validate[3], ValidateQuery)
        assert step.validate[3].target_query == "SELECT 1"


# ---------------------------------------------------------------------------
# parse_step_validation — full parsing
# ---------------------------------------------------------------------------


class TestParseStepValidation:
    def test_simple(self):
        validation = {
            "steps": [{"both": "EXECUTE dbo.GetProducts"}],
            "test_cases": [[]],
        }
        result = parse_step_validation(validation)
        assert isinstance(result, StepBasedValidation)
        assert len(result.steps) == 1
        assert isinstance(result.steps[0], QueryStep)
        assert result.test_cases == [[]]
        assert result.modifies_data is None
        assert result.affected_tables is None

    def test_with_dml_metadata(self):
        validation = {
            "steps": [
                {"both": "CALL InsertColor({0})", "validate": False},
                {"both": "SELECT * FROM Colors WHERE name = {0}"},
            ],
            "test_cases": [["Crimson"]],
            "modifies_data": True,
            "affected_tables": ["Warehouse.Colors"],
        }
        result = parse_step_validation(validation)
        assert result.modifies_data is True
        assert result.affected_tables == ["Warehouse.Colors"]
        assert len(result.steps) == 2
        assert result.steps[0].validate is False
        assert result.steps[1].validate is True

    def test_multi_step_types(self):
        validation = {
            "steps": [
                {"source_query": "DECLARE @out VARCHAR = {1};", "validate": False},
                {"source_query": "EXEC P @id = {0}", "target_query": "CALL P({0})"},
                {"source_params": ["@out"], "target_params": ["OUT"]},
            ],
            "test_cases": [[3, ""]],
        }
        result = parse_step_validation(validation)
        assert len(result.steps) == 3
        assert isinstance(result.steps[0], QueryStep)
        assert isinstance(result.steps[1], QueryStep)
        assert isinstance(result.steps[2], ParamStep)

    def test_empty_steps_raises(self):
        with pytest.raises(ValueError, match="non-empty list"):
            parse_step_validation({"steps": [], "test_cases": [[]]})

    def test_missing_steps_raises(self):
        with pytest.raises(ValueError, match="non-empty list"):
            parse_step_validation({"test_cases": [[]]})

    def test_multiple_test_cases(self):
        validation = {
            "steps": [{"both": "SELECT {0}"}],
            "test_cases": [[1], [2], [3]],
        }
        result = parse_step_validation(validation)
        assert len(result.test_cases) == 3


# ---------------------------------------------------------------------------
# extract_step_parameter_names
# ---------------------------------------------------------------------------


class TestExtractStepParameterNames:
    def test_named_params(self):
        steps = [QueryStep(source_query="EXEC dbo.P @a = {0}, @b = {1}")]
        assert extract_step_parameter_names(steps) == ["a", "b"]

    def test_declare_params(self):
        steps = [
            QueryStep(
                source_query="DECLARE @out VARCHAR(MAX) = {1};",
                validate=False,
            ),
            QueryStep(
                source_query="EXEC dbo.P @id = {0}",
                target_query="CALL P({0})",
            ),
        ]
        assert extract_step_parameter_names(steps) == ["id", "out"]

    def test_multiple_declare_params(self):
        steps = [
            QueryStep(
                source_query="DECLARE @a INT = {1};\nDECLARE @b INT = {2};",
                validate=False,
            ),
            QueryStep(source_query="EXEC dbo.P @id = {0}"),
        ]
        assert extract_step_parameter_names(steps) == ["id", "a", "b"]

    def test_positional_fallback(self):
        steps = [QueryStep(source_query="SELECT dbo.fn_calc({0}, {1})")]
        assert extract_step_parameter_names(steps) == ["param_0", "param_1"]

    def test_positional_single_param(self):
        steps = [QueryStep(source_query="SELECT fn_now({0})")]
        assert extract_step_parameter_names(steps) == ["param_0"]

    def test_positional_non_sequential(self):
        steps = [QueryStep(source_query="SELECT fn_calc({0}, {2})")]
        assert extract_step_parameter_names(steps) == ["param_0", "param_2"]

    def test_redshift_call_positional(self):
        steps = [
            QueryStep(source_query="CALL proc({0}, {1})", validate=False),
            CursorStep(source_cursor="{1}", target_cursor="{1}"),
        ]
        assert extract_step_parameter_names(steps) == ["param_0", "param_1"]

    def test_no_params(self):
        steps = [QueryStep(source_query="EXECUTE dbo.GetProducts")]
        assert extract_step_parameter_names(steps) == []
