import pytest
from django.db import models

from djino.models import SyncModel
from djino.typegen.field_maps import introspect_field, _get_pk_ts_type


class TestFieldTypeMapping:
    def test_char_field(self):
        class M(SyncModel):
            name = models.CharField(max_length=100)

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("name"), "M")
        assert info.ts_type == "string"
        assert info.descriptor_class == "StringField"

    def test_text_field(self):
        class M(SyncModel):
            body = models.TextField()

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("body"), "M")
        assert info.ts_type == "string"
        assert info.descriptor_class == "StringField"

    def test_integer_field(self):
        class M(SyncModel):
            count = models.IntegerField()

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("count"), "M")
        assert info.ts_type == "number"
        assert info.descriptor_class == "NumberField"

    def test_boolean_field(self):
        class M(SyncModel):
            active = models.BooleanField(default=True)

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("active"), "M")
        assert info.ts_type == "boolean"
        assert info.descriptor_class == "BooleanField"

    def test_datetime_field(self):
        class M(SyncModel):
            created = models.DateTimeField()

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("created"), "M")
        assert info.ts_type == "string"
        assert info.descriptor_class == "DateTimeField"

    def test_date_field(self):
        class M(SyncModel):
            birthday = models.DateField()

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("birthday"), "M")
        assert info.ts_type == "string"
        assert info.descriptor_class == "DateField"

    def test_uuid_field(self):
        class M(SyncModel):
            uid = models.UUIDField()

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("uid"), "M")
        assert info.ts_type == "string"
        assert info.descriptor_class == "UUIDField"

    def test_float_field(self):
        class M(SyncModel):
            price = models.FloatField()

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("price"), "M")
        assert info.ts_type == "number"
        assert info.descriptor_class == "NumberField"

    def test_decimal_field(self):
        class M(SyncModel):
            amount = models.DecimalField(max_digits=10, decimal_places=2)

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("amount"), "M")
        assert info.ts_type == "string"
        assert info.descriptor_class == "NumberField"

    def test_nullable_adds_null(self):
        class M(SyncModel):
            name = models.CharField(max_length=100, null=True)

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("name"), "M")
        assert "null" in info.ts_type
        assert info.is_nullable is True

    def test_choices_create_union(self):
        class M(SyncModel):
            status = models.CharField(
                max_length=20,
                choices=[("draft", "Draft"), ("published", "Published")],
            )

            class Meta:
                app_label = "test"

        info = introspect_field(M._meta.get_field("status"), "M")
        assert info.choices == ["draft", "published"]
        assert info.choice_type_name == "MStatus"
        assert info.descriptor_class == "StringChoiceField"

    def test_foreign_key(self):
        from tests.testapp.models import Note

        info = introspect_field(Note._meta.get_field("author"), "Note")
        assert info.descriptor_class == "ForeignKeyField"
        assert info.is_relation is True
        assert info.related_model == "auth.User"
        assert info.column == "author_id"

    def test_many_to_many(self):
        from tests.testapp.models import Note

        info = introspect_field(Note._meta.get_field("tags"), "Note")
        assert info.descriptor_class == "ManyToManyField"
        assert info.is_relation is True
        assert info.related_model == "testapp.Tag"

    def test_auto_field_is_number(self):
        from tests.testapp.models import Note

        info = introspect_field(Note._meta.get_field("id"), "Note")
        assert info.ts_type == "number"
        assert info.is_primary_key is True

    def test_djino_fields_included(self):
        from tests.testapp.models import Note

        field_names = set()
        for field in Note._meta.get_fields():
            info = introspect_field(field, "Note")
            if info:
                field_names.add(info.name)

        assert "_djino_id" in field_names
        assert "_djino_rev" in field_names
        assert "_djino_deleted" in field_names
        assert "_djino_updated_at" in field_names

    def test_reverse_relations_skipped(self):
        from tests.testapp.models import Tag

        # Tag has a reverse M2M from Note. It should be skipped.
        for field in Tag._meta.get_fields():
            info = introspect_field(field, "Tag")
            if info:
                assert not info.is_reverse_relation


class TestPkTypeDetection:
    def test_default_pk_is_number(self):
        from tests.testapp.models import Note

        assert _get_pk_ts_type(Note) == "number"

    def test_uuid_pk(self):
        class M(SyncModel):
            id = models.UUIDField(primary_key=True)

            class Meta:
                app_label = "test"

        assert _get_pk_ts_type(M) == "string"
