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

import pytest
from flask import Flask

from fittrackee import db
from fittrackee.users.models import User, UserDataExport, UserSportPreference
from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Sport, Workout

from ..mixins import ApiTestCaseMixin
from ..utils import OAUTH_SCOPES, jsonify_dict


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

        response = client.get(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_403(response)

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

        response = client.get(
            f'/api/users/{user_1.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert data['status'] == 'success'
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user['username'] == user_1.username

    def test_it_gets_inactive_user(
        self, app: Flask, user_1_admin: User, inactive_user: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            f'/api/users/{inactive_user.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert data['status'] == 'success'
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user == jsonify_dict(inactive_user.serialize(user_1_admin))

    def test_it_gets_single_user_without_workouts(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert data['status'] == 'success'
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user == jsonify_dict(user_2.serialize(user_1_admin))

    def test_it_gets_single_user_with_workouts(
        self,
        app: Flask,
        user_1: User,
        user_2_admin: User,
        sport_1_cycling: Sport,
        sport_2_running: Sport,
        workout_cycling_user_1: Workout,
        workout_running_user_1: Workout,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_2_admin.email
        )

        response = client.get(
            f'/api/users/{user_1.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert data['status'] == 'success'
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user == jsonify_dict(user_1.serialize(user_2_admin))

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

        response = client.get(
            '/api/users/not_existing',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_404_with_entity(response, 'user')

    @pytest.mark.parametrize(
        'client_scope, can_access',
        {**OAUTH_SCOPES, 'users:read': True}.items(),
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1_admin: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1_admin, scope=client_scope
        )

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

        self.assert_response_scope(response, can_access)


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

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

        self.assert_403(response)

    def test_it_get_users_list_regardless_their_account_status(
        self, app: Flask, user_1_admin: User, inactive_user: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

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

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert data['data']['users'][0] == jsonify_dict(
            user_1_admin.serialize(user_1_admin)
        )
        assert data['data']['users'][1] == jsonify_dict(
            inactive_user.serialize(user_1_admin)
        )
        assert data['data']['users'][2] == jsonify_dict(
            user_3.serialize(user_1_admin)
        )
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    @patch('fittrackee.users.users.USER_PER_PAGE', 2)
    def test_it_gets_first_page_on_users_list(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?page=1',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 2
        assert data['pagination'] == {
            'has_next': True,
            'has_prev': False,
            'page': 1,
            'pages': 2,
            'total': 3,
        }

    @patch('fittrackee.users.users.USER_PER_PAGE', 2)
    def test_it_gets_next_page_on_users_list(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?page=2',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': True,
            'page': 2,
            'pages': 2,
            'total': 3,
        }

    def test_it_gets_empty_next_page_on_users_list(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?page=2',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 0
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': True,
            'page': 2,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_user_list_with_2_per_page(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?per_page=2',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 2
        assert data['pagination'] == {
            'has_next': True,
            'has_prev': False,
            'page': 1,
            'pages': 2,
            'total': 3,
        }

    def test_it_gets_next_page_on_user_list_with_2_per_page(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?page=2&per_page=2',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': True,
            'page': 2,
            'pages': 2,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_username(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=username',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'admin' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'toto' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_username_ascending(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=username&order=asc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'admin' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'toto' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_username_descending(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=username&order=desc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'toto' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'admin' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_creation_date(
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
    ) -> None:
        user_2.created_at = datetime.utcnow() - timedelta(days=1)
        user_3.created_at = datetime.utcnow() - timedelta(hours=1)
        user_1_admin.created_at = datetime.utcnow()
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=created_at',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'toto' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'admin' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_creation_date_ascending(
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
    ) -> None:
        user_2.created_at = datetime.utcnow() - timedelta(days=1)
        user_3.created_at = datetime.utcnow() - timedelta(hours=1)
        user_1_admin.created_at = datetime.utcnow()
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=created_at&order=asc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'toto' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'admin' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_creation_date_descending(
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
    ) -> None:
        user_2.created_at = datetime.utcnow() - timedelta(days=1)
        user_3.created_at = datetime.utcnow() - timedelta(hours=1)
        user_1_admin.created_at = datetime.utcnow()
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=created_at&order=desc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'admin' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'toto' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_admin_rights(
        self, app: Flask, user_2: User, user_1_admin: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=admin',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'toto' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'admin' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_admin_rights_ascending(
        self, app: Flask, user_2: User, user_1_admin: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=admin&order=asc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'toto' in data['data']['users'][0]['username']
        assert 'sam' in data['data']['users'][1]['username']
        assert 'admin' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_admin_rights_descending(
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=admin&order=desc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'admin' in data['data']['users'][0]['username']
        assert 'toto' in data['data']['users'][1]['username']
        assert 'sam' in data['data']['users'][2]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_workouts_count(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
        sport_1_cycling: Sport,
        workout_cycling_user_2: Workout,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=workouts_count',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'admin' in data['data']['users'][0]['username']
        assert 0 == data['data']['users'][0]['nb_workouts']
        assert 'sam' in data['data']['users'][1]['username']
        assert 0 == data['data']['users'][1]['nb_workouts']
        assert 'toto' in data['data']['users'][2]['username']
        assert 1 == data['data']['users'][2]['nb_workouts']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_workouts_count_ascending(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
        sport_1_cycling: Sport,
        workout_cycling_user_2: Workout,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=workouts_count&order=asc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'admin' in data['data']['users'][0]['username']
        assert 0 == data['data']['users'][0]['nb_workouts']
        assert 'sam' in data['data']['users'][1]['username']
        assert 0 == data['data']['users'][1]['nb_workouts']
        assert 'toto' in data['data']['users'][2]['username']
        assert 1 == data['data']['users'][2]['nb_workouts']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_ordered_by_account_status(
        self,
        app: Flask,
        user_1_admin: User,
        inactive_user: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=is_active',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 2
        assert data['data']['users'][0]['username'] == inactive_user.username
        assert not data['data']['users'][0]['is_active']
        assert data['data']['users'][1]['username'] == user_1_admin.username
        assert data['data']['users'][1]['is_active']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 2,
        }

    def test_it_gets_users_list_ordered_by_account_status_ascending(
        self,
        app: Flask,
        user_1_admin: User,
        inactive_user: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=is_active&order=asc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 2
        assert data['data']['users'][0]['username'] == inactive_user.username
        assert not data['data']['users'][0]['is_active']
        assert data['data']['users'][1]['username'] == user_1_admin.username
        assert data['data']['users'][1]['is_active']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 2,
        }

    def test_it_gets_users_list_ordered_by_account_status_descending(
        self,
        app: Flask,
        user_1_admin: User,
        inactive_user: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=is_active&order=desc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 2
        assert data['data']['users'][0]['username'] == user_1_admin.username
        assert data['data']['users'][0]['is_active']
        assert data['data']['users'][1]['username'] == inactive_user.username
        assert not data['data']['users'][1]['is_active']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 2,
        }

    def test_it_gets_users_list_ordered_by_workouts_count_descending(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
        sport_1_cycling: Sport,
        workout_cycling_user_2: Workout,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=workouts_count&order=desc',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 3
        assert 'toto' in data['data']['users'][0]['username']
        assert 1 == data['data']['users'][0]['nb_workouts']
        assert 'admin' in data['data']['users'][1]['username']
        assert 0 == data['data']['users'][1]['nb_workouts']
        assert 'sam' in data['data']['users'][2]['username']
        assert 0 == data['data']['users'][2]['nb_workouts']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 3,
        }

    def test_it_gets_users_list_filtering_on_username(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?q=toto',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        assert 'toto' in data['data']['users'][0]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 1,
            'total': 1,
        }

    def test_it_returns_username_matching_query(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?q=oto',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        assert 'toto' in data['data']['users'][0]['username']

    def test_it_filtering_on_username_is_case_insensitive(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?q=TOTO',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        assert 'toto' in data['data']['users'][0]['username']

    def test_it_returns_empty_users_list_filtering_on_username(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?q=not_existing',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 0
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': False,
            'page': 1,
            'pages': 0,
            'total': 0,
        }

    def test_it_users_list_with_complex_query(
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.get(
            '/api/users?order_by=username&order=desc&page=2&per_page=2',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        data = json.loads(response.data.decode())
        assert response.status_code == 200
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        assert 'admin' in data['data']['users'][0]['username']
        assert data['pagination'] == {
            'has_next': False,
            'has_prev': True,
            'page': 2,
            'pages': 2,
            'total': 3,
        }

    @pytest.mark.parametrize(
        'client_scope, can_access',
        {**OAUTH_SCOPES, 'users:read': True}.items(),
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1_admin: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1_admin, scope=client_scope
        )

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

        self.assert_response_scope(response, can_access)


class TestGetUserPicture(ApiTestCaseMixin):
    def test_it_return_error_if_user_has_no_picture(
        self, app: Flask, user_1: User
    ) -> None:
        client = app.test_client()

        response = client.get(f'/api/users/{user_1.username}/picture')

        self.assert_404_with_message(response, 'No picture.')

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

        response = client.get('/api/users/not_existing/picture')

        self.assert_404_with_entity(response, 'user')


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

        response = client.patch(
            f'/api/users/{user_2.username}',
            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_payload_for_admin_rights_is_invalid(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(admin="")),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 500
        data = json.loads(response.data.decode())
        assert 'error' in data['status']
        assert (
            'error, please try again or contact the administrator'
            in data['message']
        )

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

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(admin=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_403(response)

    def test_it_adds_admin_rights_to_a_user(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(admin=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user['email'] == 'toto@toto.com'
        assert user['admin'] is True

    def test_it_removes_admin_rights_to_a_user(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(admin=False)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1

        user = data['data']['users'][0]
        assert user['email'] == 'toto@toto.com'
        assert user['admin'] is False

    def test_it_does_not_send_email_when_only_admin_rights_update(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_password_change_email_mock: MagicMock,
        user_reset_password_email: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(admin=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        user_password_change_email_mock.send.assert_not_called()
        user_reset_password_email.send.assert_not_called()

    def test_it_resets_user_password(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )
        user_2_password = user_2.password

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(reset_password=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        assert user_2.password != user_2_password

    def test_it_calls_password_change_email_when_password_reset_is_successful(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(reset_password=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        user_password_change_email_mock.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_2.email,
            },
            {
                'username': user_2.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
            },
        )

    def test_it_does_not_call_password_change_email_when_email_sending_is_disabled(  # noqa
        self,
        app_wo_email_activation: Flask,
        user_1_admin: User,
        user_2: User,
        user_password_change_email_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_wo_email_activation, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(reset_password=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

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

    def test_it_calls_reset_password_email_when_password_reset_is_successful(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_reset_password_email: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        with patch(
            'fittrackee.users.users.User.encode_password_reset_token',
            return_value='xxx',
        ):
            response = client.patch(
                f'/api/users/{user_2.username}',
                content_type='application/json',
                data=json.dumps(dict(reset_password=True)),
                headers=dict(Authorization=f'Bearer {auth_token}'),
            )

        assert response.status_code == 200
        user_reset_password_email.send.assert_called_once_with(
            {
                'language': 'en',
                'email': user_2.email,
            },
            {
                'expiration_delay': get_readable_duration(
                    app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
                    'en',
                ),
                'username': user_2.username,
                'password_reset_url': (
                    'http://0.0.0.0:5000/password-reset?token=xxx'
                ),
                'fittrackee_url': 'http://0.0.0.0:5000',
            },
        )

    def test_it_does_not_call_reset_password_email_when_email_sending_is_disabled(  # noqa
        self,
        app_wo_email_activation: Flask,
        user_1_admin: User,
        user_2: User,
        user_reset_password_email: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_wo_email_activation, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(reset_password=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

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

    def test_it_returns_error_when_updating_email_with_invalid_address(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(new_email=self.random_string())),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

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

    def test_it_returns_error_when_new_email_is_same_as_current_email(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(new_email=user_2.email)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_400(
            response, 'new email must be different than current email'
        )

    def test_it_does_not_send_email_when_error_on_updating_email(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_email_updated_to_new_address_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(new_email=self.random_string())),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        user_email_updated_to_new_address_mock.send.assert_not_called()

    def test_it_updates_user_email_to_confirm_when_email_sending_is_enabled(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )
        new_email = 'new.' + user_2.email
        user_2_email = user_2.email
        user_2_confirmation_token = user_2.confirmation_token

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(new_email=new_email)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        assert user_2.email == user_2_email
        assert user_2.email_to_confirm == new_email
        assert user_2.confirmation_token != user_2_confirmation_token

    def test_it_updates_user_email_when_email_sending_is_disabled(
        self, app_wo_email_activation: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_wo_email_activation, user_1_admin.email
        )
        new_email = 'new.' + user_2.email

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(new_email=new_email)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

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

    def test_it_calls_email_updated_to_new_address_when_password_reset_is_successful(  # noqa
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        user_email_updated_to_new_address_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )
        new_email = 'new.' + user_2.email
        expected_token = self.random_string()

        with patch('secrets.token_urlsafe', return_value=expected_token):
            response = client.patch(
                f'/api/users/{user_2.username}',
                content_type='application/json',
                data=json.dumps(dict(new_email=new_email)),
                headers=dict(Authorization=f'Bearer {auth_token}'),
            )

        assert response.status_code == 200
        user_email_updated_to_new_address_mock.send.assert_called_once_with(
            {
                'language': 'en',
                'email': new_email,
            },
            {
                'username': user_2.username,
                'fittrackee_url': 'http://0.0.0.0:5000',
                'email_confirmation_url': (
                    f'http://0.0.0.0:5000/email-update?token={expected_token}'
                ),
            },
        )

    def test_it_does_not_call_email_updated_to_new_address_when_email_sending_is_disabled(  # noqa
        self,
        app_wo_email_activation: Flask,
        user_1_admin: User,
        user_2: User,
        user_email_updated_to_new_address_mock: MagicMock,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_wo_email_activation, user_1_admin.email
        )
        new_email = 'new.' + user_2.email

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(new_email=new_email)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

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

    def test_it_activates_user_account(
        self, app: Flask, user_1_admin: User, inactive_user: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{inactive_user.username}',
            content_type='application/json',
            data=json.dumps(dict(activate=True)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user['email'] == inactive_user.email
        assert user['is_active'] is True
        assert inactive_user.confirmation_token is None

    def test_it_can_only_activate_user_account(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            data=json.dumps(dict(activate=False)),
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 200
        data = json.loads(response.data.decode())
        assert 'success' in data['status']
        assert len(data['data']['users']) == 1
        user = data['data']['users'][0]
        assert user['email'] == user_2.email
        assert user['is_active'] is True
        assert user_2.confirmation_token is None

    @pytest.mark.parametrize(
        'client_scope, can_access',
        {**OAUTH_SCOPES, 'users:write': True}.items(),
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1_admin, scope=client_scope
        )

        response = client.patch(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)


class TestDeleteUser(ApiTestCaseMixin):
    def test_user_can_delete_its_own_account(
        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/users/test',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    def test_user_with_workout_can_delete_its_own_account(
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        client.post(
            '/api/workouts',
            data=dict(
                file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
                data='{"sport_id": 1}',
            ),
            headers=dict(
                content_type='multipart/form-data',
                Authorization=f'Bearer {auth_token}',
            ),
        )

        response = client.delete(
            '/api/users/test',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    def test_user_with_preferences_can_delete_its_own_account(
        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(
            '/api/users/test',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    def test_user_with_picture_can_delete_its_own_account(
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        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}',
            ),
        )

        response = client.delete(
            '/api/users/test',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    def test_user_with_export_request_can_delete_its_own_account(
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
    ) -> None:
        db.session.add(UserDataExport(user_1.id))
        db.session.commit()
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1.email
        )
        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}',
            ),
        )

        response = client.delete(
            '/api/users/test',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

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

        response = client.delete(
            '/api/users/toto',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_403(response)

    def test_it_returns_error_when_deleting_non_existing_user(
        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/users/not_existing',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_404_with_entity(response, 'user')

    def test_admin_can_delete_another_user_account(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.delete(
            '/api/users/toto',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    def test_admin_can_delete_its_own_account(
        self, app: Flask, user_1_admin: User, user_2_admin: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.delete(
            '/api/users/admin',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        assert response.status_code == 204

    def test_admin_can_not_delete_its_own_account_if_no_other_admin(
        self, app: Flask, user_1_admin: User, user_2: User
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app, user_1_admin.email
        )

        response = client.delete(
            '/api/users/admin',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

        self.assert_403(
            response,
            'you can not delete your account, no other user has admin rights',
        )

    def test_it_enables_registration_after_user_delete_when_users_count_is_below_limit(  # noqa
        self,
        app_with_3_users_max: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_with_3_users_max, user_1_admin.email
        )
        client.delete(
            '/api/users/toto',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )

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

        assert response.status_code == 200

    def test_it_does_not_enable_registration_on_user_delete_when_users_count_is_not_below_limit(  # noqa
        self,
        app_with_3_users_max: Flask,
        user_1_admin: User,
        user_2: User,
        user_3: User,
        user_1_paris: User,
    ) -> None:
        client, auth_token = self.get_test_client_and_auth_token(
            app_with_3_users_max, user_1_admin.email
        )

        client.delete(
            '/api/users/toto',
            headers=dict(Authorization=f'Bearer {auth_token}'),
        )
        response = client.post(
            '/api/auth/register',
            data=json.dumps(
                dict(
                    username='justatest',
                    email='test@test.com',
                    password='12345678',
                    password_conf='12345678',
                    accepted_policy=True,
                )
            ),
            content_type='application/json',
        )

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

    @pytest.mark.parametrize(
        'client_scope, can_access',
        {**OAUTH_SCOPES, 'users:write': True}.items(),
    )
    def test_expected_scopes_are_defined(
        self,
        app: Flask,
        user_1_admin: User,
        user_2: User,
        client_scope: str,
        can_access: bool,
    ) -> None:
        (
            client,
            oauth_client,
            access_token,
            _,
        ) = self.create_oauth2_client_and_issue_token(
            app, user_1_admin, scope=client_scope
        )

        response = client.delete(
            f'/api/users/{user_2.username}',
            content_type='application/json',
            headers=dict(Authorization=f'Bearer {access_token}'),
        )

        self.assert_response_scope(response, can_access)
