"""Tests for test_runner.template."""

from __future__ import annotations

import pytest

from test_runner.common.models import ValidationSteps
from test_runner.common.template import (
    format_sql_literal,
    render_capture_sql,
    render_full_sql,
    render_output_fetch_statements,
    render_template,
    render_validate_resultset_sql,
)


# ---------------------------------------------------------------------------
# format_sql_literal
# ---------------------------------------------------------------------------


class TestFormatSqlLiteral:
    def test_none(self):
        assert format_sql_literal(None) == "NULL"

    def test_int(self):
        assert format_sql_literal(42) == "42"

    def test_float(self):
        assert format_sql_literal(3.14) == "3.14"

    def test_string(self):
        assert format_sql_literal("hello") == "'hello'"

    def test_string_with_quotes(self):
        assert format_sql_literal("it's") == "'it''s'"

    def test_bool_true(self):
        assert format_sql_literal(True) == "1"

    def test_bool_false(self):
        assert format_sql_literal(False) == "0"

    def test_date_string(self):
        assert format_sql_literal("2025-12-09") == "'2025-12-09'"


# ---------------------------------------------------------------------------
# render_template
# ---------------------------------------------------------------------------


class TestRenderTemplate:
    def test_basic_substitution(self):
        template = "EXEC dbo.P @a = {0}, @b = {1}"
        result = render_template(template, [42, "hello"])
        assert result == "EXEC dbo.P @a = 42, @b = 'hello'"

    def test_null_value(self):
        template = "EXEC dbo.P @a = {0}"
        result = render_template(template, [None])
        assert result == "EXEC dbo.P @a = NULL"

    def test_index_out_of_range(self):
        """Unreferenced placeholders are left as-is (manual replace)."""
        template = "EXEC dbo.P @a = {0}, @b = {1}"
        result = render_template(template, [42])
        assert result == "EXEC dbo.P @a = 42, @b = {1}"

    def test_no_placeholders(self):
        template = "EXECUTE master.dbo.GetProducts"
        result = render_template(template, [])
        assert result == "EXECUTE master.dbo.GetProducts"

    def test_string_escaping(self):
        template = "EXEC dbo.P @name = {0}"
        result = render_template(template, ["O'Brien"])
        assert result == "EXEC dbo.P @name = 'O''Brien'"


# ---------------------------------------------------------------------------
# render_full_sql
# ---------------------------------------------------------------------------


class TestRenderFullSql:
    def test_run_only(self):
        steps = ValidationSteps(run="EXEC dbo.P @a = {0}")
        result = render_full_sql(steps, [1])
        assert result == "EXEC dbo.P @a = 1"

    def test_with_pre_execute(self):
        steps = ValidationSteps(
            run="EXEC dbo.P @a = {0}",
            pre_execute="DECLARE @Out INT;",
        )
        result = render_full_sql(steps, [1])
        assert "DECLARE @Out INT;" in result
        assert "EXEC dbo.P @a = 1" in result
        lines = result.split("\n")
        assert lines[0] == "DECLARE @Out INT;"
        assert lines[1] == "EXEC dbo.P @a = 1"

    def test_empty_pre_execute_ignored(self):
        steps = ValidationSteps(run="EXEC dbo.P @a = {0}", pre_execute="")
        result = render_full_sql(steps, [1])
        assert result == "EXEC dbo.P @a = 1"


# ---------------------------------------------------------------------------
# render_full_sql with pre_execute placeholders
# ---------------------------------------------------------------------------


class TestRenderFullSqlPreExecutePlaceholders:
    def test_pre_execute_placeholders_rendered(self):
        steps = ValidationSteps(
            run="EXEC dbo.P @id = {0}, @name = @name OUTPUT",
            pre_execute="DECLARE @name VARCHAR(MAX) = {1};",
        )
        result = render_full_sql(steps, [42, "hello"])
        lines = result.split("\n")
        assert lines[0] == "DECLARE @name VARCHAR(MAX) = 'hello';"
        assert "EXEC dbo.P @id = 42" in lines[1]

    def test_pre_execute_multiple_placeholders(self):
        steps = ValidationSteps(
            run="EXEC dbo.P @id = {0}",
            pre_execute="DECLARE @a INT = {1};\nDECLARE @b INT = {2};",
        )
        result = render_full_sql(steps, [1, 10, 20])
        assert "DECLARE @a INT = 10;" in result
        assert "DECLARE @b INT = 20;" in result


# ---------------------------------------------------------------------------
# render_capture_sql with output_parameters
# ---------------------------------------------------------------------------


class TestRenderCaptureSql:
    def test_without_output_parameters_falls_back(self):
        steps = ValidationSteps(run="EXEC dbo.P @a = {0}")
        result = render_capture_sql(steps, [1])
        assert result == "EXEC dbo.P @a = 1"

    def test_with_output_parameters_uses_template(self):
        steps = ValidationSteps(
            run="EXEC dbo.P @id = {0}, @out = @out OUTPUT",
            pre_execute="DECLARE @out VARCHAR(MAX) = {1};",
            output_parameters=["@out"],
        )
        result = render_capture_sql(steps, [1, ""], "sqlserver")
        assert "DECLARE @out VARCHAR(MAX) = '';" in result
        assert "EXEC dbo.P @id = 1" in result
        assert "SELECT" in result
        assert "@out" in result

    def test_redshift_output_params_returns_plain_call(self):
        """Redshift returns OUT params as result-set columns — source CALL has only IN params."""
        steps = ValidationSteps(
            run="CALL fibonacci({0})",
            output_parameters=["result"],
        )
        result = render_capture_sql(steps, [8, 0], dialect="redshift")
        assert result == "CALL fibonacci(8)"
        assert "SELECT" not in result
        assert "BEGIN" not in result

    def test_redshift_output_params_with_pre_execute(self):
        """pre_execute is still included even for Redshift output params."""
        steps = ValidationSteps(
            run="CALL my_proc({0})",
            pre_execute="SET search_path = myschema;",
            output_parameters=["result"],
        )
        result = render_capture_sql(steps, [1], dialect="redshift")
        assert "SET search_path = myschema;" in result
        assert "CALL my_proc(1)" in result
        assert "SELECT" not in result


# ---------------------------------------------------------------------------
# render_validate_resultset_sql
# ---------------------------------------------------------------------------


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


class TestRenderOutputFetchStatements:
    def test_redshift_cursors_emits_fetch_all(self):
        """output_cursors on Redshift source → FETCH ALL FROM <cursor>."""
        steps = ValidationSteps(
            run="CALL get_sales_by_region({0}, {1})",
            output_cursors=["{1}"],
        )
        stmts = render_output_fetch_statements(steps, ["South", "sales_cursor"], "redshift")
        assert len(stmts) == 1
        assert stmts[0] == "FETCH ALL FROM sales_cursor"

    def test_snowflake_tables_emits_identifier_select(self):
        """output_tables on Snowflake target → SELECT * FROM IDENTIFIER('table')."""
        steps = ValidationSteps(
            run="CALL GET_SALES_BY_REGION({0}, {1})",
            output_tables=["{1}"],
        )
        stmts = render_output_fetch_statements(steps, ["South", "sales_cursor"], "snowflake")
        assert len(stmts) == 1
        assert "IDENTIFIER" in stmts[0]
        assert "sales_cursor" in stmts[0]

    def test_no_outputs_returns_empty_list(self):
        steps = ValidationSteps(run="CALL sp({0})")
        stmts = render_output_fetch_statements(steps, [42], "redshift")
        assert stmts == []

    def test_cursor_placeholder_resolved_raw(self):
        """Cursor names are resolved as raw strings (no SQL quoting)."""
        steps = ValidationSteps(
            run="CALL proc({0}, {1})",
            output_cursors=["{1}"],
        )
        stmts = render_output_fetch_statements(steps, [1, "cur_eng"], "redshift")
        assert stmts[0] == "FETCH ALL FROM cur_eng"

    def test_multiple_cursors_produces_multiple_stmts(self):
        steps = ValidationSteps(
            run="CALL proc({0}, {1}, {2})",
            output_cursors=["{1}", "{2}"],
        )
        stmts = render_output_fetch_statements(steps, [1, "c1", "c2"], "redshift")
        assert len(stmts) == 2
        assert stmts[0] == "FETCH ALL FROM c1"
        assert stmts[1] == "FETCH ALL FROM c2"

    def test_raises_when_output_placeholder_index_missing(self):
        steps = ValidationSteps(
            run="CALL proc({0})",
            output_tables=["tmp_{1}"],
        )
        with pytest.raises(ValueError, match="out of range"):
            render_output_fetch_statements(steps, ["only_one_param"], "snowflake")


class TestRenderValidateResultsetSql:
    def test_without_output_parameters_returns_full_sql(self):
        steps = ValidationSteps(run="CALL sp({0})")
        result = render_validate_resultset_sql(steps, [42])
        assert result == "CALL sp(42)"

    def test_with_output_parameters_wraps_in_block(self):
        steps = ValidationSteps(
            run="CALL DBO.P({0}, :X)",
            pre_execute="LET X VARCHAR := {1};",
            output_parameters=["X"],
        )
        result = render_validate_resultset_sql(steps, [1, ""])
        assert result.startswith("BEGIN")
        assert "LET X VARCHAR := '';" in result
        assert "LET _RS RESULTSET := (CALL DBO.P(1, :X))" in result
        assert "RETURN TABLE(_RS);" in result
        assert result.strip().endswith("END;")

    def test_with_pre_execute_and_no_output_parameters_wraps_in_block(self):
        steps = ValidationSteps(
            run="CALL out_param_test.sp_build_summary_table(:P_TABLE_NAME)",
            pre_execute="LET P_TABLE_NAME VARCHAR := {0};",
            output_tables=["{0}"],
        )
        result = render_validate_resultset_sql(steps, ["tmp_summary_123"])
        assert result.startswith("BEGIN")
        assert "LET P_TABLE_NAME VARCHAR := 'tmp_summary_123';" in result
        assert "CALL out_param_test.sp_build_summary_table(:P_TABLE_NAME);" in result
        assert "RETURN TABLE(_RS);" not in result
        assert result.strip().endswith("END;")
