from __future__ import unicode_literals

import warnings

from django.contrib.contenttypes.models import ContentType, ContentTypeManager
from django.contrib.contenttypes.views import shortcut
from django.contrib.sites.shortcuts import get_current_site
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
from django.http import Http404, HttpRequest
from django.test import TestCase, mock, override_settings
from django.utils import six

from .models import (
    ConcreteModel, FooWithBrokenAbsoluteUrl, FooWithoutUrl, FooWithUrl,
    ProxyModel,
)


class ContentTypesTests(TestCase):

    def setUp(self):
        ContentType.objects.clear_cache()

    def tearDown(self):
        ContentType.objects.clear_cache()

    def test_lookup_cache(self):
        """
        Make sure that the content type cache (see ContentTypeManager)
        works correctly. Lookups for a particular content type -- by model, ID
        or natural key -- should hit the database only on the first lookup.
        """

        # At this point, a lookup for a ContentType should hit the DB
        with self.assertNumQueries(1):
            ContentType.objects.get_for_model(ContentType)

        # A second hit, though, won't hit the DB, nor will a lookup by ID
        # or natural key
        with self.assertNumQueries(0):
            ct = ContentType.objects.get_for_model(ContentType)
        with self.assertNumQueries(0):
            ContentType.objects.get_for_id(ct.id)
        with self.assertNumQueries(0):
            ContentType.objects.get_by_natural_key('contenttypes',
                                                   'contenttype')

        # Once we clear the cache, another lookup will again hit the DB
        ContentType.objects.clear_cache()
        with self.assertNumQueries(1):
            ContentType.objects.get_for_model(ContentType)

        # The same should happen with a lookup by natural key
        ContentType.objects.clear_cache()
        with self.assertNumQueries(1):
            ContentType.objects.get_by_natural_key('contenttypes',
                                                   'contenttype')
        # And a second hit shouldn't hit the DB
        with self.assertNumQueries(0):
            ContentType.objects.get_by_natural_key('contenttypes',
                                                   'contenttype')

    def test_get_for_models_empty_cache(self):
        # Empty cache.
        with self.assertNumQueries(1):
            cts = ContentType.objects.get_for_models(ContentType, FooWithUrl)
        self.assertEqual(cts, {
            ContentType: ContentType.objects.get_for_model(ContentType),
            FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
        })

    def test_get_for_models_partial_cache(self):
        # Partial cache
        ContentType.objects.get_for_model(ContentType)
        with self.assertNumQueries(1):
            cts = ContentType.objects.get_for_models(ContentType, FooWithUrl)
        self.assertEqual(cts, {
            ContentType: ContentType.objects.get_for_model(ContentType),
            FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
        })

    def test_get_for_models_full_cache(self):
        # Full cache
        ContentType.objects.get_for_model(ContentType)
        ContentType.objects.get_for_model(FooWithUrl)
        with self.assertNumQueries(0):
            cts = ContentType.objects.get_for_models(ContentType, FooWithUrl)
        self.assertEqual(cts, {
            ContentType: ContentType.objects.get_for_model(ContentType),
            FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
        })

    def test_get_for_concrete_model(self):
        """
        Make sure the `for_concrete_model` kwarg correctly works
        with concrete, proxy and deferred models
        """
        concrete_model_ct = ContentType.objects.get_for_model(ConcreteModel)

        self.assertEqual(concrete_model_ct,
            ContentType.objects.get_for_model(ProxyModel))

        self.assertEqual(concrete_model_ct,
            ContentType.objects.get_for_model(ConcreteModel,
                                              for_concrete_model=False))

        proxy_model_ct = ContentType.objects.get_for_model(ProxyModel,
                                                           for_concrete_model=False)

        self.assertNotEqual(concrete_model_ct, proxy_model_ct)

        # Make sure deferred model are correctly handled
        ConcreteModel.objects.create(name="Concrete")
        DeferredConcreteModel = ConcreteModel.objects.only('pk').get().__class__
        DeferredProxyModel = ProxyModel.objects.only('pk').get().__class__

        self.assertEqual(concrete_model_ct,
            ContentType.objects.get_for_model(DeferredConcreteModel))

        self.assertEqual(concrete_model_ct,
            ContentType.objects.get_for_model(DeferredConcreteModel,
                                              for_concrete_model=False))

        self.assertEqual(concrete_model_ct,
            ContentType.objects.get_for_model(DeferredProxyModel))

        self.assertEqual(proxy_model_ct,
            ContentType.objects.get_for_model(DeferredProxyModel,
                                              for_concrete_model=False))

    def test_get_for_concrete_models(self):
        """
        Make sure the `for_concrete_models` kwarg correctly works
        with concrete, proxy and deferred models.
        """
        concrete_model_ct = ContentType.objects.get_for_model(ConcreteModel)

        cts = ContentType.objects.get_for_models(ConcreteModel, ProxyModel)
        self.assertEqual(cts, {
            ConcreteModel: concrete_model_ct,
            ProxyModel: concrete_model_ct,
        })

        proxy_model_ct = ContentType.objects.get_for_model(ProxyModel,
                                                           for_concrete_model=False)
        cts = ContentType.objects.get_for_models(ConcreteModel, ProxyModel,
                                                 for_concrete_models=False)
        self.assertEqual(cts, {
            ConcreteModel: concrete_model_ct,
            ProxyModel: proxy_model_ct,
        })

        # Make sure deferred model are correctly handled
        ConcreteModel.objects.create(name="Concrete")
        DeferredConcreteModel = ConcreteModel.objects.only('pk').get().__class__
        DeferredProxyModel = ProxyModel.objects.only('pk').get().__class__

        cts = ContentType.objects.get_for_models(DeferredConcreteModel,
                                                 DeferredProxyModel)
        self.assertEqual(cts, {
            DeferredConcreteModel: concrete_model_ct,
            DeferredProxyModel: concrete_model_ct,
        })

        cts = ContentType.objects.get_for_models(DeferredConcreteModel,
                                                 DeferredProxyModel,
                                                 for_concrete_models=False)
        self.assertEqual(cts, {
            DeferredConcreteModel: concrete_model_ct,
            DeferredProxyModel: proxy_model_ct,
        })

    def test_cache_not_shared_between_managers(self):
        with self.assertNumQueries(1):
            ContentType.objects.get_for_model(ContentType)
        with self.assertNumQueries(0):
            ContentType.objects.get_for_model(ContentType)
        other_manager = ContentTypeManager()
        other_manager.model = ContentType
        with self.assertNumQueries(1):
            other_manager.get_for_model(ContentType)
        with self.assertNumQueries(0):
            other_manager.get_for_model(ContentType)

    @override_settings(ALLOWED_HOSTS=['example.com'])
    def test_shortcut_view(self):
        """
        Check that the shortcut view (used for the admin "view on site"
        functionality) returns a complete URL regardless of whether the sites
        framework is installed
        """

        request = HttpRequest()
        request.META = {
            "SERVER_NAME": "Example.com",
            "SERVER_PORT": "80",
        }
        user_ct = ContentType.objects.get_for_model(FooWithUrl)
        obj = FooWithUrl.objects.create(name="john")

        with self.modify_settings(INSTALLED_APPS={'append': 'django.contrib.sites'}):
            response = shortcut(request, user_ct.id, obj.id)
            self.assertEqual("http://%s/users/john/" % get_current_site(request).domain,
                             response._headers.get("location")[1])

        with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.sites'}):
            response = shortcut(request, user_ct.id, obj.id)
            self.assertEqual("http://Example.com/users/john/",
                             response._headers.get("location")[1])

    def test_shortcut_view_without_get_absolute_url(self):
        """
        Check that the shortcut view (used for the admin "view on site"
        functionality) returns 404 when get_absolute_url is not defined.
        """

        request = HttpRequest()
        request.META = {
            "SERVER_NAME": "Example.com",
            "SERVER_PORT": "80",
        }
        user_ct = ContentType.objects.get_for_model(FooWithoutUrl)
        obj = FooWithoutUrl.objects.create(name="john")

        self.assertRaises(Http404, shortcut, request, user_ct.id, obj.id)

    def test_shortcut_view_with_broken_get_absolute_url(self):
        """
        Check that the shortcut view does not catch an AttributeError raised
        by the model's get_absolute_url method.
        Refs #8997.
        """
        request = HttpRequest()
        request.META = {
            "SERVER_NAME": "Example.com",
            "SERVER_PORT": "80",
        }
        user_ct = ContentType.objects.get_for_model(FooWithBrokenAbsoluteUrl)
        obj = FooWithBrokenAbsoluteUrl.objects.create(name="john")

        self.assertRaises(AttributeError, shortcut, request, user_ct.id, obj.id)

    def test_missing_model(self):
        """
        Ensures that displaying content types in admin (or anywhere) doesn't
        break on leftover content type records in the DB for which no model
        is defined anymore.
        """
        ct = ContentType.objects.create(
            app_label='contenttypes',
            model='OldModel',
        )
        self.assertEqual(six.text_type(ct), 'OldModel')
        self.assertIsNone(ct.model_class())

        # Make sure stale ContentTypes can be fetched like any other object.
        # Before Django 1.6 this caused a NoneType error in the caching mechanism.
        # Instead, just return the ContentType object and let the app detect stale states.
        ct_fetched = ContentType.objects.get_for_id(ct.pk)
        self.assertIsNone(ct_fetched.model_class())

    def test_name_deprecation(self):
        """
        ContentType.name has been removed. Test that a warning is emitted when
        creating a ContentType with a `name`, but the creation should not fail.
        """
        with warnings.catch_warnings(record=True) as warns:
            warnings.simplefilter('always')
            ContentType.objects.create(
                name='Name',
                app_label='contenttypes',
                model='OldModel',
            )
        self.assertEqual(len(warns), 1)
        self.assertEqual(
            str(warns[0].message),
            "ContentType.name field doesn't exist any longer. Please remove it from your code."
        )
        self.assertTrue(ContentType.objects.filter(model='OldModel').exists())

    @mock.patch('django.contrib.contenttypes.models.ContentTypeManager.get_or_create')
    @mock.patch('django.contrib.contenttypes.models.ContentTypeManager.get')
    def test_message_if_get_for_model_fails(self, mocked_get, mocked_get_or_create):
        """
        Check that `RuntimeError` with nice error message is raised if
        `get_for_model` fails because of database errors.
        """

        def _test_message(mocked_method):
            for ExceptionClass in (IntegrityError, OperationalError, ProgrammingError):
                mocked_method.side_effect = ExceptionClass
                with self.assertRaisesMessage(
                    RuntimeError,
                    "Error creating new content types. Please make sure contenttypes "
                    "is migrated before trying to migrate apps individually."
                ):
                    ContentType.objects.get_for_model(ContentType)

        _test_message(mocked_get)

        mocked_get.side_effect = ContentType.DoesNotExist
        _test_message(mocked_get_or_create)
