from unittest import mock

from django.contrib.auth import models
from django.contrib.auth.mixins import (
    LoginRequiredMixin,
    PermissionRequiredMixin,
    UserPassesTestMixin,
)
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.test import RequestFactory, SimpleTestCase, TestCase
from django.views.generic import View


class AlwaysTrueMixin(UserPassesTestMixin):
    def test_func(self):
        return True


class AlwaysFalseMixin(UserPassesTestMixin):
    def test_func(self):
        return False


class EmptyResponseView(View):
    def get(self, request, *args, **kwargs):
        return HttpResponse()


class AlwaysTrueView(AlwaysTrueMixin, EmptyResponseView):
    pass


class AlwaysFalseView(AlwaysFalseMixin, EmptyResponseView):
    pass


class StackedMixinsView1(
    LoginRequiredMixin, PermissionRequiredMixin, EmptyResponseView
):
    permission_required = ["auth_tests.add_customuser", "auth_tests.change_customuser"]
    raise_exception = True


class StackedMixinsView2(
    PermissionRequiredMixin, LoginRequiredMixin, EmptyResponseView
):
    permission_required = ["auth_tests.add_customuser", "auth_tests.change_customuser"]
    raise_exception = True


class AccessMixinTests(TestCase):
    factory = RequestFactory()

    def test_stacked_mixins_success(self):
        user = models.User.objects.create(username="joe", password="qwerty")
        perms = models.Permission.objects.filter(
            codename__in=("add_customuser", "change_customuser")
        )
        user.user_permissions.add(*perms)
        request = self.factory.get("/rand")
        request.user = user

        view = StackedMixinsView1.as_view()
        response = view(request)
        self.assertEqual(response.status_code, 200)

        view = StackedMixinsView2.as_view()
        response = view(request)
        self.assertEqual(response.status_code, 200)

    def test_stacked_mixins_missing_permission(self):
        user = models.User.objects.create(username="joe", password="qwerty")
        perms = models.Permission.objects.filter(codename__in=("add_customuser",))
        user.user_permissions.add(*perms)
        request = self.factory.get("/rand")
        request.user = user

        view = StackedMixinsView1.as_view()
        with self.assertRaises(PermissionDenied):
            view(request)

        view = StackedMixinsView2.as_view()
        with self.assertRaises(PermissionDenied):
            view(request)

    def test_access_mixin_permission_denied_response(self):
        user = models.User.objects.create(username="joe", password="qwerty")
        # Authenticated users receive PermissionDenied.
        request = self.factory.get("/rand")
        request.user = user
        view = AlwaysFalseView.as_view()
        with self.assertRaises(PermissionDenied):
            view(request)
        # Anonymous users are redirected to the login page.
        request.user = AnonymousUser()
        response = view(request)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, "/accounts/login/?next=/rand")

    def test_access_mixin_permission_denied_remote_login_url(self):
        class AView(AlwaysFalseView):
            login_url = "https://www.remote.example.com/login"

        view = AView.as_view()
        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        response = view(request)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(
            response.url,
            "https://www.remote.example.com/login?next=http%3A//testserver/rand",
        )

    @mock.patch.object(models.User, "is_authenticated", False)
    def test_stacked_mixins_not_logged_in(self):
        user = models.User.objects.create(username="joe", password="qwerty")
        perms = models.Permission.objects.filter(
            codename__in=("add_customuser", "change_customuser")
        )
        user.user_permissions.add(*perms)
        request = self.factory.get("/rand")
        request.user = user

        view = StackedMixinsView1.as_view()
        with self.assertRaises(PermissionDenied):
            view(request)

        view = StackedMixinsView2.as_view()
        with self.assertRaises(PermissionDenied):
            view(request)


class UserPassesTestTests(SimpleTestCase):
    factory = RequestFactory()

    def _test_redirect(self, view=None, url="/accounts/login/?next=/rand"):
        if not view:
            view = AlwaysFalseView.as_view()
        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        response = view(request)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, url)

    def test_default(self):
        self._test_redirect()

    def test_custom_redirect_url(self):
        class AView(AlwaysFalseView):
            login_url = "/login/"

        self._test_redirect(AView.as_view(), "/login/?next=/rand")

    def test_custom_redirect_parameter(self):
        class AView(AlwaysFalseView):
            redirect_field_name = "goto"

        self._test_redirect(AView.as_view(), "/accounts/login/?goto=/rand")

    def test_no_redirect_parameter(self):
        class AView(AlwaysFalseView):
            redirect_field_name = None

        self._test_redirect(AView.as_view(), "/accounts/login/")

    def test_raise_exception(self):
        class AView(AlwaysFalseView):
            raise_exception = True

        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        with self.assertRaises(PermissionDenied):
            AView.as_view()(request)

    def test_raise_exception_custom_message(self):
        msg = "You don't have access here"

        class AView(AlwaysFalseView):
            raise_exception = True
            permission_denied_message = msg

        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        view = AView.as_view()
        with self.assertRaisesMessage(PermissionDenied, msg):
            view(request)

    def test_raise_exception_custom_message_function(self):
        msg = "You don't have access here"

        class AView(AlwaysFalseView):
            raise_exception = True

            def get_permission_denied_message(self):
                return msg

        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        view = AView.as_view()
        with self.assertRaisesMessage(PermissionDenied, msg):
            view(request)

    def test_user_passes(self):
        view = AlwaysTrueView.as_view()
        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        response = view(request)
        self.assertEqual(response.status_code, 200)


class LoginRequiredMixinTests(TestCase):
    factory = RequestFactory()

    @classmethod
    def setUpTestData(cls):
        cls.user = models.User.objects.create(username="joe", password="qwerty")

    def test_login_required(self):
        """
        login_required works on a simple view wrapped in a login_required
        decorator.
        """

        class AView(LoginRequiredMixin, EmptyResponseView):
            pass

        view = AView.as_view()

        request = self.factory.get("/rand")
        request.user = AnonymousUser()
        response = view(request)
        self.assertEqual(response.status_code, 302)
        self.assertEqual("/accounts/login/?next=/rand", response.url)
        request = self.factory.get("/rand")
        request.user = self.user
        response = view(request)
        self.assertEqual(response.status_code, 200)


class PermissionsRequiredMixinTests(TestCase):
    factory = RequestFactory()

    @classmethod
    def setUpTestData(cls):
        cls.user = models.User.objects.create(username="joe", password="qwerty")
        perms = models.Permission.objects.filter(
            codename__in=("add_customuser", "change_customuser")
        )
        cls.user.user_permissions.add(*perms)

    def test_many_permissions_pass(self):
        class AView(PermissionRequiredMixin, EmptyResponseView):
            permission_required = [
                "auth_tests.add_customuser",
                "auth_tests.change_customuser",
            ]

        request = self.factory.get("/rand")
        request.user = self.user
        resp = AView.as_view()(request)
        self.assertEqual(resp.status_code, 200)

    def test_single_permission_pass(self):
        class AView(PermissionRequiredMixin, EmptyResponseView):
            permission_required = "auth_tests.add_customuser"

        request = self.factory.get("/rand")
        request.user = self.user
        resp = AView.as_view()(request)
        self.assertEqual(resp.status_code, 200)

    def test_permissioned_denied_redirect(self):
        class AView(PermissionRequiredMixin, EmptyResponseView):
            permission_required = [
                "auth_tests.add_customuser",
                "auth_tests.change_customuser",
                "nonexistent-permission",
            ]

        # Authenticated users receive PermissionDenied.
        request = self.factory.get("/rand")
        request.user = self.user
        with self.assertRaises(PermissionDenied):
            AView.as_view()(request)
        # Anonymous users are redirected to the login page.
        request.user = AnonymousUser()
        resp = AView.as_view()(request)
        self.assertEqual(resp.status_code, 302)

    def test_permissioned_denied_exception_raised(self):
        class AView(PermissionRequiredMixin, EmptyResponseView):
            permission_required = [
                "auth_tests.add_customuser",
                "auth_tests.change_customuser",
                "nonexistent-permission",
            ]
            raise_exception = True

        request = self.factory.get("/rand")
        request.user = self.user
        with self.assertRaises(PermissionDenied):
            AView.as_view()(request)