"""Test the comparison."""

from .duplicable_testapp.models import *
from .test_cases import DuplicableBaseTestCase

from ..utils import duplicate


class CompareTestCase(DuplicableBaseTestCase):
    """Test the comparison features"""

    @classmethod
    def setUpTestData(cls):
        """Duplicate the training after its creation."""

        super().setUpTestData()

        # If we save the new training in ``cls.new_training``, if we update it in tests, the
        # DB will be reset between each test, but not the values and cache in the python object,
        # so we just save the PK and reload it from DB for each test, in ``setUp``.
        new_training, __ = duplicate(cls.training)
        cls.new_training_pk = new_training.pk

    def setUp(self):
        super().setUp()

        self.new_training = Training.objects.get(pk=self.new_training_pk)
        self.new_exercise1 = self.new_training.exercise_set.get(name='exercise1')

    def test_no_update(self):
        """Testing comparison without update."""

        changed, __ = self.training.compare(self.new_training)
        self.assertFalse(changed)

    def test_update_simple_field(self):
        """Testing comparison when updating simple fields.

        Here we'll update the ``name`` field of a ``Training``. """

        self.new_training.name = 'training1bis'
        self.new_training.save()

        changed, compare_refs = self.training.compare(self.new_training)
        self.assertTrue(changed)
        training_ref = compare_refs[Training][self.training.pk, self.new_training.pk]
        self.assertTrue(training_ref['changed'])
        self.assertTrue(training_ref['fields']['name']['changed'])

    def test_update_simple_field_other_object(self):
        """Testing comparison when updating a simple field on a sub object.

        Here we'll update the ``name`` field of an ``Exercise``, that is linked to the
        ``Training`` because of the FK from ``Exercise`` to ``Training``.

        """

        self.new_exercise1.name = 'exercise1bis'
        self.new_exercise1.save()

        changed, compare_refs = self.training.compare(self.new_training)

        # The training is different because an exercise is different
        self.assertTrue(changed)
        training_ref = compare_refs[Training][self.training.pk, self.new_training.pk]
        self.assertTrue(training_ref['changed'])
        self.assertTrue(training_ref['fields']['exercise_set']['changed'])

        # The exercise should be marqued as changed
        exercise_ref = compare_refs[Exercise][self.new_exercise1.original_version_id,
                                              self.new_exercise1.pk]
        self.assertTrue(exercise_ref['changed'])
        self.assertTrue(exercise_ref['fields']['name']['changed'])

    def test_update_fk(self):
        """Testing comparison when updating a FK.

        Here, we'll update the ``author`` of the ``Training``. And because the ``Author`` is
        not duplicable, there is no entry for this model in ``compare_refs``.
        """

        author2 = Person.objects.create(name='author2')
        self.new_training.author = author2
        self.new_training.save()

        changed, compare_refs = self.training.compare(self.new_training)

        # The training is different because it has a different author
        self.assertTrue(changed)
        training_ref = compare_refs[Training][self.training.pk, self.new_training.pk]
        self.assertTrue(training_ref['changed'])
        self.assertTrue(training_ref['fields']['author']['changed'])

        self.assertNotIn(Person, compare_refs)

    def test_update_reverse_one2one(self):
        """Testing comparison when updating a reverse one to one.

        Here we'll update a ``Solution`` that has a One2One to ``Exercise``, and check that the
        linked exercise is marked as changed."""

        solution = Solution.objects.get(exercise=self.new_exercise1)

        solution.name = 'solution1bis'
        solution.save()

        changed, compare_refs = self.training.compare(self.new_training)

        # The exercise should be marqued as changed
        exercise_ref = compare_refs[Exercise][self.new_exercise1.original_version_id,
                                              self.new_exercise1.pk]
        self.assertTrue(exercise_ref['changed'])
        self.assertTrue(exercise_ref['fields']['solution']['changed'])

        # And of course, the training should be marked as changed
        self.assertTrue(changed)

    # We allow "too-many-locals" in tests
    # pylint: disable=R0914
    def test_update_m2m(self):
        """Testing comparison when updating a m2m entry.

        We'll use the relation between ``Plan`` and ``Exercise`` and test many cases:
        - not updated
        - item removed
        - item added
        - item changed
        - through entry changed

        """

        plan = Plan.objects.get(training=self.new_training, name='plan1')
        old_plan = plan.parent_version

        exercise1 = self.new_exercise1
        planned1 = PlannedExercise.objects.get(plan=plan, exercise=exercise1)
        old_exercise1 = exercise1.parent_version
        old_planned1 = PlannedExercise.objects.get(plan=old_plan, exercise=old_exercise1)

        exercise2 = self.new_training.exercise_set.get(name='exercise2')
        planned2 = PlannedExercise.objects.get(plan=plan, exercise=exercise2)
        old_exercise2 = exercise2.parent_version
        old_planned2 = PlannedExercise.objects.get(plan=old_plan, exercise=old_exercise2)

        exercise3 = self.new_training.exercise_set.get(name='exercise3')

        # dict part of ``plan_ref['fields']['exercises']['details']['changed']``
        # if exercise1 hasn't changed
        same1 = {
            'changed': False,
            'old': old_exercise1,
            'new': exercise1,
            'ref': (Exercise, (old_exercise1.pk, exercise1.pk)),
            'status': 'done',
            'through': {
                'changed': False,
                'old': old_planned1,
                'new': planned1,
                'ref': (PlannedExercise, (old_planned1.pk, planned1.pk)),
                'status': 'done',
            }
        }

        # dict part of ``plan_ref['fields']['exercises']['details']['changed']``
        # if exercise2 hasn't changed
        same2 = {
            'changed': False,
            'old': old_exercise2,
            'new': exercise2,
            'ref': (Exercise, (old_exercise2.pk, exercise2.pk)),
            'status': 'done',
            'through': {
                'changed': False,
                'old': old_planned2,
                'new': planned2,
                'ref': (PlannedExercise, (old_planned2.pk, planned2.pk)),
                'status': 'done',
            }
        }

        # Compare m2m ref if nothing changed
        changed, compare_refs = self.training.compare(self.new_training)
        self.assertFalse(changed)
        plan_ref = compare_refs[Plan][old_plan.pk, plan.pk]

        self.assertDictEqual(plan_ref['fields']['exercises'], {
            'changed': False,
            'old': [old_exercise1, old_exercise2],
            'new': [exercise1, exercise2],
            'details': {
                'added': [],
                'removed': [],
                'changed': [],
                'same': [same1, same2]
            }
        })

        # Add an entry in the m2m
        PlannedExercise.objects.create(plan=plan, exercise=exercise3, order=3)

        # And compare
        changed, compare_refs = self.training.compare(self.new_training)
        self.assertTrue(changed)
        plan_ref = compare_refs[Plan][old_plan.pk, plan.pk]

        self.assertDictEqual(plan_ref['fields']['exercises'], {
            'changed': True,
            'old': [old_exercise1, old_exercise2],
            'new': [exercise1, exercise2, exercise3],
            'details': {
                'added': [exercise3],
                'removed': [],
                'changed': [],
                'same': [same1, same2]
            }
        })

        # Change one
        exercise1.name = 'exercise1bis'
        exercise1.save()

        # And compare
        changed, compare_refs = self.training.compare(self.new_training)
        self.assertTrue(changed)
        plan_ref = compare_refs[Plan][old_plan.pk, plan.pk]

        self.assertDictEqual(plan_ref['fields']['exercises'], {
            'changed': True,
            'old': [old_exercise1, old_exercise2],
            'new': [exercise1, exercise2, exercise3],
            'details': {
                'added': [exercise3],
                'removed': [],
                'changed': [{
                    'changed': True,
                    'old': old_exercise1,
                    'new': exercise1,
                    'ref': (Exercise, (old_exercise1.pk, exercise1.pk)),
                    'status': 'done',
                    'through': {
                        'changed': False,
                        'old': old_planned1,
                        'new': planned1,
                        'ref': (PlannedExercise, (old_planned1.pk, planned1.pk)),
                        'status': 'done',
                    }
                }],
                'same': [same2]
            }
        })

        # Restore the first exercise and change the through for the second one
        exercise1.name = 'exercise1'
        exercise1.save()
        planned2.order = 5
        planned2.save()

        # And compare
        changed, compare_refs = self.training.compare(self.new_training)
        self.assertTrue(changed)
        plan_ref = compare_refs[Plan][old_plan.pk, plan.pk]

        self.assertDictEqual(plan_ref['fields']['exercises'], {
            'changed': True,
            'old': [old_exercise1, old_exercise2],
            'new': [exercise1, exercise3, exercise2],  # order changed!
            'details': {
                'added': [exercise3],
                'removed': [],
                'changed': [{
                    'changed': True,
                    'old': old_exercise2,
                    'new': exercise2,
                    'ref': (Exercise, (old_exercise2.pk, exercise2.pk)),
                    'status': 'done',
                    'through': {
                        'changed': True,
                        'old': old_planned2,
                        'new': planned2,
                        'ref': (PlannedExercise, (old_planned2.pk, planned2.pk)),
                        'status': 'done',
                    }
                }],
                'same': [same1]
            }
        })

        # Check that the through entry is marked as changed
        planned2_ref = compare_refs[PlannedExercise][old_planned2.pk, planned2.pk]
        self.assertTrue(planned2_ref['changed'])
        self.assertTrue(planned2_ref['fields']['order']['changed'])

        # But the exercise shoult not be marked as changed
        self.assertFalse(compare_refs[Exercise][old_exercise2.pk, exercise2.pk]['changed'])

        # Finally remove one
        planned2.delete()

        # And compare
        changed, compare_refs = self.training.compare(self.new_training)
        self.assertTrue(changed)
        plan_ref = compare_refs[Plan][old_plan.pk, plan.pk]

        self.assertDictEqual(plan_ref['fields']['exercises'], {
            'changed': True,
            'old': [old_exercise1, old_exercise2],
            'new': [exercise1, exercise3],
            'details': {
                'added': [exercise3],
                'removed': [old_exercise2],
                'changed': [],
                'same': [same1]
            }
        })

    def test_update_reverse_m2m(self):
        """Testing comparison when updating a reverse m2m entry.

        This should not change anything because we ignore those changes: in this test, changing the
        name of a ``Plan`` should not change the related ``Exercise`` entries (but the opposite is
        ``True``, as seen in the ``test_update_m2m`` test.)

        """

        plan = Plan.objects.get(training=self.new_training, name='plan1')
        plan.name = 'plan1bis'
        plan.save()

        __, compare_refs = self.training.compare(self.new_training)

        # The exercise should not be marked as changed
        self.assertFalse(
            compare_refs[Exercise][self.exercise1.parent_version_id, self.exercise1.pk]['changed']
        )

    def test_update_reverse_fk(self):
        """Testing comparison when updating on object having a FK onto another.

        It's a simple use case, for example, updating an ``Exercise`` or a ``Plan`` should mark
        the ``Training`` as changed, because they have a FK onto ``Training``.

        """

        plan = Plan.objects.get(training=self.new_training, name='plan1')
        plan.name = 'plan1bis'
        plan.save()

        changed, compare_refs = self.training.compare(self.new_training)

        # The training should be marked as changed
        self.assertTrue(changed)
        training_ref = compare_refs[Training][self.training.pk, self.new_training.pk]
        self.assertTrue(training_ref['changed'])
        self.assertTrue(training_ref['fields']['plan_set']['changed'])
