import datetime
from unittest import mock

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.views import shortcut
from django.contrib.sites.models import Site
from django.contrib.sites.shortcuts import get_current_site
from django.http import Http404, HttpRequest
from django.test import TestCase, override_settings

from .models import (
    Article, Author, FooWithBrokenAbsoluteUrl, FooWithoutUrl, FooWithUrl,
    ModelWithM2MToSite, ModelWithNullFKToSite, SchemeIncludedURL,
    Site as MockSite,
)


@override_settings(ROOT_URLCONF='contenttypes_tests.urls')
class ContentTypesViewsTests(TestCase):

    @classmethod
    def setUpTestData(cls):
        # Don't use the manager to ensure the site exists with pk=1, regardless
        # of whether or not it already exists.
        cls.site1 = Site(pk=1, domain='testserver', name='testserver')
        cls.site1.save()
        cls.author1 = Author.objects.create(name='Boris')
        cls.article1 = Article.objects.create(
            title='Old Article', slug='old_article', author=cls.author1,
            date_created=datetime.datetime(2001, 1, 1, 21, 22, 23),
        )
        cls.article2 = Article.objects.create(
            title='Current Article', slug='current_article', author=cls.author1,
            date_created=datetime.datetime(2007, 9, 17, 21, 22, 23),
        )
        cls.article3 = Article.objects.create(
            title='Future Article', slug='future_article', author=cls.author1,
            date_created=datetime.datetime(3000, 1, 1, 21, 22, 23),
        )
        cls.scheme1 = SchemeIncludedURL.objects.create(url='http://test_scheme_included_http/')
        cls.scheme2 = SchemeIncludedURL.objects.create(url='https://test_scheme_included_https/')
        cls.scheme3 = SchemeIncludedURL.objects.create(url='//test_default_scheme_kept/')

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

    def test_shortcut_with_absolute_url(self):
        "Can view a shortcut for an Author object that has a get_absolute_url method"
        for obj in Author.objects.all():
            with self.subTest(obj=obj):
                short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk)
                response = self.client.get(short_url)
                self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), target_status_code=404)

    def test_shortcut_with_absolute_url_including_scheme(self):
        """
        Can view a shortcut when object's get_absolute_url returns a full URL
        the tested URLs are: "http://...", "https://..." and "//..."
        """
        for obj in SchemeIncludedURL.objects.all():
            with self.subTest(obj=obj):
                short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(SchemeIncludedURL).id, obj.pk)
                response = self.client.get(short_url)
                self.assertRedirects(response, obj.get_absolute_url(), fetch_redirect_response=False)

    def test_shortcut_no_absolute_url(self):
        """
        Shortcuts for an object that has no get_absolute_url() method raise
        404.
        """
        for obj in Article.objects.all():
            with self.subTest(obj=obj):
                short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk)
                response = self.client.get(short_url)
                self.assertEqual(response.status_code, 404)

    def test_wrong_type_pk(self):
        short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects')
        response = self.client.get(short_url)
        self.assertEqual(response.status_code, 404)

    def test_shortcut_bad_pk(self):
        short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242')
        response = self.client.get(short_url)
        self.assertEqual(response.status_code, 404)

    def test_nonint_content_type(self):
        an_author = Author.objects.all()[0]
        short_url = '/shortcut/%s/%s/' % ('spam', an_author.pk)
        response = self.client.get(short_url)
        self.assertEqual(response.status_code, 404)

    def test_bad_content_type(self):
        an_author = Author.objects.all()[0]
        short_url = '/shortcut/%s/%s/' % (42424242, an_author.pk)
        response = self.client.get(short_url)
        self.assertEqual(response.status_code, 404)


@override_settings(ROOT_URLCONF='contenttypes_tests.urls')
class ContentTypesViewsSiteRelTests(TestCase):

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

    @classmethod
    def setUpTestData(cls):
        cls.site_2 = Site.objects.create(domain='example2.com', name='example2.com')
        cls.site_3 = Site.objects.create(domain='example3.com', name='example3.com')

    @mock.patch('django.apps.apps.get_model')
    def test_shortcut_view_with_null_site_fk(self, get_model):
        """
        The shortcut view works if a model's ForeignKey to site is None.
        """
        get_model.side_effect = lambda *args, **kwargs: MockSite if args[0] == 'sites.Site' else ModelWithNullFKToSite

        obj = ModelWithNullFKToSite.objects.create(title='title')
        url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(ModelWithNullFKToSite).id, obj.pk)
        response = self.client.get(url)
        expected_url = 'http://example.com%s' % obj.get_absolute_url()
        self.assertRedirects(response, expected_url, fetch_redirect_response=False)

    @mock.patch('django.apps.apps.get_model')
    def test_shortcut_view_with_site_m2m(self, get_model):
        """
        When the object has a ManyToManyField to Site, redirect to the current
        site if it's attached to the object or to the domain of the first site
        found in the m2m relationship.
        """
        get_model.side_effect = lambda *args, **kwargs: MockSite if args[0] == 'sites.Site' else ModelWithM2MToSite

        # get_current_site() will lookup a Site object, so these must match the
        # domains in the MockSite model.
        MockSite.objects.bulk_create([
            MockSite(pk=1, domain='example.com'),
            MockSite(pk=self.site_2.pk, domain=self.site_2.domain),
            MockSite(pk=self.site_3.pk, domain=self.site_3.domain),
        ])
        ct = ContentType.objects.get_for_model(ModelWithM2MToSite)
        site_3_obj = ModelWithM2MToSite.objects.create(title='Not Linked to Current Site')
        site_3_obj.sites.add(MockSite.objects.get(pk=self.site_3.pk))
        expected_url = 'http://%s%s' % (self.site_3.domain, site_3_obj.get_absolute_url())

        with self.settings(SITE_ID=self.site_2.pk):
            # Redirects to the domain of the first Site found in the m2m
            # relationship (ordering is arbitrary).
            response = self.client.get('/shortcut/%s/%s/' % (ct.pk, site_3_obj.pk))
            self.assertRedirects(response, expected_url, fetch_redirect_response=False)

        obj_with_sites = ModelWithM2MToSite.objects.create(title='Linked to Current Site')
        obj_with_sites.sites.set(MockSite.objects.all())
        shortcut_url = '/shortcut/%s/%s/' % (ct.pk, obj_with_sites.pk)
        expected_url = 'http://%s%s' % (self.site_2.domain, obj_with_sites.get_absolute_url())

        with self.settings(SITE_ID=self.site_2.pk):
            # Redirects to the domain of the Site matching the current site's
            # domain.
            response = self.client.get(shortcut_url)
            self.assertRedirects(response, expected_url, fetch_redirect_response=False)

        with self.settings(SITE_ID=None, ALLOWED_HOSTS=[self.site_2.domain]):
            # Redirects to the domain of the Site matching the request's host
            # header.
            response = self.client.get(shortcut_url, SERVER_NAME=self.site_2.domain)
            self.assertRedirects(response, expected_url, fetch_redirect_response=False)


class ShortcutViewTests(TestCase):

    def setUp(self):
        self.request = HttpRequest()
        self.request.META = {'SERVER_NAME': 'Example.com', 'SERVER_PORT': '80'}

    @override_settings(ALLOWED_HOSTS=['example.com'])
    def test_not_dependent_on_sites_app(self):
        """
        The view returns a complete URL regardless of whether the sites
        framework is installed.
        """
        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(self.request, user_ct.id, obj.id)
            self.assertEqual(
                'http://%s/users/john/' % get_current_site(self.request).domain,
                response.headers.get('location')
            )
        with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.sites'}):
            response = shortcut(self.request, user_ct.id, obj.id)
            self.assertEqual('http://Example.com/users/john/', response.headers.get('location'))

    def test_model_without_get_absolute_url(self):
        """The view returns 404 when Model.get_absolute_url() isn't defined."""
        user_ct = ContentType.objects.get_for_model(FooWithoutUrl)
        obj = FooWithoutUrl.objects.create(name='john')
        with self.assertRaises(Http404):
            shortcut(self.request, user_ct.id, obj.id)

    def test_model_with_broken_get_absolute_url(self):
        """
        The view doesn't catch an AttributeError raised by
        Model.get_absolute_url() (#8997).
        """
        user_ct = ContentType.objects.get_for_model(FooWithBrokenAbsoluteUrl)
        obj = FooWithBrokenAbsoluteUrl.objects.create(name='john')
        with self.assertRaises(AttributeError):
            shortcut(self.request, user_ct.id, obj.id)
