# #region Test.SQLGenerator [C:3] [TYPE Module] [SEMANTICS test, translate, sql, generator, upsert, insert]
# @BRIEF Verify SQLGenerator contracts — _normalize_timestamp_value, _quote_identifier, _encode_sql_value,
#         _build_values_clause, generate_insert_sql, generate_upsert_sql, SQLGenerator.generate, SQLGenerator.generate_batch.
# @RELATION BINDS_TO -> [SQLGenerator]
# @TEST_EDGE: empty_columns -> raises ValueError
# @TEST_EDGE: empty_rows -> raises ValueError
# @TEST_EDGE: empty_table -> raises ValueError
# @TEST_EDGE: key_columns_empty_for_upsert -> raises ValueError
# @TEST_EDGE: all_columns_are_keys -> DO NOTHING
# @TEST_EDGE: clickhouse_dialect -> Backtick quoting
# @TEST_EDGE: unknown_dialect -> ANSI fallback
# @TEST_EDGE: timestamp_normalization -> Converts Unix timestamps
from datetime import UTC, datetime
from typing import Any

import pytest

from src.plugins.translate.sql_generator import (
    _normalize_timestamp_value,
    _quote_identifier,
    _encode_sql_value,
    _build_values_clause,
    generate_insert_sql,
    generate_upsert_sql,
    SQLGenerator,
    POSTGRESQL_DIALECTS,
    CLICKHOUSE_DIALECTS,
    BACKTICK_DIALECTS,
)


# ---------------------------------------------------------------------------
# _normalize_timestamp_value
# ---------------------------------------------------------------------------

class TestNormalizeTimestampValue:
    """Verify _normalize_timestamp_value."""

    def test_none_input(self):
        assert _normalize_timestamp_value(None) is None

    def test_string_not_numeric(self):
        assert _normalize_timestamp_value("hello") is None

    def test_unix_seconds(self):
        # 2024-01-15 12:00:00 UTC = 1705320000
        result = _normalize_timestamp_value(1705320000)
        assert result == "2024-01-15"

    def test_unix_milliseconds(self):
        # 2024-01-15 12:00:00 UTC = 1705320000000 ms
        result = _normalize_timestamp_value(1705320000000)
        assert result == "2024-01-15"

    def test_string_unix_timestamp(self):
        result = _normalize_timestamp_value("1705320000")
        assert result == "2024-01-15"

    def test_string_unix_millis(self):
        result = _normalize_timestamp_value("1705320000000.0")
        assert result == "2024-01-15"

    def test_out_of_range_small(self):
        # Too small to be a Unix timestamp
        assert _normalize_timestamp_value(123) is None

    def test_out_of_range_large(self):
        # Way too large
        assert _normalize_timestamp_value(1e16) is None

    def test_negative_value(self):
        assert _normalize_timestamp_value(-1) is None

    def test_overflow_error(self):
        # datetime.fromtimestamp can raise OverflowError for extreme values
        # 1e15 is still within range for most systems
        result = _normalize_timestamp_value(9999999999)  # ~2286
        assert result is not None

    def test_float_milliseconds(self):
        result = _normalize_timestamp_value(1705320000000.0)
        assert result == "2024-01-15"

    def test_empty_string(self):
        assert _normalize_timestamp_value("") is None


# ---------------------------------------------------------------------------
# _quote_identifier
# ---------------------------------------------------------------------------

class TestQuoteIdentifier:
    """Verify _quote_identifier."""

    def test_postgresql_double_quotes(self):
        result = _quote_identifier("my_column", "postgresql")
        assert result == '"my_column"'

    def test_redshift_double_quotes(self):
        result = _quote_identifier("my_column", "redshift")
        assert result == '"my_column"'

    def test_clickhouse_backticks(self):
        result = _quote_identifier("my_column", "clickhouse")
        assert result == "`my_column`"

    def test_clickhousedb_backticks(self):
        result = _quote_identifier("my_column", "clickhousedb")
        assert result == "`my_column`"

    def test_mysql_backticks(self):
        result = _quote_identifier("my_column", "mysql")
        assert result == "`my_column`"

    def test_unknown_dialect_ansi_fallback(self):
        result = _quote_identifier("my_column", "mssql")
        assert result == '"my_column"'

    def test_removes_existing_quotes(self):
        result = _quote_identifier('"my_column"', "postgresql")
        assert result == '"my_column"'

    def test_removes_backticks(self):
        result = _quote_identifier("`my_column`", "clickhouse")
        assert result == "`my_column`"

    def test_removes_brackets(self):
        result = _quote_identifier("[my_column]", "postgresql")
        assert result == '"my_column"'

    def test_empty_identifier(self):
        assert _quote_identifier("", "postgresql") == ""


# ---------------------------------------------------------------------------
# _encode_sql_value
# ---------------------------------------------------------------------------

class TestEncodeSqlValue:
    """Verify _encode_sql_value."""

    def test_none(self):
        assert _encode_sql_value(None) == "NULL"

    def test_bool_true(self):
        assert _encode_sql_value(True) == "TRUE"

    def test_bool_false(self):
        assert _encode_sql_value(False) == "FALSE"

    def test_integer(self):
        assert _encode_sql_value(42) == "42"

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

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

    def test_string_with_quote(self):
        assert _encode_sql_value("it's") == "'it''s'"

    def test_clickhouse_with_timestamp_string(self):
        result = _encode_sql_value("1705320000", dialect="clickhouse")
        assert result == "'2024-01-15'"

    def test_clickhouse_with_timestamp_number(self):
        result = _encode_sql_value(1705320000, dialect="clickhouse")
        assert result == "'2024-01-15'"

    def test_clickhouse_regular_string(self):
        result = _encode_sql_value("hello", dialect="clickhouse")
        assert result == "'hello'"

    def test_clickhouse_no_timestamp_empty_string(self):
        result = _encode_sql_value("", dialect="clickhouse")
        assert result == "''"

    def test_clickhouse_none(self):
        assert _encode_sql_value(None, dialect="clickhouse") == "NULL"

    def test_non_clickhouse_passes_through(self):
        result = _encode_sql_value("1705320000", dialect="postgresql")
        assert result == "'1705320000'"  # Not normalized


# ---------------------------------------------------------------------------
# _build_values_clause
# ---------------------------------------------------------------------------

class TestBuildValuesClause:
    """Verify _build_values_clause."""

    def test_single_row(self):
        rows = [{"col1": "a", "col2": 1}]
        result = _build_values_clause(["col1", "col2"], rows)
        assert result == "('a', 1)"

    def test_multiple_rows(self):
        rows = [
            {"col1": "a", "col2": 1},
            {"col1": "b", "col2": 2},
        ]
        result = _build_values_clause(["col1", "col2"], rows)
        assert "('a', 1)" in result
        assert "('b', 2)" in result
        assert ",\n" in result

    def test_quoted_column_lookup(self):
        """Columns may be quoted; row keys are unquoted."""
        rows = [{"col1": "a"}]
        result = _build_values_clause(['"col1"'], rows)
        assert result == "('a',)" or result == "('a')"

    def test_null_value(self):
        rows = [{"col1": None}]
        result = _build_values_clause(["col1"], rows)
        assert "NULL" in result


# ---------------------------------------------------------------------------
# generate_insert_sql
# ---------------------------------------------------------------------------

class TestGenerateInsertSql:
    """Verify generate_insert_sql."""

    def test_basic_insert(self):
        rows = [{"col1": "a", "col2": 1}]
        sql = generate_insert_sql("public", "my_table", ["col1", "col2"], rows)
        assert sql.startswith("INSERT INTO public.my_table")
        assert "VALUES" in sql
        assert "('a', 1)" in sql

    def test_no_schema(self):
        rows = [{"col1": "a"}]
        sql = generate_insert_sql(None, "my_table", ["col1"], rows)
        assert sql.startswith("INSERT INTO my_table")

    def test_empty_table_raises(self):
        with pytest.raises(ValueError, match="target_table is required"):
            generate_insert_sql(None, "", ["col1"], [{"col1": "a"}])

    def test_empty_columns_raises(self):
        with pytest.raises(ValueError, match="At least one column is required"):
            generate_insert_sql(None, "t", [], [{"col1": "a"}])

    def test_empty_rows_raises(self):
        with pytest.raises(ValueError, match="At least one row is required"):
            generate_insert_sql(None, "t", ["col1"], [])


# ---------------------------------------------------------------------------
# generate_upsert_sql
# ---------------------------------------------------------------------------

class TestGenerateUpsertSql:
    """Verify generate_upsert_sql."""

    def test_basic_upsert(self):
        rows = [{"col1": "a", "col2": 1}]
        sql = generate_upsert_sql(
            "public", "my_table", ["col1", "col2"], ["col1"], rows,
        )
        assert "INSERT INTO" in sql
        assert "ON CONFLICT" in sql
        assert "DO UPDATE SET" in sql
        assert "col2 = EXCLUDED.col2" in sql

    def test_all_columns_are_keys_do_nothing(self):
        """When all columns are conflict keys, use DO NOTHING."""
        rows = [{"col1": "a"}]
        sql = generate_upsert_sql(
            None, "my_table", ["col1"], ["col1"], rows,
        )
        assert "ON CONFLICT" in sql
        assert "DO NOTHING" in sql
        assert "DO UPDATE" not in sql

    def test_no_schema(self):
        rows = [{"col1": "a", "col2": 1}]
        sql = generate_upsert_sql(
            None, "my_table", ["col1", "col2"], ["col1"], rows,
        )
        assert sql.startswith("INSERT INTO my_table")

    def test_empty_table_raises(self):
        with pytest.raises(ValueError, match="target_table is required"):
            generate_upsert_sql(None, "", ["col1"], ["col1"], [{"col1": "a"}])

    def test_empty_columns_raises(self):
        with pytest.raises(ValueError, match="At least one column is required"):
            generate_upsert_sql(None, "t", [], ["col1"], [{"col1": "a"}])

    def test_empty_key_columns_raises(self):
        with pytest.raises(ValueError, match="key_columns are required"):
            generate_upsert_sql(None, "t", ["col1"], [], [{"col1": "a"}])

    def test_empty_rows_raises(self):
        with pytest.raises(ValueError, match="At least one row is required"):
            generate_upsert_sql(None, "t", ["col1"], ["col1"], [])


# ---------------------------------------------------------------------------
# SQLGenerator.generate
# ---------------------------------------------------------------------------

class TestSQLGeneratorGenerate:
    """Verify SQLGenerator.generate."""

    def test_postgresql_insert(self):
        rows = [{"col1": "a"}]
        sql, count = SQLGenerator.generate(
            dialect="postgresql",
            target_schema="public",
            target_table="my_table",
            columns=["col1"],
            rows=rows,
            upsert_strategy="INSERT",
        )
        assert count == 1
        assert '"public"."my_table"' in sql
        assert '"col1"' in sql

    def test_postgresql_upsert(self):
        rows = [{"col1": "a", "col2": 1}]
        sql, count = SQLGenerator.generate(
            dialect="postgresql",
            target_schema="public",
            target_table="my_table",
            columns=["col1", "col2"],
            rows=rows,
            key_columns=["col1"],
            upsert_strategy="MERGE",
        )
        assert count == 1
        assert "ON CONFLICT" in sql

    def test_clickhouse_insert(self):
        rows = [{"col1": "a"}]
        sql, count = SQLGenerator.generate(
            dialect="clickhouse",
            target_schema="default",
            target_table="my_table",
            columns=["col1"],
            rows=rows,
        )
        assert count == 1
        assert "`default`.`my_table`" in sql
        assert "`col1`" in sql

    def test_clickhouse_upsert_fallsback_to_insert(self):
        """ClickHouse does not support ON CONFLICT; fallback to plain INSERT."""
        rows = [{"col1": "a", "col2": 1}]
        sql, count = SQLGenerator.generate(
            dialect="clickhouse",
            target_schema="default",
            target_table="my_table",
            columns=["col1", "col2"],
            rows=rows,
            key_columns=["col1"],
            upsert_strategy="MERGE",
        )
        assert count == 1
        assert "ON CONFLICT" not in sql

    def test_unknown_dialect_insert(self):
        rows = [{"col1": "a"}]
        sql, count = SQLGenerator.generate(
            dialect="mssql",
            target_schema="dbo",
            target_table="my_table",
            columns=["col1"],
            rows=rows,
        )
        assert count == 1
        assert '"dbo"."my_table"' in sql

    def test_unknown_dialect_upsert_fallback(self):
        """Unknown dialect falls back to INSERT with warning."""
        rows = [{"col1": "a", "col2": 1}]
        sql, count = SQLGenerator.generate(
            dialect="mssql",
            target_schema="dbo",
            target_table="my_table",
            columns=["col1", "col2"],
            rows=rows,
            key_columns=["col1"],
            upsert_strategy="MERGE",
        )
        assert count == 1
        assert "ON CONFLICT" not in sql

    def test_no_schema(self):
        rows = [{"col1": "a"}]
        sql, count = SQLGenerator.generate(
            dialect="postgresql",
            target_schema=None,
            target_table="my_table",
            columns=["col1"],
            rows=rows,
        )
        assert '"my_table"' in sql

    def test_empty_table_raises(self):
        with pytest.raises(ValueError, match="target_table is required"):
            SQLGenerator.generate(
                dialect="postgresql", target_schema=None,
                target_table="", columns=["c"], rows=[{"c": "a"}],
            )

    def test_empty_columns_raises(self):
        with pytest.raises(ValueError, match="At least one column is required"):
            SQLGenerator.generate(
                dialect="postgresql", target_schema=None,
                target_table="t", columns=[], rows=[{"c": "a"}],
            )

    def test_empty_rows_raises(self):
        with pytest.raises(ValueError, match="At least one row is required"):
            SQLGenerator.generate(
                dialect="postgresql", target_schema=None,
                target_table="t", columns=["c"], rows=[],
            )


# ---------------------------------------------------------------------------
# SQLGenerator.generate_batch
# ---------------------------------------------------------------------------

class TestSQLGeneratorGenerateBatch:
    """Verify SQLGenerator.generate_batch."""

    def test_empty_rows(self):
        statements = SQLGenerator.generate_batch(
            dialect="postgresql",
            target_schema=None,
            target_table="t",
            columns=["c"],
            rows=[],
        )
        assert statements == []

    def test_splits_large_batch(self):
        rows = [{"c": str(i)} for i in range(1500)]
        statements = SQLGenerator.generate_batch(
            dialect="postgresql",
            target_schema=None,
            target_table="t",
            columns=["c"],
            rows=rows,
            max_rows_per_statement=500,
        )
        assert len(statements) == 3
        for sql, count in statements:
            assert count == 500

    def test_single_batch(self):
        rows = [{"c": "a"}, {"c": "b"}]
        statements = SQLGenerator.generate_batch(
            dialect="postgresql",
            target_schema=None,
            target_table="t",
            columns=["c"],
            rows=rows,
            max_rows_per_statement=500,
        )
        assert len(statements) == 1
        assert statements[0][1] == 2

    def test_with_upsert(self):
        rows = [{"c": "a", "d": 1}]
        statements = SQLGenerator.generate_batch(
            dialect="postgresql",
            target_schema="public",
            target_table="t",
            columns=["c", "d"],
            rows=rows,
            key_columns=["c"],
            upsert_strategy="MERGE",
        )
        assert len(statements) == 1
        assert "ON CONFLICT" in statements[0][0]
# #endregion Test.SQLGenerator
