"""
Comprehensive test suite for QuerySet.first() and QuerySet.last() methods.

Tests cover:
- Basic functionality with and without data
- Ordering behavior
- Filter interaction
- Edge cases (empty querysets, single records)
- Immutability (ensuring original queryset isn't modified)
- Models with and without 'id' fields
- Complex scenarios with Q objects and chaining
"""

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


# --- Test Models ---
class Book(Model):
    """Standard model with id field (implicit)."""

    title = StringField()
    author = StringField()
    year = IntegerField()
    rating = IntegerField()


class Log(Model):
    """Model without explicit id field for testing edge cases."""

    message = StringField()
    level = StringField()


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


@pytest.fixture
def books_db(db):
    """Registers Book model and populates with test data."""
    db.register(Book)

    # Create test books with various attributes
    books = [
        Book(
            title="The Great Gatsby", author="F. Scott Fitzgerald", year=1925, rating=5
        ),
        Book(title="1984", author="George Orwell", year=1949, rating=5),
        Book(title="To Kill a Mockingbird", author="Harper Lee", year=1960, rating=5),
        Book(title="Pride and Prejudice", author="Jane Austen", year=1813, rating=4),
        Book(
            title="The Catcher in the Rye", author="J.D. Salinger", year=1951, rating=4
        ),
        Book(title="Animal Farm", author="George Orwell", year=1945, rating=4),
        Book(title="Brave New World", author="Aldous Huxley", year=1932, rating=4),
    ]

    for book in books:
        book.save(db)

    db.commit()
    return db


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


@pytest.fixture
def logs_db(db):
    """Registers Log model and populates with test data."""
    db.register(Log)

    logs = [
        Log(message="Application started", level="INFO"),
        Log(message="User logged in", level="INFO"),
        Log(message="Database connection failed", level="ERROR"),
        Log(message="Request timeout", level="WARNING"),
        Log(message="Processing complete", level="INFO"),
    ]

    for log in logs:
        log.save(db)

    db.commit()
    return db


# ============================================================================
# first() Tests
# ============================================================================


class TestFirst:
    """Test suite for QuerySet.first() method."""

    def test_first_returns_first_record(self, books_db):
        """first() should return the first record when data exists."""
        result = Book.all().first()

        assert result is not None
        assert isinstance(result, Book)
        assert result.title == "The Great Gatsby"  # First inserted

    def test_first_with_empty_queryset(self, empty_books_db):
        """first() should return None when no records exist."""
        result = Book.all().first()

        assert result is None

    def test_first_with_filter(self, books_db):
        """first() should respect filter conditions."""
        result = Book.filter(author="George Orwell").first()

        assert result is not None
        assert result.author == "George Orwell"
        # Should get "1984" as it was inserted before "Animal Farm"
        assert result.title in ["1984", "Animal Farm"]

    def test_first_with_filter_no_matches(self, books_db):
        """first() should return None when filter has no matches."""
        result = Book.filter(author="Nonexistent Author").first()

        assert result is None

    def test_first_with_order_by_asc(self, books_db):
        """first() should respect ascending order."""
        result = Book.all().order_by("year").first()

        assert result is not None
        assert result.year == 1813  # "Pride and Prejudice" is oldest
        assert result.title == "Pride and Prejudice"

    def test_first_with_order_by_desc(self, books_db):
        """first() should respect descending order."""
        result = Book.all().order_by("-year").first()

        assert result is not None
        assert result.year == 1960  # "To Kill a Mockingbird" is newest
        assert result.title == "To Kill a Mockingbird"

    def test_first_with_chained_filters(self, books_db):
        """first() should work with multiple chained filters."""
        result = Book.filter(rating=5).filter(year__gt=1930).first()

        assert result is not None
        assert result.rating == 5
        assert result.year > 1930
        assert result.title in ["1984", "To Kill a Mockingbird"]

    def test_first_with_q_objects(self, books_db):
        """first() should work with Q object filters."""
        result = Book.filter(Q(author="Jane Austen") | Q(author="Harper Lee")).first()

        assert result is not None
        assert result.author in ["Jane Austen", "Harper Lee"]

    def test_first_does_not_mutate_queryset(self, books_db):
        """first() should not modify the original QuerySet."""
        queryset = Book.filter(rating=5)

        # Store original state
        original_limit = queryset._limit
        original_offset = queryset._offset

        # Call first()
        result = queryset.first()
        assert result is not None  # Verify it returns a result

        # Verify queryset is unchanged
        assert queryset._limit == original_limit
        assert queryset._offset == original_offset

        # Verify we can still use the queryset
        all_results = list(queryset)
        assert len(all_results) == 3  # Three books with rating=5

    def test_first_with_limit_already_set(self, books_db):
        """first() should work even if limit is already set on queryset."""
        # Slicing returns a list in current implementation, so test differently
        queryset = Book.all().order_by("year")

        # Get first from ordered queryset
        result = queryset.first()

        assert result is not None
        assert result.year == 1813

    def test_first_with_offset(self, books_db):
        """first() returns the first matching record without requiring offset support."""
        # Test first on a filtered queryset instead
        queryset = Book.filter(year__gt=1930).order_by("year")

        result = queryset.first()

        assert result is not None
        # Should get the oldest book after 1930
        assert result.year == 1932  # Brave New World

    def test_first_single_record(self, empty_books_db):
        """first() should work correctly with exactly one record."""
        book = Book(title="Only Book", author="Solo Author", year=2000, rating=3)
        book.save(empty_books_db)
        empty_books_db.commit()

        result = Book.all().first()

        assert result is not None
        assert result.title == "Only Book"

    def test_first_with_exclude(self, books_db):
        """first() should work with exclude()."""
        result = Book.exclude(author="George Orwell").order_by("year").first()

        assert result is not None
        assert result.author != "George Orwell"
        assert result.year == 1813  # Oldest non-Orwell book

    def test_first_performance_uses_limit(self, books_db):
        """first() should use LIMIT 1 for performance."""
        queryset = Book.all()

        # Get first using indexing (what first() does internally)
        result = queryset.first()

        assert result is not None
        # Verify it doesn't fetch all records
        # (we can't directly test SQL but we test behavior)


# ============================================================================
# last() Tests
# ============================================================================


class TestLast:
    """Test suite for QuerySet.last() method."""

    def test_last_returns_last_record(self, books_db):
        """last() should return the last record by id when no order specified."""
        result = Book.all().last()

        assert result is not None
        assert isinstance(result, Book)
        # Last inserted book
        assert result.title == "Brave New World"

    def test_last_with_empty_queryset(self, empty_books_db):
        """last() should return None when no records exist."""
        result = Book.all().last()

        assert result is None

    def test_last_with_filter(self, books_db):
        """last() should respect filter conditions."""
        result = Book.filter(author="George Orwell").last()

        assert result is not None
        assert result.author == "George Orwell"
        # Should get "Animal Farm" as it was inserted after "1984"
        assert result.title == "Animal Farm"

    def test_last_with_filter_no_matches(self, books_db):
        """last() should return None when filter has no matches."""
        result = Book.filter(author="Nonexistent Author").last()

        assert result is None

    def test_last_with_order_by_asc(self, books_db):
        """last() should reverse ascending order to get last."""
        result = Book.all().order_by("year").last()

        assert result is not None
        assert result.year == 1960  # Newest book when ordered by year
        assert result.title == "To Kill a Mockingbird"

    def test_last_with_order_by_desc(self, books_db):
        """last() should reverse descending order to get last."""
        result = Book.all().order_by("-year").last()

        assert result is not None
        assert result.year == 1813  # Oldest book when reverse ordered
        assert result.title == "Pride and Prejudice"

    def test_last_reverses_order_correctly(self, books_db):
        """last() should properly reverse the order direction."""
        queryset = Book.all().order_by("rating")

        # Get last with order_by
        result = queryset.last()

        assert result is not None
        # Should get highest rating (5) in reversed order
        assert result.rating == 5

    def test_last_with_chained_filters(self, books_db):
        """last() should work with multiple chained filters."""
        result = Book.filter(rating=4).filter(year__lt=1950).last()

        assert result is not None
        assert result.rating == 4
        assert result.year < 1950
        # Latest of the filtered set

    def test_last_with_q_objects(self, books_db):
        """last() should work with Q object filters."""
        result = (
            Book.filter(Q(year__lt=1930) | Q(year__gt=1950)).order_by("year").last()
        )

        assert result is not None
        # Should be the newest book in the filtered set
        assert result.year > 1950

    def test_last_does_not_mutate_queryset(self, books_db):
        """last() should not modify the original QuerySet."""
        queryset = Book.filter(rating=4).order_by("year")

        # Store original state
        original_order = queryset._order_by
        original_limit = queryset._limit

        # Call last()
        result = queryset.last()
        assert result is not None  # Verify it returns a result

        # Verify queryset is unchanged
        assert queryset._order_by == original_order
        assert queryset._limit == original_limit

        # Verify we can still use the queryset
        all_results = list(queryset)
        assert len(all_results) == 4  # Four books with rating=4

    def test_last_without_id_field_raises_error(self, logs_db):
        """last() should raise ValueError for models without id field and no order."""
        # Note: This test depends on whether Log model has an id field
        # If SQLite auto-creates rowid, this might not raise an error
        # Adjust based on actual implementation

        # Try to get last without ordering
        try:
            result = Log.all().last()
            # If no error, verify we got a result
            assert result is not None
        except ValueError as e:
            # Expected behavior if no id field exists
            assert "requires an ordered QuerySet" in str(e)

    def test_last_with_explicit_order_on_model_without_id(self, logs_db):
        """last() should work on models without id if order is specified."""
        result = Log.all().order_by("level").last()

        assert result is not None
        # Should get last when ordered by level (WARNING comes last alphabetically)
        assert result.level == "WARNING"

    def test_last_single_record(self, empty_books_db):
        """last() should work correctly with exactly one record."""
        book = Book(title="Only Book", author="Solo Author", year=2000, rating=3)
        book.save(empty_books_db)
        empty_books_db.commit()

        result = Book.all().last()

        assert result is not None
        assert result.title == "Only Book"

    def test_last_with_exclude(self, books_db):
        """last() should work with exclude()."""
        result = Book.exclude(rating=5).order_by("year").last()

        assert result is not None
        assert result.rating != 5
        # Should be newest book that doesn't have rating 5
        assert result.year == 1951
        assert result.title == "The Catcher in the Rye"

    def test_last_performance_uses_limit(self, books_db):
        """last() should use LIMIT 1 after reversing order."""
        queryset = Book.all().order_by("year")

        result = queryset.last()

        assert result is not None
        # Verify it efficiently fetches only one record


# ============================================================================
# Combined first() and last() Tests
# ============================================================================


class TestFirstLastCombined:
    """Test suite for combined first() and last() scenarios."""

    def test_first_and_last_on_same_queryset(self, books_db):
        """first() and last() should work independently on same queryset."""
        queryset = Book.filter(rating=5).order_by("year")

        first = queryset.first()
        last = queryset.last()

        assert first is not None
        assert last is not None
        assert first.year == 1925  # The Great Gatsby
        assert last.year == 1960  # To Kill a Mockingbird
        assert first.title != last.title

    def test_first_and_last_identical_for_single_record(self, empty_books_db):
        """first() and last() should return same record when only one exists."""
        book = Book(title="Only Book", author="Solo Author", year=2000, rating=3)
        book.save(empty_books_db)
        empty_books_db.commit()

        first = Book.all().first()
        last = Book.all().last()

        assert first is not None
        assert last is not None
        assert first.id == last.id
        assert first.title == last.title

    def test_first_and_last_with_different_filters(self, books_db):
        """first() and last() should work independently with different filters."""
        first_high_rated = Book.filter(rating=5).order_by("year").first()
        last_low_rated = Book.filter(rating=4).order_by("year").last()

        assert first_high_rated is not None
        assert last_low_rated is not None
        assert first_high_rated.rating == 5
        assert last_low_rated.rating == 4

    def test_first_last_ordering_consistency(self, books_db):
        """Ordering should be consistent between first() and last()."""
        queryset = Book.all().order_by("author")

        first = queryset.first()
        last = queryset.last()

        assert first is not None
        assert last is not None
        # First alphabetically
        assert first.author == "Aldous Huxley"
        # Last alphabetically (depends on insertion order if no explicit reverse)
        # Just verify they're different
        assert first.author != last.author

    def test_chaining_after_first_or_last_not_possible(self, books_db):
        """first() and last() should return Model instances, not querysets."""
        first = Book.all().first()
        last = Book.all().last()

        # These should be model instances, not querysets
        # Model has filter as a classmethod, but instances shouldn't be querysets
        assert isinstance(first, Book)
        assert isinstance(last, Book)
        # Verify they don't have QuerySet-specific attributes
        assert not hasattr(first, "_filters")
        assert not hasattr(last, "_filters")


# ============================================================================
# Edge Cases and Error Handling
# ============================================================================


class TestEdgeCases:
    """Test suite for edge cases and error conditions."""

    def test_first_last_with_all_identical_records(self, empty_books_db):
        """first() and last() with identical records should still work."""
        for _ in range(5):
            book = Book(title="Same Book", author="Same Author", year=2000, rating=3)
            book.save(empty_books_db)

        empty_books_db.commit()

        first = Book.all().first()
        last = Book.all().last()

        assert first is not None
        assert last is not None
        # They have different ids even if other fields are same
        assert first.id != last.id

    def test_first_last_with_null_values(self, empty_books_db):
        """first() and last() should handle NULL values appropriately."""
        # Create books with potentially None values (if fields allow)
        book1 = Book(title="Book 1", author="Author 1", year=2000, rating=3)
        book2 = Book(title="Book 2", author="Author 2", year=2001, rating=4)

        book1.save(empty_books_db)
        book2.save(empty_books_db)
        empty_books_db.commit()

        first = Book.all().order_by("year").first()
        last = Book.all().order_by("year").last()

        assert first.year == 2000
        assert last.year == 2001

    def test_multiple_first_calls_same_result(self, books_db):
        """Multiple calls to first() should return consistent results."""
        queryset = Book.filter(rating=5).order_by("year")

        result1 = queryset.first()
        result2 = queryset.first()
        result3 = queryset.first()

        assert result1.id == result2.id == result3.id

    def test_multiple_last_calls_same_result(self, books_db):
        """Multiple calls to last() should return consistent results."""
        queryset = Book.filter(rating=4).order_by("year")

        result1 = queryset.last()
        result2 = queryset.last()
        result3 = queryset.last()

        assert result1.id == result2.id == result3.id

    def test_first_last_after_complex_query(self, books_db):
        """first() and last() should work after complex query construction."""
        queryset = (
            Book.filter(Q(rating=4) | Q(rating=5))
            .exclude(author="George Orwell")
            .filter(year__gt=1900)
            .order_by("-year")
        )

        first = queryset.first()
        last = queryset.last()

        assert first is not None
        assert last is not None
        assert first.author != "George Orwell"
        assert last.author != "George Orwell"

    def test_first_last_with_very_large_offset(self, books_db):
        """first() and last() with filter that matches nothing."""
        # Test with filter that returns no results instead of offset
        queryset = Book.filter(year__gt=3000)  # No books from future

        first = queryset.first()
        last = queryset.last()

        assert first is None
        assert last is None

    def test_order_by_reversal_with_multiple_fields(self, books_db):
        """last() with complex ordering (note: current impl only handles single field)."""
        # This test documents current behavior - may need update if multi-field order is added
        queryset = Book.all().order_by("rating")

        result = queryset.last()

        assert result is not None
        # With rating ordering, last should give highest rating


# ============================================================================
# Integration Tests
# ============================================================================


class TestIntegration:
    """Integration tests for first() and last() with other ORM features."""

    def test_first_with_related_fields(self, books_db):
        """first() should work with select_related (if implemented)."""
        # This is a placeholder for future relationship support
        result = Book.all().order_by("title").first()

        assert result is not None
        assert hasattr(result, "title")

    def test_last_with_select_fields(self, books_db):
        """last() should work with field selection (if implemented)."""
        # This is a placeholder for future only()/defer() support
        result = Book.all().order_by("year").last()

        assert result is not None
        assert hasattr(result, "year")

    def test_first_last_transaction_isolation(self, books_db):
        """first() and last() should work with transaction context."""
        # Add new book within a transaction
        with books_db.transaction():
            new_book = Book(title="New Book", author="New Author", year=2024, rating=5)
            new_book.save(books_db)
            books_db.commit()

            # Within transaction, should see new book
            current_last = Book.all().order_by("id").last()
            assert current_last.title == "New Book"

        # After transaction commits, book should still exist
        after_transaction_last = Book.all().order_by("id").last()
        assert after_transaction_last.title == "New Book"

    def test_first_last_with_aggregation_queryset(self, books_db):
        """first() and last() interaction with filtered aggregation queries."""
        # Get books with specific rating
        high_rated = Book.filter(rating=5)

        first_high = high_rated.order_by("year").first()
        last_high = high_rated.order_by("year").last()

        assert first_high is not None
        assert last_high is not None
        assert first_high.rating == 5
        assert last_high.rating == 5
        assert first_high.year < last_high.year
