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)