"""Tests for QueryTurbo parameter extraction without as_sql()."""

from __future__ import annotations

import pytest
from django.db.models import Count, F, Q, Value
from django.db.models.functions import Coalesce

from query_doctor.turbo.params import ParamExtractionError, extract_params
from tests.testapp.models import Author, Book


def _get_compiler(queryset):
    """Get the SQLCompiler for a queryset without executing it."""
    query = queryset.query
    compiler = query.get_compiler(using="default")
    return query, compiler


def _get_as_sql_params(queryset):
    """Get the params from as_sql() for comparison."""
    query = queryset.query
    compiler = query.get_compiler(using="default")
    _sql, params = compiler.as_sql()
    return tuple(params)


@pytest.mark.django_db
class TestParamExtractionSimple:
    """Parameter extraction for simple filter queries."""

    def test_single_exact_filter(self):
        """filter(field=value) extracts the value."""
        qs = Book.objects.filter(price=10)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_multiple_filters(self):
        """filter(a=1, b=2) extracts both values in correct order."""
        qs = Book.objects.filter(price=10, title="A")
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_exclude(self):
        """exclude(field=value) extracts the value."""
        qs = Book.objects.exclude(price=10)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_chained_filters(self):
        """filter().exclude() chain extracts all values."""
        qs = Book.objects.filter(price__gt=5).exclude(title="X")
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_no_filters(self):
        """Unfiltered query has no params."""
        qs = Book.objects.all()
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected
        assert len(extracted) == 0


@pytest.mark.django_db
class TestParamExtractionLookups:
    """Parameter extraction for various lookup types."""

    def test_gt_lookup(self):
        """price__gt=10 extracts the value."""
        qs = Book.objects.filter(price__gt=10)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_contains_lookup(self):
        """title__contains='abc' extracts the pattern."""
        qs = Book.objects.filter(title__contains="abc")
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_in_lookup(self):
        """id__in=[1,2,3] extracts all values."""
        qs = Book.objects.filter(id__in=[1, 2, 3])
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected
        assert len(extracted) == 3

    def test_isnull_lookup(self):
        """field__isnull=True has no param."""
        qs = Book.objects.filter(publisher__isnull=True)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_range_lookup(self):
        """price__range=(10, 50) extracts both bounds."""
        qs = Book.objects.filter(price__range=(10, 50))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected


@pytest.mark.django_db
class TestParamExtractionExpressions:
    """Parameter extraction with F(), Q(), Value() expressions."""

    def test_f_expression_no_params(self):
        """F('field') on RHS adds no params (it's a column reference)."""
        qs = Book.objects.filter(price=F("id"))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_q_or(self):
        """Q(a=1) | Q(b=2) extracts both values."""
        qs = Book.objects.filter(Q(price=10) | Q(title="A"))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_q_and(self):
        """Q(a=1) & Q(b=2) extracts both values."""
        qs = Book.objects.filter(Q(price=10) & Q(title="A"))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_negated_q(self):
        """~Q(a=1) extracts the value."""
        qs = Book.objects.filter(~Q(price=10))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected


@pytest.mark.django_db
class TestParamExtractionAnnotations:
    """Parameter extraction with annotations."""

    def test_count_annotation(self):
        """annotate(cnt=Count('books')) has no filter params."""
        qs = Author.objects.annotate(book_count=Count("books"))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_annotation_with_filter(self):
        """annotate().filter() extracts the filter value."""
        qs = Author.objects.annotate(book_count=Count("books")).filter(book_count__gt=1)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_coalesce_with_value(self):
        """Coalesce with Value() default extracts the default value."""
        from decimal import Decimal

        qs = Book.objects.annotate(safe_price=Coalesce("price", Value(Decimal("0"))))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected


@pytest.mark.django_db
class TestParamExtractionRelated:
    """Parameter extraction with select_related and joins."""

    def test_select_related_with_filter(self):
        """select_related('fk').filter() extracts the filter value."""
        qs = Book.objects.select_related("author").filter(price=10)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_related_field_filter(self):
        """filter(author__name='X') extracts the value."""
        qs = Book.objects.filter(author__name="Alice")
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected


@pytest.mark.django_db
class TestParamExtractionCount:
    """Verify param count matches as_sql() for various query patterns."""

    def test_param_count_simple(self):
        """Param count matches for simple filter."""
        qs = Book.objects.filter(price=10)
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert len(extracted) == len(expected)

    def test_param_count_complex(self):
        """Param count matches for complex query."""
        qs = Book.objects.filter(price__gt=5).exclude(title="X").filter(Q(price=10) | Q(price=20))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert len(extracted) == len(expected)

    def test_param_count_in_lookup(self):
        """Param count matches for __in with N values."""
        for n in [1, 3, 5, 10]:
            qs = Book.objects.filter(id__in=list(range(1, n + 1)))
            query, compiler = _get_compiler(qs)
            extracted = extract_params(query, compiler)
            expected = _get_as_sql_params(qs)

            assert len(extracted) == len(expected), f"Mismatch for __in with {n} values"


@pytest.mark.django_db
class TestParamExtractionError:
    """Error handling in param extraction."""

    def test_extraction_error_is_query_doctor_error(self):
        """ParamExtractionError inherits from QueryDoctorError."""
        from query_doctor.exceptions import QueryDoctorError

        assert issubclass(ParamExtractionError, QueryDoctorError)


@pytest.mark.django_db
class TestParamExtractionEdgeCases:
    """Edge-case parameter extraction paths."""

    def test_case_when_expression(self):
        """Case/When param count matches as_sql()."""
        from django.db.models import Case, IntegerField, When

        qs = Book.objects.annotate(
            status=Case(
                When(price__gt=20, then=Value(1)),
                When(price__gt=10, then=Value(2)),
                default=Value(3),
                output_field=IntegerField(),
            )
        )
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_coalesce_multiple_values(self):
        """Coalesce with multiple Value nodes extracts all."""
        from decimal import Decimal

        qs = Book.objects.annotate(
            safe_price=Coalesce("price", Value(Decimal("0")), Value(Decimal("-1")))
        )
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_negated_q_inside_and(self):
        """~Q inside an AND compound extracts all values."""
        qs = Book.objects.filter(Q(price__gt=5) & ~Q(title="Banned"))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_deeply_nested_where_nodes(self):
        """3+ levels of nested Q objects extract correctly."""
        qs = Book.objects.filter(
            Q(Q(price__gt=5) | Q(price__lt=1)) & Q(Q(title="A") | Q(title="B"))
        )
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_raw_sql_annotation_does_not_crash(self):
        """RawSQL annotation should not crash extraction."""
        from django.db.models.expressions import RawSQL

        qs = Book.objects.annotate(custom=RawSQL("SELECT 1", []))
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert extracted == expected

    def test_complex_query_param_count_matches(self):
        """Complex query with many features: param count matches as_sql."""
        from decimal import Decimal

        qs = (
            Book.objects.select_related("author", "publisher")
            .filter(Q(price__gt=5) | Q(title__contains="test"))
            .exclude(price=0)
            .annotate(
                safe_price=Coalesce("price", Value(Decimal("0"))),
            )
            .filter(author__name__contains="Alice")
        )
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        expected = _get_as_sql_params(qs)

        assert len(extracted) == len(expected)
        assert extracted == expected

    def test_empty_in_list_extraction(self):
        """Param extraction for filter(id__in=[]) must not crash."""
        qs = Book.objects.filter(id__in=[])
        query, compiler = _get_compiler(qs)
        extracted = extract_params(query, compiler)
        assert isinstance(extracted, tuple)
