from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.checks import Error
from django.core.exceptions import FieldDoesNotExist, FieldError
from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps


@isolate_apps("model_inheritance")
class AbstractInheritanceTests(SimpleTestCase):
    def test_single_parent(self):
        class AbstractBase(models.Model):
            name = models.CharField(max_length=30)

            class Meta:
                abstract = True

        class AbstractDescendant(AbstractBase):
            name = models.CharField(max_length=50)

            class Meta:
                abstract = True

        class DerivedChild(AbstractBase):
            name = models.CharField(max_length=50)

        class DerivedGrandChild(AbstractDescendant):
            pass

        self.assertEqual(AbstractDescendant._meta.get_field("name").max_length, 50)
        self.assertEqual(DerivedChild._meta.get_field("name").max_length, 50)
        self.assertEqual(DerivedGrandChild._meta.get_field("name").max_length, 50)

    def test_multiple_inheritance_allows_inherited_field(self):
        """
        Single layer multiple inheritance is as expected, deriving the
        inherited field from the first base.
        """

        class ParentA(models.Model):
            name = models.CharField(max_length=255)

            class Meta:
                abstract = True

        class ParentB(models.Model):
            name = models.IntegerField()

            class Meta:
                abstract = True

        class Child(ParentA, ParentB):
            pass

        self.assertEqual(Child.check(), [])
        inherited_field = Child._meta.get_field("name")
        self.assertIsInstance(inherited_field, models.CharField)
        self.assertEqual(inherited_field.max_length, 255)

    def test_diamond_shaped_multiple_inheritance_is_depth_first(self):
        """
        In contrast to standard Python MRO, resolution of inherited fields is
        strictly depth-first, rather than breadth-first in diamond-shaped cases.

        This is because a copy of the parent field descriptor is placed onto
        the model class in ModelBase.__new__(), rather than the attribute
        lookup going via bases. (It only **looks** like inheritance.)

        Here, Child inherits name from Root, rather than ParentB.
        """

        class Root(models.Model):
            name = models.CharField(max_length=255)

            class Meta:
                abstract = True

        class ParentA(Root):
            class Meta:
                abstract = True

        class ParentB(Root):
            name = models.IntegerField()

            class Meta:
                abstract = True

        class Child(ParentA, ParentB):
            pass

        self.assertEqual(Child.check(), [])
        inherited_field = Child._meta.get_field("name")
        self.assertIsInstance(inherited_field, models.CharField)
        self.assertEqual(inherited_field.max_length, 255)

    def test_target_field_may_be_pushed_down(self):
        """
        Where the Child model needs to inherit a field from a different base
        than that given by depth-first resolution, the target field can be
        **pushed down** by being re-declared.
        """

        class Root(models.Model):
            name = models.CharField(max_length=255)

            class Meta:
                abstract = True

        class ParentA(Root):
            class Meta:
                abstract = True

        class ParentB(Root):
            name = models.IntegerField()

            class Meta:
                abstract = True

        class Child(ParentA, ParentB):
            name = models.IntegerField()

        self.assertEqual(Child.check(), [])
        inherited_field = Child._meta.get_field("name")
        self.assertIsInstance(inherited_field, models.IntegerField)

    def test_multiple_inheritance_cannot_shadow_concrete_inherited_field(self):
        class ConcreteParent(models.Model):
            name = models.CharField(max_length=255)

        class AbstractParent(models.Model):
            name = models.IntegerField()

            class Meta:
                abstract = True

        class FirstChild(ConcreteParent, AbstractParent):
            pass

        class AnotherChild(AbstractParent, ConcreteParent):
            pass

        self.assertIsInstance(FirstChild._meta.get_field("name"), models.CharField)
        self.assertEqual(
            AnotherChild.check(),
            [
                Error(
                    "The field 'name' clashes with the field 'name' "
                    "from model 'model_inheritance.concreteparent'.",
                    obj=AnotherChild._meta.get_field("name"),
                    id="models.E006",
                )
            ],
        )

    def test_virtual_field(self):
        class RelationModel(models.Model):
            content_type = models.ForeignKey(ContentType, models.CASCADE)
            object_id = models.PositiveIntegerField()
            content_object = GenericForeignKey("content_type", "object_id")

        class RelatedModelAbstract(models.Model):
            field = GenericRelation(RelationModel)

            class Meta:
                abstract = True

        class ModelAbstract(models.Model):
            field = models.CharField(max_length=100)

            class Meta:
                abstract = True

        class OverrideRelatedModelAbstract(RelatedModelAbstract):
            field = models.CharField(max_length=100)

        class ExtendModelAbstract(ModelAbstract):
            field = GenericRelation(RelationModel)

        self.assertIsInstance(
            OverrideRelatedModelAbstract._meta.get_field("field"), models.CharField
        )
        self.assertIsInstance(
            ExtendModelAbstract._meta.get_field("field"), GenericRelation
        )

    def test_cannot_override_indirect_abstract_field(self):
        class AbstractBase(models.Model):
            name = models.CharField(max_length=30)

            class Meta:
                abstract = True

        class ConcreteDescendant(AbstractBase):
            pass

        msg = (
            "Local field 'name' in class 'Descendant' clashes with field of "
            "the same name from base class 'ConcreteDescendant'."
        )
        with self.assertRaisesMessage(FieldError, msg):

            class Descendant(ConcreteDescendant):
                name = models.IntegerField()

    def test_override_field_with_attr(self):
        class AbstractBase(models.Model):
            first_name = models.CharField(max_length=50)
            last_name = models.CharField(max_length=50)
            middle_name = models.CharField(max_length=30)
            full_name = models.CharField(max_length=150)

            class Meta:
                abstract = True

        class Descendant(AbstractBase):
            middle_name = None

            def full_name(self):
                return self.first_name + self.last_name

        msg = "Descendant has no field named %r"
        with self.assertRaisesMessage(FieldDoesNotExist, msg % "middle_name"):
            Descendant._meta.get_field("middle_name")

        with self.assertRaisesMessage(FieldDoesNotExist, msg % "full_name"):
            Descendant._meta.get_field("full_name")

    def test_overriding_field_removed_by_concrete_model(self):
        class AbstractModel(models.Model):
            foo = models.CharField(max_length=30)

            class Meta:
                abstract = True

        class RemovedAbstractModelField(AbstractModel):
            foo = None

        class OverrideRemovedFieldByConcreteModel(RemovedAbstractModelField):
            foo = models.CharField(max_length=50)

        self.assertEqual(
            OverrideRemovedFieldByConcreteModel._meta.get_field("foo").max_length, 50
        )

    def test_shadowed_fkey_id(self):
        class Foo(models.Model):
            pass

        class AbstractBase(models.Model):
            foo = models.ForeignKey(Foo, models.CASCADE)

            class Meta:
                abstract = True

        class Descendant(AbstractBase):
            foo_id = models.IntegerField()

        self.assertEqual(
            Descendant.check(),
            [
                Error(
                    "The field 'foo_id' clashes with the field 'foo' "
                    "from model 'model_inheritance.descendant'.",
                    obj=Descendant._meta.get_field("foo_id"),
                    id="models.E006",
                )
            ],
        )

    def test_shadow_related_name_when_set_to_none(self):
        class AbstractBase(models.Model):
            bar = models.IntegerField()

            class Meta:
                abstract = True

        class Foo(AbstractBase):
            bar = None
            foo = models.IntegerField()

        class Bar(models.Model):
            bar = models.ForeignKey(Foo, models.CASCADE, related_name="bar")

        self.assertEqual(Bar.check(), [])

    def test_reverse_foreign_key(self):
        class AbstractBase(models.Model):
            foo = models.CharField(max_length=100)

            class Meta:
                abstract = True

        class Descendant(AbstractBase):
            pass

        class Foo(models.Model):
            foo = models.ForeignKey(Descendant, models.CASCADE, related_name="foo")

        self.assertEqual(
            Foo._meta.get_field("foo").check(),
            [
                Error(
                    "Reverse accessor 'Descendant.foo' for "
                    "'model_inheritance.Foo.foo' clashes with field name "
                    "'model_inheritance.Descendant.foo'.",
                    hint=(
                        "Rename field 'model_inheritance.Descendant.foo', or "
                        "add/change a related_name argument to the definition "
                        "for field 'model_inheritance.Foo.foo'."
                    ),
                    obj=Foo._meta.get_field("foo"),
                    id="fields.E302",
                ),
                Error(
                    "Reverse query name for 'model_inheritance.Foo.foo' "
                    "clashes with field name "
                    "'model_inheritance.Descendant.foo'.",
                    hint=(
                        "Rename field 'model_inheritance.Descendant.foo', or "
                        "add/change a related_name argument to the definition "
                        "for field 'model_inheritance.Foo.foo'."
                    ),
                    obj=Foo._meta.get_field("foo"),
                    id="fields.E303",
                ),
            ],
        )

    def test_multi_inheritance_field_clashes(self):
        class AbstractBase(models.Model):
            name = models.CharField(max_length=30)

            class Meta:
                abstract = True

        class ConcreteBase(AbstractBase):
            pass

        class AbstractDescendant(ConcreteBase):
            class Meta:
                abstract = True

        class ConcreteDescendant(AbstractDescendant):
            name = models.CharField(max_length=100)

        self.assertEqual(
            ConcreteDescendant.check(),
            [
                Error(
                    "The field 'name' clashes with the field 'name' from "
                    "model 'model_inheritance.concretebase'.",
                    obj=ConcreteDescendant._meta.get_field("name"),
                    id="models.E006",
                )
            ],
        )

    def test_override_one2one_relation_auto_field_clashes(self):
        class ConcreteParent(models.Model):
            name = models.CharField(max_length=255)

        class AbstractParent(models.Model):
            name = models.IntegerField()

            class Meta:
                abstract = True

        msg = (
            "Auto-generated field 'concreteparent_ptr' in class 'Descendant' "
            "for parent_link to base class 'ConcreteParent' clashes with "
            "declared field of the same name."
        )
        with self.assertRaisesMessage(FieldError, msg):

            class Descendant(ConcreteParent, AbstractParent):
                concreteparent_ptr = models.CharField(max_length=30)

    def test_abstract_model_with_regular_python_mixin_mro(self):
        class AbstractModel(models.Model):
            name = models.CharField(max_length=255)
            age = models.IntegerField()

            class Meta:
                abstract = True

        class Mixin:
            age = None

        class Mixin2:
            age = 2

        class DescendantMixin(Mixin):
            pass

        class ConcreteModel(models.Model):
            foo = models.IntegerField()

        class ConcreteModel2(ConcreteModel):
            age = models.SmallIntegerField()

        def fields(model):
            if not hasattr(model, "_meta"):
                return []
            return [(f.name, f.__class__) for f in model._meta.get_fields()]

        model_dict = {"__module__": "model_inheritance"}
        model1 = type("Model1", (AbstractModel, Mixin), model_dict.copy())
        model2 = type("Model2", (Mixin2, AbstractModel), model_dict.copy())
        model3 = type("Model3", (DescendantMixin, AbstractModel), model_dict.copy())
        model4 = type("Model4", (Mixin2, Mixin, AbstractModel), model_dict.copy())
        model5 = type(
            "Model5", (Mixin2, ConcreteModel2, Mixin, AbstractModel), model_dict.copy()
        )

        self.assertEqual(
            fields(model1),
            [
                ("id", models.AutoField),
                ("name", models.CharField),
                ("age", models.IntegerField),
            ],
        )

        self.assertEqual(
            fields(model2), [("id", models.AutoField), ("name", models.CharField)]
        )
        self.assertEqual(getattr(model2, "age"), 2)

        self.assertEqual(
            fields(model3), [("id", models.AutoField), ("name", models.CharField)]
        )

        self.assertEqual(
            fields(model4), [("id", models.AutoField), ("name", models.CharField)]
        )
        self.assertEqual(getattr(model4, "age"), 2)

        self.assertEqual(
            fields(model5),
            [
                ("id", models.AutoField),
                ("foo", models.IntegerField),
                ("concretemodel_ptr", models.OneToOneField),
                ("age", models.SmallIntegerField),
                ("concretemodel2_ptr", models.OneToOneField),
                ("name", models.CharField),
            ],
        )
