import pytest
from django.contrib.contenttypes.models import ContentType
from django.db import models

try:
    import yaml

    PYYAML_AVAILABLE = True
    del yaml
except ImportError:
    PYYAML_AVAILABLE = False

from django.core import serializers
from django.core.exceptions import FieldError

from testapp.models import (
    AbstractVegetable,
    AngryBigCat,
    Animal,
    BaseModelWithIndex,
    BigCat,
    Canine,
    Child2,
    Employee,
    Feline,
    Fruit,
    Parrot,
    SubModelA,
    SubModelB,
    UniqueIdentifier,
    Vegetable,
)

from .models import TypedModelManager


@pytest.fixture
def animals(db):
    kitteh = Feline.objects.create(name="kitteh")
    UniqueIdentifier.objects.create(
        name="kitteh",
        object_id=kitteh.pk,
        content_type=ContentType.objects.get_for_model(kitteh),
    )
    cheetah = Feline.objects.create(name="cheetah")
    UniqueIdentifier.objects.create(
        name="cheetah",
        object_id=cheetah.pk,
        content_type=ContentType.objects.get_for_model(cheetah),
    )
    fido = Canine.objects.create(name="fido")
    UniqueIdentifier.objects.create(
        name="fido",
        object_id=fido.pk,
        content_type=ContentType.objects.get_for_model(fido),
    )
    simba = BigCat.objects.create(name="simba")
    UniqueIdentifier.objects.create(
        name="simba",
        object_id=simba.pk,
        content_type=ContentType.objects.get_for_model(simba),
    )
    mufasa = AngryBigCat.objects.create(name="mufasa")
    UniqueIdentifier.objects.create(
        name="mufasa",
        object_id=mufasa.pk,
        content_type=ContentType.objects.get_for_model(mufasa),
    )
    kajtek = Parrot.objects.create(name="Kajtek")
    UniqueIdentifier.objects.create(
        name="kajtek",
        object_id=kajtek.pk,
        content_type=ContentType.objects.get_for_model(kajtek),
    )


def test_can_instantiate_base_model(db):
    # direct instantiation works fine without a type, as long as you don't save
    animal = Animal()
    assert not animal.type
    assert type(animal) is Animal


def test_cant_save_untyped_base_model(db):
    # direct instantiation shouldn't work
    with pytest.raises(RuntimeError):
        Animal.objects.create(name="uhoh")

    # ... unless a type is specified
    Animal.objects.create(name="dingo", type="testapp.canine")

    # ... unless that type is stupid
    with pytest.raises(ValueError):
        Animal.objects.create(name="dingo", type="macaroni.buffaloes")


def test_get_types():
    assert set(Animal.get_types()) == {
        "testapp.canine",
        "testapp.bigcat",
        "testapp.parrot",
        "testapp.angrybigcat",
        "testapp.feline",
    }
    assert set(Canine.get_types()) == {"testapp.canine"}
    assert set(Feline.get_types()) == {
        "testapp.bigcat",
        "testapp.angrybigcat",
        "testapp.feline",
    }


def test_get_type_classes():
    assert set(Animal.get_type_classes()) == {
        Canine,
        BigCat,
        Parrot,
        AngryBigCat,
        Feline,
    }
    assert set(Canine.get_type_classes()) == {Canine}
    assert set(Feline.get_type_classes()) == {BigCat, AngryBigCat, Feline}


def test_type_choices():
    type_choices = Animal._meta.get_field("type").choices
    assert type_choices == [
        ("testapp.angrybigcat", "angry big cat"),
        ("testapp.bigcat", "big cat"),
        ("testapp.canine", "canine"),
        ("testapp.feline", "feline"),
        ("testapp.parrot", "parrot"),
    ]
    assert {cls for cls, _ in type_choices} == set(Animal.get_types())


def test_base_model_queryset(animals):
    # all objects returned
    qs = Animal.objects.all().order_by("type")
    assert [obj.type for obj in qs] == [
        "testapp.angrybigcat",
        "testapp.bigcat",
        "testapp.canine",
        "testapp.feline",
        "testapp.feline",
        "testapp.parrot",
    ]
    assert [type(obj) for obj in qs] == [
        AngryBigCat,
        BigCat,
        Canine,
        Feline,
        Feline,
        Parrot,
    ]


def test_proxy_model_queryset(animals):
    qs = Canine.objects.all().order_by("type")
    assert qs.count() == 1
    assert len(qs) == 1
    assert [obj.type for obj in qs] == ["testapp.canine"]
    assert [type(obj) for obj in qs] == [Canine]

    qs = Feline.objects.all().order_by("type")
    assert qs.count() == 4
    assert len(qs) == 4
    assert [obj.type for obj in qs] == [
        "testapp.angrybigcat",
        "testapp.bigcat",
        "testapp.feline",
        "testapp.feline",
    ]
    assert [type(obj) for obj in qs] == [AngryBigCat, BigCat, Feline, Feline]


def test_doubly_proxied_model_queryset(animals):
    qs = BigCat.objects.all().order_by("type")
    assert qs.count() == 2
    assert len(qs) == 2
    assert [obj.type for obj in qs] == ["testapp.angrybigcat", "testapp.bigcat"]
    assert [type(obj) for obj in qs] == [AngryBigCat, BigCat]


def test_triply_proxied_model_queryset(animals):
    qs = AngryBigCat.objects.all().order_by("type")
    assert qs.count() == 1
    assert len(qs) == 1
    assert [obj.type for obj in qs] == ["testapp.angrybigcat"]
    assert [type(obj) for obj in qs] == [AngryBigCat]


def test_recast_auto(animals):
    cat = Feline.objects.get(name="kitteh")
    cat.type = "testapp.bigcat"
    cat.recast()
    assert cat.type == "testapp.bigcat"
    assert type(cat) == BigCat


def test_recast_to_subclass_string(animals):
    cat = Feline.objects.get(name="kitteh")
    cat.recast("testapp.bigcat")
    assert cat.type == "testapp.bigcat"
    assert type(cat) == BigCat


def test_recast_to_subclass_modelclass(animals):
    cat = Feline.objects.get(name="kitteh")
    cat.recast(BigCat)
    assert cat.type == "testapp.bigcat"
    assert type(cat) == BigCat


def test_recast_string(animals):
    cat = Feline.objects.get(name="kitteh")
    cat.recast("testapp.canine")
    assert cat.type == "testapp.canine"
    assert type(cat) == Canine


def test_recast_modelclass(animals):
    cat = Feline.objects.get(name="kitteh")
    cat.recast(Canine)
    assert cat.type == "testapp.canine"
    assert type(cat) == Canine


def test_recast_fail(animals):
    cat = Feline.objects.get(name="kitteh")
    with pytest.raises(ValueError):
        cat.recast(AbstractVegetable)
    with pytest.raises(ValueError):
        cat.recast("typedmodels.abstractvegetable")
    with pytest.raises(ValueError):
        cat.recast(Vegetable)
    with pytest.raises(ValueError):
        cat.recast("typedmodels.vegetable")


def test_fields_in_subclasses(animals):
    canine = Canine.objects.all()[0]
    angry = AngryBigCat.objects.all()[0]

    angry.mice_eaten = 5
    angry.save()
    assert AngryBigCat.objects.get(pk=angry.pk).mice_eaten == 5

    angry.canines_eaten.add(canine)
    assert list(angry.canines_eaten.all()) == [canine]

    # Feline class was created before Parrot and has mice_eaten field which is non-m2m, so it may break accessing
    # known_words field in Parrot instances (since Django 1.5).
    parrot = Parrot.objects.all()[0]
    parrot.known_words = 500
    parrot.save()
    assert Parrot.objects.get(pk=parrot.pk).known_words == 500


def test_fields_cache():
    mice_eaten = Feline._meta.get_field("mice_eaten")
    known_words = Parrot._meta.get_field("known_words")
    assert mice_eaten in AngryBigCat._meta.fields
    assert mice_eaten in Feline._meta.fields
    assert mice_eaten not in Parrot._meta.fields
    assert known_words in Parrot._meta.fields
    assert known_words not in AngryBigCat._meta.fields
    assert known_words not in Feline._meta.fields


def test_m2m_cache():
    canines_eaten = AngryBigCat._meta.get_field("canines_eaten")
    assert canines_eaten in AngryBigCat._meta.many_to_many
    assert canines_eaten not in Feline._meta.many_to_many
    assert canines_eaten not in Parrot._meta.many_to_many


def test_related_names(animals):
    """Ensure that accessor names for reverse relations are generated properly."""

    canine = Canine.objects.all()[0]
    assert hasattr(canine, "angrybigcat_set")


def test_queryset_defer(db):
    """
    Ensure that qs.defer() works correctly
    """
    Vegetable.objects.create(name="cauliflower", color="white", yumness=1)
    Vegetable.objects.create(name="spinach", color="green", yumness=5)
    Vegetable.objects.create(name="sweetcorn", color="yellow", yumness=10)
    Fruit.objects.create(name="Apple", color="red", yumness=7)

    qs = AbstractVegetable.objects.defer("yumness")

    objs = set(qs)
    for o in objs:
        assert isinstance(o, AbstractVegetable)
        assert set(o.get_deferred_fields()) == {"yumness"}
        # does a query, since this field was deferred
        assert isinstance(o.yumness, float)


def test_queryset_defer_type(db):
    Vegetable.objects.create(name="cauliflower", color="white", yumness=1)
    Fruit.objects.create(name="Apple", color="red", yumness=7)

    qs = AbstractVegetable.objects.only("id")
    assert len(qs) == 2
    assert type(qs[0]) is AbstractVegetable
    assert type(qs[1]) is AbstractVegetable

    qs = Vegetable.objects.only("id")
    assert len(qs) == 1
    assert type(qs[0]) is Vegetable


def test_queryset_defer_type_with_subclass_fields(db, animals):
    # <relatedQuerySet>.delete() tends to do this .only('id') thing while collating referenced models.
    # So it needs to work even if you think it's a weird thing to do
    # In this case we *don't* auto-cast to anything; all returned models are Animal instances
    # this avoids the following error:
    #     django.core.exceptions.FieldDoesNotExist: AngryBigCat has no field named 'known_words'
    qs = list(Animal.objects.only("id"))
    assert len(qs) == 6
    assert all(type(x) is Animal for x in qs)


@pytest.mark.parametrize(
    "fmt",
    [
        "xml",
        "json",
        pytest.param(
            "yaml",
            marks=[pytest.mark.skipif(not PYYAML_AVAILABLE, reason="PyYAML is not available")],
        ),
    ],
)
def test_serialization(fmt, animals):
    """Helper function used to check serialization and deserialization for concrete format."""
    animals = Animal.objects.order_by("pk")
    serialized_animals = serializers.serialize(fmt, animals)
    deserialized_animals = [
        wrapper.object for wrapper in serializers.deserialize(fmt, serialized_animals)
    ]
    assert set(deserialized_animals) == set(animals)


def test_generic_relation(animals):
    for animal in Animal.objects.all():
        assert hasattr(animal, "unique_identifiers")
        assert animal.unique_identifiers.all()

    Feline._meta.get_field("unique_identifiers")
    for feline in Feline.objects.all():
        assert hasattr(feline, "unique_identifiers")
        assert feline.unique_identifiers.all()

    for uid in UniqueIdentifier.objects.all():
        cls = uid.referent.__class__
        animal = cls.objects.filter(unique_identifiers=uid)
        assert isinstance(animal.first(), Animal)

    for uid in UniqueIdentifier.objects.all():
        cls = uid.referent.__class__
        animal = cls.objects.filter(unique_identifiers__name=uid.name)
        assert isinstance(animal.first(), Animal)


def test_manager_classes():
    assert isinstance(Animal.objects, TypedModelManager)
    assert isinstance(Feline.objects, TypedModelManager)
    assert isinstance(BigCat.objects, TypedModelManager)

    # This one has a custom manager defined, but that shouldn't prevent objects from working
    assert isinstance(AbstractVegetable.mymanager, models.Manager)
    assert isinstance(AbstractVegetable.objects, TypedModelManager)

    # subclasses work the same way
    assert isinstance(Vegetable.mymanager, models.Manager)
    assert isinstance(Vegetable.objects, TypedModelManager)


def test_uniqueness_check_on_child(db):
    child2 = Child2.objects.create(a="a")

    # Regression test for https://github.com/craigds/django-typed-models/issues/42
    # FieldDoesNotExist: Child2 has no field named 'b'
    child2.validate_unique()


def test_uniqueness_check_include_meta_constraints(db):
    """Django 4.1 introduces a new required kwarg
    django/forms/models.py in validate_unique at line 809
    for form in valid_forms:
    exclude = form._get_validation_exclusions()
    unique_checks, date_checks = form.instance._get_unique_checks(
        exclude=exclude,
        include_meta_constraints=True,
    )
    """
    child2 = Child2.objects.create(a="a")
    child2._get_unique_checks()


def test_non_nullable_subclass_field_error():
    with pytest.raises(FieldError):

        class Bug(Animal):
            # should have null=True
            num_legs = models.PositiveIntegerField()


def test_explicit_recast_with_class_on_untyped_instance():
    animal = Animal()
    assert not animal.type
    animal.recast(Feline)
    assert animal.type == "testapp.feline"
    assert type(animal) is Feline


def test_explicit_recast_with_string_on_untyped_instance():
    animal = Animal()
    assert not animal.type
    animal.recast("testapp.feline")
    assert animal.type == "testapp.feline"
    assert type(animal) is Feline


def test_same_field_name_in_two_subclasses():
    with pytest.raises(ValueError):

        class Tester1(Employee):
            name = models.CharField(max_length=255, blank=True, null=True)

    with pytest.raises(ValueError):

        class Tester2(Employee):
            name = models.CharField(max_length=254, null=True)

    with pytest.raises(ValueError):

        class Tester3(Employee):
            name = models.IntegerField(null=True)


def test_base_model_with_indexes(db):
    """Test that indexes on base TypedModel work correctly (issue #58).

    The issue: When a TypedModel base class has indexes in Meta, and proxy
    subclasses inherit from that Meta, they would inherit the indexes attribute.
    Django's system checks would then fail because the indexed fields aren't
    local to the proxy models.

    The fix: TypedModelMetaclass removes the 'indexes' attribute from the Meta
    class before Django's ModelBase processes proxy models, preventing the error.
    """
    # Verify that SubModelA and SubModelB are proxy models
    assert SubModelA._meta.proxy is True
    assert SubModelB._meta.proxy is True

    # The base model should have the index in its _meta
    assert (
        len(BaseModelWithIndex._meta.indexes) == 1
    ), "Base model should have its index defined in _meta"

    # Proxy models should NOT have indexes in their _meta (they share the base table)
    assert (
        len(SubModelA._meta.indexes) == 0
    ), "Proxy models shouldn't have indexes in _meta (they use the base model's table)"
    assert (
        len(SubModelB._meta.indexes) == 0
    ), "Proxy models shouldn't have indexes in _meta (they use the base model's table)"

    # Verify the models work at runtime
    obj_a = SubModelA.objects.create(name="test_a", tag="tag1", field_a="a_value")
    obj_b = SubModelB.objects.create(name="test_b", tag="tag2", field_b=42)

    assert SubModelA.objects.get(pk=obj_a.pk).field_a == "a_value"
    assert SubModelB.objects.get(pk=obj_b.pk).field_b == 42
    assert SubModelA.objects.count() == 1
    assert SubModelB.objects.count() == 1
    assert BaseModelWithIndex.objects.count() == 2
