# tests/test_field_choices.py
import pytest
from atomsql import Database, Model
from atomsql.orm.fields import CharField, IntegerField, BooleanField


class TestFieldChoicesValidation:
    """Test choices validation at the Python level."""

    def test_charfield_with_choices(self):
        """Test CharField with string choices."""

        class User(Model):
            status = CharField(max_length=20, choices=["active", "inactive", "pending"])

        db = Database("sqlite:///:memory:")
        db.register(User)

        # Valid choice should work
        user = User(status="active")
        assert user.status == "active"

        # Invalid choice should raise ValueError
        with pytest.raises(ValueError) as exc_info:
            user.status = "invalid"
        assert "not in choices" in str(exc_info.value)

    def test_integerfield_with_choices(self):
        """Test IntegerField with integer choices."""

        class Product(Model):
            rating = IntegerField(choices=[1, 2, 3, 4, 5])

        db = Database("sqlite:///:memory:")
        db.register(Product)

        # Valid choice
        product = Product(rating=5)
        assert product.rating == 5

        # Invalid choice
        with pytest.raises(ValueError) as exc_info:
            product.rating = 10
        assert "not in choices" in str(exc_info.value)

    def test_booleanfield_with_choices(self):
        """Test BooleanField with boolean choices (edge case)."""

        class Setting(Model):
            enabled = BooleanField(choices=[True])  # Only True allowed

        db = Database("sqlite:///:memory:")
        db.register(Setting)

        # Valid choice
        setting = Setting(enabled=True)
        assert setting.enabled is True

        # Invalid choice (False not in choices)
        with pytest.raises(ValueError) as exc_info:
            setting.enabled = False
        assert "not in choices" in str(exc_info.value)

    def test_choices_with_nullable(self):
        """Test that None is allowed when nullable=True even if not in choices."""

        class Item(Model):
            category = CharField(max_length=20, choices=["A", "B", "C"], nullable=True)

        db = Database("sqlite:///:memory:")
        db.register(Item)

        # None should be allowed with nullable=True
        item = Item(category=None)
        assert item.category is None

        # Can set to valid choice
        item.category = "A"
        assert item.category == "A"

        # Can set back to None
        item.category = None
        assert item.category is None

    def test_choices_with_nullable_false(self):
        """Test that None is not allowed when nullable=False."""

        class Item(Model):
            category = CharField(max_length=20, choices=["A", "B", "C"], nullable=False)

        db = Database("sqlite:///:memory:")
        db.register(Item)

        # None should not be allowed (caught by StringField.validate_type)
        with pytest.raises(ValueError) as exc_info:
            Item(category=None)
        # The error comes from type validation, not nullable check
        assert "expected a str" in str(exc_info.value) or "cannot be None" in str(
            exc_info.value
        )

    def test_empty_choices_raises_error(self):
        """Test that empty choices list raises ValueError."""
        with pytest.raises(ValueError) as exc_info:

            class BadModel(Model):
                status = CharField(max_length=20, choices=[])

        assert "must not be empty" in str(exc_info.value)

    def test_invalid_choices_type_raises_error(self):
        """Test that non-list/tuple choices raises TypeError."""
        with pytest.raises(TypeError) as exc_info:

            class BadModel(Model):
                status = CharField(max_length=20, choices="invalid")

        assert "must be a list or tuple" in str(exc_info.value)

    def test_choices_stored_as_tuple(self):
        """Test that choices are stored as immutable tuple."""

        class Item(Model):
            status = CharField(max_length=20, choices=["A", "B"])

        assert isinstance(Item._fields["status"].choices, tuple)
        assert Item._fields["status"].choices == ("A", "B")


class TestFieldChoicesPersistence:
    """Test choices with database operations."""

    def test_save_and_retrieve_with_choices(self):
        """Test saving and retrieving models with choice fields."""

        class Status(Model):
            name = CharField(max_length=50, choices=["draft", "published", "archived"])

        db = Database("sqlite:///:memory:")
        db.register(Status)

        # Create and save
        status = Status(name="draft")
        status.save()

        # Retrieve
        retrieved = Status.get(status.id)
        assert retrieved.name == "draft"

        # Update to valid choice
        retrieved.name = "published"
        retrieved.save()

        # Verify update
        updated = Status.get(status.id)
        assert updated.name == "published"

    def test_invalid_choice_before_save(self):
        """Test that invalid choice raises error before save."""

        class Article(Model):
            state = CharField(max_length=20, choices=["draft", "review", "published"])

        db = Database("sqlite:///:memory:")
        db.register(Article)

        article = Article(state="draft")

        # Try to set invalid choice
        with pytest.raises(ValueError) as exc_info:
            article.state = "invalid_state"
        assert "not in choices" in str(exc_info.value)

        # Article should still have valid state
        assert article.state == "draft"

    def test_filter_with_choices(self):
        """Test querying with choice field values."""

        class Task(Model):
            title = CharField(max_length=100)
            priority = CharField(max_length=10, choices=["low", "medium", "high"])

        db = Database("sqlite:///:memory:")
        db.register(Task)

        # Create tasks with different priorities
        Task.create(title="Task 1", priority="low")
        Task.create(title="Task 2", priority="high")
        Task.create(title="Task 3", priority="medium")
        Task.create(title="Task 4", priority="high")

        # Filter by choice value
        high_priority = list(Task.filter(priority="high"))
        assert len(high_priority) == 2
        assert all(t.priority == "high" for t in high_priority)

        low_priority = list(Task.filter(priority="low"))
        assert len(low_priority) == 1
        assert low_priority[0].title == "Task 1"


class TestCheckConstraints:
    """Test CHECK constraint generation in SQL."""

    def test_check_constraint_in_sqlite(self):
        """Test that CHECK constraint is created in SQLite."""

        class Product(Model):
            size = CharField(max_length=10, choices=["S", "M", "L", "XL"])

        db = Database("sqlite:///:memory:")
        db.register(Product)

        # Try to insert invalid value directly via SQL (should fail with CHECK constraint)
        # Note: In practice, Python validation catches this first
        product = Product(size="M")
        product.save()
        assert product.size == "M"

    def test_check_constraint_with_multiple_types(self):
        """Test CHECK constraints with different value types."""

        class Mixed(Model):
            text_choice = CharField(max_length=20, choices=["option1", "option2"])
            int_choice = IntegerField(choices=[10, 20, 30])
            bool_choice = BooleanField(choices=[True, False])

        db = Database("sqlite:///:memory:")
        db.register(Mixed)

        # Create valid instance
        instance = Mixed(text_choice="option1", int_choice=10, bool_choice=True)
        instance.save()

        # Verify
        retrieved = Mixed.get(instance.id)
        assert retrieved.text_choice == "option1"
        assert retrieved.int_choice == 10
        assert retrieved.bool_choice is True

    def test_choices_with_special_characters(self):
        """Test choices containing special characters like quotes."""

        class Comment(Model):
            sentiment = CharField(
                max_length=50, choices=["positive", "negative", "it's neutral"]
            )

        db = Database("sqlite:///:memory:")
        db.register(Comment)

        # Should handle quotes in choices
        comment = Comment(sentiment="it's neutral")
        comment.save()

        retrieved = Comment.get(comment.id)
        assert retrieved.sentiment == "it's neutral"


class TestChoicesEdgeCases:
    """Test edge cases and corner scenarios."""

    def test_choices_with_duplicate_values(self):
        """Test that duplicate values in choices are handled (de-duplicated by tuple)."""

        class Item(Model):
            status = CharField(max_length=20, choices=["active", "active", "inactive"])

        # Duplicates are preserved in tuple but both work
        db = Database("sqlite:///:memory:")
        db.register(Item)

        item = Item(status="active")
        assert item.status == "active"

    def test_default_value_must_be_in_choices(self):
        """Test that default value should be in choices."""

        class Item(Model):
            status = CharField(max_length=20, choices=["A", "B"], default="A")

        db = Database("sqlite:///:memory:")
        db.register(Item)

        # Default should work
        item = Item()
        assert item.status == "A"

    def test_default_value_not_in_choices_caught_on_access(self):
        """Test behavior when default is not in choices."""

        # This is a programming error - default should be in choices
        # The validation now catches this when setting the default value
        class Item(Model):
            status = CharField(max_length=20, choices=["A", "B"], default="C")

        db = Database("sqlite:///:memory:")
        db.register(Item)

        # Should raise error when creating instance with invalid default
        with pytest.raises(ValueError) as exc_info:
            Item()
        assert "not in choices" in str(exc_info.value)

    def test_update_or_create_with_choices(self):
        """Test update_or_create with choice fields."""

        class Config(Model):
            key = CharField(max_length=50, unique=True)
            env = CharField(max_length=20, choices=["dev", "staging", "prod"])

        db = Database("sqlite:///:memory:")
        db.register(Config)

        # Create
        config, created = Config.update_or_create(
            key="app_mode", defaults={"env": "dev"}
        )
        assert created
        assert config.env == "dev"

        # Update
        config, created = Config.update_or_create(
            key="app_mode", defaults={"env": "prod"}
        )
        assert not created
        assert config.env == "prod"

    def test_choices_with_numeric_strings(self):
        """Test choices with numeric strings."""

        class Item(Model):
            code = CharField(max_length=10, choices=["001", "002", "003"])

        db = Database("sqlite:///:memory:")
        db.register(Item)

        item = Item(code="001")
        assert item.code == "001"

        # Numeric value should not match string choice
        with pytest.raises(ValueError):
            item.code = 1  # int 1, not string "001"


class TestChoicesWithoutChoices:
    """Test that fields without choices still work normally."""

    def test_field_without_choices_unrestricted(self):
        """Test that fields without choices accept any valid value."""

        class User(Model):
            name = CharField(max_length=100)  # No choices
            status = CharField(max_length=20, choices=["active", "inactive"])

        db = Database("sqlite:///:memory:")
        db.register(User)

        # Name can be anything
        user = User(name="Any Name Here!", status="active")
        assert user.name == "Any Name Here!"

        # Status is restricted
        with pytest.raises(ValueError):
            user.status = "random"
