"""
Comprehensive test suite for UUIDField.

Tests cover:
- Type validation (accepts UUID, string UUIDs, rejects invalid types)
- Database storage and retrieval
- Nullable vs non-nullable fields
- Auto-generation with auto_add=True
- Default values
- Unique constraint enforcement
- Filter operations
- Comparison operators
- String UUID conversion
- Edge cases and error handling
"""

import pytest
import uuid
from atomsql import Database, Model
from atomsql.orm.fields import StringField, UUIDField


# --- Test Models ---
class Document(Model):
    """Model with nullable UUID field."""

    title = StringField()
    doc_id = UUIDField(nullable=True)


class Asset(Model):
    """Model with non-nullable UUID field."""

    name = StringField()
    asset_id = UUIDField(nullable=False)


class Resource(Model):
    """Model with auto-generated UUID field."""

    name = StringField()
    resource_id = UUIDField(auto_add=True, unique=True)


class Session(Model):
    """Model with unique UUID field."""

    user = StringField()
    session_token = UUIDField(unique=True, nullable=False)


class Product(Model):
    """Model with default UUID value."""

    name = StringField()
    product_code = UUIDField(default=lambda: uuid.uuid4(), nullable=False)


class MultiUUID(Model):
    """Model with multiple UUID fields."""

    name = StringField()
    primary_id = UUIDField(nullable=False)
    secondary_id = UUIDField(nullable=True)
    tracking_id = UUIDField(auto_add=True)


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


@pytest.fixture
def document_db(db):
    """Registers Document model."""
    db.register(Document)
    return db


@pytest.fixture
def asset_db(db):
    """Registers Asset model."""
    db.register(Asset)
    return db


@pytest.fixture
def resource_db(db):
    """Registers Resource model."""
    db.register(Resource)
    return db


@pytest.fixture
def session_db(db):
    """Registers Session model."""
    db.register(Session)
    return db


@pytest.fixture
def product_db(db):
    """Registers Product model."""
    db.register(Product)
    return db


@pytest.fixture
def multi_db(db):
    """Registers MultiUUID model."""
    db.register(MultiUUID)
    return db


# ============================================================================
# Type Validation Tests
# ============================================================================


class TestUUIDFieldValidation:
    """Test suite for UUIDField type validation."""

    def test_accepts_uuid_object(self, document_db):
        """UUIDField should accept uuid.UUID objects."""
        test_uuid = uuid.uuid4()
        doc = Document(title="Test Doc", doc_id=test_uuid)
        assert doc.doc_id == test_uuid
        assert isinstance(doc.doc_id, uuid.UUID)

    def test_accepts_valid_uuid_string(self, document_db):
        """UUIDField should accept valid UUID strings and convert them."""
        uuid_str = "123e4567-e89b-12d3-a456-426614174000"
        doc = Document(title="Test Doc", doc_id=uuid_str)
        # The field should store it (validation passes)
        assert doc.doc_id == uuid_str

    def test_accepts_uuid1(self, document_db):
        """UUIDField should accept UUID1 (time-based)."""
        test_uuid = uuid.uuid1()
        doc = Document(title="Test Doc", doc_id=test_uuid)
        assert doc.doc_id == test_uuid
        assert isinstance(doc.doc_id, uuid.UUID)

    def test_accepts_uuid3(self, document_db):
        """UUIDField should accept UUID3 (name-based MD5)."""
        test_uuid = uuid.uuid3(uuid.NAMESPACE_DNS, "example.com")
        doc = Document(title="Test Doc", doc_id=test_uuid)
        assert doc.doc_id == test_uuid
        assert isinstance(doc.doc_id, uuid.UUID)

    def test_accepts_uuid4(self, document_db):
        """UUIDField should accept UUID4 (random)."""
        test_uuid = uuid.uuid4()
        doc = Document(title="Test Doc", doc_id=test_uuid)
        assert doc.doc_id == test_uuid
        assert isinstance(doc.doc_id, uuid.UUID)

    def test_accepts_uuid5(self, document_db):
        """UUIDField should accept UUID5 (name-based SHA-1)."""
        test_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
        doc = Document(title="Test Doc", doc_id=test_uuid)
        assert doc.doc_id == test_uuid
        assert isinstance(doc.doc_id, uuid.UUID)

    def test_accepts_none_when_nullable(self, document_db):
        """UUIDField should accept None when nullable=True."""
        doc = Document(title="No UUID", doc_id=None)
        assert doc.doc_id is None

    def test_rejects_none_when_not_nullable(self, asset_db):
        """UUIDField should reject None when nullable=False."""
        with pytest.raises(ValueError, match="cannot be None"):
            Asset(name="Test Asset", asset_id=None)

    def test_rejects_invalid_string(self, document_db):
        """UUIDField should reject invalid UUID strings."""
        with pytest.raises(ValueError, match="expected a valid UUID"):
            Document(title="Invalid", doc_id="not-a-uuid")

    def test_rejects_integer(self, document_db):
        """UUIDField should reject integer values."""
        with pytest.raises(ValueError, match="expected a UUID"):
            Document(title="Invalid", doc_id=12345)

    def test_rejects_float(self, document_db):
        """UUIDField should reject float values."""
        with pytest.raises(ValueError, match="expected a UUID"):
            Document(title="Invalid", doc_id=123.45)

    def test_rejects_list(self, document_db):
        """UUIDField should reject list values."""
        with pytest.raises(ValueError, match="expected a UUID"):
            Document(title="Invalid", doc_id=[])

    def test_rejects_dict(self, document_db):
        """UUIDField should reject dict values."""
        with pytest.raises(ValueError, match="expected a UUID"):
            Document(title="Invalid", doc_id={})

    def test_rejects_empty_string(self, document_db):
        """UUIDField should reject empty strings."""
        with pytest.raises(ValueError, match="expected a valid UUID"):
            Document(title="Invalid", doc_id="")

    def test_rejects_malformed_uuid_string(self, document_db):
        """UUIDField should reject malformed UUID strings."""
        with pytest.raises(ValueError, match="expected a valid UUID"):
            Document(title="Invalid", doc_id="123e4567-e89b-12d3-a456")


# ============================================================================
# Database Storage and Retrieval Tests
# ============================================================================


class TestUUIDFieldDatabaseOperations:
    """Test suite for UUIDField database operations."""

    def test_stores_and_retrieves_uuid(self, document_db):
        """UUIDField should store and retrieve UUID correctly."""
        test_uuid = uuid.uuid4()
        doc = Document(title="Stored Doc", doc_id=test_uuid)
        doc.save()

        retrieved = Document.objects().filter(title="Stored Doc")[0]
        assert retrieved.doc_id == test_uuid
        assert isinstance(retrieved.doc_id, uuid.UUID)

    def test_stores_uuid_as_string_in_db(self, document_db):
        """UUIDField should store UUID as string in database."""
        test_uuid = uuid.uuid4()
        doc = Document(title="String Storage", doc_id=test_uuid)
        doc.save()

        # Query the raw database to verify storage format
        cursor = document_db.connection.cursor()
        cursor.execute(
            "SELECT doc_id FROM document WHERE title = ?", ("String Storage",)
        )
        row = cursor.fetchone()
        assert isinstance(row[0], str)
        assert row[0] == str(test_uuid)

    def test_retrieves_none_when_null(self, document_db):
        """UUIDField should retrieve None for NULL database values."""
        doc = Document(title="Null UUID", doc_id=None)
        doc.save()

        retrieved = Document.objects().filter(title="Null UUID")[0]
        assert retrieved.doc_id is None

    def test_converts_string_from_db_to_uuid(self, document_db):
        """UUIDField should convert string from database to UUID object."""
        test_uuid = uuid.uuid4()
        doc = Document(title="Convert Test", doc_id=test_uuid)
        doc.save()

        retrieved = Document.get(doc.id)
        assert isinstance(retrieved.doc_id, uuid.UUID)
        assert retrieved.doc_id == test_uuid

    def test_multiple_uuid_fields_same_model(self, multi_db):
        """Model should support multiple UUID fields."""
        primary = uuid.uuid4()
        secondary = uuid.uuid4()

        multi = MultiUUID(name="Multi Test", primary_id=primary, secondary_id=secondary)
        multi.save()

        retrieved = MultiUUID.get(multi.id)
        assert retrieved.primary_id == primary
        assert retrieved.secondary_id == secondary
        assert isinstance(retrieved.tracking_id, uuid.UUID)  # auto-generated


# ============================================================================
# Auto-generation Tests
# ============================================================================


class TestUUIDFieldAutoGeneration:
    """Test suite for UUIDField auto-generation."""

    def test_auto_add_generates_uuid(self, resource_db):
        """UUIDField with auto_add=True should generate UUID automatically."""
        resource = Resource(name="Auto Resource")
        resource.save()

        assert resource.resource_id is not None
        assert isinstance(resource.resource_id, uuid.UUID)

    def test_auto_add_unique_uuids(self, resource_db):
        """UUIDField with auto_add=True should generate unique UUIDs."""
        resource1 = Resource(name="Resource 1")
        resource1.save()

        resource2 = Resource(name="Resource 2")
        resource2.save()

        assert resource1.resource_id != resource2.resource_id

    def test_auto_add_can_be_overridden(self, resource_db):
        """UUIDField with auto_add=True can be overridden with explicit value."""
        custom_uuid = uuid.uuid4()
        resource = Resource(name="Custom Resource", resource_id=custom_uuid)
        resource.save()

        retrieved = Resource.get(resource.id)
        assert retrieved.resource_id == custom_uuid

    def test_default_callable_generates_uuid(self, product_db):
        """UUIDField with default callable should generate UUID."""
        product = Product(name="Product 1")
        product.save()

        assert product.product_code is not None
        assert isinstance(product.product_code, uuid.UUID)

    def test_auto_generated_uuids_persist(self, resource_db):
        """Auto-generated UUIDs should persist to database."""
        resource = Resource(name="Persist Test")
        resource.save()

        original_uuid = resource.resource_id

        retrieved = Resource.get(resource.id)
        assert retrieved.resource_id == original_uuid


# ============================================================================
# Filter and Query Tests
# ============================================================================


class TestUUIDFieldFiltering:
    """Test suite for UUIDField filtering and queries."""

    def test_filter_by_exact_uuid(self, document_db):
        """Should filter by exact UUID match."""
        uuid1 = uuid.uuid4()
        uuid2 = uuid.uuid4()

        Document(title="Doc 1", doc_id=uuid1).save()
        Document(title="Doc 2", doc_id=uuid2).save()

        results = list(Document.objects().filter(doc_id=uuid1))
        assert len(results) == 1
        assert results[0].title == "Doc 1"
        assert results[0].doc_id == uuid1

    def test_filter_by_uuid_string(self, document_db):
        """Should filter by UUID as string."""
        test_uuid = uuid.uuid4()
        Document(title="String Filter", doc_id=test_uuid).save()

        results = list(Document.objects().filter(doc_id=str(test_uuid)))
        assert len(results) == 1
        assert results[0].doc_id == test_uuid

    def test_filter_by_none(self, document_db):
        """Should filter by None value."""
        Document(title="Has UUID", doc_id=uuid.uuid4()).save()
        Document(title="No UUID", doc_id=None).save()

        results = list(Document.objects().filter(doc_id=None))
        assert len(results) == 1
        assert results[0].title == "No UUID"

    def test_filter_not_equal(self, document_db):
        """Should filter with not equal operator."""
        uuid1 = uuid.uuid4()
        uuid2 = uuid.uuid4()

        Document(title="Doc 1", doc_id=uuid1).save()
        Document(title="Doc 2", doc_id=uuid2).save()

        results = list(Document.objects().filter(doc_id__ne=uuid1))
        assert len(results) == 1
        assert results[0].doc_id == uuid2

    def test_exists_with_uuid(self, document_db):
        """Should check existence by UUID."""
        test_uuid = uuid.uuid4()
        Document(title="Exists Test", doc_id=test_uuid).save()

        exists = Document.objects().filter(doc_id=test_uuid).exists()
        assert exists is True

        fake_uuid = uuid.uuid4()
        not_exists = Document.objects().filter(doc_id=fake_uuid).exists()
        assert not_exists is False

    def test_get_by_uuid(self, document_db):
        """Should retrieve single object by UUID."""
        test_uuid = uuid.uuid4()
        doc = Document(title="Get Test", doc_id=test_uuid)
        doc.save()

        retrieved = Document.objects().filter(doc_id=test_uuid).first()
        assert retrieved is not None
        assert retrieved.title == "Get Test"


# ============================================================================
# Unique Constraint Tests
# ============================================================================


class TestUUIDFieldUnique:
    """Test suite for UUIDField unique constraint."""

    def test_unique_constraint_enforced(self, session_db):
        """UUIDField with unique=True should enforce uniqueness."""
        test_uuid = uuid.uuid4()

        Session(user="user1", session_token=test_uuid).save()

        # Attempting to save duplicate should raise an error
        with pytest.raises(Exception):  # Database integrity error
            Session(user="user2", session_token=test_uuid).save()

    def test_unique_allows_different_uuids(self, session_db):
        """UUIDField with unique=True should allow different UUIDs."""
        uuid1 = uuid.uuid4()
        uuid2 = uuid.uuid4()

        Session(user="user1", session_token=uuid1).save()
        Session(user="user2", session_token=uuid2).save()

        # Should succeed without errors
        sessions = list(Session.objects().all())
        assert len(sessions) == 2

    def test_auto_add_with_unique_generates_unique(self, resource_db):
        """UUIDField with auto_add and unique should generate unique values."""
        resources = []
        for i in range(10):
            resource = Resource(name=f"Resource {i}")
            resource.save()
            resources.append(resource)

        # All UUIDs should be unique
        uuids = [r.resource_id for r in resources]
        assert len(uuids) == len(set(uuids))


# ============================================================================
# Comparison Operations Tests
# ============================================================================


class TestUUIDFieldComparisons:
    """Test suite for UUIDField comparison operations."""

    def test_equality_comparison(self, document_db):
        """Should support equality comparison."""
        test_uuid = uuid.uuid4()

        # Using BinaryExpression
        expr = Document.doc_id == test_uuid
        assert expr.field == Document.doc_id
        assert expr.operator == "="
        assert expr.value == test_uuid

    def test_inequality_comparison(self, document_db):
        """Should support inequality comparison."""
        test_uuid = uuid.uuid4()

        expr = Document.doc_id != test_uuid
        assert expr.operator == "!="
        assert expr.value == test_uuid

    def test_less_than_comparison(self, document_db):
        """Should support less than comparison."""
        test_uuid = uuid.uuid4()

        expr = Document.doc_id < test_uuid
        assert expr.operator == "<"

    def test_greater_than_comparison(self, document_db):
        """Should support greater than comparison."""
        test_uuid = uuid.uuid4()

        expr = Document.doc_id > test_uuid
        assert expr.operator == ">"

    def test_less_equal_comparison(self, document_db):
        """Should support less than or equal comparison."""
        test_uuid = uuid.uuid4()

        expr = Document.doc_id <= test_uuid
        assert expr.operator == "<="

    def test_greater_equal_comparison(self, document_db):
        """Should support greater than or equal comparison."""
        test_uuid = uuid.uuid4()

        expr = Document.doc_id >= test_uuid
        assert expr.operator == ">="


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


class TestUUIDFieldEdgeCases:
    """Test suite for UUIDField edge cases."""

    def test_nil_uuid(self, document_db):
        """Should handle nil UUID (all zeros)."""
        nil_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000")
        doc = Document(title="Nil UUID", doc_id=nil_uuid)
        doc.save()

        retrieved = Document.get(doc.id)
        assert retrieved.doc_id == nil_uuid

    def test_uuid_case_insensitive(self, document_db):
        """Should handle UUID strings case-insensitively."""
        uuid_lower = "123e4567-e89b-12d3-a456-426614174000"
        uuid_upper = "123E4567-E89B-12D3-A456-426614174000"

        doc1 = Document(title="Lower", doc_id=uuid_lower)
        doc2 = Document(title="Upper", doc_id=uuid_upper)

        # Both should be valid (UUID string validation is case-insensitive)
        assert doc1.doc_id == uuid_lower
        assert doc2.doc_id == uuid_upper

    def test_uuid_with_hyphens_normalized(self, document_db):
        """Should handle standard hyphenated UUID format."""
        uuid_str = "123e4567-e89b-12d3-a456-426614174000"
        doc = Document(title="Hyphenated", doc_id=uuid_str)
        doc.save()

        retrieved = Document.get(doc.id)
        # Should be converted to UUID object
        assert isinstance(retrieved.doc_id, uuid.UUID)

    def test_invalid_database_value_raises_error(self, document_db):
        """Should raise error when database contains invalid UUID string."""
        doc = Document(title="Manual Insert", doc_id=uuid.uuid4())
        doc.save()

        # Manually corrupt the database value
        cursor = document_db.connection.cursor()
        cursor.execute(
            "UPDATE document SET doc_id = ? WHERE id = ?", ("invalid-uuid", doc.id)
        )
        document_db.connection.commit()

        # Retrieving should raise an error
        with pytest.raises(ValueError, match="could not parse UUID"):
            Document.get(doc.id)

    def test_to_sql_value_returns_string(self, document_db):
        """to_sql_value should convert UUID to string."""
        test_uuid = uuid.uuid4()
        doc = Document(title="SQL Value", doc_id=test_uuid)

        field = Document._fields["doc_id"]
        sql_value = field.to_sql_value(doc)

        assert isinstance(sql_value, str)
        assert sql_value == str(test_uuid)

    def test_to_sql_value_with_none(self, document_db):
        """to_sql_value should return None for None value."""
        doc = Document(title="Null SQL", doc_id=None)

        field = Document._fields["doc_id"]
        sql_value = field.to_sql_value(doc)

        assert sql_value is None

    def test_from_db_value_with_uuid_object(self, document_db):
        """from_db_value should handle UUID objects."""
        test_uuid = uuid.uuid4()
        field = Document._fields["doc_id"]

        result = field.from_db_value(test_uuid)
        assert result == test_uuid
        assert isinstance(result, uuid.UUID)

    def test_from_db_value_with_string(self, document_db):
        """from_db_value should convert string to UUID."""
        uuid_str = str(uuid.uuid4())
        field = Document._fields["doc_id"]

        result = field.from_db_value(uuid_str)
        assert isinstance(result, uuid.UUID)
        assert str(result) == uuid_str

    def test_from_db_value_with_none(self, document_db):
        """from_db_value should handle None."""
        field = Document._fields["doc_id"]

        result = field.from_db_value(None)
        assert result is None


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


class TestUUIDFieldIntegration:
    """Integration tests for UUIDField with other features."""

    def test_uuid_with_foreign_key_reference(self, db):
        """UUIDField should work alongside ForeignKey fields."""
        from atomsql.orm.fields import ForeignKey

        class Organization(Model):
            name = StringField()
            org_uuid = UUIDField(auto_add=True, unique=True)

        class Employee(Model):
            name = StringField()
            employee_uuid = UUIDField(auto_add=True)
            organization = ForeignKey(Organization)

        db.register(Organization)
        db.register(Employee)

        org = Organization(name="TechCorp")
        org.save()

        emp = Employee(name="John Doe", organization=org)
        emp.save()

        assert isinstance(org.org_uuid, uuid.UUID)
        assert isinstance(emp.employee_uuid, uuid.UUID)
        assert emp.organization.id == org.id

    def test_uuid_in_bulk_operations(self, document_db):
        """UUIDField should work in bulk create operations."""
        documents = []
        for i in range(5):
            doc = Document(title=f"Bulk Doc {i}", doc_id=uuid.uuid4())
            doc.save()
            documents.append(doc)

        retrieved = list(Document.objects().all())
        assert len(retrieved) == 5

        # All should have valid UUIDs
        for doc in retrieved:
            assert isinstance(doc.doc_id, uuid.UUID)

    def test_uuid_update_operations(self, document_db):
        """UUIDField values should be updatable."""
        original_uuid = uuid.uuid4()
        doc = Document(title="Update Test", doc_id=original_uuid)
        doc.save()

        new_uuid = uuid.uuid4()
        doc.doc_id = new_uuid
        doc.save()

        retrieved = Document.get(doc.id)
        assert retrieved.doc_id == new_uuid
        assert retrieved.doc_id != original_uuid

    def test_uuid_with_ordering(self, document_db):
        """UUIDField should work with ordering operations."""
        uuids = [uuid.uuid4() for _ in range(3)]
        uuids.sort()  # Sort by string representation

        for u in reversed(uuids):
            Document(title=f"Doc-{u}", doc_id=u).save()

        # Retrieve all documents
        all_docs = list(Document.objects().all())
        assert len(all_docs) == 3

    def test_uuid_field_metadata(self, document_db):
        """UUIDField should have correct metadata."""
        field = Document._fields["doc_id"]

        assert isinstance(field, UUIDField)
        assert field.name == "doc_id"
        assert field.nullable is True
        assert field.get_sql_type() == "TEXT"
