# tests/test_exclude.py

import pytest
from atomsql.orm.models import Model
from atomsql.orm.fields import StringField, IntegerField
from atomsql.orm.query import Q


class Product(Model):
    name = StringField()
    category = StringField()
    price = IntegerField()
    in_stock = IntegerField()  # 1 for True, 0 for False


@pytest.fixture
def product_data(db):
    """Fixture to register the model and seed diverse test data."""
    db.register(Product)

    # Category: Electronics
    Product(name="Laptop", category="Electronics", price=1000, in_stock=1).save()
    Product(name="Smartphone", category="Electronics", price=500, in_stock=1).save()
    Product(name="Headphones", category="Electronics", price=100, in_stock=0).save()

    # Category: Furniture
    Product(name="Desk", category="Furniture", price=200, in_stock=1).save()
    Product(name="Chair", category="Furniture", price=150, in_stock=0).save()

    return Product


def test_exclude_basic(product_data):
    """Test simple exclusion based on a single field."""
    Product = product_data

    # Exclude all Electronics
    qs = Product.objects().exclude(category="Electronics")
    results = list(qs)

    # Should only contain Furniture: Desk and Chair
    assert len(results) == 2
    names = [p.name for p in results]
    assert "Desk" in names
    assert "Chair" in names
    assert "Laptop" not in names


def test_exclude_multiple_kwargs(product_data):
    """
    Test multiple kwargs in a single exclude call.
    Note: exclude(a=1, b=2) translates to NOT (a=1 AND b=2).
    """
    Product = product_data

    # Exclude products that are both Electronics AND in stock
    # Laptop and Smartphone are (Electronics, in_stock=1) -> Should be excluded
    # Headphones is (Electronics, in_stock=0) -> Should be kept
    qs = Product.objects().exclude(category="Electronics", in_stock=1)
    results = list(qs)

    names = [p.name for p in results]
    assert "Headphones" in names
    assert "Desk" in names
    assert "Chair" in names
    assert "Laptop" not in names
    assert "Smartphone" not in names


def test_exclude_chained(product_data):
    """
    Test multiple chained exclude calls.
    Note: .exclude(a=1).exclude(b=1) translates to NOT(a=1) AND NOT(b=1).
    """
    Product = product_data

    # Exclude Category Electronics, then exclude anything in stock
    # 1. Exclude Electronics -> Desk, Chair left
    # 2. Exclude in_stock=1 -> Desk is excluded
    # Result: Only Chair (Furniture, in_stock=0)
    qs = Product.objects().exclude(category="Electronics").exclude(in_stock=1)
    results = list(qs)

    assert len(results) == 1
    assert results[0].name == "Chair"


def test_exclude_with_lookups(product_data):
    """Test exclusion using double-underscore lookups like __gt and __in."""
    Product = product_data

    # Exclude prices greater than 200
    # Excluded: Laptop (1000), Smartphone (500)
    # Kept: Headphones (100), Desk (200), Chair (150)
    qs = Product.objects().exclude(price__gt=200)
    assert len(list(qs)) == 3

    # Exclude specific names using __in
    qs = Product.objects().exclude(name__in=["Laptop", "Desk"])
    names = [p.name for p in list(qs)]
    assert "Laptop" not in names
    assert "Desk" not in names
    assert "Smartphone" in names


def test_exclude_with_q_objects(product_data):
    """Test complex exclusion logic using nested Q objects."""
    Product = product_data

    # Exclude (category is Electronics OR price is less than 150)
    # Logic: NOT (category='Electronics' OR price < 150)
    # Laptop (E): Excluded
    # Smartphone (E): Excluded
    # Headphones (E): Excluded
    # Desk (F, 200): Kept
    # Chair (F, 150): Kept (price is exactly 150, not < 150)
    qs = Product.objects().exclude(Q(category="Electronics") | Q(price__lt=150))
    results = list(qs)

    names = [p.name for p in results]
    assert len(names) == 2
    assert "Desk" in names
    assert "Chair" in names


def test_mixed_filter_and_exclude(product_data):
    """Test the combination of filter() and exclude() in the same chain."""
    Product = product_data

    # Filter Electronics, then exclude those priced under 200
    # Electronics: Laptop (1000), Smartphone (500), Headphones (100)
    # Exclude < 200: Headphones removed
    # Result: Laptop, Smartphone
    qs = Product.objects().filter(category="Electronics").exclude(price__lt=200)
    results = list(qs)

    assert len(results) == 2
    names = [p.name for p in results]
    assert "Laptop" in names
    assert "Smartphone" in names
    assert "Headphones" not in names


def test_exclude_none(product_data):
    """Ensure calling exclude() without arguments returns the original queryset."""
    Product = product_data
    initial_count = len(list(Product.objects()))

    qs = Product.objects().exclude()
    assert len(list(qs)) == initial_count


def test_exclude_binary_expression(product_data):
    """Test exclusion using overloaded operators (e.g., Model.field > value)."""
    Product = product_data

    # Exclude where price > 500
    qs = Product.objects().exclude(Product.price > 500)
    results = list(qs)

    # Laptop (1000) is excluded
    names = [p.name for p in results]
    assert "Laptop" not in names
    assert "Smartphone" in names
