"""This module holds api views tests for ``trainings`` app."""

import json
from datetime import datetime, timedelta
from mock import patch
import pytz

from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.test.utils import override_settings

from rest_framework import status

from mtp.apps.accounts.apiv2.serializers import MTPUserSerializer
from mtp.apps.accounts.factories import TrainerFactory
from mtp.apps.api.tests.test_cases import MTPAPITestCase, MTPOAuthClientTestCaseMixin
from mtp.apps.assessments.apiv2.tests.test_serializers import BaseAssessmentScoresTestCase
from mtp.apps.brainstormers.api.serializers import BrainstormerSerializer
from mtp.apps.brainstormers.models import Brainstormer
from mtp.apps.common.tests.test_cases import DEFAULT_PASSWORD
from mtp.apps.common.utils import get_current_timestamp
from mtp.apps.documents.factories import NoteFactory
from mtp.apps.trainings.factories import TrainingFactory, TrainingSessionFactory
from mtp.apps.trainings.models import Training, TrainingActivity, TrainingSession
from mtp.apps.trainings.models.sessions import bypass_session_state_check
from mtp.apps.trainings.redis_models import TrainingSession as RedisTrainingSession
from mtp.apps.trainings.tests.test_cases import TrainingWithActivitiesTestCase

from ..serializers import TrainingActivitySerializer


class TrainingViewSetTestCase(MTPAPITestCase):
    """Test case for the ``TrainingViewSet``"""

    @classmethod
    def setUpTestData(cls):
        """store the list and the detail urls"""

        super().setUpTestData()

        cls.list_url = reverse('apiv2:training-list')
        cls.detail_url = reverse('apiv2:training-detail', kwargs={'pk': cls.training.pk})

    def test_trainee_cannot_access_views(self):
        """Test that the views from the ViewSet cannot be accessed by trainees"""

        self.login_as_trainee()

        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, 403)

        response = self.client.get(self.detail_url)
        self.assertEqual(response.status_code, 403)

    def test_viewset_list(self):
        """Test that the list action of the ViewSet should behave correctly"""

        self.login_as_trainer()

        # 8 Queries:
        # - 2 for the savepoints
        # - 2 for the permissions
        # - 4 for the 2 trainings
        with self.assertNumQueries(8):
            response = self.client.get(self.list_url)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            len(response.data),
            Training.objects.non_deleted().available_for_user(self.trainer).count()
        )

        # With three trainings, 1 additional query
        TrainingFactory(owner=self.trainer)

        # 9 Queries:
        # - 2 for the savepoints
        # - 2 for the permissions
        # - 5 for the 3 trainings (one new for the new training)
        with self.assertNumQueries(9):
            self.client.get(self.list_url)

    def test_viewset_detail(self):
        """Test that the detail action of the ViewSet should behave correctly"""

        self.login_as_trainer()
        # 7 Queries:
        # - 2 for the savepoints
        # - 2 for the permissions
        # - 3 for the training
        with self.assertNumQueries(7):
            response = self.client.get(self.detail_url)

        self.assertEqual(response.status_code, 200)


class SessionViewSetTestCase(MTPOAuthClientTestCaseMixin, MTPAPITestCase):
    """Test cases for ``SessionViewSet`` viewset."""

    @classmethod
    def setUpTestData(cls):
        """Create a second trainer and a second session."""

        super().setUpTestData()

        cls.trainer2 = TrainerFactory()
        cls.trainer2.set_password(DEFAULT_PASSWORD)
        cls.trainer2.save()

        TrainingSessionFactory(
            state=TrainingSession.STATE_CHOICES.NOT_YET_OPEN,
            permissions=TrainingSession.PERMISSION_CHOICES.PUBLIC,
            trainer=cls.trainer,
            training=cls.training,
        )

        with bypass_session_state_check():
            closed_session = TrainingSessionFactory(
                state=TrainingSession.STATE_CHOICES.CLOSED,
                permissions=TrainingSession.PERMISSION_CHOICES.PUBLIC,
                trainer=cls.trainer,
                training=cls.training,
            )

        closed_session.users.add(cls.trainee)

    def setUp(self):
        """Clear the cache before each test because we test cached views and reload some data"""

        cache.clear()

        super().setUp()

        self.training_session.refresh_from_db()


    def test_list_sessions(self):
        """Test the route to list sessions."""

        url = reverse('apiv2:session-list')

        # The trainer2 don't own any sessions, so he shouldn't see any
        self.client.login(username=self.trainer2.username, password=DEFAULT_PASSWORD)
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 0)

        # The trainer is the owner of 3 sessions, he should see 3 sessions
        self.login_as_trainer()
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 3)
        self.assertEqual(response.data[0]['trainer']['id'], self.trainer.id)
        self.assertEqual(response.data[1]['trainer']['id'], self.trainer.id)
        self.assertEqual(response.data[2]['trainer']['id'], self.trainer.id)

        # We have 3 "open" sessions
        self.login_as_trainee()
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 3)

    def test_list_sessions_not_list_deleted(self):
        """Test the route to list sessions not list soft deleted sessions."""

        # We create a session
        new_session = TrainingSessionFactory(
            training=self.training,
            trainer=self.trainer,
            state=TrainingSession.STATE_CHOICES.OPEN,
            permissions=TrainingSession.PERMISSION_CHOICES.PUBLIC,
        )
        self.login_as_trainer()

        # We have now 4 sessions
        url = '%s?a' % reverse('apiv2:session-list') # to force refresh
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 4)

        # we soft delete new_session
        new_session.remove()

        # then we don't see it after
        url = '%s?b' % reverse('apiv2:session-list') # to force refresh
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 3)

    def test_create_session_from_training(self):
        """Test the creation of session from a training"""
        self.login_as_trainer()

        url = reverse('apiv2:session-list')

        response = self.client.post(
            url,
            {'start_date': '2015-01-01 18:30',
             'end_date': '2015-01-01 19:30',
             'training_id': self.training.pk
            }
        )
        self.assertTrue(response.status_code, 201)

    def test_list_session_is_cached_for_trainer(self):
        """Test the cache behavior for the session list, logged as trainer"""

        url = reverse('apiv2:session-list')

        self.login_as_trainer()

        # First call, 15 queries (we can do better !)
        with self.assertNumQueries(15):
            self.client.get(url)

        # Second call, 4 queries, it's better
        with self.assertNumQueries(4):
            self.client.get(url)

        # If we paginate, 16 queries (the COUNT add another query)
        with self.assertNumQueries(16):
            self.client.get(url + '?limit=10')

        # If we filter, 15 queries again
        with self.assertNumQueries(15):
            self.client.get(url + '?state__in=op,cl')

        # Another trainer, 5 queries (he don't own any sessions)
        self.login_as_admin()
        with self.assertNumQueries(5):
            self.client.get(url)

    def test_list_session_is_cached_for_trainee(self):
        """Test the cache behavior for the session list, logged as trainee"""

        url = reverse('apiv2:session-list')

        self.login_as_trainee()

        # First call, 15 queries (we can do better !)
        with self.assertNumQueries(15):
            self.client.get(url)

        # Second call, 4 queries, it's better
        with self.assertNumQueries(4):
            self.client.get(url)

        # Another trainee, 4 queries too
        self.client.login(username=self.trainees[1].username, password=DEFAULT_PASSWORD)
        with self.assertNumQueries(4):
            self.client.get(url)

    def test_list_sessions_data_structure(self):
        """Test that the JSON holds the expected data"""

        # we use limit param to trigger LimitOffsetPagination format
        url = '%s?limit=1' % reverse('apiv2:session-list')

        self.login_as_trainer()
        response = self.client.get(url)

        # LimitOffsetPagination metadata
        self.assertIn('next', response.data)
        self.assertIn('previous', response.data)
        self.assertIn('count', response.data)
        self.assertIn('results', response.data)

        # limit=1
        self.assertEqual(len(response.data['results']), 1)

        session = TrainingSession.objects.get(pk=response.data['results'][0]['id'])
        end_date_iso = session.end_date.isoformat()
        start_date_iso = session.start_date.isoformat()
        original_version = session.training.original_version
        expected_data = {
            'id': session.pk,
            # remove +00:00 and add Z, see DRF's code:
            # https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/fields.py#L1079
            'start_date': '%sZ' % start_date_iso[:-6],
            'end_date': '%sZ' % end_date_iso[:-6],
            'is_live': session.is_live,
            'permissions': session.permissions,
            'state': session.state,
            'token': session.token,
            # this is already tested elsewhere, just use serializer output
            'trainer': MTPUserSerializer(session.trainer).data,
            'training': {
                'business_unit': session.training.business_unit.name,
                'description': session.training.description,
                'external_id': session.training.external_id,
                'id': session.training.id,
                'is_assessing': session.training.is_assessing,
                'job_profile': session.training.job_profile.name,
                'name': session.training.name,
                'original_id': original_version.pk if original_version else None,
                'tags': [session.training.get_version_state_display()],
                'header_image': session.training.get_header_image().url,
            },
            'users': [user.pk for user in session.users.all()],
        }

        self.assertDictEqual(response.data['results'][0], expected_data)

    def test_list_sessions_ordering(self):
        """Test the ordering of the route to list sessions."""

        url = reverse('apiv2:session-list')
        self.login_as_trainer()

        # Update the dates to ensure an order. Some dates are a day back in the past.
        # We use `session.start_date` to avoid "End date cannot be earlier than start date" errors

        for i, session in enumerate(TrainingSession.objects.filter(trainer=self.trainer)):
            # use "-" to change the original order
            session.start_date = session.start_date - timedelta(hours=i)
            session.save()

        sessions = TrainingSession.objects.filter(trainer=self.trainer)

        # Use default ordering, which is ``start_date`` (we pass "clear_cache=1" to not use
        # the existing cached response, as the url params are used to generate the cache key)
        response = self.client.get(url + '?clear_cache=1')

        self.assertEqual(
            response.data[0]['id'],
            sessions.first().pk
        )

        # Now, we order by ``-start_date``
        response = self.client.get(url + '?ordering=-start_date&clear_cache=1')

        self.assertEqual(
            response.data[0]['id'],
            sessions.order_by('-start_date').first().pk
        )

    def test_list_sessions_filtering_by_start_date(self):
        """Test the filtering by start_date range on the route to list sessions."""

        # let's set a very specific start_date for our test session
        self.training_session.start_date = datetime(1955, 12, 13, 13, 0, 0, tzinfo=pytz.utc)
        self.training_session.save()

        url = '%(url)s?%(start_date)s&%(end_date)s' % {
            'url': reverse('apiv2:session-list'),
            'start_date': 'start_date_0=%s' % datetime(1955, 12, 13, 12, 0, 0),
            'end_date': 'start_date_1=%s' % datetime(1955, 12, 13, 14, 0, 0),
        }
        self.login_as_trainer()

        response = self.client.get(url)

        # only one session
        self.assertEqual(len(response.data), 1)
        # which is our test session
        self.assertEqual(response.data[0]['id'], self.training_session.pk)

    def test_list_sessions_filtering_by_state(self):
        """Test the filtering by state on the route to list sessions."""

        # closed sessions only url
        url = reverse('apiv2:session-list') + '?state=cl'
        self.login_as_trainer()

        closed_sessions = TrainingSession.objects.filter(
            trainer=self.trainer,
            state=TrainingSession.STATE_CHOICES.CLOSED,
        )

        response = self.client.get(url)

        self.assertEqual(len(response.data), closed_sessions.count())

    def test_list_sessions_filtering_by_permissions(self):
        """Test the filtering by permissions on the route to list sessions."""

        for permissions in (
                TrainingSession.PERMISSION_CHOICES.PUBLIC,
                TrainingSession.PERMISSION_CHOICES.PRIVATE,
            ):

            with self.subTest(filter=filter):

                count = TrainingSession.objects.filter(
                    trainer=self.trainer,
                    permissions=permissions,
                ).count()

                url = reverse('apiv2:session-list') + '?permissions=%s' % permissions

                self.login_as_trainer()

                response = self.client.get(url)

                self.assertEqual(len(response.data), count)

    def test_list_sessions_closed_for_trainees(self):
        """Test the filtering by state as a trainee"""

        # Closed sessions only url
        url = reverse('apiv2:session-list') + '?state=cl'

        self.login_as_trainee()
        response = self.client.get(url)

        # The first trainee participated in one closed session
        self.assertEqual(len(response.data), 1)

        self.client.login(username=self.trainees[1].username, password=DEFAULT_PASSWORD)
        response = self.client.get(url)

        # The second trainee participated in 0 closed session
        self.assertEqual(len(response.data), 0)

    def test_retrieve_session(self):
        """Test the route to retrieve session."""

        url = reverse('apiv2:session-detail', kwargs={
            'pk': self.training_session.pk,
        })

        # The trainer2 is not the owner of the session, he shouldn't be able to access to it
        self.client.login(username=self.trainer2.username, password=DEFAULT_PASSWORD)
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

        # The trainer is the owner of the session, he should be able to access it
        self.login_as_trainer()
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # The trainee is registered in the session, he should be able to access it
        self.login_as_trainee()
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # The trainee2 isn't registered in any session, but we got one session marked as "public"
        # He should be able to access it
        self.client.login(username=self.trainees[1].username, password=DEFAULT_PASSWORD)
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # The token should be set
        self.training_session.refresh_from_db()
        self.assertIsNotNone(self.training_session.token)

    def test_create_session(self):
        """Test the route for create session."""

        url = reverse('apiv2:session-list')

        data = {
            'end_date': '2016-04-03T18:00Z',
            'permissions': 'pu',
            'training_id': self.training.id,
            'start_date': '2016-04-03T09:00Z',
            'state': 'op',
        }

        # A trainee shouldn't be able to create a session
        self.login_as_trainee()
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

        # A trainer should be able to create a session
        self.login_as_trainer()
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # The trainer of this new session should be the current authenticated user
        created_session = TrainingSession.objects.get(id=response.data['id'])
        self.assertEqual(created_session.trainer, self.trainer)

        data.update({'trainer_id': self.trainee.id})
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # The trainer of this new session should be the provided user
        created_session = TrainingSession.objects.get(id=response.data['id'])
        self.assertEqual(created_session.trainer, self.trainee)

    def test_create_session_start_date_end_date(self):
        """Test the validation of start_date and end_date."""

        url = reverse('apiv2:session-list')

        data = {
            'end_date': '2016-04-03T09:00Z',
            'permissions': 'pu',
            'training_id': self.training.id,
            'start_date': '2016-04-03T18:00Z',
            'state': 'op',
        }

        self.login_as_trainer()
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        # start_date cannot be prior to end_date
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

        # check the error message
        response_data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response_data['text'][0], 'End date cannot be earlier than start date')

    def test_edit_session_permission(self):
        """Test permission to edit session."""

        url = reverse('apiv2:session-detail', kwargs={
            'pk': self.training_session.pk
        })

        data = {'state': 'op'}

        # A trainee shouldn't be able to edit a session
        self.login_as_trainee()
        response = self.client.patch(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

        # A trainer who is the owner should be able to edit a session
        self.login_as_trainer()
        response = self.client.patch(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # Now we create a session with an other trainer
        session_new = TrainingSessionFactory(training=self.training)
        url_new = reverse('apiv2:session-detail', kwargs={
            'pk': session_new.id
        })

        # A trainer who is NOT the owner should NOT be able to edit a session
        # As the session queryset is filter by request.user === session.trainer
        # We are still login as default trainer
        response = self.client.patch(url_new, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    @override_settings(INSTANCE_IS_MASTER=True, INSTANCE_IS_SLAVE=False)
    def test_close_session_should_close_the_session_if_instance_is_master(self):
        """Test that calling the close session should close the session"""

        url = reverse('apiv2:session-detail', kwargs={
            'pk': self.training_session.pk
        })
        data = {
            'state': self.training_session.STATE_CHOICES.CLOSED
        }

        # A trainee should not be able to close the session
        self.login_as_trainee()
        response = self.client.patch(url, data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

        # The trainer should be able to close the session
        self.login_as_trainer()
        response = self.client.patch(url, data)

        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # The session is now closed
        self.training_session.refresh_from_db()
        self.assertEqual(self.training_session.state, TrainingSession.STATE_CHOICES.CLOSED)

    def test_oauth_client_can_access_the_session_list(self):
        """Test that an "oauth client" can list the session"""

        url = reverse('apiv2:session-list')

        response = self.oauth_client.get(url)
        self.assertEqual(response.status_code, 200)

        # The oauth client should see all the sessions
        self.assertEqual(len(response.data), 3)


class SessionViewSetActivitiesTestCase(TrainingWithActivitiesTestCase, MTPAPITestCase):
    """Test case for the ``activities`` route on the ``SessionViewSet`` view"""

    def test_session_activities(self):
        """Test that the endpoint should display correctly the list of activities"""

        self.login_as_trainer()
        response = self.client.get(
            reverse('apiv2:session-activities', kwargs={
                'pk': self.training_session.id
            })
        )

        self.assertEqual(response.status_code, 200)

        expected_data = TrainingActivitySerializer(
            TrainingActivity.objects.for_session(self.training_session).available().non_deleted(),
            many=True
        ).data + BrainstormerSerializer(
            Brainstormer.objects.filter(session=self.training_session).non_deleted(),
            many=True
        ).data

        self.assertEqual(
            response.data,
            expected_data
        )


class SessionViewSetAssessmentScoresTestCase(MTPOAuthClientTestCaseMixin,
                                             BaseAssessmentScoresTestCase):
    """Dedicated test class for the assessment_scores method of ``SessionViewSet``

    Notes
    -----
    - We use a dedicated class because we need the ``BaseAssessmentScoresTestCase`` data
    for this one.
    - Views are tested through ``oauth_client`` because it has been designed for a third party
    application (eg. ekimetrics)

    """

    def test_assessment_scores(self):
        """Test the assessment scores detail route

        Notes
        -----
        The data structure returned by this view is tested in
        ``mtp.apps.assessments.apiv2.tests.test_serializers``

        """

        # we need a closed session
        with override_settings(INSTANCE_IS_MASTER=True):
            self.training_session.close()

        url = reverse('apiv2:session-assessment-scores', kwargs={
            'pk': self.training_session.pk,
        })
        response = self.oauth_client.get(url)

        self.assertEqual(response.status_code, 200)

    def test_assessment_scores_with_error_on_open_session(self):
        """Test the assessment scores detail route w/ error on open session"""

        url = reverse('apiv2:session-assessment-scores', kwargs={
            'pk': self.training_session.pk,
        })
        response = self.oauth_client.get(url)

        self.assertEqual(response.status_code, 500)
        self.assertEqual(response.data, {
            'detail': 'Session must be closed.'
        })

    def test_assessment_scores_with_user_not_passed_session(self):
        """Test w/ a user that did not pass the session"""

        # we need a closed session
        with override_settings(INSTANCE_IS_MASTER=True):
            self.training_session.close()

        # verify our test assumption
        self.assertNotIn(self.trainees[1], self.training_session.users.all())

        # add a new user to the session, w/o making him pass the session
        self.training_session.users.add(self.trainees[1])

        url = reverse('apiv2:session-assessment-scores', kwargs={
            'pk': self.training_session.pk,
        })
        response = self.oauth_client.get(url)

        # only one assessment score
        self.assertEqual(len(response.data), 1)
        # and it's not for trainees[1]
        self.assertNotEqual(response.data[0]['user']['username'], self.trainees[1].username)


class TokenViewSetTestCase(MTPOAuthClientTestCaseMixin, MTPAPITestCase):
    """Test case for ``TokenViewSet`` viewset."""

    def test_retrieve_session(self):
        """Test the route for retrieve session by its token."""

        url = reverse('apiv2:token-detail', kwargs={
            'token': self.training_session.get_or_assign_token(),
        })

        self.client.login(username=self.trainees[1].username, password=DEFAULT_PASSWORD)
        response = self.client.get(url)

        # The session id should be returned
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['id'], self.training_session.id)

        # Trainee2 should be registered in session users
        self.assertIn(self.trainees[1], self.training_session.users.all())

    def test_retrieve_session_with_trainer(self):
        """Test that the route work with logged-in as a trainer"""

        url = reverse('apiv2:token-detail', kwargs={
            'token': self.training_session.get_or_assign_token(),
        })

        self.login_as_trainer()
        response = self.client.get(url)

        session_users = self.training_session.users.all()

        # The session id should be returned
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['id'], self.training_session.id)

        # The session users are the same
        self.training_session.refresh_from_db()
        self.assertCountEqual(session_users, self.training_session.users.all())

    def test_retrieve_session_with_oauth_application(self):
        """Test that the route work with logged-in as an oauth application"""

        url = reverse('apiv2:token-detail', kwargs={
            'token': self.training_session.get_or_assign_token(),
        })

        response = self.oauth_client.get(url)

        session_users = self.training_session.users.all()

        # The session id should be returned
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['id'], self.training_session.id)

        # The session users are the same
        self.training_session.refresh_from_db()
        self.assertCountEqual(session_users, self.training_session.users.all())

class TraineeViewSetTestCase(MTPAPITestCase):
    """Test case for ``TraineeViewSet`` viewset."""

    def test_list_trainees(self):
        """Test the route for list trainees."""

        url = reverse(
            'apiv2:session-trainee-list',
            kwargs={
                'parent_lookup_training_sessions': self.training_session.pk,
            }
        )

        # The trainer should see one user in this session
        self.login_as_trainer()
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)

        # The trainee should see one user in this session too
        self.login_as_trainee()
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)

    def test_add_trainee(self):
        """Test the route for add a trainee in session."""

        url = reverse(
            'apiv2:session-trainee-list',
            kwargs={
                'parent_lookup_training_sessions': self.training_session.pk,
            }
        )

        data = {
            'id': self.trainees[1].id,
        }

        # A trainee should be able to add himself in the session (with data)
        self.client.login(username=self.trainees[1].username, password=DEFAULT_PASSWORD)
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # The session should have trainee1 in these users
        users = self.training_session.users.all()
        self.assertEqual(len(users), 2)
        self.assertIn(self.trainees[1], users)

        # A trainee should be able to add himself in the session (without data)
        self.client.login(username=self.trainees[2].username, password=DEFAULT_PASSWORD)
        response = self.client.post(url, content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # The session should have trainee2 in these users
        users = self.training_session.users.all()
        self.assertEqual(len(users), 3)
        self.assertIn(self.trainees[2], users)

        # We delete trainee1 in session users
        self.training_session.users.remove(self.trainees[1])
        users = self.training_session.users.all()
        self.assertEqual(len(users), 2)
        self.assertNotIn(self.trainees[1], users)

        # A trainer should be able to add a trainee in the session
        self.login_as_trainer()
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

        # The session should have trainee1 in these users
        users = self.training_session.users.all()
        self.assertEqual(len(users), 3)
        self.assertIn(self.trainees[1], users)

    def test_remove_trainee(self):
        """Test the route for remove a trainee in session."""

        url = reverse(
            'apiv2:session-trainee-detail',
            kwargs={
                'parent_lookup_training_sessions': self.training_session.pk,
                'pk': self.trainees[1].pk,
            }
        )

        # We add trainee1 in sessions users
        self.training_session.users.add(self.trainees[1])
        users = self.training_session.users.all()
        self.assertIn(self.trainees[1], users)

        # Only trainer of the session should be able to remove a trainee
        self.client.login(username=self.trainees[1].username, password=DEFAULT_PASSWORD)
        response = self.client.delete(url, content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

        self.login_as_admin()
        response = self.client.delete(url, content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

        self.login_as_trainer()
        response = self.client.delete(url, content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

        # The session shouldn't have trainee1 in these users
        users = self.training_session.users.all()
        self.assertNotIn(self.trainees[1], users)

    def test_list_notes_url_should_work(self):
        """Test that the notes list route return correct data"""

        notes_url = reverse(
            'apiv2:session-trainee-notes',
            kwargs={
                'parent_lookup_training_sessions': self.training_session.pk,
                'pk': self.trainee.pk,
            }
        )

        self.login_as_trainee()
        response = self.client.get(notes_url)

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data, [])

        # Create a new ``Note``
        NoteFactory(
            session=self.training_session,
            user=self.trainee
        )

        response = self.client.get(notes_url)

        # We got our new ``Note``
        self.assertEqual(
            len(response.data), 1
        )

    def test_notes_download_url_should_work(self):
        """Test that the free notes route return correct data"""

        freenotes_url = reverse(
            'apiv2:session-trainee-freenotes',
            kwargs={
                'parent_lookup_training_sessions': self.training_session.pk,
                'pk': self.trainee.pk,
            }
        )

        self.login_as_trainee()
        response = self.client.get(freenotes_url)

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data, 'No notes taken')

        # Create a new ``Note``
        note_1 = NoteFactory(
            session=self.training_session,
            user=self.trainee
        )

        response = self.client.get(freenotes_url)

        # We got our new ``Note``
        self.assertEqual(response.data, note_1.text)

        # Create another ``Note``
        note_2 = NoteFactory(
            session=self.training_session,
            user=self.trainee
        )

        response = self.client.get(freenotes_url)

        # We got our two ``Note``s
        self.assertEqual(
            response.data,
            '\n'.join(
                [note_2.text, note_1.text]
            )
        )


class TraineeViewSetRecommendationsTestCase(MTPOAuthClientTestCaseMixin,
                                            BaseAssessmentScoresTestCase):
    """Dedicated test class for the recommendations method of ``TraineeViewSet``

    Notes
    -----
    - We use a dedicated class because we need the ``BaseAssessmentScoresTestCase`` data
    for this one.
    - Views are tested through ``oauth_client`` because it has been designed for a third party
    application (eg. ekimetrics)

    """

    def test_recommendations(self):
        """Test the recommendations detail route

        Notes
        -----
        The data structure is tested in ``mtp.apps.assessments.apiv2.tests.test_serializers``

        """

        # we need a closed session
        with override_settings(INSTANCE_IS_MASTER=True):
            self.training_session.close()

        url = reverse('apiv2:session-trainee-recommendations', kwargs={
            'parent_lookup_training_sessions': self.training_session.pk,
            'pk': self.trainee.pk,
        })
        response = self.oauth_client.get(url)

        self.assertEqual(response.status_code, 200)

    def test_recommendations_open_session_fails(self):
        """Test the recommendations detail route fails on an open session"""

        url = reverse('apiv2:session-trainee-recommendations', kwargs={
            'parent_lookup_training_sessions': self.training_session.pk,
            'pk': self.trainee.pk,
        })
        response = self.oauth_client.get(url)

        self.assertEqual(response.status_code, 500)
        self.assertEqual(response.data, {
            'detail': 'Session must be closed.'
        })


class SessionUsersStatusViewTestCase(MTPAPITestCase):
    """Test case for the ``SessionUsersStatusView`` view"""

    @classmethod
    def setUpTestData(cls):
        """Store the url to call"""

        super().setUpTestData()

        cls.url = reverse('apiv2:session-users-status', kwargs={
            'session_uuid': cls.training_session.pk,
            'user_uuid': cls.trainee.pk
        })

        cls.redis_instance, __ = RedisTrainingSession.get_or_connect(pk=cls.training_session.pk)

    def test_patch_view_should_set_the_user_status(self):
        """Test that calling the view should update the current user status"""

        with patch.object(RedisTrainingSession, 'set_user_online') as mocked:
            response = self.client.patch(self.url)

        mocked.assert_called_once_with(self.trainee.pk)
        self.assertEqual(response.data, {})  # Empty response

    def test_get_view_should_output_the_users_statuses(self):
        """Test that the view should output the users'statuses"""

        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)

        # At first, we got 0 user online
        self.assertEqual(
            response.data,
            []
        )

        # Call the endpoint using post
        self.client.patch(self.url)

        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)

        # Now, our user is considered as "online"
        self.assertCountEqual(
            response.data,
            [
                {
                    "user_id": self.trainee.pk,
                    "online": True
                }
            ]
        )

        # Call the endpoint using post with another user
        url = reverse('apiv2:session-users-status', kwargs={
            'session_uuid': self.training_session.pk,
            'user_uuid': self.trainees[1].pk
        })
        self.client.patch(url)

        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)

        # We got two users online
        self.assertCountEqual(
            response.data,
            [
                {
                    "user_id": self.trainee.pk,
                    "online": True
                },
                {
                    "user_id": self.trainees[1].pk,
                    "online": True
                }
            ]
        )

        # Make the last ping of the trainee to be > 15 seconds ago
        self.redis_instance.last_pings.zadd(get_current_timestamp() - 15 * 100000, self.trainee.pk)
        response = self.client.get(url)

        # The trainee should now appear "offline"
        self.assertCountEqual(
            response.data,
            [
                {
                    "user_id": self.trainee.pk,
                    "online": False
                },
                {
                    "user_id": self.trainees[1].pk,
                    "online": True
                }
            ]
        )

    def test_view_should_only_output_zero_query(self):
        """Test that the view should only output 0 BDD query

        The 2 queries are from the savepoints.

        """

        with self.assertNumQueries(2):
            self.client.get(self.url)

        with self.assertNumQueries(2):
            self.client.patch(self.url)
