from django.apps.registry import Apps
from django.contrib.contenttypes.models import ContentType
from django.core import checks
from django.db import models
from django.test.testcases import SimpleTestCase

from polymodels.models import (
    EMPTY_ACCESSOR, BasePolymorphicModel, SubclassAccessor, SubclassAccessors,
)

from .base import TestCase
from .models import Animal, BigSnake, HugeSnake, Mammal, Snake


class BasePolymorphicModelTest(TestCase):
    def test_checks(self):
        test_apps = Apps()
        options = type(str('Meta'), (), {'apps': test_apps, 'app_label': 'polymodels'})

        class NoCtFieldModel(BasePolymorphicModel):
            Meta = options

        self.assertIn(checks.Error(
            '`BasePolymorphicModel` subclasses must define a `CONTENT_TYPE_FIELD`.',
            hint=None,
            obj=NoCtFieldModel,
            id='polymodels.E001',
        ), NoCtFieldModel.check())

        class InexistentCtFieldModel(BasePolymorphicModel):
            CONTENT_TYPE_FIELD = 'inexistent_field'

            Meta = options

        self.assertIn(checks.Error(
            "`CONTENT_TYPE_FIELD` points to an inexistent field 'inexistent_field'.",
            hint=None,
            obj=InexistentCtFieldModel,
            id='polymodels.E002',
        ), InexistentCtFieldModel.check())

        class InvalidCtFieldModel(BasePolymorphicModel):
            CONTENT_TYPE_FIELD = 'a_char_field'
            a_char_field = models.CharField(max_length=255)

            Meta = options

        self.assertIn(checks.Error(
            "`a_char_field` must be a `ForeignKey` to `ContentType`.",
            hint=None,
            obj=InvalidCtFieldModel._meta.get_field('a_char_field'),
            id='polymodels.E003',
        ), InvalidCtFieldModel.check())

        class InvalidCtFkFieldToModel(BasePolymorphicModel):
            CONTENT_TYPE_FIELD = 'a_fk'
            a_fk = models.ForeignKey('self', on_delete=models.CASCADE)

            Meta = options

        self.assertIn(checks.Error(
            "`a_fk` must be a `ForeignKey` to `ContentType`.",
            hint=None,
            obj=InvalidCtFkFieldToModel._meta.get_field('a_fk'),
            id='polymodels.E003',
        ), InvalidCtFkFieldToModel.check())

    def test_type_cast(self):
        animal_dog = Animal.objects.create(name='dog')
        with self.assertNumQueries(0):
            self.assertEqual(
                animal_dog.type_cast(), animal_dog,
                'Type casting a correctly typed class should work.'
            )

        mammal_cat = Mammal.objects.create(name='cat')
        with self.assertNumQueries(0):
            self.assertEqual(
                mammal_cat.type_cast(), mammal_cat,
                'Type casting a correctly typed subclass should work.'
            )

        animal_cat = Animal.objects.get(pk=mammal_cat.pk)
        with self.assertNumQueries(1):
            self.assertEqual(animal_cat.type_cast(), mammal_cat)

        # When trying to type cast to an inexistent model an exception
        # should be raised.'
        with self.assertRaises(Mammal.DoesNotExist):
            animal_dog.type_cast(Mammal)

        # That's a big snake
        anaconda_snake = Snake.objects.create(name='anaconda', length=152, color='green')

        with self.assertNumQueries(0):
            self.assertIsInstance(
                anaconda_snake.type_cast(BigSnake), BigSnake,
                'Proxy type casting should work'
            )

        with self.assertNumQueries(0):
            self.assertIsInstance(
                anaconda_snake.type_cast(HugeSnake), HugeSnake,
                'Two level proxy type casting should work'
            )

        for subclass in [Snake, BigSnake, HugeSnake]:
            anaconda_animal = Animal.objects.get(pk=anaconda_snake.pk)
            with self.assertNumQueries(1):
                anaconda_animal_type_casted = anaconda_animal.type_cast(subclass)
            self.assertIsInstance(anaconda_animal_type_casted, subclass)
            self.assertEqual(anaconda_animal_type_casted.color, 'green')

    def test_content_type_saving(self):
        # Creating a base class should assign the correct content_type.
        animal_content_type = ContentType.objects.get_for_model(Animal)
        with self.assertNumQueries(1):
            animal = Animal.objects.create(name='dog')
        self.assertEqual(animal.content_type, animal_content_type)

        # Creating subclass should assign the correct content_type.
        mammal_content_type = ContentType.objects.get_for_model(Mammal)
        with self.assertNumQueries(2):
            mammal = Mammal.objects.create(name='cat')
        self.assertEqual(mammal.content_type, mammal_content_type)

        # Updating a subclass's base class pointer should preserve content_type.
        mammal.animal_ptr.save()
        self.assertEqual(mammal.animal_ptr.content_type, mammal_content_type)
        self.assertEqual(mammal.content_type, mammal_content_type)

        # Creating a base class should honor explicit content_type.
        with self.assertNumQueries(1):
            explicit_mammal = Animal.objects.create(name='beaver', content_type=mammal_content_type)
        self.assertEqual(explicit_mammal.content_type, mammal_content_type)
        with self.assertNumQueries(2):
            beaver = Mammal.objects.create(animal_ptr=explicit_mammal)
        self.assertEqual(explicit_mammal.content_type, mammal_content_type)
        self.assertEqual(beaver.content_type, mammal_content_type)

    def test_delete_keep_parents(self):
        snake = HugeSnake.objects.create(name='snek', length=30)
        animal = snake.animal_ptr
        snake.delete(keep_parents=True)
        animal.refresh_from_db()
        animal_content_type = ContentType.objects.get_for_model(Animal)
        self.assertEqual(animal.content_type, animal_content_type)


class SubclassAccessorsTests(SimpleTestCase):
    def test_dynamic_model_creation_cache_busting(self):
        test_apps = Apps(['tests'])

        class Base(models.Model):
            class Meta:
                apps = test_apps

            accessors = SubclassAccessors()

        self.assertEqual(Base.accessors['tests', 'base'], {Base: EMPTY_ACCESSOR})

        class DynamicChild(Base):
            class Meta:
                apps = test_apps

        self.assertEqual(Base.accessors['tests', 'base'], {
            Base: EMPTY_ACCESSOR,
            DynamicChild: (('dynamicchild',), None, 'dynamicchild'),
        })

        self.assertEqual(DynamicChild.accessors, {
            DynamicChild: EMPTY_ACCESSOR,
        })

    def test_key_error(self):
        test_apps = Apps(['tests'])

        class Base(models.Model):
            class Meta:
                apps = test_apps

            accessors = SubclassAccessors()

        class Other(models.Model):
            class Meta:
                apps = test_apps

        with self.assertRaises(KeyError):
            Base.accessors['tests', 'other']

    def test_proxy_accessors(self):
        test_apps = Apps(['tests'])

        class Base(models.Model):
            class Meta:
                apps = test_apps
                abstract = True

            accessors = SubclassAccessors()

        class Polymorphic(Base):
            class Meta:
                apps = test_apps
                abstract = True

        class Root(Polymorphic):
            class Meta:
                apps = test_apps

        class Subclass(Root):
            class Meta:
                apps = test_apps

        class SubclassProxy(Subclass):
            class Meta:
                apps = test_apps
                proxy = True

        class SubclassProxyProxy(SubclassProxy):
            class Meta:
                apps = test_apps
                proxy = True

        self.assertEqual(Root.accessors[Subclass], SubclassAccessor(('subclass',), None, 'subclass'))
        self.assertEqual(Root.accessors[SubclassProxy], SubclassAccessor(('subclass',), SubclassProxy, 'subclass'))
        self.assertEqual(
            Root.accessors[SubclassProxyProxy],
            SubclassAccessor(('subclass',), SubclassProxyProxy, 'subclass')
        )
        self.assertEqual(Subclass.accessors[SubclassProxy], SubclassAccessor((), SubclassProxy, ''))
        self.assertEqual(Subclass.accessors[SubclassProxyProxy], SubclassAccessor((), SubclassProxyProxy, ''))
        self.assertEqual(SubclassProxy.accessors[SubclassProxyProxy], SubclassAccessor((), SubclassProxyProxy, ''))
