import pytest
from atomsql import Database, Model, StringField, ForeignKey


class Author(Model):
    name = StringField()
    email = StringField()


class Book(Model):
    title = StringField()
    author_id = ForeignKey(to=Author)


@pytest.fixture
def db():
    """Create an in-memory SQLite database."""
    database = Database("sqlite://:memory:")
    database.create_all()
    return database


def test_identity_map_same_instance_on_multiple_gets(db):
    """Test that calling get() twice returns the same object instance."""
    # Create and save an author
    author = Author(name="Alice", email="alice@example.com")
    author.save(db)
    author_id = author.id

    # Get the same author twice
    author1 = Author.get(author_id)
    author2 = Author.get(author_id)

    # Should be the exact same object in memory
    assert author1 is author2
    assert id(author1) == id(author2)


def test_identity_map_survives_after_save(db):
    """Test that the saved instance is in the identity map."""
    author = Author(name="Bob", email="bob@example.com")
    author.save(db)
    author_id = author.id

    # Get it back - should be same instance
    retrieved = Author.get(author_id)
    assert retrieved is author


def test_identity_map_with_queryset(db):
    """Test that QuerySet also uses the identity map."""
    # Create an author
    author = Author(name="Charlie", email="charlie@example.com")
    author.save(db)
    author_id = author.id

    # Get via get()
    author1 = Author.get(author_id)

    # Get via filter
    author2 = list(Author.filter(id=author_id))[0]

    # Should be same instance
    assert author1 is author2


def test_identity_map_weak_references(db):
    """Test that identity map uses weak references (allows garbage collection)."""
    author = Author(name="David", email="david@example.com")
    author.save(db)
    author_id = author.id

    # Get instance
    retrieved = Author.get(author_id)
    assert retrieved.id == author_id

    # Delete all strong references
    del author
    del retrieved

    # Identity map should allow garbage collection
    # If we get again, it might create a new instance (depending on GC)
    # This test mainly ensures WeakValueDictionary is working
    new_retrieved = Author.get(author_id)
    assert new_retrieved.id == author_id


def test_identity_map_with_updates(db):
    """Test that updates to an instance are visible across all references."""
    author = Author(name="Eve", email="eve@example.com")
    author.save(db)
    author_id = author.id

    # Get two references
    ref1 = Author.get(author_id)
    ref2 = Author.get(author_id)

    # Modify via ref1
    ref1.name = "Eve Updated"

    # Should be visible in ref2 (same object)
    assert ref2.name == "Eve Updated"


def test_identity_map_clear(db):
    """Test clearing the identity map."""
    author = Author(name="Frank", email="frank@example.com")
    author.save(db)
    author_id = author.id

    # Get instance (adds to identity map)
    author1 = Author.get(author_id)

    # Clear identity map
    db.clear_identity_map()

    # Get again - should be a different instance now
    author2 = Author.get(author_id)

    assert author1 is not author2
    assert author1.name == author2.name  # But same data


def test_identity_map_with_multiple_models(db):
    """Test identity map works with multiple different model types."""
    author = Author(name="Grace", email="grace@example.com")
    author.save(db)
    author_id = author.id

    book = Book(title="Book 1", author_id=author_id)
    book.save(db)
    book_id = book.id

    # Get each twice
    author1 = Author.get(author_id)
    author2 = Author.get(author_id)
    book1 = Book.get(book_id)
    book2 = Book.get(book_id)

    # Each should be same instance
    assert author1 is author2
    assert book1 is book2

    # But author and book are different
    assert author1 is not book1


def test_identity_map_nonexistent_returns_none(db):
    """Test that getting nonexistent record returns None and doesn't crash."""
    result = Author.get(99999)
    assert result is None


def test_identity_map_with_filter_multiple_results(db):
    """Test that multiple results from filter each use identity map."""
    # Create multiple authors
    author1 = Author(name="Helen", email="helen@example.com")
    author1.save(db)
    author2 = Author(name="Ivan", email="ivan@example.com")
    author2.save(db)

    # Get via filter
    results = list(Author.filter())

    # Get individually
    retrieved1 = Author.get(author1.id)
    retrieved2 = Author.get(author2.id)

    # Find matching instances
    found1 = next(r for r in results if r.id == author1.id)
    found2 = next(r for r in results if r.id == author2.id)

    assert found1 is retrieved1
    assert found2 is retrieved2


def test_identity_map_add_to_map_method(db):
    """Test the add_to_identity_map method directly."""
    author = Author(name="Jack", email="jack@example.com")
    author.save(db)

    # Manually add to identity map
    db.add_to_identity_map(author)

    # Get it back
    retrieved = db.get_from_identity_map(Author, author.id)
    assert retrieved is author


def test_identity_map_remove_from_map_method(db):
    """Test the remove_from_identity_map method."""
    author = Author(name="Kate", email="kate@example.com")
    author.save(db)
    author_id = author.id

    # It's in the map
    retrieved1 = Author.get(author_id)
    assert retrieved1 is author

    # Remove from map
    db.remove_from_identity_map(author)

    # Get again - should be None in map
    from_map = db.get_from_identity_map(Author, author_id)
    assert from_map is None

    # But get() will load from DB (new instance)
    retrieved2 = Author.get(author_id)
    assert retrieved2 is not author
    assert retrieved2.name == author.name


def test_identity_map_with_related_objects(db):
    """Test that related objects also use identity map."""
    author = Author(name="Leo", email="leo@example.com")
    author.save(db)

    book = Book(title="Book 2", author_id=author.id)
    book.save(db)

    # Get book with related author using all() to avoid ambiguous column
    all_books = list(Book.all().select_related("author_id"))
    book_with_author = next(b for b in all_books if b.id == book.id)

    # Get author separately
    author_separate = Author.get(author.id)

    # The related author should be same instance as separately loaded author
    related_author = book_with_author.author_id
    assert related_author is author_separate


def test_identity_map_instance_without_id(db):
    """Test that instances without ID don't cause issues."""
    author = Author(name="Mike", email="mike@example.com")

    # Try to add to identity map (should be no-op)
    db.add_to_identity_map(author)

    # Try to remove (should be no-op)
    db.remove_from_identity_map(author)

    # Should not crash
    assert author.id is None


def test_identity_map_preserves_modified_attributes(db):
    """Test that modifications are preserved in identity map."""
    author = Author(name="Nina", email="nina@example.com")
    author.save(db)
    author_id = author.id

    # Modify without saving
    author.name = "Nina Modified"

    # Get from identity map
    retrieved = Author.get(author_id)

    # Should have modified value (same instance)
    assert retrieved.name == "Nina Modified"


def test_identity_map_after_database_reload(db):
    """Test that identity map doesn't interfere with fresh database loads."""
    author = Author(name="Oscar", email="oscar@example.com")
    author.save(db)
    author_id = author.id

    # Get and modify in memory
    author_ref = Author.get(author_id)
    author_ref.name = "Modified in Memory"

    # Clear identity map
    db.clear_identity_map()

    # Get again - should load fresh from DB
    fresh_author = Author.get(author_id)
    assert fresh_author.name == "Oscar"  # Original value
    assert fresh_author is not author_ref


def test_identity_map_with_slicing(db):
    """Test that identity map works with QuerySet slicing."""
    authors = [
        Author(name=f"Author{i}", email=f"author{i}@example.com") for i in range(5)
    ]
    for author in authors:
        author.save(db)

    # Get a slice
    slice_results = Author.all()[1:3]

    # Get individually
    for result in slice_results:
        individual = Author.get(result.id)
        assert individual is result


def test_identity_map_consistency_across_transactions(db):
    """Test identity map behavior with transactions."""
    author = Author(name="Paul", email="paul@example.com")

    with db.transaction():
        author.save(db)
        author_id = author.id

        # Get within transaction
        author_in_transaction = Author.get(author_id)
        assert author_in_transaction is author

    # Get after transaction
    author_after = Author.get(author_id)
    assert author_after is author  # Still same instance
