import pytest
from atomsql.core.db import Database
from atomsql.orm import Model, CharField, ManyToManyField


class Author(Model):
    name = CharField(max_length=100)

    def __repr__(self):
        return f"<Author: {self.name}>"


class Book(Model):
    title = CharField(max_length=200)
    authors = ManyToManyField(Author, related_name="books")

    def __repr__(self):
        return f"<Book: {self.title}>"


class Student(Model):
    name = CharField(max_length=100)


class Course(Model):
    name = CharField(max_length=100)
    students = ManyToManyField(Student, related_name="courses")


class TestManyToManyField:
    """Test suite for ManyToManyField functionality."""

    def setup_method(self):
        """Set up a fresh database for each test."""
        self.db = Database("sqlite:///:memory:")

        # Register models in proper order to ensure dependencies are met
        self.db.register(Author)
        self.db.register(Book)
        self.db.register(Student)
        self.db.register(Course)

        # Force through table creation by accessing the managers
        dummy_book = Book(title="Dummy")
        dummy_book._db = self.db
        dummy_book.authors  # This should trigger through table creation

        dummy_course = Course(name="Dummy")
        dummy_course._db = self.db
        dummy_course.students  # This should trigger through table creation

    def test_field_creation(self):
        """Test that ManyToManyField can be created properly."""
        book = Book(title="Test Book")
        assert hasattr(book, "authors")

        # The field should return a manager
        manager = book.authors
        assert hasattr(manager, "add")
        assert hasattr(manager, "remove")
        assert hasattr(manager, "clear")
        assert hasattr(manager, "set")
        assert hasattr(manager, "all")
        assert hasattr(manager, "count")
        assert hasattr(manager, "filter")

    def test_through_table_creation(self):
        """Test that through tables are created automatically."""
        # Through table should be created when models are registered
        tables = self.db.backend.get_tables()

        # Check that junction table exists
        # The table name should be alphabetically sorted model names
        assert "author_book" in tables
        assert "course_student" in tables

    def test_reverse_relationship(self):
        """Test that reverse relationships are set up correctly."""
        author = Author(name="Test Author")
        assert hasattr(author, "books")

        # The reverse field should also be a manager
        manager = author.books
        assert hasattr(manager, "add")
        assert hasattr(manager, "remove")
        assert hasattr(manager, "clear")
        assert hasattr(manager, "set")

    def test_add_method(self):
        """Test adding relationships."""
        author = Author.create(name="J.K. Rowling")
        book = Book.create(title="Harry Potter")

        book.authors.add(author)

        # Check relationship exists
        authors = book.authors.all()
        assert len(authors) == 1
        assert authors[0].name == "J.K. Rowling"

        # Check reverse relationship
        books = author.books.all()
        assert len(books) == 1
        assert books[0].title == "Harry Potter"

    def test_add_multiple(self):
        """Test adding multiple relationships at once."""
        author1 = Author.create(name="Author 1")
        author2 = Author.create(name="Author 2")
        book = Book.create(title="Test Book")

        book.authors.add(author1, author2)

        authors = book.authors.all()
        assert len(authors) == 2
        author_names = [a.name for a in authors]
        assert "Author 1" in author_names
        assert "Author 2" in author_names

    def test_add_duplicate(self):
        """Test that adding the same relationship twice doesn't create duplicates."""
        author = Author.create(name="Test Author")
        book = Book.create(title="Test Book")

        book.authors.add(author)
        book.authors.add(author)  # Add again

        authors = book.authors.all()
        assert len(authors) == 1

    def test_remove_method(self):
        """Test removing relationships."""
        author1 = Author.create(name="Author 1")
        author2 = Author.create(name="Author 2")
        book = Book.create(title="Test Book")

        book.authors.add(author1, author2)
        assert len(book.authors.all()) == 2

        book.authors.remove(author1)
        authors = book.authors.all()
        assert len(authors) == 1
        assert authors[0].name == "Author 2"

    def test_remove_nonexistent(self):
        """Test removing a relationship that doesn't exist."""
        author = Author.create(name="Test Author")
        book = Book.create(title="Test Book")

        # This should not raise an error
        book.authors.remove(author)
        assert len(book.authors.all()) == 0

    def test_clear_method(self):
        """Test clearing all relationships."""
        author1 = Author.create(name="Author 1")
        author2 = Author.create(name="Author 2")
        book = Book.create(title="Test Book")

        book.authors.add(author1, author2)
        assert len(book.authors.all()) == 2

        book.authors.clear()
        assert len(book.authors.all()) == 0

    def test_set_method(self):
        """Test setting relationships to a specific set."""
        author1 = Author.create(name="Author 1")
        author2 = Author.create(name="Author 2")
        author3 = Author.create(name="Author 3")
        book = Book.create(title="Test Book")

        # First set
        book.authors.set([author1, author2])
        assert len(book.authors.all()) == 2

        # Replace with different set
        book.authors.set([author2, author3])
        authors = book.authors.all()
        assert len(authors) == 2
        author_names = [a.name for a in authors]
        assert "Author 1" not in author_names
        assert "Author 2" in author_names
        assert "Author 3" in author_names

    def test_set_single_instance(self):
        """Test setting with a single instance (not a list)."""
        author = Author.create(name="Test Author")
        book = Book.create(title="Test Book")

        book.authors.set(author)
        authors = book.authors.all()
        assert len(authors) == 1
        assert authors[0].name == "Test Author"

    def test_count_method(self):
        """Test counting relationships."""
        author1 = Author.create(name="Author 1")
        author2 = Author.create(name="Author 2")
        book = Book.create(title="Test Book")

        assert book.authors.count() == 0

        book.authors.add(author1)
        assert book.authors.count() == 1

        book.authors.add(author2)
        assert book.authors.count() == 2

    def test_filter_method(self):
        """Test filtering related objects."""
        author1 = Author.create(name="Alice")
        author2 = Author.create(name="Bob")
        book = Book.create(title="Test Book")

        book.authors.add(author1, author2)

        # Filter by name
        filtered = book.authors.filter(name="Alice")
        assert len(filtered) == 1
        assert filtered[0].name == "Alice"

        # Filter with no matches
        filtered = book.authors.filter(name="Charlie")
        assert len(filtered) == 0

    def test_unsaved_instance_operations(self):
        """Test operations on unsaved instances."""
        author = Author.create(name="Test Author")
        book = Book(title="Test Book")  # Not saved

        # Should raise error when trying to add to unsaved instance
        with pytest.raises(ValueError, match="Instance must be saved"):
            book.authors.add(author)

    def test_unsaved_related_instance(self):
        """Test adding unsaved related instances."""
        author = Author(name="Test Author")  # Not saved
        book = Book.create(title="Test Book")

        # Should raise error when trying to add unsaved instance
        with pytest.raises(ValueError, match="Related instance must be saved"):
            book.authors.add(author)

    def test_wrong_type_instance(self):
        """Test adding instance of wrong type."""
        student = Student.create(name="Test Student")
        book = Book.create(title="Test Book")

        # Should raise error when trying to add wrong type
        with pytest.raises(TypeError):
            book.authors.add(student)

    def test_field_assignment(self):
        """Test assigning directly to the field."""
        author1 = Author.create(name="Author 1")
        author2 = Author.create(name="Author 2")
        book = Book.create(title="Test Book")

        # Direct assignment should work like set()
        book.authors = [author1, author2]
        assert len(book.authors.all()) == 2

        # Assignment with None should clear
        book.authors = None
        assert len(book.authors.all()) == 0

    def test_bidirectional_relationship(self):
        """Test that relationships work in both directions."""
        author = Author.create(name="Test Author")
        book1 = Book.create(title="Book 1")
        book2 = Book.create(title="Book 2")

        # Add from one side
        book1.authors.add(author)
        book2.authors.add(author)

        # Check from the other side
        books = author.books.all()
        assert len(books) == 2
        book_titles = [b.title for b in books]
        assert "Book 1" in book_titles
        assert "Book 2" in book_titles

        # Remove from the other side
        author.books.remove(book1)

        # Check original side
        assert len(book1.authors.all()) == 0
        assert len(book2.authors.all()) == 1

    def test_custom_related_name(self):
        """Test that custom related_name works."""
        # Already tested implicitly above (books reverse relationship)
        author = Author.create(name="Test Author")
        assert hasattr(author, "books")

        # Verify it's the correct type
        manager = author.books
        assert hasattr(manager, "add")


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
