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 django.utils.translation import gettext as _

from spirit.core.tests import utils

from ...models import UserProfile
from ...utils.tokens import UserActivationTokenGenerator
from ..backends import EmailAuthBackend, UsernameAuthBackend
from ..forms import LoginForm, RegistrationForm, ResendActivationForm
from .urls import CustomRegisterForm

User = get_user_model()


class UserViewTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user()
        self.user2 = utils.create_user()
        self.category = utils.create_category()
        self.topic = utils.create_topic(self.category, user=self.user2)
        self.topic2 = utils.create_topic(self.category)

    def test_login_email(self):
        """
        try to login by email
        """
        # get
        response = self.client.get(reverse("spirit:user:auth:login"))
        self.assertEqual(response.status_code, 200)

        # post
        form_data = {"username": self.user.email, "password": "bar"}
        response = self.client.post(reverse("spirit:user:auth:login"), form_data)
        expected_url = reverse("spirit:user:update")
        self.assertRedirects(response, expected_url, status_code=302)

    def test_login_redirect(self):
        """
        try to login with a logged in user
        """
        utils.login(self)
        response = self.client.get(reverse("spirit:user:auth:login"))
        expected_url = self.user.st.get_absolute_url()
        self.assertRedirects(response, expected_url, status_code=302)
        # next
        response = self.client.get(
            reverse("spirit:user:auth:login") + "?next=/fakepath/"
        )
        self.assertRedirects(
            response, "/fakepath/", status_code=302, target_status_code=404
        )

    def test_login_open_redirect(self):
        utils.login(self)
        response = self.client.get(
            reverse("spirit:user:auth:login") + "?next=https%3A%2F%2Fevil.com"
        )
        self.assertRedirects(response, "/", status_code=302)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=True)
    def test_login_email_case_insensitive(self):
        """
        try to login by email
        """
        self.assertNotEqual(self.user.email, self.user.email.upper())
        form_data = {"username": self.user.email.upper(), "password": "bar"}
        response = self.client.post(reverse("spirit:user:auth:login"), form_data)
        expected_url = reverse("spirit:user:update")
        self.assertRedirects(response, expected_url, status_code=302)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=False)
    def test_login_email_case_insensitive_off(self):
        """
        try to login by email
        """
        self.assertNotEqual(self.user.email, self.user.email.upper())
        form_data = {"username": self.user.email.upper(), "password": "bar"}
        response = self.client.post(reverse("spirit:user:auth:login"), form_data)
        self.assertEqual(response.status_code, 200)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=True)
    def test_login_username_case_insensitive(self):
        self.assertNotEqual(self.user.username, self.user.username.upper())
        form_data = {"username": self.user.username.upper(), "password": "bar"}
        response = self.client.post(reverse("spirit:user:auth:login"), form_data)
        expected_url = reverse("spirit:user:update")
        self.assertRedirects(response, expected_url, status_code=302)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=False)
    def test_login_username_case_insensitive_off(self):
        self.assertNotEqual(self.user.username, self.user.username.upper())
        form_data = {"username": self.user.username.upper(), "password": "bar"}
        response = self.client.post(reverse("spirit:user:auth:login"), form_data)
        self.assertEqual(response.status_code, 200)

    def test_register(self):
        """
        register
        """
        # get
        response = self.client.get(reverse("spirit:user:auth:register"))
        self.assertEqual(response.status_code, 200)

        # post
        form_data = {
            "username": "uniquefoo",
            "email": "some@some.com",
            "email2": "some@some.com",
            "password": "pass",
        }
        response = self.client.post(reverse("spirit:user:auth:register"), form_data)
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)

        # redirect logged in user
        utils.login(self)
        response = self.client.get(reverse("spirit:user:auth:register"))
        self.assertRedirects(response, reverse("spirit:user:update"), status_code=302)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=True)
    def test_register_username_case_insensitive(self):
        form_data = {
            "username": "UnIqUeFoO",
            "email": "some@some.com",
            "email2": "some@some.com",
            "password": "pass",
        }
        response = self.client.post(reverse("spirit:user:auth:register"), form_data)
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertTrue(User.objects.filter(username="uniquefoo").exists())
        self.assertFalse(User.objects.filter(username="UnIqUeFoO").exists())

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=False)
    def test_register_username_case_insensitive_off(self):
        form_data = {
            "username": "UnIqUeFoO",
            "email": "some@some.com",
            "email2": "some@some.com",
            "password": "pass",
        }
        response = self.client.post(reverse("spirit:user:auth:register"), form_data)
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertFalse(User.objects.filter(username="uniquefoo").exists())
        self.assertTrue(User.objects.filter(username="UnIqUeFoO").exists())

    @utils.immediate_on_commit
    def test_register_email_sent(self):
        """
        register and send activation email
        """
        form_data = {
            "username": "uniquefoo",
            "email": "some@some.com",
            "email2": "some@some.com",
            "password": "pass",
        }
        response = self.client.post(reverse("spirit:user:auth:register"), form_data)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, _("User activation"))

    def test_register_next_logged_in(self):
        """
        redirect next on register
        """
        # redirect logged in user
        utils.login(self)
        response = self.client.get(
            reverse("spirit:user:auth:register") + "?next=/fakepath/"
        )
        self.assertRedirects(
            response, "/fakepath/", status_code=302, target_status_code=404
        )

    @override_settings(ROOT_URLCONF="spirit.user.auth.tests.urls")
    def test_register_custom_form(self):
        """
        Should allow a custom form
        """
        response = self.client.get(reverse("spirit:user:auth:register"))
        self.assertIsInstance(response.context["form"], CustomRegisterForm)

        response = self.client.post(reverse("spirit:user:auth:register"), {})
        self.assertIsInstance(response.context["form"], CustomRegisterForm)

    @override_settings(ST_TESTS_RATELIMIT_NEVER_EXPIRE=True)
    def test_login_rate_limit(self):
        """
        test rate limit 5/5m
        """
        form_data = {"username": self.user.email, "password": "badpassword"}

        for attempt in range(5):
            url = reverse("spirit:user:auth:login")
            response = self.client.post(url, form_data)
            self.assertTemplateUsed(response, "spirit/user/auth/login.html")

        url = reverse("spirit:user:auth:login") + "?next=/path/"
        response = self.client.post(url, form_data)
        self.assertRedirects(response, url, status_code=302)

    @override_settings(ST_TESTS_RATELIMIT_NEVER_EXPIRE=True)
    def test_custom_reset_password(self):
        """
        test rate limit 5/5m
        """
        form_data = {"email": "bademail@bad.com"}

        for attempt in range(5):
            response = self.client.post(
                reverse("spirit:user:auth:password-reset"), form_data
            )
            expected_url = reverse("spirit:user:auth:password-reset-done")
            self.assertRedirects(response, expected_url, status_code=302)

        response = self.client.post(
            reverse("spirit:user:auth:password-reset"), form_data
        )
        expected_url = reverse("spirit:user:auth:password-reset")
        self.assertRedirects(response, expected_url, status_code=302)

    @utils.immediate_on_commit
    def test_custom_reset_password_email(self):
        self.assertEqual(len(mail.outbox), 0)
        form_data = {"email": self.user.email}
        response = self.client.post(
            reverse("spirit:user:auth:password-reset"), form_data
        )
        expected_url = reverse("spirit:user:auth:password-reset-done")
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertEqual(len(mail.outbox), 1)
        self.assertIn("Password reset on", mail.outbox[0].subject)
        self.assertIn("you requested a password reset", mail.outbox[0].body)
        self.assertEqual(mail.outbox[0].to, [self.user.email])

    def test_password_reset_confirm(self):
        """
        test access
        """
        response = self.client.get(
            reverse(
                "spirit:user:auth:password-reset-confirm",
                kwargs={"uidb64": "f-a-k-e", "token": "f-a-k-e"},
            )
        )
        self.assertEqual(response.status_code, 200)

    def test_admin_login(self):
        """
        Redirect to regular user login (optional)
        make sure you added:
        admin.site.login = login_required(admin.site.login)
        to urls.py (the one in your project's root)
        """
        # TODO: document that devs should be doing this.
        try:
            url = reverse("admin:login")
        except NoReverseMatch:
            return

        response = self.client.get(url)
        expected_url = (
            reverse("spirit:user:auth:login") + "?next=" + reverse("admin:login")
        )
        self.assertRedirects(response, expected_url, status_code=302)

    def test_registration_activation(self):
        """
        registration activation
        """
        self.user.st.is_verified = False
        self.user.is_active = False
        self.user.save()
        token = UserActivationTokenGenerator().generate(self.user)
        response = self.client.get(
            reverse(
                "spirit:user:auth:registration-activation",
                kwargs={"pk": self.user.pk, "token": token},
            )
        )
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertTrue(User.objects.get(pk=self.user.pk).is_active)

    def test_registration_activation_invalid(self):
        """
        Activation token should not work if user is verified
        ActiveUserMiddleware required
        """
        self.user.st.is_verified = False
        token = UserActivationTokenGenerator().generate(self.user)

        utils.login(self)
        User.objects.filter(pk=self.user.pk).update(is_active=False)
        UserProfile.objects.filter(user__pk=self.user.pk).update(is_verified=True)
        response = self.client.get(
            reverse(
                "spirit:user:auth:registration-activation",
                kwargs={"pk": self.user.pk, "token": token},
            )
        )
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertFalse(User.objects.get(pk=self.user.pk).is_active)

    @utils.immediate_on_commit
    def test_resend_activation_email(self):
        """
        resend_activation_email
        """
        user = utils.create_user(password="foo")

        form_data = {"email": user.email, "password": "foo"}
        response = self.client.post(
            reverse("spirit:user:auth:resend-activation"), form_data
        )
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, _("User activation"))

        # get
        response = self.client.get(reverse("spirit:user:auth:resend-activation"))
        self.assertEqual(response.status_code, 200)

    def test_resend_activation_email_invalid_previously_logged_in(self):
        """
        resend_activation_email invalid if is_verified was set
        """
        user = utils.create_user(password="foo")
        user.st.is_verified = True
        user.st.save()

        form_data = {"email": user.email, "password": "foo"}
        response = self.client.post(
            reverse("spirit:user:auth:resend-activation"), form_data
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(len(mail.outbox), 0)

    def test_resend_activation_email_invalid_email(self):
        """
        resend_activation_email invalid password
        """
        utils.create_user(password="foo")

        form_data = {"email": "bad@foo.com"}
        response = self.client.post(
            reverse("spirit:user:auth:resend-activation"), form_data
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(len(mail.outbox), 0)

    def test_resend_activation_email_redirect_logged(self):
        """
        resend_activation_email redirect to profile if user is logged in
        """
        utils.login(self)
        response = self.client.get(reverse("spirit:user:auth:resend-activation"))
        expected_url = reverse("spirit:user:update")
        self.assertRedirects(response, expected_url, status_code=302)

    def test_logout(self):
        """
        should log out on POST only
        """
        utils.login(self)

        # get should display confirmation message
        response = self.client.get(reverse("spirit:user:auth:logout"))
        self.assertEqual(response.status_code, 200)
        self.assertTrue(self.client.session.items())

        # post should log out the user (clear the session)
        response = self.client.post(reverse("spirit:user:auth:logout"))
        expected_url = "/"
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertFalse(self.client.session.items())

        # next
        utils.login(self)
        self.assertTrue(self.client.session.items())
        response = self.client.post(
            reverse("spirit:user:auth:logout") + "?next=/fakepath/"
        )
        self.assertRedirects(
            response, "/fakepath/", status_code=302, target_status_code=404
        )
        self.assertFalse(self.client.session.items())

    def test_logout_anonymous_redirect(self):
        """
        should log out on POST only
        """
        # redirect to login if user is anonymous
        response = self.client.get(reverse("spirit:user:auth:logout"))
        expected_url = reverse("spirit:user:auth:login")
        self.assertRedirects(response, expected_url, status_code=302)

        # next if user is anonymous
        response = self.client.get(
            reverse("spirit:user:auth:logout") + "?next=/fakepath/"
        )
        self.assertRedirects(
            response, "/fakepath/", status_code=302, target_status_code=404
        )


class UserFormTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user()

    def test_registration(self):
        """
        register
        """
        form_data = {
            "username": "foo",
            "email": "foo@foo.com",
            "email2": "foo@foo.com",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), True)

    def test_registration_login(self):
        """
        Register and login
        """
        form_data = {
            "username": "foo",
            "email": "foo@foo.com",
            "email2": "foo@foo.com",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), True)

        user = form.save()
        self.assertFalse(user.is_active)

        user.is_active = True
        user.save()
        utils.login(self, user=user, password="pass")  # Asserts if can't login

    def test_registration_email_required(self):
        """
        Registration should require the email field
        """
        form_data = {"username": "foo", "password": "pass"}
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), False)
        self.assertIn("email", form.errors)

    def test_registration_invalid(self):
        """
        invalid email and user
        """
        User.objects.create_user(username="foo", password="bar", email="foo@foo.com")
        form_data = {
            "username": "foo",
            "email": "foo@foo.com",
            "email2": "foo@foo.com",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), False)
        self.assertNotIn("username", form.cleaned_data)
        self.assertNotIn("foo@foo.com", form.cleaned_data)

    def test_registration_honeypot(self):
        """
        registration honeypot
        """
        form_data = {
            "username": "foo",
            "email": "foo@foo.com",
            "email2": "foo@foo.com",
            "password": "pass",
            "honeypot": "im a robot",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), False)
        self.assertNotIn("honeypot", form.cleaned_data)

    def test_registration_email_duplication(self):
        """
        register, don't allow email duplication
        """
        utils.create_user(email="duplicated@bar.com")
        form_data = {
            "username": "foo",
            "email": "duplicated@bar.com",
            "email2": "duplicated@bar.com",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), False)
        self.assertNotIn("email", form.cleaned_data)

    @override_settings(ST_UNIQUE_EMAILS=False)
    def test_registration_email_duplication_allowed(self):
        """
        Duplicated email allowed
        """
        utils.create_user(email="duplicated@bar.com")
        form_data = {
            "username": "foo",
            "email": "duplicated@bar.com",
            "email2": "duplicated@bar.com",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), True)

    def test_registration_email_confirmation(self):
        """
        Confirmation email should match email
        """
        form_data = {
            "username": "foo",
            "email": "foo@bar.com",
            "email2": "foofoo@bar.com",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), False)
        self.assertNotIn("email2", form.cleaned_data)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=True)
    def test_registration_email_confirmation_case_insensitive(self):
        """
        Confirmation email should match email
        """
        form_data = {
            "username": "foo",
            "email": "FOO@bar.com",
            "email2": "FOO@BAR.COM",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), True)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=False)
    def test_registration_email_confirmation_case_sensitive(self):
        """
        Confirmation email should match email
        """
        form_data = {
            "username": "foo",
            "email": "FOO@bar.com",
            "email2": "FOO@BAR.COM",
            "password": "pass",
        }
        form = RegistrationForm(data=form_data)
        self.assertEqual(form.is_valid(), False)
        self.assertNotIn("email2", form.cleaned_data)

    def test_resend_activation_email(self):
        """
        resend activation
        """
        user = utils.create_user(email="newfoo@bar.com")
        form_data = {"email": "newfoo@bar.com"}
        form = ResendActivationForm(form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.get_user(), user)

    def test_resend_activation_email_invalid_email(self):
        """
        resend activation invalid
        """
        form_data = {"email": "bad@bar.com"}
        form = ResendActivationForm(form_data)
        self.assertFalse(form.is_valid())

    def test_resend_activation_email_duplication(self):
        """
        Send email to the first *not verified* user found
        """
        utils.create_user(email="duplicated@bar.com")
        user2 = utils.create_user(email="duplicated@bar.com")
        user3 = utils.create_user(email="duplicated@bar.com")
        form_data = {"email": "duplicated@bar.com"}
        form = ResendActivationForm(form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.get_user(), user3)

        user3.st.is_verified = True
        user3.st.save()
        form = ResendActivationForm(form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.get_user(), user2)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=True)
    def test_resend_activation_email_case_insensitive(self):
        """
        Should lower the email before checking it
        """
        user = utils.create_user(email="newfoo@bar.com")
        form_data = {"email": "NeWfOO@bAr.COM"}
        form = ResendActivationForm(form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.get_user(), user)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=False)
    def test_resend_activation_email_case_sensitive(self):
        """
        Should NOT lower the email before checking it
        """
        utils.create_user(email="newfoo@bar.com")
        form_data = {"email": "NeWfOO@bAr.COM"}
        form = ResendActivationForm(form_data)
        self.assertFalse(form.is_valid())
        self.assertRaises(AttributeError, form.get_user)

    def test_login(self):
        """
        Should login the user
        """
        utils.create_user(username="foobar", password="foo")
        form_data = {"username": "foobar", "password": "foo"}
        form = LoginForm(data=form_data)
        self.assertTrue(form.is_valid())

    def test_login_email(self):
        """
        Should login the user by email
        """
        utils.create_user(email="foobar@bar.com", password="foo")
        form_data = {"username": "foobar@bar.com", "password": "foo"}
        form = LoginForm(data=form_data)
        self.assertTrue(form.is_valid())

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=False)
    def test_login_email_case_sensitive(self):
        """
        Should login the user by email
        """
        utils.create_user(email="foobar@bar.com", password="foo")
        form_data = {"username": "FOOBAR@bar.com", "password": "foo"}
        form = LoginForm(data=form_data)
        self.assertFalse(form.is_valid())

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=True)
    def test_login_email_case_insensitive(self):
        """
        Should login the user by email
        """
        utils.create_user(email="foobar@bar.com", password="foo")
        form_data = {"username": "FOOBAR@bar.com", "password": "foo"}
        form = LoginForm(data=form_data)
        self.assertTrue(form.is_valid())

    def test_login_invalid(self):
        """
        Should not login invalid user
        """
        form = LoginForm(data={})
        self.assertFalse(form.is_valid())

    def test_login_password_invalid(self):
        """
        Should not login invalid user
        """
        utils.create_user(username="foobar", password="foo")
        form_data = {"username": "foobar", "password": "bad"}
        form = LoginForm(data=form_data)
        self.assertFalse(form.is_valid())

    def test_login_username_invalid(self):
        """
        Should not login invalid user
        """
        utils.create_user(username="foobar", password="foo")
        form_data = {"username": "bad", "password": "foo"}
        form = LoginForm(data=form_data)
        self.assertFalse(form.is_valid())


class UserBackendTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user(email="foobar@bar.com", password="bar")

    def test_email_auth_backend(self):
        user = EmailAuthBackend().authenticate(
            request=None, username="foobar@bar.com", password="bar"
        )
        self.assertEqual(user, self.user)

    def test_email_auth_backend_email_duplication(self):
        """
        it should NOT authenticate when the email is not unique (current behaviour, sorry)
        """
        utils.create_user(email="duplicated@bar.com", password="foo")
        utils.create_user(email="duplicated@bar.com", password="foo2")
        user = EmailAuthBackend().authenticate(
            request=None, username="duplicated@bar.com", password="foo"
        )
        self.assertIsNone(user)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=True)
    def test_email_auth_backend_case_insensitive(self):
        user = EmailAuthBackend().authenticate(
            request=None, username="FooBar@bAr.COM", password="bar"
        )
        self.assertEqual(user, self.user)

    @override_settings(ST_CASE_INSENSITIVE_EMAILS=False)
    def test_email_auth_backend_case_sensitive(self):
        user = EmailAuthBackend().authenticate(
            request=None, username="FooBar@bAr.COM", password="bar"
        )
        self.assertIsNone(user)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=True)
    def test_username_auth_backend_case_sensitive(self):
        usr = utils.create_user(username="FooBar", password="bar")
        user = UsernameAuthBackend().authenticate(
            request=None, username="FooBar", password="bar"
        )
        self.assertEqual(user.pk, usr.pk)
        user = UsernameAuthBackend().authenticate(
            request=None, username="foobar", password="bar"
        )
        self.assertEqual(user.pk, usr.pk)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=False)
    def test_username_auth_backend_case_sensitive_off(self):
        usr = utils.create_user(username="FooBar", password="bar")
        user = UsernameAuthBackend().authenticate(
            request=None, username="FooBar", password="bar"
        )
        self.assertEqual(user.pk, usr.pk)
        user = UsernameAuthBackend().authenticate(
            request=None, username="foobar", password="bar"
        )
        self.assertIsNone(user)
