"""Tests for test_runner.discovery."""

from __future__ import annotations

import shutil
from pathlib import Path

import pytest

from test_runner.common.config import SelectorsConfig
from test_runner.common.models.step_types import VALIDATE_SKIP, ValidateTable
from test_runner.common.discovery import (
    _parse_steps,
    discover_test_files,
    extract_all_parameter_names,
    extract_parameter_names,
)

FIXTURES_DIR = Path(__file__).parent / "fixtures"


# ---------------------------------------------------------------------------
# extract_parameter_names
# ---------------------------------------------------------------------------


class TestExtractParameterNames:
    def test_basic_params(self):
        template = "EXEC dbo.P @a = {0}, @b = {1}"
        assert extract_parameter_names(template) == ["a", "b"]

    def test_no_params(self):
        assert extract_parameter_names("EXECUTE dbo.GetProducts") == []

    def test_mixed_spacing(self):
        template = "EXEC dbo.P @x={0}, @y ={1}, @z = {2}"
        assert extract_parameter_names(template) == ["x", "y", "z"]


# ---------------------------------------------------------------------------
# discover_test_files
# ---------------------------------------------------------------------------


class TestDiscoverTestFiles:
    def test_discovers_files(self, tmp_project: Path):
        files = discover_test_files(tmp_project)
        assert len(files) == 2
        names = {f.code_unit_name for f in files}
        assert "master.dbo.GetProducts" in names
        assert "master.dbo.GetCustomerOrders" in names

    def test_extracts_test_cases(self, tmp_project: Path):
        files = discover_test_files(tmp_project)
        orders = next(f for f in files if "GetCustomerOrders" in f.code_unit_name)
        assert len(orders.test_cases) == 3
        assert orders.test_cases[0] == [1001, "active"]

    def test_extracts_parameter_names(self, tmp_project: Path):
        files = discover_test_files(tmp_project)
        orders = next(f for f in files if "GetCustomerOrders" in f.code_unit_name)
        assert orders.parameter_names == ["customerId", "status"]

    def test_no_artifacts_dir(self, tmp_path: Path):
        files = discover_test_files(tmp_path)
        assert files == []

    def test_selector_schema_filter(self, tmp_project_with_rpt: Path):
        """Only RPT files when schemas filter is ['RPT']."""
        selectors = SelectorsConfig(schemas=["RPT"])
        files = discover_test_files(tmp_project_with_rpt, selectors)
        assert len(files) == 1
        assert "RPT" in files[0].file_path.upper()

    def test_selector_exclude(self, tmp_project: Path):
        selectors = SelectorsConfig(exclude=["getproducts.a1b2c3d4"])
        files = discover_test_files(tmp_project, selectors)
        assert len(files) == 1
        assert "GetCustomerOrders" in files[0].code_unit_name

    def test_rpt_schema_procedure(self, tmp_project_with_rpt: Path):
        files = discover_test_files(tmp_project_with_rpt)
        rpt_file = next(f for f in files if f.parameter_names and "lspId" in f.parameter_names)
        assert rpt_file.parameter_names == ["lspId", "locId", "fromDate"]
        assert len(rpt_file.test_cases) == 2

    def test_discovers_output_parameters(self, tmp_project_with_output_params: Path):
        files = discover_test_files(tmp_project_with_output_params)
        details = next(f for f in files if "GetProductDetails" in f.code_unit_name)
        assert details.source.output_parameters == ["@productName", "@unitPrice"]
        assert details.target is not None
        assert details.target.output_parameters == ["PRODUCTNAME", "UNITPRICE"]

    def test_output_params_parameter_names_include_all(
        self, tmp_project_with_output_params: Path,
    ):
        files = discover_test_files(tmp_project_with_output_params)
        details = next(f for f in files if "GetProductDetails" in f.code_unit_name)
        assert details.parameter_names == ["productId", "productName", "unitPrice"]


# ---------------------------------------------------------------------------
# extract_all_parameter_names
# ---------------------------------------------------------------------------


class TestExtractAllParameterNames:
    def test_run_only(self):
        names = extract_all_parameter_names("EXEC dbo.P @a = {0}, @b = {1}")
        assert names == ["a", "b"]

    def test_run_and_pre_execute(self):
        names = extract_all_parameter_names(
            "EXEC dbo.P @id = {0}, @out = @out OUTPUT",
            pre_execute="DECLARE @out VARCHAR(MAX) = {1};",
        )
        assert names == ["id", "out"]

    def test_multiple_pre_execute_params(self):
        names = extract_all_parameter_names(
            "EXEC dbo.P @id = {0}",
            pre_execute="DECLARE @a INT = {1};\nDECLARE @b INT = {2};",
        )
        assert names == ["id", "a", "b"]

    def test_snowflake_let_syntax(self):
        names = extract_all_parameter_names(
            "CALL DBO.P({0}, :X, :Y)",
            pre_execute="LET X VARCHAR := {1};\nLET Y DECIMAL := {2};",
        )
        assert names == ["X", "Y"]

    def test_no_pre_execute(self):
        names = extract_all_parameter_names("EXEC dbo.P @a = {0}")
        assert names == ["a"]

    def test_empty_run_no_pre_execute(self):
        names = extract_all_parameter_names("EXECUTE dbo.GetProducts")
        assert names == []

    # --- UDF positional parameter support ---

    def test_function_positional_params(self):
        names = extract_all_parameter_names("SELECT dbo.fn_calc({0}, {1})")
        assert names == ["param_0", "param_1"]

    def test_function_single_positional_param(self):
        names = extract_all_parameter_names("SELECT fn_now({0})")
        assert names == ["param_0"]

    def test_function_no_params(self):
        names = extract_all_parameter_names("SELECT fn_now()")
        assert names == []

    def test_function_positional_params_non_sequential(self):
        names = extract_all_parameter_names("SELECT fn_calc({0}, {2})")
        assert names == ["param_0", "param_2"]

    def test_named_params_take_precedence_over_positional(self):
        names = extract_all_parameter_names("EXEC dbo.P @a = {0}, @b = {1}")
        assert names == ["a", "b"]


# ---------------------------------------------------------------------------
# _parse_steps with output.parameters
# ---------------------------------------------------------------------------


class TestParseStepsOutputParameters:
    def test_parses_output_parameters(self):
        raw = {
            "run": "EXEC dbo.P @id = {0}",
            "output": {
                "parameters": ["@out1", "@out2"],
            },
        }
        steps = _parse_steps(raw)
        assert steps is not None
        assert steps.output_parameters == ["@out1", "@out2"]
        assert steps.output_tables is None

    def test_parses_output_tables(self):
        raw = {
            "run": "EXEC dbo.P",
            "output": {
                "tables": ["#temp"],
            },
        }
        steps = _parse_steps(raw)
        assert steps is not None
        assert steps.output_parameters is None
        assert steps.output_tables == ["#temp"]
        assert steps.output_cursors is None

    def test_parses_output_cursors(self):
        raw = {
            "run": "CALL get_sales({0}, {1})",
            "output": {
                "cursors": ["{1}"],
            },
        }
        steps = _parse_steps(raw)
        assert steps is not None
        assert steps.output_cursors == ["{1}"]
        assert steps.output_tables is None
        assert steps.output_parameters is None

    def test_parses_combined_output(self):
        raw = {
            "run": "CALL proc({0}, {1}, {2})",
            "output": {
                "parameters": ["total"],
                "cursors": ["{2}"],
            },
        }
        steps = _parse_steps(raw)
        assert steps is not None
        assert steps.output_parameters == ["total"]
        assert steps.output_cursors == ["{2}"]

    def test_no_output_section(self):
        raw = {"run": "EXEC dbo.P @a = {0}"}
        steps = _parse_steps(raw)
        assert steps is not None
        assert steps.output_parameters is None
        assert steps.output_tables is None
        assert steps.output_cursors is None


# ---------------------------------------------------------------------------
# Step-based (v2) discovery
# ---------------------------------------------------------------------------


class TestDiscoverStepBasedFiles:
    def test_discovers_step_based_files(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        assert len(files) == 5
        for f in files:
            assert f.step_validation is not None
            assert f.source.run == "<step-based>"

    def test_step_based_simple_procedure(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        simple = next(f for f in files if "GetProducts" in f.code_unit_name)
        assert len(simple.step_validation.steps) == 1
        assert simple.test_cases == [[]]

    def test_step_based_table_output(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        report = next(f for f in files if "BuildReport" in f.code_unit_name)
        assert len(report.step_validation.steps) == 2
        assert report.step_validation.steps[0].validate is False
        assert report.test_cases == [[42]]

    def test_step_based_parameter_names(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        report = next(f for f in files if "BuildReport" in f.code_unit_name)
        assert report.parameter_names == ["id"]

    def test_step_based_output_params(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        details = next(f for f in files if "GetProductDetails" in f.code_unit_name)
        assert len(details.step_validation.steps) == 3
        assert details.step_validation.steps[0].validate is False
        assert details.test_cases == [[3, "", 0]]

    def test_step_based_cursor_output(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        sales = next(f for f in files if "get_sales_by_region" in f.code_unit_name)
        assert len(sales.step_validation.steps) == 2
        assert sales.test_cases == [["North", "mycursor"]]

    def test_step_based_multi_result_set(self, tmp_project_with_step_based: Path):
        files = discover_test_files(tmp_project_with_step_based)
        dashboard = next(f for f in files if "Dashboard" in f.code_unit_name)
        assert len(dashboard.step_validation.steps) == 1
        validate = dashboard.step_validation.steps[0].validate
        assert isinstance(validate, list)
        assert len(validate) == 3
        assert isinstance(validate[0], ValidateTable)
        assert validate[0].table == "#Orders"
        assert validate[1] is VALIDATE_SKIP
        assert isinstance(validate[2], ValidateTable)
        assert validate[2].table == "#Summary"

    def test_mixed_v1_and_v2(self, tmp_project_mixed: Path):
        files = discover_test_files(tmp_project_mixed)
        v1_files = [f for f in files if f.step_validation is None]
        v2_files = [f for f in files if f.step_validation is not None]
        assert len(v1_files) >= 2
        assert len(v2_files) == 1

    def test_v1_files_unchanged_in_mixed(self, tmp_project_mixed: Path):
        files = discover_test_files(tmp_project_mixed)
        orders = next(
            f for f in files if "GetCustomerOrders" in f.code_unit_name
        )
        assert orders.step_validation is None
        assert orders.source.run == "EXECUTE master.dbo.GetCustomerOrders @customerId = {0}, @status = {1}"
        assert orders.parameter_names == ["customerId", "status"]

    def test_step_based_dml_metadata(self, tmp_project_mixed: Path):
        files = discover_test_files(tmp_project_mixed)
        dml = next(f for f in files if f.step_validation is not None)
        assert dml.step_validation.modifies_data is True
        assert dml.step_validation.affected_tables == ["Warehouse.Colors"]
        assert dml.modifies_data is True
