"""
The website's own half of authentication: register, activate, and the social
handoff exchange.

What these assert that is easy to break: a new account has NO plan (the site
reads that to open pricing instead of the app), activation does not re-date
itself, and the handoff code is single use.
"""

from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework_simplejwt.tokens import AccessToken

from users.auth_codes import generate_handoff_code, hash_code
from users.models import OAuthHandoffCode

User = get_user_model()


class RegisterTests(TestCase):

    url = '/api/auth/register/'

    def test_register_returns_tokens_and_an_unactivated_user(self):
        response = self.client.post(self.url, {
            'email': 'New.Person@Example.com',
            'password': 'a-long-enough-passphrase',
        }, content_type='application/json')

        self.assertEqual(response.status_code, 201)
        body = response.json()

        self.assertIn('access', body)
        self.assertIn('refresh', body)
        # The address is lowercased by the manager, and the site keys on it.
        self.assertEqual(body['user']['email'], 'new.person@example.com')
        self.assertEqual(body['user']['lifey_plan'], '')
        self.assertFalse(body['user']['lifey_activated'])
        self.assertTrue(body['user']['has_password'])

    def test_a_weak_password_is_rejected_with_the_reason(self):
        response = self.client.post(self.url, {
            'email': 'weak@example.com',
            'password': '123',
        }, content_type='application/json')

        self.assertEqual(response.status_code, 400)
        self.assertIn('password', response.json())
        self.assertFalse(User.objects.filter(email='weak@example.com').exists())

    def test_a_password_equal_to_the_email_is_rejected(self):
        # Only fails if validate_password is given the user, which is why the
        # check lives in validate() rather than validate_password().
        response = self.client.post(self.url, {
            'email': 'samesame@example.com',
            'password': 'samesame@example.com',
        }, content_type='application/json')

        self.assertEqual(response.status_code, 400)

    def test_a_taken_address_is_refused_and_says_so(self):
        User.objects.create_user(email='taken@example.com', password='a-long-enough-passphrase')

        response = self.client.post(self.url, {
            'email': 'TAKEN@example.com',
            'password': 'another-long-passphrase',
        }, content_type='application/json')

        self.assertEqual(response.status_code, 400)
        self.assertEqual(User.objects.filter(email='taken@example.com').count(), 1)


class ActivateTests(TestCase):

    url = '/api/auth/activate/'

    def setUp(self):
        self.user = User.objects.create_user(
            email='member@example.com', password='a-long-enough-passphrase',
        )
        # Activation is gated on a confirmed address. That gate is what
        # `test_email_links.ActivationGateTests` is about; here it is a
        # precondition, so it is set directly.
        self.user.email_verified = True
        self.user.save(update_fields=['email_verified'])

        self.auth = {'HTTP_AUTHORIZATION': f'Bearer {AccessToken.for_user(self.user)}'}

    def _activate(self, plan, **extra):
        return self.client.post(
            self.url, {'plan': plan}, content_type='application/json', **extra,
        )

    def test_activation_requires_a_signed_in_user(self):
        self.assertEqual(self._activate('beta').status_code, 401)

    def test_beta_activates_the_account(self):
        response = self._activate('beta', **self.auth)

        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.json()['lifey_activated'])

        self.user.refresh_from_db()
        self.assertEqual(self.user.lifey_plan, 'beta')
        self.assertIsNotNone(self.user.lifey_activated_at)

    def test_activating_twice_does_not_move_the_date(self):
        self._activate('beta', **self.auth)
        self.user.refresh_from_db()
        first = self.user.lifey_activated_at

        self._activate('beta', **self.auth)
        self.user.refresh_from_db()
        self.assertEqual(self.user.lifey_activated_at, first)

    def test_a_plan_that_is_not_sold_is_refused(self):
        self.assertEqual(self._activate('enterprise', **self.auth).status_code, 400)
        self.user.refresh_from_db()
        self.assertEqual(self.user.lifey_plan, '')


class OAuthExchangeTests(TestCase):

    url = '/api/auth/oauth/exchange/'

    def setUp(self):
        self.user = User.objects.create_user(
            email='social@example.com', password='a-long-enough-passphrase',
        )

    def _mint(self):
        code = generate_handoff_code()
        OAuthHandoffCode.objects.create(
            code_hash=hash_code(code), user=self.user, provider='google',
        )
        return code

    def _exchange(self, code):
        return self.client.post(self.url, {'code': code}, content_type='application/json')

    def test_a_code_works_exactly_once(self):
        code = self._mint()

        first = self._exchange(code)
        self.assertEqual(first.status_code, 200)
        self.assertIn('access', first.json())
        self.assertEqual(first.json()['user']['email'], 'social@example.com')

        self.assertEqual(self._exchange(code).status_code, 400)

    def test_an_unknown_code_reads_like_a_used_one(self):
        used = self._mint()
        self._exchange(used)

        unknown = self._exchange(generate_handoff_code())
        replayed = self._exchange(used)

        # Four different faults, one answer — the rule desktop_token holds to.
        self.assertEqual(unknown.status_code, replayed.status_code)
        self.assertEqual(unknown.json(), replayed.json())

    def test_the_plaintext_code_is_never_stored(self):
        code = self._mint()
        self.assertFalse(OAuthHandoffCode.objects.filter(code_hash=code).exists())

    def test_an_inactive_account_cannot_exchange(self):
        code = self._mint()
        self.user.is_active = False
        self.user.save(update_fields=['is_active'])

        self.assertEqual(self._exchange(code).status_code, 400)


class ProvidersTests(TestCase):

    def test_both_providers_are_listed_with_their_availability(self):
        response = self.client.get('/api/auth/oauth/providers/')

        self.assertEqual(response.status_code, 200)
        listed = {entry['id']: entry for entry in response.json()['providers']}

        self.assertEqual(set(listed), {'google', 'microsoft'})
        # No secrets in the test environment, so both report unavailable and
        # the page greys the buttons out rather than sending anybody to a
        # provider that will answer `invalid_client`.
        self.assertFalse(listed['google']['available'])

    def test_the_provider_list_never_leaks_a_secret(self):
        body = self.client.get('/api/auth/oauth/providers/').content.decode()
        self.assertNotIn('client_secret', body)
        self.assertNotIn('CLIENT_SECRET', body)


class OAuthRedirectTests(TestCase):

    def test_an_unknown_provider_does_not_reach_a_provider(self):
        response = self.client.get('/api/auth/oauth/apple/start/')

        self.assertEqual(response.status_code, 302)
        self.assertNotIn('apple.com', response['Location'])
        self.assertIn('#error=', response['Location'])

    def test_a_callback_without_a_session_is_refused(self):
        response = self.client.get('/api/auth/oauth/google/callback/?code=x&state=y')

        self.assertEqual(response.status_code, 302)
        self.assertIn('#error=', response['Location'])
        self.assertEqual(OAuthHandoffCode.objects.count(), 0)
