"""This module contains the tests for the ``sync`` app"""

import json

from datetime import datetime, timedelta
from io import StringIO
from mock import patch
from operator import itemgetter
import uuid

import responses

from django.contrib.auth.models import Group as DjangoGroup, Permission
from django.conf import settings
from django.core import serializers
from django.core.management import call_command
from django.core.serializers.json import DjangoJSONEncoder
from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase
from django.test.utils import override_settings
from django.utils.timezone import now, utc

from waffle.models import Switch

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

from mtp.apps.sync.models import Syncable
from mtp.discrete_apps.models_dependency.manager import DependencyManager

from .hooks import (
    _hooks,
    pre_sync_hook,
    post_sync_hook,
    run_pre_sync_hooks,
    run_post_sync_hooks,
)

from .views import DownloadWaffle


class HooksTestCase(TestCase):
    """Test case to test the ``sync.hooks`` module."""

    def setUp(self):
        """Clear registered hooks before each test."""

        super().setUp()
        _hooks.clear()

    @override_settings(INSTANCE_IS_MASTER=True, INSTANCE_IS_SLAVE=False)
    def test_pre_sync_for_master(self):
        """Test the ``pre_sync_hook`` decorator on a master instance."""

        result = []

        # This function should be run only on master.
        @pre_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_pre_sync_on_master():
            result.append('pre master')

        # This function should be run only on slave.
        @pre_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_pre_sync_on_slave():
            result.append('pre slave')

        # This function should be run on all servers.
        @pre_sync_hook
        def function_to_pre_sync_on_all():
            result.append('pre all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_SLAVE``.
        self.assertEqual(list(_hooks.keys()), ['pre'])
        self.assertListEqual([f.__name__ for f in _hooks['pre']],
            ['function_to_pre_sync_on_master', 'function_to_pre_sync_on_all'])

        # Running the hook should execute both activated functions.
        run_pre_sync_hooks()
        self.assertListEqual(result, ['pre master', 'pre all'])

    @override_settings(INSTANCE_IS_MASTER=False, INSTANCE_IS_SLAVE=True)
    def test_pre_sync_for_slave(self):

        result = []

        # This function should be run only on master.
        @pre_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_pre_sync_on_master():
            result.append('pre master')

        # This function should be run only on slave.
        @pre_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_pre_sync_on_slave():
            result.append('pre slave')

        # This function should be run on all servers.
        @pre_sync_hook
        def function_to_pre_sync_on_all():
            result.append('pre all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_MASTER``.
        self.assertEqual(list(_hooks.keys()), ['pre'])
        self.assertListEqual([f.__name__ for f in _hooks['pre']],
            ['function_to_pre_sync_on_slave', 'function_to_pre_sync_on_all'])

        # Running the hook should execute both activated functions.
        run_pre_sync_hooks()
        self.assertListEqual(result, ['pre slave', 'pre all'])

    @override_settings(INSTANCE_IS_MASTER=True, INSTANCE_IS_SLAVE=False)
    def test_post_sync_for_master(self):

        result = []

        # This function should be run only on master.
        @post_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_post_sync_on_master():
            result.append('post master')

        # This function should be run only on slave.
        @post_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_post_sync_on_slave():
            result.append('post slave')

        # This function should be run on all servers.
        @post_sync_hook
        def function_to_post_sync_on_all():
            result.append('post all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_SLAVE``.
        self.assertEqual(list(_hooks.keys()), ['post'])
        self.assertListEqual([f.__name__ for f in _hooks['post']],
            ['function_to_post_sync_on_master', 'function_to_post_sync_on_all'])

        # Running the hook should execute both activated functions.
        run_post_sync_hooks()
        self.assertListEqual(result, ['post master', 'post all'])

    @override_settings(INSTANCE_IS_MASTER=False, INSTANCE_IS_SLAVE=True)
    def test_post_sync_for_slave(self):

        result = []

        # This function should be run only on master.
        @post_sync_hook(settings.INSTANCE_IS_MASTER)
        def function_to_post_sync_on_master():
            result.append('post master')

        # This function should be run only on slave.
        @post_sync_hook(settings.INSTANCE_IS_SLAVE)
        def function_to_post_sync_on_slave():
            result.append('post slave')

        # This function should be run on all servers.
        @post_sync_hook
        def function_to_post_sync_on_all():
            result.append('post all')

        # We should only have ``pre`` hooks, and not the one declared with ``INSTANCE_IS_MASTER``.
        self.assertEqual(list(_hooks.keys()), ['post'])
        self.assertListEqual([f.__name__ for f in _hooks['post']],
            ['function_to_post_sync_on_slave', 'function_to_post_sync_on_all'])

        # Running the hook should execute both activated functions.
        run_post_sync_hooks()
        self.assertListEqual(result, ['post slave', 'post all'])

    def test_hook_decorator_without_argument_should_succed(self):
        """Test that the decorators could be called with no arguments."""

        # It should work with no parentheses.
        @pre_sync_hook
        def function1():
            pass

        # It should work with parentheses and no arguments.
        @pre_sync_hook()
        def function2():
            pass

        # Check that we have both methods registered.
        self.assertEqual(list(_hooks.keys()), ['pre'])
        self.assertListEqual([f.__name__ for f in _hooks['pre']], ['function1', 'function2'])

    def test_hook_decorator_with_named_argument_should_succed(self):
        """Test that the decorators could be called with named argument."""

        # It should work with using the default value.
        @post_sync_hook(activated=True)
        def function1():
            pass

        # It should work with using the non-default value (but not registered).
        @post_sync_hook(activated=False)
        def function2():
            pass

        # Check that we have only the hook with ``activated=True``.
        self.assertEqual(list(_hooks.keys()), ['post'])
        self.assertListEqual([f.__name__ for f in _hooks['post']], ['function1'])

    def test_hook_decorator_on_function_with_non_default_arguments_should_fail(self):
        """Test that a function with arguments without default values are rejected."""

        with self.assertRaises(ValueError) as context:
            @pre_sync_hook
            def function1(a):
                pass

        self.assertIn('default value', str(context.exception))

        with self.assertRaises(ValueError) as context:
            @pre_sync_hook
            def function2(a, b=2):
                pass

        self.assertIn('default value', str(context.exception))

    def test_hook_decorator_on_function_with_default_arguments_should_succeed(self):
        """Test that a function with arguments with default values are accepted."""

        result = []

        # Check with only one argument with default value.
        @pre_sync_hook
        def function1(a=1):
            result.append('called with %s' % a)

        # Check with more than one argument with default values.
        @pre_sync_hook
        def function2(a=1, b=2):
            result.append('called with %s, %s' % (a, b))

        # Check that both our methods are correctly called with their arguments.
        run_pre_sync_hooks()
        self.assertListEqual(result, ['called with 1', 'called with 1, 2'])

    def test_hook_decorator_work_only_with_simple_functions(self):
        """Test that the decorator only works with normal functions."""

        # Check instance methods.
        with self.assertRaises(ValueError) as context:
            class Klass1:
                @pre_sync_hook(True)
                def method(self):
                    pass

        # We cannot check that it's an instance method but we have a failure because
        # of the ``self`` argument
        self.assertIn('default value', str(context.exception))

        # Check class methods.
        with self.assertRaises(ValueError) as context:
            class Klass2:
                @pre_sync_hook(True)
                @classmethod
                def method(cls):
                    pass

        self.assertIn('simple functions', str(context.exception))

        # Check static methods.
        with self.assertRaises(ValueError) as context:
            class Klass3:
                @pre_sync_hook(True)
                @staticmethod
                def method():
                    pass

        self.assertIn('simple functions', str(context.exception))


class DownloadWaffleBaseTestCase(TestCase):
    """Base test case for testing downloading waffle. Creates two switches.

    Attributes
    ----------
    switch1 : Switch
        A switch object name 'test1', NOT active.
    switch2 : Switch
        A switch object name 'test2', active.

    """

    @classmethod
    def setUpTestData(cls):
        """Create sample switches."""

        super().setUpTestData()

        cls.switch1 = Switch.objects.create(
            name='test1',
            active=False,
            created=datetime(2015, 6, 2, 12, 12, 12, tzinfo=utc),
            modified=datetime(2015, 6, 3, 13, 13, 13, tzinfo=utc),
        )
        cls.switch2 = Switch.objects.create(
            name='test2',
            active=True,
            created=datetime(2015, 6, 4, 14, 14, 14, tzinfo=utc),
            modified=datetime(2015, 6, 5, 15, 15, 15, tzinfo=utc),
        )


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 UpdateWaffleManagementCommantTestCase(DownloadWaffleBaseTestCase):
    """Test the ``update_waffle_from_master`` management command.

    Attributes
    ----------
    download_waffle_url : string
        The url to download waffle data on the master server.
    master_data : dict
        A dict that mimics the format returned by the ``DownloadWaffle`` view.
        Used to mock the call to the master endpoint.

    Notes
    -----
    As this command is doing a request on a master server, we mock the request to avoid
    the need to have a test server.

    """

    @classmethod
    def setUpTestData(cls):
        """Prepare the url to download waffle and mock data."""

        super().setUpTestData()
        cls.download_waffle_url = settings.MASTER_INSTANCE_URL + reverse('sync:download_waffle')

        json_switches = serializers.serialize('json', [cls.switch1, cls.switch2])
        cls.master_data = {'waffle': {'switch': json_switches}}

    def call_command(self):
        """Call the command faking the response from master and return the output.

        Returns
        -------
        str
            The output of the call to the ``update_waffle_from_master`` management command.

        """

        responses.add(
            responses.POST, self.download_waffle_url,
            body=json.dumps(self.master_data, cls=DjangoJSONEncoder),
            status=200,
        )

        out = StringIO()
        call_command('update_waffle_from_master', stdout=out)
        return out.getvalue()

    def check_output(self, output, expected_lines):
        """Check that the output contains the same lines as the expected ones.

        Parameters
        ----------
        output : str
            The string to test that will be stripped and splitted into lines.
        expected_lines : list
            The list of lines we expect.

        """
        output_lines = output.strip().split('\n')
        self.assertListEqual(output_lines, expected_lines)

    def check_switches(self):
        """Check that the switches are the same than the ones created in ``setUpTestData``.

        The switches created in ``setUpTestData`` are used

        """

        expected_switches = [
            {'name': 'test1', 'active': False},
            {'name': 'test2', 'active': True}
        ]

        existing_switches = sorted([
            {'name': s.name, 'active': s.active}
            for s in Switch.objects.all()
        ], key=itemgetter('name'))

        self.assertListEqual(existing_switches, expected_switches)

    @responses.activate
    def test_nothing_new(self):
        """Test updating switches from master that are the same on local server."""

        output = self.call_command()

        expected = [
            'Switch "test1" is still NOT active',
            'Switch "test2" is still active'
        ]

        self.check_output(output, expected)

    @responses.activate
    def test_all_new(self):
        """Test updating switches from master with no switches on local server."""

        # Delete all switches to have all switches from master appear as new.
        Switch.objects.all().delete()

        output = self.call_command()

        expected = [
            'Switch "test1" is new and is NOT active',
            'Switch "test2" is new and is active',
            'Note: no switches existed before'
        ]

        self.check_output(output, expected)
        self.check_switches()

    @responses.activate
    def test_updated_switches(self):
        """Test updating switches from master with different active state on local server."""

        # Update the switches to make then updated by the management command.
        self.switch1.active = True
        self.switch1.save()
        self.switch2.active = False
        self.switch2.save()

        output = self.call_command()

        expected = [
            'Switch "test1" is now NOT active',
            'Switch "test2" is now active',
        ]

        self.check_output(output, expected)
        self.check_switches()

    @responses.activate
    def test_deleted_switches(self):
        """Test updating switches from master with more switches on local server."""

        # Add new switches that should be deleted after the call.
        Switch.objects.create(name='test3', active=False)
        Switch.objects.create(name='test4', active=True)

        output = self.call_command()

        expected = [
            'Switch "test1" is still NOT active',
            'Switch "test2" is still active',
            'Switch "test3" (NOT active) was removed',
            'Switch "test4" (active) was removed',
        ]

        self.check_output(output, expected)
        self.check_switches()

    @responses.activate
    def test_no_switches_on_master(self):
        """Test updating switches from master which has no switches."""

        # Mimic no switches on the server
        self.master_data = {'waffle': {'switch': serializers.serialize('json', [])}}

        output = self.call_command()

        expected = [
            'Switch "test1" (NOT active) was removed',
            'Switch "test2" (active) was removed',
            'Note: no switches retrieved from master'
        ]

        self.check_output(output, expected)

        # We shouldn't have switches anymore.
        self.assertEqual(Switch.objects.count(), 0)


class SyncableDependencyTestCase(TestCase):
    """Test that the ``Syncable`` dependency tree can be computed."""

    def test_get_dependencies(self):
        """Test that the ``get_dependencies`` method doesn't raise an exception."""

        try:
            DependencyManager.get_dependencies(Syncable)
        except Exception as e:
            self.fail('Syncable dependency tree could not be generated:\n%s' % e)


class ManyToManyTestCase(MTPTestCase):
    """Test that syncable models having auto-M2M are correctly managed.

    Attributes
    ----------
    set1 : GroupSet
    set2 : GroupSet
    set_qs : Queryset(GroupSet)
        ``Queryset`` of ``GroupSet`` filtered to have ``set1`` and ``set2``
    group1 : Group
    group2 : Group
    group3 : Group
    group4 : Group
    group_qs : Queryset(Group)
        ``Queryset`` of ``Group`` filtered to have ``group1``, ``group2``, ``group3`` and ``group4``
    min_last_update : datetime
        Datetime after which all updated objects are updated during the tests.

    Notes
    -----
    ``group1`` and ``group2`` are added to ``set1`` and ``set2`` to easily test removal

    """

    @classmethod
    def setUpTestData(cls):
        """Create some groups and group sets."""
        super().setUpTestData()

        cls.set1 = GroupSetFactory(name='testm2msync-groupset1')
        cls.set2 = GroupSetFactory(name='testm2msync-groupset2')
        cls.set_qs = GroupSet.objects.filter(pk__in=(cls.set1.pk, cls.set2.pk)).order_by('name')

        cls.group1 = GroupFactory(name='testm2msync-group1')
        cls.group2 = GroupFactory(name='testm2msync-group1')
        cls.group3 = GroupFactory(name='testm2msync-group3')
        cls.group4 = GroupFactory(name='testm2msync-group4')
        cls.group_qs = Group.objects.filter(pk__in=(
            cls.group1.pk, cls.group2.pk, cls.group3.pk)).order_by('name')

        # Add a group in both sets
        cls.set1.mgroups.add(cls.group1, cls.group2)
        cls.set2.mgroups.add(cls.group1, cls.group2)

        cls.min_last_update = now()

        # Set an old ``last_update`` (for master) and ``update_locally`` to False (for slave)
        past = cls.min_last_update - timedelta(days=1)
        cls.set_qs.update(last_update=past, update_locally=False)
        cls.group_qs.update(last_update=past, update_locally=False)

    def setUp(self):
        """Refresh all our groups and sets from DB to have fresh values in the python objects."""

        super().setUp()

        for obj in (self.set1, self.set2, self.group1, self.group2, self.group3, self.group4):
            obj.refresh_from_db()

    def get_updated(self):
        """Returns the updated groups and sets.

        Returning groups allow to check that groups, that don't own the m2m field, are not updated.

        Returns
        -------
        tuple(list)
            A tuple with two lists: the updated sets, and the updated groups.

        """

        if settings.INSTANCE_IS_SLAVE:
            filters = {'update_locally': True}
        else:
            filters = {'last_update__gt': self.min_last_update}

        return (
            list(self.set_qs.filter(**filters)),
            list(self.group_qs.filter(**filters)),
        )

    def test_adding_one_item_update_the_owning_instance(self):
        """The instance owning the M2M relation is updated when adding an item."""

        self.set1.mgroups.add(self.group3)

        self.assertEqual(self.get_updated(), (
            [self.set1,], []
        ))

    def test_adding_many_items_update_the_owning_instance(self):
        """The instance owning the M2M relation is updated when adding many items."""

        self.set1.mgroups.add(self.group3, self.group4)

        self.assertEqual(self.get_updated(), (
            [self.set1,], []
        ))

    def test_adding_one_item_from_other_side_update_the_owning_instance(self):
        """The instance owning the M2M relation is updated when adding item from the other side."""

        self.group3.groupset_set.add(self.set1)

        self.assertEqual(self.get_updated(), (
            [self.set1], []
        ))

    def test_adding_many_items_from_other_side_update_the_owning_instances(self):
        """The instances owning the M2M relation is updated when adding items from the other side."""

        self.group3.groupset_set.add(self.set1, self.set2)

        self.assertEqual(self.get_updated(), (
            [self.set1, self.set2], []
        ))

    def test_removing_one_item_update_the_owning_instance(self):
        """The instance owning the M2M relation is updated when removing one item."""

        self.set1.mgroups.remove(self.group1)

        self.assertEqual(self.get_updated(), (
            [self.set1,], []
        ))

    def test_removing_many_items_update_the_owning_instance(self):
        """The instance owning the M2M relation is updated when removing many items."""

        self.set1.mgroups.remove(self.group1, self.group2)

        self.assertEqual(self.get_updated(), (
            [self.set1,], []
        ))

    def test_removing_one_item_from_other_side_update_the_owning_instance(self):
        """Instance owning the M2M relation is updated when removing item from the other side."""

        self.group2.groupset_set.remove(self.set1)

        self.assertEqual(self.get_updated(), (
            [self.set1], []
        ))

    def test_removing_many_items_from_other_side_update_the_owning_instances(self):
        """Instances owning the M2M relation is updated when removing item from the other side."""

        self.group2.groupset_set.remove(self.set1, self.set2)

        self.assertEqual(self.get_updated(), (
            [self.set1, self.set2], []
        ))

    def test_clearing_items_update_the_owning_instance(self):
        """The instance owning the M2M relation is updated when clearing all items."""

        self.set1.mgroups.clear()

        self.assertEqual(self.get_updated(), (
            [self.set1,], []
        ))

    def test_clearing_items_from_other_side_update_the_owning_instance(self):
        """Instance owning the M2M relation is updated when clearing items from the other side."""

        self.group2.groupset_set.clear()

        self.assertEqual(self.get_updated(), (
            [self.set1, self.set2], []
        ))

    def test_non_syncable_models_are_not_updated(self):
        """Updating a M2M relation defined on a non-syncable model doesn't update the instance."""

        group = DjangoGroup.objects.create(name='testm2msync-djgroup')

        with patch.object(DjangoGroup, 'save') as patched:
            group.permissions.add(Permission.objects.first())

        self.assertFalse(patched.called)

    def test_models_owning_syncable_through_tables_are_not_updated(self):
        """Updating a manual through table doesn't update the owning instance."""

        user = MTPUserFactory(username='testm2msync-user')

        with patch.object(MTPUser, 'save') as patched:
            GroupRole.objects.create(user=user, group=self.group1)

        self.assertFalse(patched.called)


class BaseSyncUploadTestCase:
    """Base class to use when testing the sync upload endpoint"""

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

        super().setUpClass()

        cls.url = reverse('sync:upload')

    def post_objects(self, objects):
        """Helper that posts some objects to the upload endpoint.

        Parameters
        ----------
        objects : list(models.Model)
            List of model instances to upload

        Returns
        -------
        Response
            The response of the post request from the client

        """

        json_stream = serializers.serialize('json', objects)

        params = {
            'json_stream': json_stream,
            'mac': "666",
        }

        # Sync the data
        return self.client.post(self.url, params)


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
        )
