import json
from datetime import datetime, timedelta
from io import BytesIO
from typing import Optional
from unittest.mock import MagicMock, Mock, patch

import pytest
from flask import Flask
from freezegun import freeze_time

from fittrackee import db
from fittrackee.users.models import BlacklistedToken, User, UserSportPreference
from fittrackee.users.utils.token import get_user_token
from fittrackee.workouts.models import Sport

from ..mixins import ApiTestCaseMixin
from ..utils import jsonify_dict

USER_AGENT = (
    'Mozilla/5.0 (X11; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0'
)


class TestUserRegistration(ApiTestCaseMixin):
    def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_username_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response)

    @pytest.mark.parametrize(
        'input_username_length',
        [1, 31],
    )
    def test_it_returns_error_if_username_length_is_invalid(
        self, app: Flask, input_username_length: int
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(length=input_username_length),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response, 'username: 3 to 30 characters required\n')

    @pytest.mark.parametrize(
        'input_description,input_username',
        [
            ('account_handle', '@sam@example.com'),
            ('with special characters', 'sam*'),
        ],
    )
    def test_it_returns_error_if_username_is_invalid(
        self, app: Flask, input_description: str, input_username: str
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=input_username,
                    email=self.random_email(),
                    password=self.random_email(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(
            response,
            'username: only alphanumeric characters and '
            'the underscore character "_" allowed\n',
        )

    @pytest.mark.parametrize(
        'text_transformation',
        ['upper', 'lower'],
    )
    def test_it_returns_error_if_user_already_exists_with_same_username(
        self, app: Flask, user_1: User, text_transformation: str
    ) -> None:
        client = app.test_client()
        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=(
                        user_1.username.upper()
                        if text_transformation == 'upper'
                        else user_1.username.lower()
                    ),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response, 'sorry, that username is already taken')

    def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_password_is_too_short(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(length=7),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response, 'password: 8 characters required\n')

    def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_email_is_invalid(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_string(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response, 'email: valid email must be provided\n')

    def test_it_does_not_send_email_after_error(
        self, app: Flask, account_confirmation_email_mock: Mock
    ) -> None:
        client = app.test_client()

        client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        account_confirmation_email_mock.send.assert_not_called()

    def test_it_returns_success_if_payload_is_valid(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        assert response.status_code == 200
        assert response.content_type == 'application/json'
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert 'auth_token' not in data

    def test_it_creates_user_with_default_date_format(
        self, app: Flask
    ) -> None:
        client = app.test_client()
        username = self.random_string()

        client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=username,
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        new_user = User.query.filter_by(username=username).first()
        assert new_user.date_format == 'MM/dd/yyyy'

    @pytest.mark.parametrize(
        'input_language,expected_language',
        [('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
    )
    def test_it_creates_user_with_inactive_account(
        self,
        app: Flask,
        input_language: Optional[str],
        expected_language: str,
    ) -> None:
        client = app.test_client()
        username = self.random_string()
        email = self.random_email()

        client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=username,
                    email=email,
                    password=self.random_string(),
                    language=input_language,
                )
            ),
            content_type='application/json',
        )

        new_user = User.query.filter_by(username=username).first()
        assert new_user.email == email
        assert new_user.password is not None
        assert new_user.is_active is False
        assert new_user.language == expected_language

    @pytest.mark.parametrize(
        'input_language,expected_language',
        [('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
    )
    def test_it_calls_account_confirmation_email_when_payload_is_valid(
        self,
        app: Flask,
        account_confirmation_email_mock: Mock,
        input_language: Optional[str],
        expected_language: str,
    ) -> None:
        client = app.test_client()
        email = self.random_email()
        username = self.random_string()
        expected_token = self.random_string()

        with patch('secrets.token_urlsafe', return_value=expected_token):
            client.post(
                '/api/auth/register',
                data=json.dumps(
                    dict(
                        username=username,
                        email=email,
                        password='12345678',
                        language=input_language,
                    )
                ),
                content_type='application/json',
                environ_base={'HTTP_USER_AGENT': USER_AGENT},
            )

        account_confirmation_email_mock.send.assert_called_once_with(
            {
                'language': expected_language,
                'email': email,
            },
            {
                'username': username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
                'account_confirmation_url': (
                    'http://0.0.0.0:5000/account-confirmation'
                    f'?token={expected_token}'
                ),
            },
        )

    def test_it_does_not_call_account_confirmation_email_when_email_sending_is_disabled(  # noqa
        self,
        app_wo_email_activation: Flask,
        account_confirmation_email_mock: Mock,
    ) -> None:
        client = app_wo_email_activation.test_client()
        email = self.random_email()
        username = self.random_string()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=username,
                    email=email,
                    password='12345678',
                )
            ),
            content_type='application/json',
            environ_base={'HTTP_USER_AGENT': USER_AGENT},
        )

        assert response.status_code == 200
        account_confirmation_email_mock.send.assert_not_called()

    @pytest.mark.parametrize(
        'text_transformation',
        ['upper', 'lower'],
    )
    def test_it_does_not_return_error_if_a_user_already_exists_with_same_email(
        self, app: Flask, user_1: User, text_transformation: str
    ) -> None:
        client = app.test_client()
        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=(
                        user_1.email.upper()
                        if text_transformation == 'upper'
                        else user_1.email.lower()
                    ),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        assert response.status_code == 200
        assert response.content_type == 'application/json'
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert 'auth_token' not in data

    def test_it_does_not_call_account_confirmation_email_if_user_already_exists(  # noqa
        self, app: Flask, user_1: User, account_confirmation_email_mock: Mock
    ) -> None:
        client = app.test_client()

        client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=user_1.email,
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        account_confirmation_email_mock.send.assert_not_called()


class TestUserLogin(ApiTestCaseMixin):
    def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/login',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_user_does_not_exists(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/login',
            data=json.dumps(
                dict(email=self.random_email(), password=self.random_string())
            ),
            content_type='application/json',
        )

        self.assert_401(response, 'invalid credentials')

    def test_it_returns_error_if_user_account_is_inactive(
        self, app: Flask, inactive_user: User
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/login',
            data=json.dumps(
                dict(email=inactive_user.email, password='12345678')
            ),
            content_type='application/json',
        )

        self.assert_401(response, 'invalid credentials')

    def test_it_returns_error_if_password_is_invalid(
        self, app: Flask, user_1: User
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/login',
            data=json.dumps(
                dict(email=user_1.email, password=self.random_email())
            ),
            content_type='application/json',
        )

        self.assert_401(response, 'invalid credentials')

    @pytest.mark.parametrize(
        'text_transformation',
        ['upper', 'lower'],
    )
    def test_user_can_login_regardless_username_case(
        self, app: Flask, user_1: User, text_transformation: str
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/login',
            data=json.dumps(
                dict(
                    email=(
                        user_1.email.upper()
                        if text_transformation == 'upper'
                        else user_1.email.lower()
                    ),
                    password='12345678',
                )
            ),
            content_type='application/json',
        )

        assert response.status_code == 200
        assert response.content_type == 'application/json'
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'successfully logged in'
        assert data['auth_token']


class TestUserProfile(ApiTestCaseMixin):
    def test_it_returns_error_if_auth_token_is_missing(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.get('/api/auth/profile')

        self.assert_401(response, 'provide a valid auth token')

    def test_it_returns_error_if_auth_token_is_invalid(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.get(
            '/api/auth/profile', headers=dict(Authorization='Bearer invalid')
        )

        self.assert_invalid_token(response)

    def test_it_returns_error_if_token_is_blacklisted(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        db.session.add(BlacklistedToken(token=auth_token))
        db.session.commit()

        response = client.get(
            '/api/auth/profile',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_invalid_token(response)

    def test_it_returns_user(self, app: Flask, user_1: User) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.get(
            '/api/auth/profile',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['data'] == jsonify_dict(user_1.serialize(user_1))

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', True),
            ('profile:write', False),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self, app: Flask, user_1: User, client_scope: str, can_access: bool
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.get(
            '/api/auth/profile',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestUserProfileUpdate(ApiTestCaseMixin):
    def test_it_returns_error_if_payload_is_empty(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit',
            content_type='application/json',
            data=json.dumps(dict()),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_returns_error_if_fields_are_missing(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit',
            content_type='application/json',
            data=json.dumps(dict(first_name=self.random_string())),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_updates_user_profile(self, app: Flask, user_1: User) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        first_name = self.random_string()
        last_name = self.random_string()
        location = self.random_string()
        bio = self.random_string()
        birth_date = '1980-01-01'

        response = client.post(
            '/api/auth/profile/edit',
            content_type='application/json',
            data=json.dumps(
                dict(
                    first_name=first_name,
                    last_name=last_name,
                    location=location,
                    bio=bio,
                    birth_date=birth_date,
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user profile updated'
        assert data['data'] == jsonify_dict(user_1.serialize(user_1))

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', False),
            ('profile:write', True),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.post(
            '/api/auth/profile/edit',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestUserAccountUpdate(ApiTestCaseMixin):
    @staticmethod
    def assert_no_emails_sent(
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        email_updated_to_current_address_mock.send.assert_not_called()
        email_updated_to_new_address_mock.send.assert_not_called()
        password_change_email_mock.send.assert_not_called()

    def test_it_returns_error_if_payload_is_empty(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(dict()),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_returns_error_if_current_password_is_missing(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response, error_message='current password is missing')

    def test_it_returns_error_if_email_is_missing(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response, 'email is missing')

    def test_it_returns_error_if_current_password_is_invalid(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password=self.random_string(),
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_401(response, error_message='invalid credentials')

    def test_it_does_not_send_emails_when_error_occurs(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password=self.random_string(),
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_no_emails_sent(
            email_updated_to_current_address_mock,
            email_updated_to_new_address_mock,
            password_change_email_mock,
        )

    def test_it_does_not_returns_error_if_no_new_password_provided(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user account updated'

    def test_it_does_not_send_emails_if_no_change(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_no_emails_sent(
            email_updated_to_current_address_mock,
            email_updated_to_new_address_mock,
            password_change_email_mock,
        )

    def test_it_returns_error_if_new_email_is_invalid(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=self.random_string(),
                    password='12345678',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response, 'email: valid email must be provided\n')

    def test_it_only_updates_email_to_confirm_if_new_email_provided(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        current_email = user_1.email
        new_email = 'new.email@example.com'

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=new_email,
                    password='12345678',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        assert current_email == user_1.email
        assert new_email == user_1.email_to_confirm
        assert user_1.confirmation_token is not None

    def test_it_updates_email_when_email_sending_is_disabled(
        self,
        app_wo_email_activation: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_wo_email_activation, user_1.email
        )
        new_email = 'new.email@example.com'

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=new_email,
                    password='12345678',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        assert user_1.email == new_email
        assert user_1.email_to_confirm is None
        assert user_1.confirmation_token is None

    def test_it_calls_email_updated_to_current_email_send_when_new_email_provided(  # noqa
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        new_email = 'new.email@example.com'

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=new_email,
                    password='12345678',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
            environ_base={'HTTP_USER_AGENT': USER_AGENT},
        )

        email_updated_to_current_address_mock.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_1.email,
            },
            {
                'username': user_1.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
                'new_email_address': new_email,
            },
        )

    def test_it_calls_email_updated_to_new_email_send_when_new_email_provided(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        new_email = 'new.email@example.com'
        expected_token = self.random_string()

        with patch('secrets.token_urlsafe', return_value=expected_token):
            client.patch(
                '/api/auth/profile/edit/account',
                content_type='application/json',
                data=json.dumps(
                    dict(
                        email=new_email,
                        password='12345678',
                    )
                ),
                headers=dict(Authorization=f'Bearer {auth_token}'),
                environ_base={'HTTP_USER_AGENT': USER_AGENT},
            )

        email_updated_to_new_address_mock.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_1.email_to_confirm,
            },
            {
                'username': user_1.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
                'email_confirmation_url': (
                    f'http://0.0.0.0:5000/email-update?token={expected_token}'
                ),
            },
        )

    def test_it_does_not_calls_password_change_email_send_when_new_email_provided(  # noqa
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        new_email = 'new.email@example.com'
        expected_token = self.random_string()

        with patch('secrets.token_urlsafe', return_value=expected_token):
            client.patch(
                '/api/auth/profile/edit/account',
                content_type='application/json',
                data=json.dumps(
                    dict(
                        email=new_email,
                        password='12345678',
                    )
                ),
                headers=dict(Authorization=f'Bearer {auth_token}'),
                environ_base={'HTTP_USER_AGENT': USER_AGENT},
            )

        password_change_email_mock.send.assert_not_called()

    def test_it_returns_error_if_controls_fail_on_new_password(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                    new_password=self.random_string(length=3),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response, 'password: 8 characters required')

    def test_it_updates_auth_user_password_when_new_password_provided(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        current_hashed_password = user_1.password

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user account updated'
        assert current_hashed_password != user_1.password

    def test_new_password_is_hashed(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        new_password = self.random_string()

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                    new_password=new_password,
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        assert new_password != user_1.password

    def test_it_calls_password_change_email_when_new_password_provided(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
            environ_base={'HTTP_USER_AGENT': USER_AGENT},
        )

        password_change_email_mock.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_1.email,
            },
            {
                'username': user_1.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
            },
        )

    def test_it_does_not_call_email_updated_emails_send_when_new_password_provided(  # noqa
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=user_1.email,
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        email_updated_to_current_address_mock.send.assert_not_called()
        email_updated_to_new_address_mock.send.assert_not_called()

    def test_it_updates_email_to_confirm_and_password_when_new_email_and_password_provided(  # noqa
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        current_email = user_1.email
        current_hashed_password = user_1.password
        new_email = 'new.email@example.com'

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email=new_email,
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user account updated'
        assert user_1.email == current_email
        assert user_1.email_to_confirm == new_email
        assert user_1.password != current_hashed_password

    def test_it_calls_all_email_send_when_new_email_and_password_provided(
        self,
        app: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email='new.email@example.com',
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        email_updated_to_current_address_mock.send.assert_called_once()
        email_updated_to_new_address_mock.send.assert_called_once()
        password_change_email_mock.send.assert_called_once()

    def test_it_does_not_calls_all_email_send_when_email_sending_is_disabled(
        self,
        app_wo_email_activation: Flask,
        user_1: User,
        email_updated_to_current_address_mock: MagicMock,
        email_updated_to_new_address_mock: MagicMock,
        password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_wo_email_activation, user_1.email
        )

        client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            data=json.dumps(
                dict(
                    email='new.email@example.com',
                    password='12345678',
                    new_password=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_no_emails_sent(
            email_updated_to_current_address_mock,
            email_updated_to_new_address_mock,
            password_change_email_mock,
        )

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', False),
            ('profile:write', True),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.patch(
            '/api/auth/profile/edit/account',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestUserPreferencesUpdate(ApiTestCaseMixin):
    def test_it_returns_error_if_payload_is_empty(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/preferences',
            content_type='application/json',
            data=json.dumps(dict()),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_returns_error_if_fields_are_missing(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/preferences',
            content_type='application/json',
            data=json.dumps(dict(weekm=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    @pytest.mark.parametrize(
        'input_language,expected_language',
        [('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
    )
    def test_it_updates_user_preferences(
        self,
        app: Flask,
        user_1: User,
        input_language: Optional[str],
        expected_language: str,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/preferences',
            content_type='application/json',
            data=json.dumps(
                dict(
                    timezone='America/New_York',
                    weekm=True,
                    language=input_language,
                    imperial_units=True,
                    display_ascent=False,
                    date_format='yyyy-MM-dd',
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user preferences updated'
        assert data['data']['display_ascent'] is False
        assert data['data']['imperial_units'] is True
        assert data['data']['language'] == expected_language
        assert data['data']['timezone'] == 'America/New_York'
        assert data['data']['date_format'] == 'yyyy-MM-dd'
        assert data['data']['weekm'] is True

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', False),
            ('profile:write', True),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.post(
            '/api/auth/profile/edit/preferences',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
    def test_it_returns_error_if_payload_is_empty(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(dict()),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_returns_error_if_sport_id_is_missing(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(dict(is_active=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_returns_error_if_sport_not_found(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(dict(sport_id=1, is_active=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_404_with_entity(response, 'sport')

    def test_it_returns_error_if_payload_contains_only_sport_id(
        self, app: Flask, user_1: User, sport_1_cycling: Sport
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(dict(sport_id=1)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response)

    def test_it_returns_error_if_color_is_invalid(
        self, app: Flask, user_1: User, sport_1_cycling: Sport
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(
                dict(
                    sport_id=sport_1_cycling.id,
                    color=self.random_string(),
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(response, 'invalid hexadecimal color')

    @pytest.mark.parametrize(
        'input_color',
        ['#000000', '#FFF'],
    )
    def test_it_updates_sport_color_for_auth_user(
        self,
        app: Flask,
        user_1: User,
        sport_2_running: Sport,
        input_color: str,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(
                dict(
                    sport_id=sport_2_running.id,
                    color=input_color,
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user sport preferences updated'
        assert response.status_code == 200
        assert data['data']['user_id'] == user_1.id
        assert data['data']['sport_id'] == sport_2_running.id
        assert data['data']['color'] == input_color
        assert data['data']['is_active'] is True
        assert data['data']['stopped_speed_threshold'] == 0.1

    def test_it_disables_sport_for_auth_user(
        self, app: Flask, user_1: User, sport_1_cycling: Sport
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(
                dict(
                    sport_id=sport_1_cycling.id,
                    is_active=False,
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user sport preferences updated'
        assert response.status_code == 200
        assert data['data']['user_id'] == user_1.id
        assert data['data']['sport_id'] == sport_1_cycling.id
        assert data['data']['color'] is None
        assert data['data']['is_active'] is False
        assert data['data']['stopped_speed_threshold'] == 1

    def test_it_updates_stopped_speed_threshold_for_auth_user(
        self, app: Flask, user_1: User, sport_1_cycling: Sport
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            data=json.dumps(
                dict(
                    sport_id=sport_1_cycling.id,
                    stopped_speed_threshold=0.5,
                )
            ),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user sport preferences updated'
        assert response.status_code == 200
        assert data['data']['user_id'] == user_1.id
        assert data['data']['sport_id'] == sport_1_cycling.id
        assert data['data']['color'] is None
        assert data['data']['is_active']
        assert data['data']['stopped_speed_threshold'] == 0.5

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', False),
            ('profile:write', True),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.post(
            '/api/auth/profile/edit/sports',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestUserSportPreferencesReset(ApiTestCaseMixin):
    def test_it_returns_error_if_sport_does_not_exist(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.delete(
            '/api/auth/profile/reset/sports/1',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_404_with_entity(response, 'sport')

    def test_it_resets_sport_preferences(
        self,
        app: Flask,
        user_1: User,
        sport_1_cycling: Sport,
        user_sport_1_preference: UserSportPreference,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.delete(
            f'/api/auth/profile/reset/sports/{sport_1_cycling.id}',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204
        assert (
            UserSportPreference.query.filter_by(
                user_id=user_1.id,
                sport_id=sport_1_cycling.id,
            ).first()
            is None
        )

    def test_it_does_not_raise_error_if_sport_preferences_do_not_exist(
        self, app: Flask, user_1: User, sport_1_cycling: Sport
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.delete(
            f'/api/auth/profile/reset/sports/{sport_1_cycling.id}',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', False),
            ('profile:write', True),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1: User,
        client_scope: str,
        can_access: bool,
        sport_1_cycling: Sport,
        user_sport_1_preference: UserSportPreference,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.delete(
            f'/api/auth/profile/reset/sports/{sport_1_cycling.id}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestUserPicture(ApiTestCaseMixin):
    def test_it_returns_error_if_file_is_missing(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/picture',
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        self.assert_400(response, 'no file part', 'fail')

    def test_it_returns_error_if_file_is_invalid(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/picture',
            data=dict(file=(BytesIO(b'avatar'), 'avatar.bmp')),
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        self.assert_400(response, 'file extension not allowed', 'fail')

    def test_it_returns_error_if_image_size_exceeds_file_limit(
        self,
        app_with_max_file_size: Flask,
        user_1: User,
        sport_1_cycling: Sport,
        gpx_file: str,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_with_max_file_size, user_1.email
        )

        response = client.post(
            '/api/auth/picture',
            data=dict(
                file=(BytesIO(b'test_file_for_avatar' * 50), 'avatar.jpg')
            ),
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        data = self.assert_413(
            response,
            'Error during picture upload, file size (1.2KB) exceeds 1.0KB.',
        )
        assert 'data' not in data

    def test_it_returns_error_if_image_size_exceeds_archive_limit(
        self,
        app_with_max_zip_file_size: Flask,
        user_1: User,
        sport_1_cycling: Sport,
        gpx_file: str,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_with_max_zip_file_size, user_1.email
        )

        response = client.post(
            '/api/auth/picture',
            data=dict(
                file=(BytesIO(b'test_file_for_avatar' * 50), 'avatar.jpg')
            ),
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        data = self.assert_413(
            response,
            'Error during picture upload, file size (1.2KB) exceeds 1.0KB.',
        )
        assert 'data' not in data

    def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/picture',
            data=dict(file=(BytesIO(b'avatar'), 'avatar.png')),
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user picture updated'
        assert response.status_code == 200
        assert 'avatar.png' in user_1.picture

        response = client.post(
            '/api/auth/picture',
            data=dict(file=(BytesIO(b'avatar2'), 'avatar2.png')),
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'user picture updated'
        assert response.status_code == 200
        assert 'avatar.png' not in user_1.picture
        assert 'avatar2.png' in user_1.picture

    @pytest.mark.parametrize(
        'client_scope, can_access',
        [
            ('application:write', False),
            ('profile:read', False),
            ('profile:write', True),
            ('users:read', False),
            ('users:write', False),
            ('workouts:read', False),
            ('workouts:write', False),
        ],
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1, scope=client_scope
        )

        response = client.post(
            '/api/auth/picture',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestRegistrationConfiguration(ApiTestCaseMixin):
    def test_it_returns_error_if_it_exceeds_max_users(
        self,
        app_with_3_users_max: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client = app_with_3_users_max.test_client()

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_403(response, 'error, registration is disabled')

    def test_it_disables_registration_on_user_registration(
        self,
        app_with_3_users_max: Flask,
        user_1_admin: User,
        user_2: User,
    ) -> None:
        client = app_with_3_users_max.test_client()
        client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_403(response, 'error, registration is disabled')

    def test_it_does_not_disable_registration_if_users_count_below_limit(
        self,
        app_with_3_users_max: Flask,
        user_1: User,
    ) -> None:
        client = app_with_3_users_max.test_client()
        client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username=self.random_string(),
                    email=self.random_email(),
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        assert response.status_code == 200


class TestPasswordResetRequest(ApiTestCaseMixin):
    def test_it_returns_error_on_empty_payload(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/reset-request',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_on_invalid_payload(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/reset-request',
            data=json.dumps(dict(username=self.random_string())),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_when_email_sending_is_disabled(
        self, app_wo_email_activation: Flask
    ) -> None:
        client = app_wo_email_activation.test_client()

        response = client.post(
            '/api/auth/password/reset-request',
            data=json.dumps(dict(email='test@test.com')),
            content_type='application/json',
        )

        self.assert_404_with_message(
            response, 'the requested URL was not found on the server'
        )

    def test_it_requests_password_reset_when_user_exists(
        self, app: Flask, user_1: User, user_reset_password_email: Mock
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/reset-request',
            data=json.dumps(dict(email='test@test.com')),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'password reset request processed'

    def test_it_calls_reset_password_email_when_user_exists(
        self, app: Flask, user_1: User, reset_password_email: Mock
    ) -> None:
        client = app.test_client()
        token = self.random_string()

        with patch('jwt.encode', return_value=token):
            client.post(
                '/api/auth/password/reset-request',
                data=json.dumps(dict(email='test@test.com')),
                content_type='application/json',
                environ_base={'HTTP_USER_AGENT': USER_AGENT},
            )

        reset_password_email.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_1.email,
            },
            {
                'expiration_delay': '3 seconds',
                'username': user_1.username,
                'password_reset_url': (
                    f'http://0.0.0.0:5000/password-reset?token={token}'
                ),
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
            },
        )

    def test_it_does_not_return_error_when_user_does_not_exist(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/reset-request',
            data=json.dumps(dict(email='test@test.com')),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'password reset request processed'

    def test_it_does_not_call_reset_password_email_when_user_does_not_exist(
        self, app: Flask, reset_password_email: Mock
    ) -> None:
        client = app.test_client()

        client.post(
            '/api/auth/password/reset-request',
            data=json.dumps(dict(email='test@test.com')),
            content_type='application/json',
        )

        reset_password_email.assert_not_called()


class TestPasswordUpdate(ApiTestCaseMixin):
    def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None:
        token = get_user_token(1)
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=token,
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        self.assert_401(response, 'invalid token, please request a new token')

    def test_it_returns_error_if_token_is_expired(
        self, app: Flask, user_1: User
    ) -> None:
        now = datetime.utcnow()
        token = get_user_token(user_1.id, password_reset=True)
        client = app.test_client()

        with freeze_time(now + timedelta(seconds=4)):
            response = client.post(
                '/api/auth/password/update',
                data=json.dumps(
                    dict(
                        token=token,
                        password=self.random_string(),
                    )
                ),
                content_type='application/json',
            )

            self.assert_401(
                response, 'invalid token, please request a new token'
            )

    def test_it_returns_error_if_password_is_invalid(
        self, app: Flask, user_1: User
    ) -> None:
        token = get_user_token(user_1.id, password_reset=True)
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=token,
                    password=self.random_string(length=7),
                )
            ),
            content_type='application/json',
        )

        self.assert_400(response, 'password: 8 characters required\n')

    def test_it_does_not_send_email_after_error(
        self,
        app: Flask,
        user_1: User,
        password_change_email_mock: MagicMock,
    ) -> None:
        token = get_user_token(user_1.id, password_reset=True)
        client = app.test_client()

        client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=token,
                    password=self.random_string(length=7),
                )
            ),
            content_type='application/json',
        )

        password_change_email_mock.assert_not_called()

    def test_it_updates_password(
        self,
        app: Flask,
        user_1: User,
        password_change_email_mock: MagicMock,
    ) -> None:
        token = get_user_token(user_1.id, password_reset=True)
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=token,
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'password updated'

    def test_it_sends_email_after_successful_update(
        self,
        app: Flask,
        user_1: User,
        password_change_email_mock: MagicMock,
    ) -> None:
        token = get_user_token(user_1.id, password_reset=True)
        client = app.test_client()

        response = client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=token,
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
            environ_base={'HTTP_USER_AGENT': USER_AGENT},
        )

        assert response.status_code == 200
        password_change_email_mock.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_1.email,
            },
            {
                'username': user_1.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
            },
        )

    def test_it_does_not_send_email_when_email_sending_is_disabled(
        self,
        app_wo_email_activation: Flask,
        user_1: User,
        password_change_email_mock: MagicMock,
    ) -> None:
        token = get_user_token(user_1.id, password_reset=True)
        client = app_wo_email_activation.test_client()

        client.post(
            '/api/auth/password/update',
            data=json.dumps(
                dict(
                    token=token,
                    password=self.random_string(),
                )
            ),
            content_type='application/json',
            environ_base={'HTTP_USER_AGENT': USER_AGENT},
        )

        password_change_email_mock.send.assert_not_called()


class TestEmailUpdateWitUnauthenticatedUser(ApiTestCaseMixin):
    def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/email/update',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/email/update',
            data=json.dumps(dict(token=self.random_string())),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_does_not_update_email_if_token_mismatches(
        self, app: Flask, user_1: User
    ) -> None:
        user_1.confirmation_token = self.random_string()
        new_email = 'new.email@example.com'
        user_1.email_to_confirm = new_email
        client = app.test_client()

        response = client.post(
            '/api/auth/email/update',
            data=json.dumps(dict(token=self.random_string())),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_updates_email(self, app: Flask, user_1: User) -> None:
        token = self.random_string()
        user_1.confirmation_token = token
        new_email = 'new.email@example.com'
        user_1.email_to_confirm = new_email
        client = app.test_client()

        response = client.post(
            '/api/auth/email/update',
            data=json.dumps(dict(token=token)),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'email updated'
        assert user_1.email == new_email
        assert user_1.email_to_confirm is None
        assert user_1.confirmation_token is None


class TestConfirmationAccount(ApiTestCaseMixin):
    def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/account/confirm',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/account/confirm',
            data=json.dumps(dict(token=self.random_string())),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_activates_user_account(
        self, app: Flask, inactive_user: User
    ) -> None:
        token = self.random_string()
        inactive_user.confirmation_token = token
        client = app.test_client()

        response = client.post(
            '/api/auth/account/confirm',
            data=json.dumps(dict(token=token)),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'account confirmation successful'
        assert inactive_user.is_active is True
        assert inactive_user.confirmation_token is None


class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
    def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict()),
            content_type='application/json',
        )

        self.assert_400(response)

    def test_it_does_not_return_error_if_account_does_not_exist(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict(email=self.random_email())),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'confirmation email resent'

    def test_it_does_not_return_error_if_account_already_active(
        self, app: Flask, user_1: User
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict(email=user_1.email)),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'confirmation email resent'

    def test_it_does_not_call_account_confirmation_email_if_user_is_active(
        self,
        app: Flask,
        user_1: User,
        account_confirmation_email_mock: Mock,
    ) -> None:
        client = app.test_client()

        client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict(email=user_1.email)),
            content_type='application/json',
            environ_base={'HTTP_USER_AGENT': USER_AGENT},
        )

        account_confirmation_email_mock.send.assert_not_called()

    def test_it_returns_success_if_user_is_inactive(
        self, app: Flask, inactive_user: User
    ) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict(email=inactive_user.email)),
            content_type='application/json',
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'confirmation email resent'

    def test_it_updates_token_if_user_is_inactive(
        self, app: Flask, inactive_user: User
    ) -> None:
        client = app.test_client()
        previous_token = inactive_user.confirmation_token

        client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict(email=inactive_user.email)),
            content_type='application/json',
        )

        assert inactive_user.confirmation_token != previous_token

    def test_it_calls_account_confirmation_email_if_user_is_inactive(
        self,
        app: Flask,
        inactive_user: User,
        account_confirmation_email_mock: Mock,
    ) -> None:
        client = app.test_client()
        expected_token = self.random_string()
        inactive_user.language = 'fr'

        with patch('secrets.token_urlsafe', return_value=expected_token):
            client.post(
                '/api/auth/account/resend-confirmation',
                data=json.dumps(dict(email=inactive_user.email)),
                content_type='application/json',
                environ_base={'HTTP_USER_AGENT': USER_AGENT},
            )

        account_confirmation_email_mock.send.assert_called_once_with(
            {
                'language': inactive_user.language,
                'email': inactive_user.email,
            },
            {
                'username': inactive_user.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'operating_system': 'Linux',
                'browser_name': 'Firefox',
                'account_confirmation_url': (
                    'http://0.0.0.0:5000/account-confirmation'
                    f'?token={expected_token}'
                ),
            },
        )

    def test_it_returns_error_if_email_sending_is_disabled(
        self, app_wo_email_activation: Flask, inactive_user: User
    ) -> None:
        client = app_wo_email_activation.test_client()

        response = client.post(
            '/api/auth/account/resend-confirmation',
            data=json.dumps(dict(email=inactive_user.email)),
            content_type='application/json',
        )

        self.assert_404_with_message(
            response, 'the requested URL was not found on the server'
        )


class TestUserLogout(ApiTestCaseMixin):
    def test_it_returns_error_when_headers_are_missing(
        self, app: Flask
    ) -> None:
        client = app.test_client()

        response = client.post('/api/auth/logout', headers=dict())

        self.assert_401(response, 'provide a valid auth token')

    def test_it_returns_error_when_token_is_invalid(self, app: Flask) -> None:
        client = app.test_client()

        response = client.post(
            '/api/auth/logout', headers=dict(Authorization='Bearer invalid')
        )

        self.assert_invalid_token(response)

    def test_it_returns_error_when_token_is_expired(
        self, app: Flask, user_1: User
    ) -> None:
        now = datetime.utcnow()
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        with freeze_time(now + timedelta(seconds=4)):

            response = client.post(
                '/api/auth/logout',
                headers=dict(Authorization=f'Bearer {auth_token}'),
            )

            self.assert_invalid_token(response)

    def test_user_can_logout(self, app: Flask, user_1: User) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        response = client.post(
            '/api/auth/logout',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert data['status'] == 'success'
        assert data['message'] == 'successfully logged out'
        assert response.status_code == 200

    def test_token_is_blacklisted_on_logout(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )

        client.post(
            '/api/auth/logout',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        token = BlacklistedToken.query.filter_by(token=auth_token).first()
        assert token.blacklisted_on is not None

    def test_it_returns_error_if_token_is_already_blacklisted(
        self, app: Flask, user_1: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        db.session.add(BlacklistedToken(token=auth_token))
        db.session.commit()

        response = client.post(
            '/api/auth/logout',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_invalid_token(response)
