"""
Unit tests for RabbitBigQueryAdapter incremental strategy support.

These tests verify that the Rabbit adapter declares the same set of valid
incremental strategies as native dbt-bigquery.  The canonical source of truth
is the Jinja-level validation inside BigQuery's own incremental materialization
macro (`dbt_bigquery_validate_get_incremental_strategy`), which accepts only
``merge``, ``insert_overwrite``, and ``microbatch``.

The Rabbit adapter uses a **different** code path for the same validation
(Python-level ``valid_incremental_strategies()`` called from the default
incremental materialization).  If the two lists diverge, users would see
different behaviour depending on whether they target ``bigquery`` or
``rabbitbigquery`` — exactly the kind of parity break this adapter must avoid.
"""

import re
from pathlib import Path

import pytest

from dbt.adapters.rabbitbigquery.impl import RabbitBigQueryAdapter


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _read_bigquery_jinja_accepted_strategies() -> list[str]:
    """Parse the strategy allow-list from BigQuery's incremental macro.

    Reads ``dbt_bigquery_validate_get_incremental_strategy`` and extracts the
    list literal from the ``{% if strategy not in [...] %}`` guard.
    """
    import dbt.include.bigquery as bq_include

    bq_macros_root = Path(bq_include.PACKAGE_PATH) / "macros"
    incremental_sql = bq_macros_root / "materializations" / "incremental.sql"

    if not incremental_sql.exists():
        pytest.skip(f"BigQuery incremental macro not found at {incremental_sql}")

    content = incremental_sql.read_text(encoding="utf-8")

    # The guard looks like:
    #   {% if strategy not in ['merge', 'insert_overwrite', 'microbatch'] %}
    match = re.search(
        r"strategy\s+not\s+in\s+\[([^\]]+)\]",
        content,
    )
    if not match:
        pytest.fail("Could not locate strategy allow-list in " "dbt_bigquery_validate_get_incremental_strategy")

    raw = match.group(1)
    strategies = [s.strip().strip("'\"") for s in raw.split(",")]
    return strategies


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestValidIncrementalStrategies:
    """Verify the adapter's Python-level strategy list is correct."""

    def _get_rabbit_strategies(self) -> list[str]:
        adapter = RabbitBigQueryAdapter.__new__(RabbitBigQueryAdapter)
        return adapter.valid_incremental_strategies()

    def test_matches_bigquery_jinja_validation(self):
        """valid_incremental_strategies must match BigQuery's Jinja allow-list.

        BigQuery's own incremental materialization rejects strategies through
        ``dbt_bigquery_validate_get_incremental_strategy``.  The Rabbit
        adapter's Python list must be identical so users see the same
        behaviour regardless of adapter type.
        """
        bq_strategies = set(_read_bigquery_jinja_accepted_strategies())
        rabbit_strategies = set(self._get_rabbit_strategies())
        assert rabbit_strategies == bq_strategies, (
            f"Rabbit adapter strategies {sorted(rabbit_strategies)} don't match "
            f"BigQuery Jinja strategies {sorted(bq_strategies)}.  "
            f"Extra in Rabbit: {sorted(rabbit_strategies - bq_strategies)}.  "
            f"Missing in Rabbit: {sorted(bq_strategies - rabbit_strategies)}."
        )

    def test_append_not_accepted(self):
        """'append' must NOT be in valid_incremental_strategies.

        Native BigQuery explicitly rejects 'append' in its Jinja validation.
        Including it in the Rabbit adapter's Python list would create a parity
        break: models using 'append' would be rejected on native BigQuery but
        silently accepted through the Rabbit adapter's default materialization
        path, potentially causing data-quality issues (duplicates, missing
        partition overwrites).
        """
        strategies = self._get_rabbit_strategies()
        assert "append" not in strategies, (
            "'append' should not be in valid_incremental_strategies. " "Native BigQuery rejects it — the Rabbit adapter must do the same."
        )

    def test_core_strategies_present(self):
        """merge, insert_overwrite, microbatch must all be present."""
        strategies = set(self._get_rabbit_strategies())
        required = {"merge", "insert_overwrite", "microbatch"}
        missing = required - strategies
        assert not missing, f"Missing required strategies: {sorted(missing)}"

    def test_delete_insert_not_accepted(self):
        """'delete+insert' must NOT be in valid_incremental_strategies."""
        strategies = self._get_rabbit_strategies()
        assert "delete+insert" not in strategies
        assert "delete_insert" not in strategies


class TestStrategyValidationBehavior:
    """Verify the dbt-core validation logic with the adapter's list.

    ``get_incremental_strategy_macro`` rejects a strategy when it appears
    in ``builtin_incremental_strategies`` but NOT in
    ``valid_incremental_strategies``.  These tests exercise that logic
    without needing a running dbt project.
    """

    BUILTIN = ["append", "delete+insert", "merge", "insert_overwrite", "microbatch"]

    def _would_be_rejected(self, strategy: str) -> bool:
        adapter = RabbitBigQueryAdapter.__new__(RabbitBigQueryAdapter)
        valid = adapter.valid_incremental_strategies() + ["default"]
        return strategy in self.BUILTIN and strategy not in valid

    @pytest.mark.parametrize("strategy", ["merge", "insert_overwrite", "microbatch"])
    def test_supported_strategies_pass_validation(self, strategy):
        assert not self._would_be_rejected(strategy), f"'{strategy}' should pass validation but would be rejected"

    @pytest.mark.parametrize("strategy", ["append", "delete+insert"])
    def test_unsupported_strategies_rejected(self, strategy):
        assert self._would_be_rejected(strategy), f"'{strategy}' should be rejected but would pass validation"

    def test_default_strategy_passes(self):
        assert not self._would_be_rejected("default")

    def test_custom_strategy_passes(self):
        """Non-builtin strategies are treated as custom and never rejected."""
        assert not self._would_be_rejected("my_custom_strategy")
