import json
from http import HTTPStatus
from unittest.mock import ANY, patch

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import NoReverseMatch, reverse

from pytest_django.asserts import assertTemplateUsed

from allauth.account import app_settings
from allauth.account.authentication import AUTHENTICATION_METHODS_SESSION_KEY
from allauth.account.forms import LoginForm
from allauth.account.models import EmailAddress


@override_settings(
    ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
    ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
    ACCOUNT_LOGIN_METHODS={app_settings.AuthenticationMethod.USERNAME},
    ACCOUNT_SIGNUP_FORM_CLASS=None,
    ACCOUNT_EMAIL_SUBJECT_PREFIX=None,
    LOGIN_REDIRECT_URL="/accounts/profile/",
    ACCOUNT_SIGNUP_REDIRECT_URL="/accounts/welcome/",
    ACCOUNT_ADAPTER="allauth.account.adapter.DefaultAccountAdapter",
    ACCOUNT_USERNAME_REQUIRED=True,
)
class LoginTests(TestCase):
    @override_settings(
        ACCOUNT_LOGIN_METHODS={
            app_settings.LoginMethod.USERNAME,
            app_settings.LoginMethod.EMAIL,
        }
    )
    def test_username_containing_at(self):
        user = get_user_model().objects.create(username="@raymond.penners")
        user.set_password("psst")
        user.save()
        EmailAddress.objects.create(
            user=user,
            email="raymond.penners@example.com",
            primary=True,
            verified=True,
        )
        resp = self.client.post(
            reverse("account_login"),
            {"login": "@raymond.penners", "password": "psst"},
        )
        self.assertRedirects(
            resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
        )
        self.assertEqual(
            self.client.session[AUTHENTICATION_METHODS_SESSION_KEY],
            [
                {
                    "at": ANY,
                    "username": "@raymond.penners",
                    "method": "password",
                }
            ],
        )

    def _create_user(self, username="john", password="doe", **kwargs):
        user = get_user_model().objects.create(
            username=username, is_active=True, **kwargs
        )
        if password:
            user.set_password(password)
        else:
            user.set_unusable_password()
        user.save()
        return user

    def _create_user_and_login(self, usable_password=True):
        password = "doe" if usable_password else False
        user = self._create_user(password=password)
        self.client.force_login(user)
        return user

    def test_redirect_when_authenticated(self):
        self._create_user_and_login()
        c = self.client
        resp = c.get(reverse("account_login"))
        self.assertRedirects(resp, "/accounts/profile/", fetch_redirect_response=False)

    def test_ajax_password_change(self):
        self._create_user_and_login()
        resp = self.client.post(
            reverse("account_change_password"),
            data={
                "oldpassword": "doe",
                "password1": "AbCdEf!123",
                "password2": "AbCdEf!123456",
            },
            HTTP_X_REQUESTED_WITH="XMLHttpRequest",
        )
        self.assertEqual(resp["content-type"], "application/json")
        data = json.loads(resp.content.decode("utf8"))
        assert "same password" in data["form"]["fields"]["password2"]["errors"][0]

    @override_settings(
        ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL
    )
    def test_login_unverified_account_optional(self):
        """Tests login behavior when email verification is optional."""
        user = get_user_model().objects.create(username="john")
        user.set_password("doe")
        user.save()
        EmailAddress.objects.create(
            user=user, email="user@example.com", primary=True, verified=False
        )
        resp = self.client.post(
            reverse("account_login"), {"login": "john", "password": "doe"}
        )
        self.assertRedirects(
            resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
        )

    @override_settings(
        ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL,
        ACCOUNT_LOGIN_ATTEMPTS_LIMIT=3,
        CACHES={
            "default": {
                "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
            }
        },
    )
    def test_login_failed_attempts_exceeded(self):
        user = get_user_model().objects.create(username="john")
        user.set_password("doe")
        user.save()
        EmailAddress.objects.create(
            user=user, email="user@example.com", primary=True, verified=False
        )
        for i in range(5):
            is_valid_attempt = i == 4
            is_locked = i >= 3
            resp = self.client.post(
                reverse("account_login"),
                {
                    "login": ["john", "John", "JOHN", "JOhn", "joHN"][i],
                    "password": ("doe" if is_valid_attempt else "wrong"),
                },
            )
            self.assertFormError(
                resp.context["form"],
                None,
                (
                    "Too many failed login attempts. Try again later."
                    if is_locked
                    else "The username and/or password you specified are not correct."
                ),
            )

    @override_settings(
        ACCOUNT_LOGIN_METHODS={app_settings.LoginMethod.EMAIL},
        ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
        ACCOUNT_LOGIN_ATTEMPTS_LIMIT=1,
        CACHES={
            "default": {
                "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
            }
        },
    )
    def test_login_failed_attempts_exceeded_cleared_on_password_reset(self):
        # Ensure that login attempts, once they hit the limit,
        # can use the password reset mechanism to regain access.
        user = get_user_model().objects.create(
            username="john", email="john@example.org", is_active=True
        )
        user.set_password("doe")
        user.save()

        EmailAddress.objects.create(
            user=user, email="john@example.org", primary=True, verified=True
        )

        resp = self.client.post(
            reverse("account_login"), {"login": user.email, "password": "bad"}
        )
        self.assertFormError(
            resp.context["form"],
            None,
            "The email address and/or password you specified are not correct.",
        )
        resp = self.client.post(
            reverse("account_login"), {"login": user.email, "password": "bad"}
        )
        self.assertFormError(
            resp.context["form"],
            None,
            "Too many failed login attempts. Try again later.",
        )
        self.client.post(reverse("account_reset_password"), data={"email": user.email})

        body = mail.outbox[0].body
        self.assertGreater(body.find("https://"), 0)

        # Extract URL for `password_reset_from_key` view and access it
        url = body[body.find("/accounts/password/reset/") :].split()[0]
        resp = self.client.get(url)
        # Follow the redirect the actual password reset page with the key
        # hidden.
        url = resp.url
        resp = self.client.get(url)
        self.assertTemplateUsed(
            resp,
            f"account/password_reset_from_key.{app_settings.TEMPLATE_EXTENSION}",
        )
        self.assertFalse("token_fail" in resp.context_data)

        new_password = "newpass123"

        # Reset the password
        resp = self.client.post(
            url, {"password1": new_password, "password2": new_password}
        )
        self.assertRedirects(resp, reverse("account_reset_password_from_key_done"))

        # Check the new password is in effect
        user = get_user_model().objects.get(pk=user.pk)
        self.assertTrue(user.check_password(new_password))

        resp = self.client.post(
            reverse("account_login"),
            {"login": user.email, "password": new_password},
        )

        self.assertRedirects(
            resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
        )

    @override_settings(
        ACCOUNT_LOGIN_METHODS={app_settings.LoginMethod.EMAIL},
        ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
        ACCOUNT_LOGIN_ATTEMPTS_LIMIT=1,
    )
    def test_login_using_unverified_email_address_is_prohibited(self):
        user = get_user_model().objects.create(
            username="john", email="john@example.org", is_active=True
        )
        user.set_password("doe")
        user.save()

        EmailAddress.objects.create(
            user=user, email="john@example.org", primary=True, verified=True
        )
        EmailAddress.objects.create(
            user=user, email="john@example.com", primary=False, verified=False
        )

        resp = self.client.post(
            reverse("account_login"), {"login": "john@example.com", "password": "doe"}
        )
        self.assertRedirects(
            resp,
            reverse("account_email_verification_sent"),
            fetch_redirect_response=False,
        )
        self.assertEqual(len(mail.outbox), 1)
        assert mail.outbox[0].to == ["john@example.com"]

    def test_login_unverified_account_mandatory(self):
        """Tests login behavior when email verification is mandatory."""
        user = get_user_model().objects.create(username="john")
        user.set_password("doe")
        user.save()
        EmailAddress.objects.create(
            user=user, email="user@example.com", primary=True, verified=False
        )
        resp = self.client.post(
            reverse("account_login"), {"login": "john", "password": "doe"}
        )
        self.assertRedirects(resp, reverse("account_email_verification_sent"))

    def test_login_inactive_account(self):
        """
        Tests login behavior with inactive accounts.

        Inactive user accounts should be prevented from performing any actions,
        regardless of their verified state.
        """
        # Inactive and verified user account
        user = get_user_model().objects.create(username="john", is_active=False)
        user.set_password("doe")
        user.save()
        EmailAddress.objects.create(
            user=user, email="john@example.com", primary=True, verified=True
        )
        resp = self.client.post(
            reverse("account_login"), {"login": "john", "password": "doe"}
        )
        self.assertRedirects(resp, reverse("account_inactive"))

        # Inactive and unverified user account
        user = get_user_model().objects.create(username="doe", is_active=False)
        user.set_password("john")
        user.save()
        EmailAddress.objects.create(
            user=user, email="user@example.com", primary=True, verified=False
        )
        resp = self.client.post(
            reverse("account_login"), {"login": "doe", "password": "john"}
        )
        self.assertRedirects(resp, reverse("account_inactive"))

    @override_settings(ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS=False)
    def test_account_authenticated_login_redirects_is_false(self):
        self._create_user_and_login()
        resp = self.client.get(reverse("account_login"))
        self.assertEqual(resp.status_code, HTTPStatus.OK)


def test_login_password_forgotten_link_not_present(client, db):
    with patch("allauth.account.forms.reverse") as reverse_mock:
        reverse_mock.side_effect = NoReverseMatch
        form = LoginForm()
        assert form.fields["password"].help_text == ""


def test_login_password_forgotten_link_present(client, db):
    form = LoginForm()
    assert (
        form.fields["password"].help_text
        == '<a href="/accounts/password/reset/">Forgot your password?</a>'
    )


def test_login_while_authenticated(settings, client, user_factory):
    settings.ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS = False
    user_factory(username="john", email="john@example.org", password="doe")
    user_factory(username="jane", email="jane@example.org", password="doe")
    redirect_url = settings.LOGIN_REDIRECT_URL

    resp = client.post(reverse("account_login"), {"login": "john", "password": "doe"})
    assert resp.status_code == HTTPStatus.FOUND
    assert resp["location"] == redirect_url
    resp = client.post(reverse("account_login"), {"login": "jane", "password": "doe"})
    assert resp.status_code == HTTPStatus.FOUND
    assert resp["location"] == redirect_url


def test_login_page(client, db):
    resp = client.get(reverse("account_login"))
    assert resp.status_code == HTTPStatus.OK
    assertTemplateUsed(resp, "account/login.html")
