"""
Comprehensive test suite for QuerySet.distinct() method.

Tests cover:
- Basic DISTINCT functionality
- DISTINCT with field specification (DISTINCT ON for PostgreSQL)
- Integration with filtering, ordering, and other QuerySet methods
- Edge cases (empty querysets, duplicate removal)
- SQL generation verification
- Complex scenarios with relationships
"""

import pytest
from atomsql import Database, Model, StringField, IntegerField, ForeignKey
from atomsql.orm.query import Q


# --- Test Models ---
class User(Model):
    """User model for testing distinct operations."""

    _table_name = "users"

    name = StringField()
    email = StringField()
    age = IntegerField()
    department = StringField()


class Post(Model):
    """Post model with foreign key for testing distinct with relationships."""

    _table_name = "posts"

    title = StringField()
    author = ForeignKey(User)
    category = StringField()
    views = IntegerField(default=0)


@pytest.fixture
def db():
    """Provides a fresh in-memory database."""
    database = Database("sqlite:///:memory:")
    yield database
    database.close()


@pytest.fixture
def users_db(db):
    """Registers User model and populates with test data including duplicates."""
    db.register(User)

    # Create users with some duplicate values to test distinct
    users = [
        User(name="Alice", email="alice@example.com", age=25, department="Engineering"),
        User(name="Bob", email="bob@example.com", age=30, department="Marketing"),
        User(
            name="Charlie",
            email="charlie@example.com",
            age=25,
            department="Engineering",
        ),  # duplicate age, dept
        User(
            name="David", email="david@example.com", age=30, department="Sales"
        ),  # duplicate age
        User(
            name="Eve", email="eve@example.com", age=28, department="Marketing"
        ),  # duplicate dept
        User(
            name="Frank", email="frank@example.com", age=25, department="Sales"
        ),  # duplicate age
    ]

    for user in users:
        user.save(db)

    db.commit()
    return db


@pytest.fixture
def posts_db(db):
    """Registers User and Post models with test data for relationship testing."""
    db.register(User)
    db.register(Post)

    # Create users
    alice = User(
        name="Alice", email="alice@example.com", age=25, department="Engineering"
    )
    alice.save(db)

    bob = User(name="Bob", email="bob@example.com", age=30, department="Marketing")
    bob.save(db)

    # Create posts - some by same author to test distinct
    posts = [
        Post(title="Python Tips", author=alice, category="Tech", views=100),
        Post(
            title="SQL Queries", author=alice, category="Tech", views=150
        ),  # same author, category
        Post(title="Marketing 101", author=bob, category="Business", views=75),
        Post(
            title="Advanced Python", author=alice, category="Tech", views=200
        ),  # same author, category
        Post(
            title="Sales Tactics", author=bob, category="Business", views=120
        ),  # same author, category
    ]

    for post in posts:
        post.save(db)

    db.commit()
    return db


@pytest.fixture
def empty_users_db(db):
    """Registers User model but adds no data."""
    db.register(User)
    return db


class TestDistinctBasic:
    """Test basic distinct() functionality."""

    def test_distinct_without_fields(self, users_db):
        """Test basic DISTINCT functionality without specifying fields."""
        # Get all users - should return all 6
        all_users = list(User.objects().all())
        assert len(all_users) == 6

        # Test distinct() on full records
        # Since each user is unique (different names/emails), should still return 6
        distinct_users = list(User.objects().distinct())
        assert len(distinct_users) == 6

        # All users should still be returned since each record is unique
        names = [user.name for user in distinct_users]
        expected_names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank"]
        assert sorted(names) == sorted(expected_names)

    def test_distinct_with_field_specification(self, users_db):
        """Test DISTINCT ON with specific fields (PostgreSQL style)."""
        # This test verifies the method accepts field parameters
        # SQL generation will depend on database backend
        distinct_qs = User.objects().distinct("age")

        # Verify the queryset has distinct settings configured
        assert distinct_qs._distinct is True
        assert distinct_qs._distinct_fields == ["age"]

        # Test with multiple fields
        distinct_multi = User.objects().distinct("age", "department")
        assert distinct_multi._distinct is True
        assert distinct_multi._distinct_fields == ["age", "department"]

    def test_distinct_immutability(self, users_db):
        """Test that distinct() returns a new QuerySet instance."""
        original_qs = User.objects()
        distinct_qs = original_qs.distinct()

        # Should be different instances
        assert original_qs is not distinct_qs

        # Original should not be modified
        assert original_qs._distinct is False
        assert original_qs._distinct_fields == []

        # New queryset should have distinct enabled
        assert distinct_qs._distinct is True
        assert distinct_qs._distinct_fields == []

    def test_distinct_chaining(self, users_db):
        """Test chaining distinct() with other QuerySet methods."""
        # Test distinct with filter
        filtered_distinct = User.objects().filter(age=25).distinct()
        assert filtered_distinct._distinct is True

        # Test distinct with order_by
        ordered_distinct = User.objects().distinct().order_by("name")
        assert ordered_distinct._distinct is True
        assert ordered_distinct._order_by == "name"

        # Test distinct with limit
        limited_distinct = User.objects().distinct().limit(3)
        assert limited_distinct._distinct is True
        assert limited_distinct._limit == 3

        # Test multiple chaining
        complex_qs = (
            User.objects()
            .filter(department="Engineering")
            .distinct("age")
            .order_by("-name")
            .limit(2)
        )
        assert complex_qs._distinct is True
        assert complex_qs._distinct_fields == ["age"]
        assert complex_qs._order_by == "-name"
        assert complex_qs._limit == 2

    def test_distinct_empty_queryset(self, empty_users_db):
        """Test distinct() on empty queryset."""
        distinct_empty = User.objects().distinct()
        results = list(distinct_empty)
        assert len(results) == 0
        assert distinct_empty._distinct is True


class TestDistinctSQLGeneration:
    """Test SQL generation for distinct queries."""

    def test_distinct_sql_without_fields(self, users_db):
        """Test SQL generation for basic DISTINCT."""
        qs = User.objects().distinct()
        sql, params = qs._build_sql()

        # Should contain DISTINCT keyword
        assert "DISTINCT" in sql.upper()
        assert "SELECT DISTINCT" in sql.upper()
        assert "FROM" in sql.upper()
        assert '"user"' in sql

    def test_distinct_sql_with_fields(self, users_db):
        """Test SQL generation for DISTINCT ON with fields."""
        qs = User.objects().distinct("age", "department")
        sql, params = qs._build_sql()

        # Should contain DISTINCT ON with field names
        assert "DISTINCT" in sql.upper()
        # The specific format depends on backend - SQLite vs PostgreSQL handle this differently
        # But the method should not raise errors

    def test_distinct_sql_with_filter(self, users_db):
        """Test SQL generation for DISTINCT with WHERE clause."""
        qs = User.objects().filter(age__gt=25).distinct()
        sql, params = qs._build_sql()

        assert "DISTINCT" in sql.upper()
        assert "WHERE" in sql.upper()
        assert len(params) == 1
        assert params[0] == 25

    def test_distinct_sql_with_ordering(self, users_db):
        """Test SQL generation for DISTINCT with ORDER BY."""
        qs = User.objects().distinct().order_by("name")
        sql, params = qs._build_sql()

        assert "DISTINCT" in sql.upper()
        assert "ORDER BY" in sql.upper()
        assert '"name"' in sql

    def test_distinct_invalid_field(self, users_db):
        """Test that invalid field names raise appropriate errors."""
        with pytest.raises(ValueError, match="does not exist on model"):
            qs = User.objects().distinct("nonexistent_field")
            # Error should occur during SQL generation, not method call
            sql, params = qs._build_sql()


class TestDistinctWithRelationships:
    """Test distinct() with related models."""

    def test_distinct_with_select_related(self, posts_db):
        """Test distinct() combined with select_related()."""
        # This tests that distinct works with JOINs
        qs = Post.objects().select_related("author").distinct()

        assert qs._distinct is True
        assert "author" in qs.related_fields

        # Should be able to generate SQL without errors
        sql, params = qs._build_sql()
        assert "DISTINCT" in sql.upper()
        assert "JOIN" in sql.upper()

    def test_distinct_on_foreign_key_field(self, posts_db):
        """Test distinct() on foreign key relationships."""
        qs = Post.objects().distinct("author")

        assert qs._distinct is True
        assert qs._distinct_fields == ["author"]

        # Should generate valid SQL
        sql, params = qs._build_sql()
        assert "DISTINCT" in sql.upper()


class TestDistinctEdgeCases:
    """Test edge cases and error conditions."""

    def test_distinct_multiple_calls(self, users_db):
        """Test calling distinct() multiple times."""
        # Second call should override first
        qs1 = User.objects().distinct("age")
        qs2 = qs1.distinct("department")

        assert qs2._distinct is True
        assert qs2._distinct_fields == ["department"]  # Should override, not append

    def test_distinct_with_aggregation(self, users_db):
        """Test distinct() with aggregation methods."""
        # Count with distinct should work
        qs = User.objects().distinct()
        count = qs.count()
        assert isinstance(count, int)

        # Other aggregations should work too
        qs_with_filter = User.objects().filter(age__gte=25).distinct()
        count_filtered = qs_with_filter.count()
        assert isinstance(count_filtered, int)

    def test_distinct_clone_preserves_state(self, users_db):
        """Test that _clone() properly preserves distinct state."""
        original = User.objects().distinct("age", "department")

        # Methods that clone should preserve distinct settings
        cloned = original.filter(name="Alice")
        assert cloned._distinct is True
        assert cloned._distinct_fields == ["age", "department"]

        # first() and last() use _clone() internally
        original_with_data = User.objects().distinct()
        original_with_data.first()  # Should not modify original's distinct settings
        assert original_with_data._distinct is True

    def test_distinct_with_slicing(self, users_db):
        """Test distinct() with QuerySet slicing."""
        distinct_qs = User.objects().distinct()

        # Test slice access
        first_three = distinct_qs[:3]
        assert len(first_three) <= 3

        # Test single index access
        if len(list(distinct_qs)) > 0:
            first_user = distinct_qs[0]
            assert isinstance(first_user, User)

    def test_distinct_exists_method(self, users_db):
        """Test distinct() with exists() method."""
        # exists() should work with distinct
        exists_result = User.objects().distinct().exists()
        assert isinstance(exists_result, bool)
        assert exists_result is True  # We have users in the database

        # Test with filter that has no results
        no_results = User.objects().filter(age=999).distinct().exists()
        assert no_results is False


class TestDistinctIntegration:
    """Integration tests combining distinct() with various QuerySet operations."""

    def test_complete_workflow(self, users_db):
        """Test a complete workflow using distinct() with multiple operations."""
        # Complex query: filter -> distinct -> order -> limit
        results = list(
            User.objects().filter(age__gte=25).distinct().order_by("name").limit(3)
        )

        # Should return at most 3 users, aged 25+, ordered by name
        assert len(results) <= 3
        for user in results:
            assert user.age >= 25

        # Should be ordered by name
        if len(results) > 1:
            names = [user.name for user in results]
            assert names == sorted(names)

    def test_distinct_update_delete_compatibility(self, users_db):
        """Test that distinct QuerySets work with update/delete operations."""
        # These operations should work on distinct querysets
        # though the practical use might be limited

        distinct_qs = User.objects().filter(department="Engineering").distinct()

        # Should be able to call update (though distinct might not be meaningful)
        update_count = distinct_qs.update(department="Tech")
        assert isinstance(update_count, int)

        # Verify the update worked
        tech_users = list(User.objects().filter(department="Tech"))
        assert len(tech_users) > 0

    def test_distinct_with_q_objects(self, users_db):
        """Test distinct() with Q object filtering."""

        # Complex Q object query with distinct
        complex_filter = Q(age__gte=25) & (
            Q(department="Engineering") | Q(department="Marketing")
        )
        distinct_qs = User.objects().filter(complex_filter).distinct("department")

        assert distinct_qs._distinct is True
        assert distinct_qs._distinct_fields == ["department"]

        # Should generate valid SQL
        sql, params = distinct_qs._build_sql()
        assert "DISTINCT" in sql.upper()
        assert "WHERE" in sql.upper()
