"""Test the duplication."""

from mock import patch

from django.core.exceptions import ObjectDoesNotExist
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage

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


class DuplicableTestCase(DuplicableBaseTestCase):
    """Test the duplication features"""

    def test_non_duplicable_relations(self):
        """Test that ``get_non_duplicable_relations`` returns the expected set."""

        self.assertSetEqual(Training.get_non_duplicable_relations(), {
            'all_duplicates', 'direct_duplicates',
            'parent_version', 'original_version',
            'foo', 'survey_set',
        })
        self.assertSetEqual(Exercise.get_non_duplicable_relations(), {
            'all_duplicates', 'direct_duplicates',
            'parent_version', 'original_version',
            'foo',
        })

    def test_queryset(self):
        """Test the ``DuplicateQueryset``"""

        dup1 = self.training.duplicate()
        dup2 = self.training.duplicate()
        dup3 = dup1.duplicate()
        dup4 = dup3.duplicate()

        self.assertEqual(
            set(Training.objects.having_parent_version(self.training)),
            {dup1, dup2}
        )

        self.assertEqual(
            set(Training.objects.having_original_version(self.training)),
            {dup1, dup2, dup3, dup4}
        )

    def test_overriden_queryset(self):
        """Test when overriding ``DuplicateQueryset`` and ``DuplicateManager``"""
        dup1 = self.exercise1.duplicate()
        dup2 = dup1.duplicate(name='foo')

        # Check default queryset methods
        self.assertEqual(
            set(Exercise.objects.having_parent_version(self.exercise1)),
            {dup1}
        )

        self.assertEqual(
            set(Exercise.objects.having_original_version(self.exercise1)),
            {dup1, dup2}
        )
        # Access the new methd on the new queryset
        self.assertSetEqual(
            set(Exercise.objects.name_is_foo()),
            {dup2}
        )

        # And access the new method on the new manager
        Exercise.objects.do_nothing()

    # We allow "too-many-locals" in tests
    # pylint: disable=R0914
    def test_duplicate(self):
        """Test that duplicating an object duplicates all expected relations."""

        def check_duplicated(obj):
            """Check that the given object was correctly duplicated."""
            obj_repr = (obj._meta.model_name, obj.pk)
            self.assertIn(obj_repr, dups)
            self.assertNotEqual(obj.pk, dups[obj_repr].pk)
            return dups[obj_repr]

        def check_not_duplicated(obj, ref=None):
            """Check that the given object was NOT duplicated."""
            obj_repr = (obj._meta.model_name, obj.pk)
            self.assertNotIn(obj_repr, dups)
            if ref is not None:
                self.assertEqual(obj_repr, (ref._meta.model_name, ref.pk))

        new_training, duplicated_objects = duplicate(self.training)

        # Convert a tuple (old_object, duplicate) into a dict {(model, pl): duplicate}
        dups = {(orig._meta.model_name, orig.pk): dupl for orig, dupl in duplicated_objects}

        # Training should be duplicated
        self.assertEqual(check_duplicated(self.training), new_training)

        # Its author should NOT be duplicated
        check_not_duplicated(new_training.author, self.author)
        # Same for attendees
        check_not_duplicated(self.attendee1)
        check_not_duplicated(self.attendee2)

        # Exercises should be duplicated
        exercises = list(self.training.exercise_set.all())
        new_exercises = list(new_training.exercise_set.all())
        self.assertEqual(len(new_exercises), len(exercises))

        for exercise in exercises:
            with self.subTest(exercise=exercise):
                new_exercise = check_duplicated(exercise)
                self.assertIn(new_exercise, new_exercises)

                # Solution should be duplicated
                new_solution = check_duplicated(exercise.solution)
                self.assertEqual(new_exercise.solution, new_solution)

                # Hints should be duplicated
                hints = list(exercise.hint_set.all())
                new_hints = list(new_exercise.hint_set.all())
                self.assertEqual(len(new_hints), len(hints))

                for hint in hints:
                    with self.subTest(hint=hint):
                        new_hint = check_duplicated(hint)
                        self.assertIn(new_hint, new_hints)

                # Rewards should NOT be duplicated
                rewards = list(exercise.rewards.all())
                new_rewards = list(exercise.rewards.all())
                self.assertEqual(len(new_rewards), len(rewards))

                for reward in rewards:
                    with self.subTest(reward=reward):
                        check_not_duplicated(reward)
                        self.assertIn(reward, new_rewards)

        # Plans should be duplicated
        plans = list(self.training.plan_set.all())
        new_plans = list(new_training.plan_set.all())
        self.assertEqual(len(new_plans), len(plans))

        for plan in plans:
            with self.subTest(plan=plan):
                new_plan = check_duplicated(plan)
                self.assertIn(new_plan, new_plans)

            # Exercises should be duplicated
            plan_exercises = list(plan.exercises.all())
            plan_new_exercises = list(new_plan.exercises.all())
            self.assertEqual(len(plan_new_exercises), len(plan_exercises))

            for plan_exercise in plan_exercises:
                with self.subTest(plan_exercise=plan_exercise):
                    plan_new_exercise = check_duplicated(plan_exercise)
                    self.assertIn(plan_new_exercise, plan_new_exercises)
                    # Check that the new exercise is well referenced in the new training
                    self.assertIn(plan_new_exercise, new_exercises)

            # PlannedExercise (through table) should be duplicated
            planned_exercises = list(plan.plannedexercise_set.all())
            new_planned_exercises = list(new_plan.plannedexercise_set.all())
            self.assertEqual(len(new_planned_exercises), len(planned_exercises))

            for planned_exercise in planned_exercises:
                with self.subTest(planned_exercise=planned_exercise):
                    new_planned_exercise = check_duplicated(planned_exercise)
                    self.assertIn(new_planned_exercise, new_planned_exercises)

                    # Check exercises in this through table
                    new_exercise = check_duplicated(planned_exercise.exercise)
                    self.assertEqual(new_exercise.pk, new_planned_exercise.exercise.pk)

            # The certificate should NOT be duplicated and should be empty on the duplicated plan
            with self.assertRaises(ObjectDoesNotExist):
                new_plan.certificate  # pylint: disable=pointless-statement

        # Favorite exercise of attendees should NOT be duplicated
        self.assertEqual(
            [ex.pk for ex in self.attendee1.favorite_exercises.all()],
            [self.exercise1.pk, self.exercise3.pk]
        )
        self.assertEqual(list(self.attendee2.favorite_exercises.all()), [])

        # "Participated training" should still link to the original training
        self.assertEqual(
            [obj.pk for obj in self.attendee1.participated_trainings.all()],
            [self.training.pk]
        )
        self.assertEqual(
            [obj.pk for obj in self.attendee2.participated_trainings.all()],
            [self.training.pk]
        )
        # And to the original plan
        self.assertEqual(
            ParticipatedTraining.objects.get(
                attendee=self.attendee1, training=self.training
            ).plan.pk,
            self.plan1.pk
        )
        self.assertEqual(
            ParticipatedTraining.objects.get(
                attendee=self.attendee2, training=self.training
            ).plan.pk,
            self.plan2.pk
        )

        # Ensure that survey is not duplicated
        check_not_duplicated(self.survey)
        self.assertEqual(Survey.objects.count(), 1)

        # Ensure ``parent_version`` and ``original_version`` are correctly set
        self.assertEqual(new_training.parent_version.pk, self.training.pk)
        self.assertEqual(new_training.original_version.pk, self.training.pk)

        # Make a new duplicate from the duplicate to check ``original_version``
        new_new_training, _ = duplicate(new_training)
        self.assertEqual(new_new_training.parent_version.pk, new_training.pk)
        self.assertEqual(new_new_training.original_version.pk, self.training.pk)

    def test_duplicate_null_fk(self):
        """Test that duplicating an object with an empty FK still works."""

        # Test with a fk to a non duplicable relation
        training = Training.objects.get(id=self.training.id)
        training.author = None
        training.save()
        new_training = training.duplicate()
        self.assertIsNone(new_training.author)

        # And a FK to a duplicable relation
        solution = Solution.objects.get(id=self.solution1.id)
        solution.exercise = None
        solution.save()
        new_solution = solution.duplicate()
        self.assertIsNone(new_solution.exercise)



    def test_update_field_does_not_propagate(self):
        """Make sure that updating a field in a duplicate does not touch the original."""

        new_training, __ = duplicate(self.training)

        # Update the new training
        new_training.name = 'new training'
        new_training.save()

        # Be sure it didn't update the original one
        self.training.refresh_from_db()
        self.assertEqual(self.training.name, 'training1')

        # And we have the correct name in DB or the new training
        new_training.refresh_from_db()
        self.assertEqual(new_training.name, 'new training')

    def test_update_file_does_not_propagate(self):
        """Make sure that updating a file in a duplicate does not touch the original."""

        # Set a file to the original training
        default_storage.save('content1.txt', ContentFile('content1'))
        self.training.test_file = 'content1.txt'
        self.training.save()

        # Check our file
        self.training.refresh_from_db()
        self.assertEqual(self.training.test_file.name, 'content1.txt')
        self.assertEqual(self.training.test_file.read(), b'content1')

        # Do the duplicate
        new_training, __ = duplicate(self.training)

        # Ensure the new training has the same file
        new_training.refresh_from_db()
        self.assertEqual(new_training.test_file.name, 'content1.txt')
        self.assertEqual(new_training.test_file.read(), b'content1')

        # Now set a different file on the duplicate
        default_storage.save('content2.txt', ContentFile('content2'))
        new_training.test_file = 'content2.txt'
        new_training.save()

        # Check our file
        new_training.refresh_from_db()
        self.assertEqual(new_training.test_file.name, 'content2.txt')
        self.assertEqual(new_training.test_file.read(), b'content2')

        # Ensure that the old training has the old file
        self.training.refresh_from_db()
        self.assertEqual(self.training.test_file.name, 'content1.txt')
        self.assertEqual(self.training.test_file.read(), b'content1')

    def test_overriding_value(self):
        """Test that passing a value when duplicating a field works only when expected."""

        # Overriding a simple field is allowed
        new_training = self.training.duplicate(name='new training')
        self.assertEqual(new_training.name, 'new training')

        # Overriding a FK is allowed
        new_author = Person.objects.create(name='new author')
        new_training = self.training.duplicate(author=new_author)
        self.assertEqual(new_training.author, new_author)

        # Overriding a M2M without through is allowed
        new_exercise = self.exercise1.duplicate(rewards=[self.reward1, self.reward2])
        self.assertEqual(set(new_exercise.rewards.all()), {self.reward1, self.reward2})

        # Overriding a M2M with through is NOT allowed
        with self.assertRaises(ValueError):
            self.plan1.duplicate(exercises=[self.exercise1, self.exercise3])

        # Overriding a reverse M2M is NOT allowed
        with self.assertRaises(ValueError):
            self.exercise1.duplicate(plan_set=[self.plan1, self.plan2])

        # Overriding a reverse FK is NOT allowed
        with self.assertRaises(ValueError):
            self.training.duplicate(exercise_set=[self.exercise1, self.exercise3])

        # Overriding a reverse OneToOne is NOT allowed
        with self.assertRaises(ValueError):
            self.exercise1.duplicate(solution=self.solution2)

    def test_non_duplicable_relations_overriding(self):
        """Test that we can pass some relations to not duplicate."""

        # Duplicating a solution should duplicate the exercise, because nothing is done to prevent
        # it
        new_solution = self.solution1.duplicate()
        self.assertNotEqual(new_solution.exercise_id, self.solution1.exercise_id)

        # Duplicating the hint should not duplicate the exercise because ``exercise`` is passed
        # to the ``non_duplicable_relations`` argument of the ``duplicate`` method of the ``Hint``
        # model
        new_hint = self.hint1.duplicate()
        self.assertEqual(new_hint.exercise_id, self.hint1.exercise_id)

    def test_whole_duplicate_is_atomic(self):
        """Test that the whole duplicate is done, or nothing is done."""

        # Check how many trainings we have to compare at the end of the test
        nb_trainings = Training.objects.count()

        # We'll raise an error when saving a model duplicated after the save of the main
        # duplicated
        with patch.object(Exercise, 'save', side_effect=RuntimeError()):
            with self.assertRaises(RuntimeError):
                self.training.duplicate()

        # We should have the exact same number of training, no duplicate should have been created
        new_nb_traininings = Training.objects.count()
        self.assertEqual(new_nb_traininings, nb_trainings)

        # Do the same but without ``atomic`` to check that it was really the ``atomic`` call
        # that prevented the duplicate to be created in case of error

        with patch.object(Exercise, 'save', side_effect=RuntimeError()):
            with self.assertRaises(RuntimeError):
                # The code called in ``self.training.duplicate``, without the ``with atomic`` part
                duplicate(self.training)

        # We should have one more training, the duplicate should have been created
        new_nb_traininings = Training.objects.count()
        self.assertEqual(new_nb_traininings, nb_trainings + 1)
