from django.apps import apps
from django.contrib.auth import authenticate, signals
from django.contrib.auth.models import User
from django.core.exceptions import FieldDoesNotExist
from django.test import TestCase, override_settings
from django.test.client import RequestFactory

from .models import MinimalUser, UserWithDisabledLastLoginField


@override_settings(ROOT_URLCONF="auth_tests.urls")
class SignalTestCase(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.u1 = User.objects.create_user(username="testclient", password="password")
        cls.u3 = User.objects.create_user(username="staff", password="password")

    def listener_login(self, user, **kwargs):
        self.logged_in.append(user)

    def listener_logout(self, user, **kwargs):
        self.logged_out.append(user)

    def listener_login_failed(self, sender, **kwargs):
        self.login_failed.append(kwargs)

    def setUp(self):
        """Set up the listeners and reset the logged in/logged out counters"""
        self.logged_in = []
        self.logged_out = []
        self.login_failed = []
        signals.user_logged_in.connect(self.listener_login)
        signals.user_logged_out.connect(self.listener_logout)
        signals.user_login_failed.connect(self.listener_login_failed)

    def tearDown(self):
        """Disconnect the listeners"""
        signals.user_logged_in.disconnect(self.listener_login)
        signals.user_logged_out.disconnect(self.listener_logout)
        signals.user_login_failed.disconnect(self.listener_login_failed)

    def test_login(self):
        # Only a successful login will trigger the success signal.
        self.client.login(username="testclient", password="bad")
        self.assertEqual(len(self.logged_in), 0)
        self.assertEqual(len(self.login_failed), 1)
        self.assertEqual(self.login_failed[0]["credentials"]["username"], "testclient")
        # verify the password is cleansed
        self.assertIn("***", self.login_failed[0]["credentials"]["password"])
        self.assertIn("request", self.login_failed[0])

        # Like this:
        self.client.login(username="testclient", password="password")
        self.assertEqual(len(self.logged_in), 1)
        self.assertEqual(self.logged_in[0].username, "testclient")

        # Ensure there were no more failures.
        self.assertEqual(len(self.login_failed), 1)

    def test_logout_anonymous(self):
        # The log_out function will still trigger the signal for anonymous
        # users.
        self.client.post("/logout/next_page/")
        self.assertEqual(len(self.logged_out), 1)
        self.assertIsNone(self.logged_out[0])

    def test_logout(self):
        self.client.login(username="testclient", password="password")
        self.client.post("/logout/next_page/")
        self.assertEqual(len(self.logged_out), 1)
        self.assertEqual(self.logged_out[0].username, "testclient")

    def test_update_last_login(self):
        """Only `last_login` is updated in `update_last_login`"""
        user = self.u3
        old_last_login = user.last_login

        user.username = "This username shouldn't get saved"
        request = RequestFactory().get("/login")
        signals.user_logged_in.send(sender=user.__class__, request=request, user=user)
        user = User.objects.get(pk=user.pk)
        self.assertEqual(user.username, "staff")
        self.assertNotEqual(user.last_login, old_last_login)

    def test_failed_login_without_request(self):
        authenticate(username="testclient", password="bad")
        self.assertIsNone(self.login_failed[0]["request"])

    def test_login_with_custom_user_without_last_login_field(self):
        """
        The user_logged_in signal is only registered if the user model has a
        last_login field.
        """
        last_login_receivers = signals.user_logged_in.receivers
        try:
            signals.user_logged_in.receivers = []
            with self.assertRaises(FieldDoesNotExist):
                MinimalUser._meta.get_field("last_login")
            with self.settings(AUTH_USER_MODEL="auth_tests.MinimalUser"):
                apps.get_app_config("auth").ready()
            self.assertEqual(signals.user_logged_in.receivers, [])

            # last_login is a property whose value is None.
            self.assertIsNone(UserWithDisabledLastLoginField().last_login)
            with self.settings(
                AUTH_USER_MODEL="auth_tests.UserWithDisabledLastLoginField"
            ):
                apps.get_app_config("auth").ready()
            self.assertEqual(signals.user_logged_in.receivers, [])

            with self.settings(AUTH_USER_MODEL="auth.User"):
                apps.get_app_config("auth").ready()
            self.assertEqual(len(signals.user_logged_in.receivers), 1)
        finally:
            signals.user_logged_in.receivers = last_login_receivers
