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 (  # isort:skip
    Article,
    Author,
    FooWithBrokenAbsoluteUrl,
    FooWithoutUrl,
    FooWithUrl,
    ModelWithM2MToSite,
    ModelWithNullFKToSite,
    SchemeIncludedURL,
    Site as MockSite,
    UUIDModel,
)


@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)

    def test_invalid_uuid_pk_raises_404(self):
        content_type = ContentType.objects.get_for_model(UUIDModel)
        invalid_uuid = "1234-zzzz-5678-0000-invaliduuid"
        with self.assertRaisesMessage(
            Http404,
            f"Content type {content_type.id} object {invalid_uuid} doesn’t exist",
        ):
            shortcut(self.request, content_type.id, invalid_uuid)
