import json

from django.core.cache import cache
from django.test import TestCase
from django.urls import reverse

from users.forms import LOCKOUT_ATTEMPTS
from users.models import CustomUser

PASSWORD = 'pw-for-tests-123'


class LoginTests(TestCase):
    """The website's JSON login — `POST /api/auth/login/`."""

    def setUp(self):
        cache.clear()
        self.user = CustomUser.objects.create_user('signin@example.com', PASSWORD)
        self.login = reverse('users:login')

    def _post(self, **body):
        return self.client.post(self.login, data=json.dumps(body), content_type='application/json')

    def test_returns_both_tokens(self):
        response = self._post(email=self.user.email, password=PASSWORD)

        self.assertEqual(response.status_code, 200)
        self.assertIn('access', response.json())
        self.assertIn('refresh', response.json())

    def test_is_case_insensitive_on_the_email(self):
        response = self._post(email='SignIn@Example.COM', password=PASSWORD)
        self.assertEqual(response.status_code, 200)

    def test_a_wrong_password_is_refused(self):
        response = self._post(email=self.user.email, password='wrong')
        self.assertEqual(response.status_code, 400)
        self.assertNotIn('access', response.json())

    def test_an_unknown_address_reads_the_same_as_a_wrong_password(self):
        """Or the endpoint becomes a way to ask who has an account."""
        unknown = self._post(email='nobody@example.com', password='wrong')
        wrong = self._post(email=self.user.email, password='wrong')

        self.assertEqual(unknown.json(), wrong.json())

    def test_an_inactive_account_is_refused(self):
        self.user.is_active = False
        self.user.save(update_fields=['is_active'])

        response = self._post(email=self.user.email, password=PASSWORD)
        self.assertEqual(response.status_code, 400)

    def test_repeated_failures_lock_the_address_out(self):
        for _ in range(LOCKOUT_ATTEMPTS):
            self._post(email=self.user.email, password='wrong')

        response = self._post(email=self.user.email, password=PASSWORD)

        self.assertEqual(response.status_code, 429)
        self.assertNotIn('access', response.json())

    def test_lockout_is_shared_with_the_browser_sign_in_form(self):
        """
        Same counter, keyed on the email — a lockout earned by hammering the
        website login must also block the desktop browser form, and vice
        versa, or an attacker just picks whichever entry point is not locked.
        """
        for _ in range(LOCKOUT_ATTEMPTS):
            self._post(email=self.user.email, password='wrong')

        desktop_start = reverse('auth-browser:desktop-sign-in')
        self.client.get(
            f'{desktop_start}?state=s&code_challenge=c&code_challenge_method=S256'
        )
        response = self.client.post(desktop_start, {'email': self.user.email, 'password': PASSWORD})

        self.assertContains(response, 'Too many attempts')

    def test_a_successful_login_clears_the_lockout_counter(self):
        self._post(email=self.user.email, password='wrong')
        self._post(email=self.user.email, password=PASSWORD)

        for _ in range(LOCKOUT_ATTEMPTS - 1):
            self._post(email=self.user.email, password='wrong')

        response = self._post(email=self.user.email, password=PASSWORD)
        self.assertEqual(response.status_code, 200)

    def test_a_malformed_body_reads_the_same_as_wrong_credentials(self):
        response = self._post(email='not-an-email', password='')
        self.assertEqual(response.json(), {'detail': 'Email or password is incorrect.'})
