from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, alogin, alogout
from django.contrib.auth.middleware import (
    AuthenticationMiddleware,
    LoginRequiredMiddleware,
)
from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.test import TestCase, modify_settings, override_settings
from django.urls import reverse


class TestAuthenticationMiddleware(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create_user(
            "test_user", "test@example.com", "test_password"
        )
        cls.user2 = User.objects.create_user(
            "test_user2", "test2@example.com", "test_password2"
        )

    def setUp(self):
        self.middleware = AuthenticationMiddleware(lambda req: HttpResponse())
        self.client.force_login(self.user)
        self.request = HttpRequest()
        self.request.session = self.client.session

    def test_no_password_change_doesnt_invalidate_session(self):
        self.request.session = self.client.session
        self.middleware(self.request)
        self.assertIsNotNone(self.request.user)
        self.assertFalse(self.request.user.is_anonymous)

    def test_changed_password_invalidates_session(self):
        # After password change, user should be anonymous
        self.user.set_password("new_password")
        self.user.save()
        self.middleware(self.request)
        self.assertIsNotNone(self.request.user)
        self.assertTrue(self.request.user.is_anonymous)
        # session should be flushed
        self.assertIsNone(self.request.session.session_key)

    def test_no_session(self):
        msg = (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        )
        with self.assertRaisesMessage(ImproperlyConfigured, msg):
            self.middleware(HttpRequest())

    async def test_auser(self):
        self.middleware(self.request)
        auser = await self.request.auser()
        self.assertEqual(auser, self.user)
        auser_second = await self.request.auser()
        self.assertIs(auser, auser_second)

    async def test_auser_after_alogin(self):
        self.middleware(self.request)
        auser = await self.request.auser()
        self.assertEqual(auser, self.user)
        await alogin(self.request, self.user2)
        auser_second = await self.request.auser()
        self.assertEqual(auser_second, self.user2)

    async def test_auser_after_alogout(self):
        self.middleware(self.request)
        auser = await self.request.auser()
        self.assertEqual(auser, self.user)
        await alogout(self.request)
        auser_second = await self.request.auser()
        self.assertTrue(auser_second.is_anonymous)


class TestAsyncLoginLogoutAfterSyncMiddleware(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create_user(
            "test_user", "test@example.com", "test_password"
        )
        cls.user2 = User.objects.create_user(
            "test_user2", "test2@example.com", "test_password2"
        )

    def setUp(self):
        self.middleware = AuthenticationMiddleware(lambda req: HttpResponse())
        self.client.force_login(self.user)
        self.request = HttpRequest()
        self.request.session = self.client.session
        # Populate self.request.user.
        self.middleware(self.request)
        # .user is lazy, so materialize it by accessing an attribute.
        self.request.user.is_authenticated

    async def test_user_after_alogin(self):
        await alogin(self.request, self.user2)
        self.assertEqual(self.request.user, self.user2)

    async def test_user_after_alogout(self):
        await alogout(self.request)
        self.assertEqual(self.request.user, AnonymousUser())


@override_settings(ROOT_URLCONF="auth_tests.urls")
@modify_settings(
    MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"}
)
class TestLoginRequiredMiddleware(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create_user(
            "test_user", "test@example.com", "test_password"
        )

    def setUp(self):
        self.middleware = LoginRequiredMiddleware(lambda req: HttpResponse())
        self.request = HttpRequest()

    def test_public_paths(self):
        paths = ["public_view", "public_function_view"]
        for path in paths:
            response = self.client.get(f"/{path}/")
            self.assertEqual(response.status_code, 200)

    def test_protected_paths(self):
        paths = ["protected_view", "protected_function_view"]
        for path in paths:
            response = self.client.get(f"/{path}/")
            self.assertRedirects(
                response,
                settings.LOGIN_URL + f"?next=/{path}/",
                fetch_redirect_response=False,
            )

    def test_login_required_paths(self):
        paths = ["login_required_cbv_view", "login_required_decorator_view"]
        for path in paths:
            response = self.client.get(f"/{path}/")
            self.assertRedirects(
                response,
                "/custom_login/" + f"?step=/{path}/",
                fetch_redirect_response=False,
            )

    def test_admin_path(self):
        admin_url = reverse("admin:index")
        response = self.client.get(admin_url)
        self.assertRedirects(
            response,
            reverse("admin:login") + f"?next={admin_url}",
            target_status_code=200,
        )

    def test_non_existent_path(self):
        response = self.client.get("/non_existent/")
        self.assertEqual(response.status_code, 404)

    def test_paths_with_logged_in_user(self):
        paths = [
            "public_view",
            "public_function_view",
            "protected_view",
            "protected_function_view",
            "login_required_cbv_view",
            "login_required_decorator_view",
        ]
        self.client.login(username="test_user", password="test_password")
        for path in paths:
            response = self.client.get(f"/{path}/")
            self.assertEqual(response.status_code, 200)

    def test_get_login_url_from_view_func(self):
        def view_func(request):
            return HttpResponse()

        view_func.login_url = "/custom_login/"
        login_url = self.middleware.get_login_url(view_func)
        self.assertEqual(login_url, "/custom_login/")

    @override_settings(LOGIN_URL="/settings_login/")
    def test_get_login_url_from_settings(self):
        login_url = self.middleware.get_login_url(lambda: None)
        self.assertEqual(login_url, "/settings_login/")

    @override_settings(LOGIN_URL=None)
    def test_get_login_url_no_login_url(self):
        with self.assertRaises(ImproperlyConfigured) as e:
            self.middleware.get_login_url(lambda: None)
        self.assertEqual(
            str(e.exception),
            "No login URL to redirect to. Define settings.LOGIN_URL or provide "
            "a login_url via the 'django.contrib.auth.decorators.login_required' "
            "decorator.",
        )

    def test_get_redirect_field_name_from_view_func(self):
        def view_func(request):
            return HttpResponse()

        view_func.redirect_field_name = "next_page"
        redirect_field_name = self.middleware.get_redirect_field_name(view_func)
        self.assertEqual(redirect_field_name, "next_page")

    @override_settings(
        MIDDLEWARE=[
            "django.contrib.sessions.middleware.SessionMiddleware",
            "django.contrib.auth.middleware.AuthenticationMiddleware",
            "auth_tests.test_checks.LoginRequiredMiddlewareSubclass",
        ],
        LOGIN_URL="/settings_login/",
    )
    def test_login_url_resolve_logic(self):
        paths = ["login_required_cbv_view", "login_required_decorator_view"]
        for path in paths:
            response = self.client.get(f"/{path}/")
            self.assertRedirects(
                response,
                "/custom_login/" + f"?step=/{path}/",
                fetch_redirect_response=False,
            )
        paths = ["protected_view", "protected_function_view"]
        for path in paths:
            response = self.client.get(f"/{path}/")
            self.assertRedirects(
                response,
                f"/settings_login/?redirect_to=/{path}/",
                fetch_redirect_response=False,
            )

    def test_get_redirect_field_name_default(self):
        redirect_field_name = self.middleware.get_redirect_field_name(lambda: None)
        self.assertEqual(redirect_field_name, REDIRECT_FIELD_NAME)

    def test_public_view_logged_in_performance(self):
        """
        Public views don't trigger fetching the user from the database.
        """
        self.client.force_login(self.user)
        with self.assertNumQueries(0):
            response = self.client.get("/public_view/")
        self.assertEqual(response.status_code, 200)

    def test_protected_view_logged_in_performance(self):
        """
        Protected views do trigger fetching the user from the database.
        """
        self.client.force_login(self.user)
        with self.assertNumQueries(2):  # session and user
            response = self.client.get("/protected_view/")
        self.assertEqual(response.status_code, 200)
