"""This module holds the test cases for the views of the ``sync`` app"""

import json
from io import BytesIO
import os
import uuid
from zipfile import ZipFile, ZIP_LZMA

from mock import patch

from django.conf import settings
from django.core import serializers
from django.core.urlresolvers import reverse
from django.test import RequestFactory
from django.utils.timezone import now

from waffle.models import Switch

import mtp

from mtp.apps.accounts.factories import (
    GroupFactory, GroupRoleFactory,
    MTPUserFactory
)
from mtp.apps.accounts.models import GroupRole, MTPUser
from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.trainings.models import TrainingSession

from .test_cases import BaseSyncUploadTestCase, DownloadWaffleBaseTestCase

from ..models import Update
from ..views import DownloadWaffle


class DownloadWaffleTestCase(DownloadWaffleBaseTestCase):
    """Test the ``DownloadWaffle`` view to get waffle data to sync."""

    def check_data(self, data):
        """Helper to check that the data matches the expecting one."""

        # We should have a main key 'waffle' (the app)
        self.assertSetEqual(set(data.keys()), {'waffle'})
        # We should have a sub-key 'switch' (the model)
        self.assertSetEqual(set(data['waffle'].keys()), {'switch'})
        # We should have all our models
        json_switches = serializers.serialize('json', Switch.objects.all())
        self.assertSetEqual(set(data['waffle']['switch']), set(json_switches))

    def test_get_data(self):
        """Test the ``get_data`` method of the ``DownloadWaffle`` view."""

        view = DownloadWaffle()
        data = view.get_data()
        self.check_data(data)

    def test_render_to_response(self):
        """Test the ``render_to_response`` method of the ``DownloadWaffle`` view."""

        switches = Switch.objects.all()
        json_switches = serializers.serialize('json', switches)
        data = {'waffle': {'switch': json_switches}}

        view = DownloadWaffle()
        response = view.render_to_response(data)

        data = json.loads(response.content.decode('utf-8'))
        self.check_data(data)

    def test_post_needs_post_data(self):
        """Test that the ``post`` method of the ``DownloadWaffle`` correctly handles post data."""

        request = RequestFactory().post('/fake/path/not/used')
        response = DownloadWaffle.as_view()(request)
        self.assertEqual(response.status_code, 403)

        request = RequestFactory().post('/fake/path/not/used', {'foo': 1})
        response = DownloadWaffle.as_view()(request)
        self.assertEqual(response.status_code, 403)

        request = RequestFactory().post('/fake/path/not/used', {'download-waffle': 1})
        response = DownloadWaffle.as_view()(request)
        self.assertEqual(response.status_code, 200)

    def test_post_response(self):
        """Test the response of the ``post`` method of the ``DownloadWaffle`` view."""

        request = RequestFactory().post('/fake/path/not/used', {'download-waffle': 1})
        response = DownloadWaffle.as_view()(request)
        data = json.loads(response.content.decode('utf-8'))
        self.check_data(data)


class UploadDataArchiveTestCase(BaseSyncUploadTestCase, MTPTestCase):
    """Test case to test the ``UploadDataArchive`` class based view"""

    def test_call_view_without_mac_address_should_fail(self):
        """Call the view without the mac address should fail"""

        response = self.client.post(self.url)
        self.assertEqual(response.status_code, 400)

        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(json_response['message'], 'No mac or server version provided')

    def test_call_view_with_a_different_server_version_should_fail(self):
        """Call the view with a bad server version should fail"""

        response = self.client.post(self.url, {'mac': 666, 'version': 'not.a.good.version'})
        self.assertEqual(response.status_code, 400)

        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(json_response['message'], 'Server version mismatch')

    def test_call_view_without_a_correct_zipfile_should_fail(self):
        """Call the view without a correct ZipFile object should fail"""

        response = self.client.post(
            self.url,
            {
                'mac': 666,
                'version': mtp.__version__,
                'data_archive': 'not.a.good.data.archive'
            }
        )
        self.assertEqual(response.status_code, 400)

        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(json_response['message'], 'Bad Zip file')

    def test_call_view_without_a_correct_json_data_should_fail(self):
        """Call the view without a correct data.json file should fail"""

        zip_buffer = self.build_zip_file('not a valid json string')

        params = {
            'mac': "666",
            'version': mtp.__version__,
            'data_archive': zip_buffer
        }

        response = self.client.post(self.url, params)
        self.assertEqual(response.status_code, 400)

        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(json_response['message'], 'data.json not found or incorrect json')

    def test_call_view_with_bad_update_data_should_fail(self):
        """Call the view with a "bad update" data should fail

        A "bad update" example is data containing some foreign key which does not exists on the
        "master" instance.
        For instance, a group role referencing a user which does not exists should raise an
        ``DoesNotExists`` during the deserialization on the "master" instance.

        """

        group_role = GroupRole(
            user_id=uuid.uuid4(),
            group=self.trainer_group,
            role=GroupRole.ROLES.MANAGER,
            pk=uuid.uuid4()
        )

        response = self.post_objects([group_role])
        self.assertEqual(response.status_code, 400)

        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(
            json_response['message'],
            'Update failed, the exception message is '
            '"MTPUser matching query does not exist."'
        )

    def test_call_view_with_valid_update_data_should_work(self):
        """Call the view with valid update data should work"""

        group_role = GroupRole(
            user_id=self.trainee.pk,
            group=self.trainer_group,
            role=GroupRole.ROLES.MANAGER,
            pk=uuid.uuid4()
        )

        response = self.post_objects([group_role])
        self.assertEqual(response.status_code, 200)

        # The "master" instance should answer with the serialized last update
        self.assertEqual(
            response.content.decode('utf-8'),
            serializers.serialize('json', (Update.objects.first(),))
        )


class UsernameIntegrityTestCase(BaseSyncUploadTestCase, MTPTestCase):
    """Test case to test the management of new users with existing usernames during sync.

    In these tests we simulate the update/creation of users on the slave by creating ``MTPUser``
    objects, with primary keys, but not saving them in the database. They are created only to be
    serialized and passed to the ``upload`` view.

    """

    @classmethod
    def setUpClass(cls):
        """Prepare the url to be called by the request client"""

        super().setUpClass()

        cls.USER_SIMPLE_FIELDS = [field.name
                                  for field
                                  in MTPUser._meta.fields
                                  if field.concrete and not field.is_relation]

        cls.USER_FK_FIELDS = [field.name
                              for field
                              in MTPUser._meta.fields
                              if field.concrete and field.is_relation and not field.many_to_many]

    @classmethod
    def setUpTestData(cls):
        """Create some users to test."""

        super().setUpTestData()

        cls.user1 = MTPUserFactory(username='sync-test-1', first_name='sync test 1')
        cls.user2 = MTPUserFactory(username='sync-test-2', first_name='sync test 2')

    @classmethod
    def clone_user(cls, source, include_fks=False, new_values=None):
        """Create a MTPUser, not saved in the database, taking values from the given one.

        Parameters
        ----------
        source : MTPUser
            The user from which to take values to create the new user
        include_fks : bool
            If ``True``, not only "simple" fields will be copied to the new user, but also the
            ids of the foreign keys. Default to ``False``.
        new_values : dict
            If set, a dict with ``{field_name: value}`` for values to override on the new user.

        Returns
        -------
        MTPUser
            A new MTPUser object, not saved in database.

        """

        user = MTPUser()

        fields_to_clone = cls.USER_SIMPLE_FIELDS
        if include_fks:
            fields_to_clone += cls.USER_FK_FIELDS

        for field_name in fields_to_clone:
            setattr(user, field_name, getattr(source, field_name))

        if new_values:
            for field_name, value in new_values.items():
                setattr(user, field_name, value)

        # We simulate a slave, simulate what's done in the ``save`` method of ``Syncable`` model.
        user.update_locally = True

        return user

    def test_updating_a_user(self):
        """Test updating an existing user."""

        # Simulate an updated user on the slave
        user1 = self.clone_user(self.user1, True, {
            'first_name': 'sync test 1 foo',
        })

        # Sync the data
        self.post_objects([user1])

        # Now the user should be updated in database
        user1 = MTPUser.objects.get(pk=self.user1.pk)
        self.assertEqual(user1.first_name, 'sync test 1 foo')

    def test_adding_a_user(self):
        """Test adding a new user with a non-existing username."""

        # Simulate a new user on the slave with a new username
        user3_pk = str(uuid.uuid4())
        user3 = self.clone_user(self.user1, False, {
            'pk': user3_pk,
            'username': 'sync test 3',
            'first_name': 'sync test 3',
            'last_name': 'sync test 3',
            'email': 'sync-test-3@magency.fr',
        })

        # Sync the data
        self.post_objects([user3])

        # Now the user should be present in database
        users = list(MTPUser.objects.filter(first_name='sync test 3'))
        self.assertEqual(len(users), 1)
        self.assertEqual(users[0].pk, user3_pk)

    def test_adding_an_existing_user(self):
        """Test adding a new user with an existing username."""

        # Simulate a new user on the slave with an existing username
        user4_pk = str(uuid.uuid4())
        user4 = self.clone_user(self.user1, False, {
            'pk': user4_pk,
            'username': 'sync-test-1',
            'first_name': 'sync test 4',
            'last_name': 'sync test 4',
            'email': 'sync-test-4@magency.fr',
        })

        # Sync the data
        self.post_objects([user4])

        # Now the user should be present in database
        users = list(MTPUser.objects.filter(first_name='sync test 4'))
        self.assertEqual(len(users), 1)

        # But it should not have the username we gave him
        self.assertNotEqual(users[0].username, 'sync-test-1')
        self.assertEqual(users[0].old_username, 'sync-test-1')
        self.assertEqual(users[0].pk, user4_pk)

        # And the original one should still be there
        self.assertEqual(MTPUser.objects.filter(username='sync-test-1').count(), 1)

    def test_many_users_upload(self):
        """Test updating and adding users."""

        # Update a user locally, not in database
        # Simulate an updated user on the slave
        user1 = self.clone_user(self.user1, True, {
            'first_name': 'sync test 1 foo',
        })

        # Simulate a new user on the slave with a new username
        user3_pk = str(uuid.uuid4())
        user3 = self.clone_user(self.user1, False, {
            'pk': user3_pk,
            'username': 'sync test 3',
            'first_name': 'sync test 3',
            'last_name': 'sync test 3',
            'email': 'sync-test-3@magency.fr',
        })

        # Simulate a new user on the slave with an existing username
        user4_pk = str(uuid.uuid4())
        user4 = self.clone_user(self.user1, False, {
            'pk': user4_pk,
            'username': 'sync-test-1',
            'first_name': 'sync test 4',
            'last_name': 'sync test 4',
            'email': 'sync-test-4@magency.fr',
        })

        # Sync the data
        self.post_objects([user1, user3, user4])

        # The updated user should be updated in database
        user1 = MTPUser.objects.get(pk=self.user1.pk)
        self.assertEqual(user1.first_name, 'sync test 1 foo')

        # The first added user should be present in database
        users = list(MTPUser.objects.filter(first_name='sync test 3'))
        self.assertEqual(len(users), 1)
        self.assertEqual(users[0].pk, user3_pk)

        # The second added user should be present in database
        users = list(MTPUser.objects.filter(first_name='sync test 4'))
        self.assertEqual(len(users), 1)

        # But it should not have the username we gave him
        self.assertNotEqual(users[0].username, 'sync-test-1')
        self.assertEqual(users[0].old_username, 'sync-test-1')
        self.assertEqual(users[0].pk, user4_pk)

        # And the original one should still be there
        self.assertEqual(MTPUser.objects.filter(username='sync-test-1').count(), 1)


class GroupRoleTestCase(BaseSyncUploadTestCase, MTPTestCase):
    """Test case for the sync behaviour of the ``GroupRole`` model"""

    @classmethod
    def setUpTestData(cls):
        """Create some users with some roles to test."""

        super().setUpTestData()

        cls.user = MTPUserFactory()
        cls.group = GroupFactory()
        cls.group_role = GroupRoleFactory(
            user=cls.user,
            group=cls.group
        )

        cls.group2 = GroupFactory()

    def test_updating_a_group_role(self):
        """Test updating an existing group role"""

        # Updated group role on the SLAVE
        group_role = GroupRole(
            user=self.user,
            group=self.group2,
            role=GroupRole.ROLES.MANAGER,
            pk=self.group_role.pk
        )

        self.post_objects([group_role])

        group_role = GroupRole.objects.get(pk=self.group_role.pk)

        # The group role should be updated
        self.assertEqual(
            group_role.role,
            GroupRole.ROLES.MANAGER
        )

    def test_creating_a_group_role(self):
        """Test creating a new group role"""

        # New group role on the SLAVE
        group_role = GroupRole(
            user=self.user,
            group=self.group2,
            role=GroupRole.ROLES.MANAGER,
            pk=uuid.uuid4()
        )

        new_pk = str(group_role.pk)

        # Sync the data
        self.post_objects([group_role])

        group_roles = GroupRole.objects.filter(user=self.user, group=self.group2)

        # Now the group role should be present in database
        self.assertEqual(
            len(group_roles),
            1
        )

        # Check the pk
        self.assertEqual(
            group_roles[0].pk,
            new_pk
        )

    def test_creating_a_group_role_with_same_unique_keys(self):
        """Test creating a new group role with existing user/group"""

        # The group role ``role`` is the default one
        self.assertEqual(
            GroupRole.objects.get(user=self.user, group=self.group).role,
            GroupRole.ROLES.MEMBER
        )

        # Create a new group role with the same group/user but a different pk
        # (Can't use a factory since we got the unique keys constraint)
        group_role = GroupRole(
            user=self.user,
            group=self.group,
            role=GroupRole.ROLES.MANAGER,
            pk=uuid.uuid4()
        )

        new_pk = str(group_role.pk)

        # Sync the data
        self.post_objects([group_role])

        # Reload the group_role
        group_role = GroupRole.objects.get(user=self.user, group=self.group)

        # The group role ``role`` is correctly saved
        self.assertEqual(
            group_role.role,
            GroupRole.ROLES.MANAGER
        )

        # The pk is not the one of the new group role
        self.assertNotEqual(
            group_role.pk,
            new_pk
        )


class TrainingSessionUploadTestCase(BaseSyncUploadTestCase, MTPTestCase):
    """Test case for the sync behaviour of the ``TrainingSession`` model"""

    def test_uploading_an_open_training_session(self):
        """Test uploading an open training session"""

        # New training session on the SLAVE
        training_session = TrainingSession(
            training=self.training,
            trainer=self.trainer,
            pk=uuid.uuid4(),
            state=TrainingSession.STATE_CHOICES.OPEN,
            start_date=now(),
            end_date=now()
        )

        # Sync the data
        with patch.object(TrainingSession, 'send_end_mail') as mocked_mail:
            with patch('mtp.apps.trainings.tasks.compute_assessment_scores.delay') as mocked_tasks:
                self.post_objects([training_session])

                # The send mail was not called, neither than the tasks
                mocked_mail.assert_not_called()
                mocked_tasks.assert_not_called()

    def test_uploading_a_closed_non_assessing_training_session(self):
        """Test uploading a closed non-assessing training session"""

        # New training session on the SLAVE
        training_session = TrainingSession(
            training=self.training,
            trainer=self.trainer,
            pk=uuid.uuid4(),
            state=TrainingSession.STATE_CHOICES.CLOSED,
            start_date=now(),
            end_date=now()
        )

        # Sync the data
        with patch.object(TrainingSession, 'send_end_mail') as mocked_mail:
            with patch('mtp.apps.trainings.tasks.compute_assessment_scores.delay') as mocked_tasks:
                self.post_objects([training_session])

                # The send mail was called, but not the task, since the session is not assessing
                mocked_mail.assert_called_with()
                mocked_tasks.assert_not_called()

    def test_uploading_a_closed_assessing_training_session(self):
        """Test uploading a closed assessing training session"""

        # New training session on the SLAVE
        training_session = TrainingSession(
            training=self.training,
            trainer=self.trainer,
            pk=uuid.uuid4(),
            state=TrainingSession.STATE_CHOICES.CLOSED,
            start_date=now(),
            end_date=now()
        )

        training_session_pk = str(training_session.pk)

        # Sync the data
        with patch.object(TrainingSession, 'send_end_mail') as mocked_mail, \
                patch('mtp.apps.trainings.tasks.compute_assessment_scores.delay') as mocked_tasks, \
                patch.object(TrainingSession, 'is_assessing', return_value=True):
            self.post_objects([training_session])

            # The send mail was called, but not the task, since the session is not assessing
            mocked_mail.assert_called_with()
            mocked_tasks.assert_called_with(training_session_pk, force_recompute=True)


class UploadMediaArchiveTestCase(MTPTestCase):
    """Test case for the ``UploadMediaArchive`` view"""

    files = ['test.txt', 'test2.txt', 'test-path/test3.txt']

    # will hold the files to upload (they will be uploaded directly in MEDIA_ROOT)
    tmp_directory = os.path.join(settings.MEDIA_ROOT, 'test', 'sync-upload-tmp')


    @classmethod
    def setUpClass(cls):
        """Store the url of the view and create an archive to upload"""

        super().setUpClass()

        cls.upload_archive_url = reverse('sync:upload_media_archive')

    @classmethod
    def tearDownClass(cls):
        """Remove the created files"""

        super().tearDownClass()
        cls.clean_media()

    def check_files_exists(self, files):
        """Helper method to check if the given files exists on the file system

        Parameters
        ----------
        files: list(str)
            A list of file path to test if they exists

        """

        for file in files:
            self.assertTrue(
                os.path.exists(os.path.join(settings.MEDIA_ROOT, file))
            )

    def upload_files(self, files):
        """Helper method to create an archive and upload it to the endpoint

        Parameters
        ----------
        files: list(str)
            A list of file path to add to the archive

        Returns
        -------
        Response instance

        """

        zip_buffer = BytesIO()
        zip_file = ZipFile(zip_buffer, 'w', ZIP_LZMA)
        zip_file.setpassword(bytes(settings.SYNC_ZIP_PASSWORD, encoding='utf-8'))

        for file in files:
            zip_file.write(
                filename=os.path.join(self.tmp_directory, file),
                arcname=os.path.relpath(file, self.tmp_directory)
            )

        zip_file.close()
        zip_buffer.seek(0)

        return self.client.post(
            self.upload_archive_url,
            {'media_archive': zip_buffer},
            format='multipart'
        )

    def test_upload_files_should_work(self):
        """Test that uploading files should work"""

        local_files = [os.path.join(self.tmp_directory, file) for file in self.files]

        # Create the files
        for file in local_files:
            os.makedirs(os.path.dirname(file), exist_ok=True)
            open(file, 'a').close()

        response = self.upload_files(local_files)
        self.assertEqual(response.status_code, 200)

        # The files should exists
        self.check_files_exists(self.files)

    def test_upload_nothing_should_fail(self):
        """Test that uploading nothing should fail"""

        response = self.client.post(self.upload_archive_url)

        self.assertEqual(response.status_code, 400)
        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(json_response['message'], 'Bad Zip file')

    def test_uploading_bad_zip_file_should_fail(self):
        """Test that uploading a bad zip file should fail"""

        zip_buffer = BytesIO()

        response = self.client.post(
            self.upload_archive_url,
            {'media_archive': zip_buffer},
            format='multipart'
        )

        self.assertEqual(response.status_code, 400)
        json_response = json.loads(response.content.decode('utf-8'))
        self.assertEqual(json_response['message'], 'Bad Zip file')
