"""This module contains the test cases for the managers of the ``questionnaires`` app"""

from django.db.models import Sum

from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.questionnaires.models import Quiz
from mtp.apps.trainings.factories import TrainingSessionFactory
from mtp.apps.workshops.factories import (
    WorkshopFactory,
    WorkshopStepImageFactory,
    WorkshopStepQuestionFactory,
    WorkshopStepQuestionAnswerFactory,
    WorkshopStepQuestionUserAnswerFactory
)

from ..models import (
    ChoicesQuestion,
    ImageChoicesQuestion,
    OpenQuestion
)


class QuizManagerTestCase(MTPTestCase):
    """Test case for the ``QuizManager`` Manager"""

    def setUp(self):
        """Create a Workshop instance to migrate"""

        self.workshop = WorkshopFactory()

        # 1 "standard" question, with 2 answers, 1 valid and 1 invalid
        self.workshop_question = WorkshopStepQuestionFactory(
            workshop=self.workshop,
            is_multiple_choices=True
        )
        self.first_answer = WorkshopStepQuestionAnswerFactory(
            question=self.workshop_question,
            is_correct=True,
            points=1
        )
        self.second_answer = WorkshopStepQuestionAnswerFactory(
            question=self.workshop_question,
            is_correct=False,
            points=0
        )

        # Two trainee answers for this question
        WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_question,
            answer=self.first_answer,
            user=self.trainee,
            training_session=self.training_session
        )
        WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_question,
            answer=self.second_answer,
            user=self.trainee,
            training_session=self.training_session
        )

        # 1 "image" question
        self.workshop_question_image = WorkshopStepImageFactory(workshop=self.workshop)

        # 1 "open" question
        self.workshop_open_question = WorkshopStepQuestionFactory(workshop=self.workshop)

        # One trainee answer for this question
        self.open_answer = WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_open_question,
            answer=None,
            user=self.trainee,
            training_session=self.training_session
        )

    def test_get_step_values_for_choice_question_should_return_the_step_values(self):
        """Test that the ``get_step_values`` for the choice question should work"""

        self.assertEqual(
            Quiz.objects.get_step_values(self.workshop_question),
            {
                'id': self.workshop_question.id,
                'klass': ChoicesQuestion,
                'sort_order': self.workshop_question.sort_order,
                'is_multiple_choices': self.workshop_question.is_multiple_choices,
                'label': self.workshop_question.title,
                'is_deleted': self.workshop_question.is_deleted,
                'points': self.workshop_question.answers.non_deleted().aggregate(
                    Sum('points')
                )['points__sum'],
                'answers': [
                    {
                        'id': answer.id,
                        'label': answer.label,
                        'is_valid': answer.is_correct,
                        'is_deleted': answer.is_deleted,
                        'sort_order': answer.sort_order
                    }
                    for answer in self.workshop_question.answers.all()
                ],
                'user_answers': Quiz.objects.get_user_answer_values_for_choices_question(
                    self.workshop_question
                )
            }
        )

    def test_get_step_values_with_answers_with_negative_points(self):
        """Test that the ``get_step_values`` method handle correctly answers with negative points"""

        # At first, our points should be equal to the sum of the answers points
        self.assertEqual(
            Quiz.objects.get_step_values(self.workshop_question)['points'],
            1
        )

        # We add a new answer with negative points
        WorkshopStepQuestionAnswerFactory(
            question=self.workshop_question,
            is_correct=False,
            points=-2
        )

        # We should still have 1 points, since we don't want to take the negative points
        self.assertEqual(
            Quiz.objects.get_step_values(self.workshop_question)['points'],
            1
        )

    def test_get_step_values_for_image_question_should_return_the_step_values(self):
        """Test that the ``get_step_values`` for the image question should work"""

        self.assertEqual(
            Quiz.objects.get_step_values(self.workshop_question_image),
            {
                'id': self.workshop_question_image.id,
                'image': self.workshop_question_image.image.name,
                'is_deleted': self.workshop_question_image.is_deleted,
                'klass': ImageChoicesQuestion,
                'sort_order': self.workshop_question_image.sort_order,
                'label': self.workshop_question_image.title,
                'points': 0
            }
        )

    def test_get_step_values_for_open_question_should_return_the_step_values(self):
        """Test that the ``get_step_values`` for the open question should work"""

        self.assertEqual(
            Quiz.objects.get_step_values(self.workshop_open_question),
            {
                'id': self.workshop_open_question.id,
                'klass': OpenQuestion,
                'label': self.workshop_open_question.title,
                'sort_order': self.workshop_open_question.sort_order,
                'is_deleted': self.workshop_open_question.is_deleted,
                'user_answers': Quiz.objects.get_user_answers_values_for_open_question(
                    self.workshop_open_question
                ),
                'points': 0
            }
        )

    def test_get_user_answer_values_for_choices_question_is_working(self):
        """Test that the ``get_user_answer_values_for_choices_question`` is working correctly"""

        # We got only one trainee who answered this question, in one session
        expected_data = {
            self.trainee.id + self.training_session.id: {
                'choices': [
                    self.second_answer.id,
                    self.first_answer.id
                ],
                'question_id': self.workshop_question.id,
                'session_id': self.training_session.id,
                'user_id': self.trainee.id
            }
        }

        self.assertEqual(
            dict(Quiz.objects.get_user_answer_values_for_choices_question(
                self.workshop_question
            )),
            expected_data
        )

        # Add answers for another trainee in the same session
        WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_question,
            answer=self.first_answer,
            user=self.trainees[1],
            training_session=self.training_session
        )
        WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_question,
            answer=self.second_answer,
            user=self.trainees[1],
            training_session=self.training_session
        )

        expected_data[self.trainees[1].id + self.training_session.id] = {
            'choices': [
                self.second_answer.id,
                self.first_answer.id
            ],
            'question_id': self.workshop_question.id,
            'session_id': self.training_session.id,
            'user_id': self.trainees[1].id
        }

        self.assertEqual(
            dict(Quiz.objects.get_user_answer_values_for_choices_question(
                self.workshop_question
            )),
            expected_data
        )

        # Add answers for another trainee, in a different session
        another_session = TrainingSessionFactory()

        WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_question,
            answer=self.first_answer,
            user=self.trainees[2],
            training_session=another_session
        )
        WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_question,
            answer=self.second_answer,
            user=self.trainees[2],
            training_session=another_session
        )

        expected_data[self.trainees[2].id + another_session.id] = {
            'choices': [
                self.second_answer.id,
                self.first_answer.id
            ],
            'question_id': self.workshop_question.id,
            'session_id': another_session.id,
            'user_id': self.trainees[2].id
        }

        self.assertEqual(
            dict(Quiz.objects.get_user_answer_values_for_choices_question(
                self.workshop_question
            )),
            expected_data
        )

    def test_get_user_answers_values_for_open_question_is_wortking(self):
        """Test that the ``get_user_answers_values_for_open_question`` method is working"""

        expected_values = [
            {
                'id': self.open_answer.id,
                'user_id': self.trainee.id,
                'session_id': self.training_session.id,
                'question_id': self.workshop_open_question.id,
                'content': self.open_answer.text
            }
        ]

        self.assertCountEqual(
            Quiz.objects.get_user_answers_values_for_open_question(self.workshop_open_question),
            expected_values
        )

        # Another trainee answer this open question (in the same session)
        trainee_1_open_answer = WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_open_question,
            answer=None,
            user=self.trainees[1],
            training_session=self.training_session
        )

        expected_values.append({
            'id': trainee_1_open_answer.id,
            'user_id': self.trainees[1].id,
            'session_id': self.training_session.id,
            'question_id': self.workshop_open_question.id,
            'content': trainee_1_open_answer.text
        })
        self.assertCountEqual(
            Quiz.objects.get_user_answers_values_for_open_question(self.workshop_open_question),
            expected_values
        )

        # Another trainee answer the same question is a different session
        another_session = TrainingSessionFactory()

        trainee_2_open_answer = WorkshopStepQuestionUserAnswerFactory(
            question=self.workshop_open_question,
            answer=None,
            user=self.trainees[2],
            training_session=another_session
        )

        expected_values.append({
            'id': trainee_2_open_answer.id,
            'user_id': self.trainees[2].id,
            'session_id': another_session.id,
            'question_id': self.workshop_open_question.id,
            'content': trainee_2_open_answer.text
        })
        self.assertCountEqual(
            Quiz.objects.get_user_answers_values_for_open_question(self.workshop_open_question),
            expected_values
        )

    def test_merge_steps_values_should_merge_the_steps_values(self):
        """Test that the ``merge_steps_values`` should work"""

        steps_values = [
            Quiz.objects.get_step_values(step)
            for step in self.workshop.steps.order_by('sort_order')
        ]

        # Our image question don't have any choices
        self.assertNotIn(
            'answers',
            steps_values[1].keys()
        )

        # Merge the steps
        merged_steps_values = Quiz.objects.merge_steps_values(steps_values)

        # Now, our image question should have one choice
        self.assertEqual(len(merged_steps_values[1]['answers']), 1)
        self.assertEqual(
            merged_steps_values[1]['answers'][0],
            {
                'id': None,
                'label': 'Ok',
                'is_valid': True,
                'sort_order': 0
            }
        )

        # We order the choice question just after our image question
        self.workshop_question.sort_order = self.workshop_question_image.sort_order
        self.workshop_question.save()

        steps_values = [
            Quiz.objects.get_step_values(step)
            for step in self.workshop.steps.order_by('sort_order')
        ]

        # We have 3 steps
        self.assertEqual(len(steps_values), 3)

        # Merge the steps
        merged_steps_values = Quiz.objects.merge_steps_values(steps_values)

        # We should have now only 2 step now...
        self.assertEqual(len(merged_steps_values), 2)

        # ...and our image step should have the attributes of our choice question
        self.assertEqual(
            merged_steps_values[0],
            {
                'id': self.workshop_question.id,
                'klass': ImageChoicesQuestion,
                'sort_order': self.workshop_question.sort_order,
                'is_deleted': self.workshop_question.is_deleted,
                'image': self.workshop_question_image.image.name,
                'is_multiple_choices': self.workshop_question.is_multiple_choices,
                'label': self.workshop_question.title,
                'points': self.workshop_question.answers.non_deleted().aggregate(
                    Sum('points')
                )['points__sum'],
                'answers': [
                    {
                        'id': answer.id,
                        'label': answer.label,
                        'is_valid': answer.is_correct,
                        'is_deleted': answer.is_deleted,
                        'sort_order': answer.sort_order
                    }
                    for answer in self.workshop_question.answers.all()
                ],
                'user_answers': Quiz.objects.get_user_answer_values_for_choices_question(
                    self.workshop_question
                )
            }
        )

    def test_get_workshop_values_should_return_the_workshop_values(self):
        """Test that the ``get_workshop_values`` method work"""

        steps_values = [
            Quiz.objects.get_step_values(step)
            for step in self.workshop.steps.order_by('sort_order')
        ]

        steps_values = Quiz.objects.merge_steps_values(steps_values)

        self.assertEqual(
            Quiz.objects.get_workshop_values(self.workshop),
            {
                'id': self.workshop.id,
                'training_id': self.workshop.training_id,
                'klass': Quiz,
                'name': self.workshop.name,
                'description': self.workshop.description,
                'is_deleted': self.workshop.is_deleted,
                'sort_order': self.workshop.sort_order,
                'is_score_displayed': self.workshop.enable_score,
                'is_broadcastable': self.workshop.is_broadcastable,
                'is_questions_order_random': self.workshop.is_steps_order_random,
                'are_questions_mandatory': self.workshop.are_questions_mandatory,
                'questions': steps_values
            }
        )

    def test_migrate_workshops_to_questionnaires_should_migrate_the_workshop(self):
        """Test that the ``migrate_workshops_to_questionnaires`` method should create a ``Quiz``"""

        # At first, we should have 0 ``Quiz`
        self.assertEqual(Quiz.objects.count(), 0)

        workshop_values = Quiz.objects.get_workshop_values(self.workshop)
        Quiz.objects.migrate_workshops_to_quiz()

        # We should have a new ``Quiz``, with the correct attributes...
        live_quiz = Quiz.objects.get()

        self.assertEqual(live_quiz.id, workshop_values['id'])
        self.assertEqual(live_quiz.name, workshop_values['name'])
        self.assertEqual(live_quiz.description, workshop_values['description'])
        self.assertEqual(live_quiz.is_deleted, workshop_values['is_deleted'])
        self.assertEqual(live_quiz.training_id, workshop_values['training_id'])

        # ...the correct questions
        zipped_questions = zip(
            live_quiz.questions.order_by('sort_order'),
            workshop_values['questions']
        )

        for question, questions_values in zipped_questions:
            with self.subTest(question=question):
                self.assertEqual(question.label, questions_values['label'])
                self.assertEqual(question.points, questions_values['points'])
                self.assertEqual(question.is_deleted, questions_values['is_deleted'])

                if isinstance(question, OpenQuestion):
                    self.assertEqual(question.answers.count(), 1)

                if isinstance(question, ImageChoicesQuestion):
                    self.assertEqual(question.answers.count(), 0)
                    self.assertEqual(question.image.name, questions_values['image'])

                if isinstance(question, ChoicesQuestion):
                    self.assertEqual(question.answers.get().choices.count(), 2)

                    zipped_choices = zip(
                        question.choices.order_by('sort_order'),
                        questions_values['answers']
                    )

                    for choice, choice_values in zipped_choices:
                        self.assertEqual(choice.label, choice_values['label'])
                        self.assertEqual(choice.is_valid, choice_values['is_valid'])
                        self.assertEqual(choice.is_deleted, choice_values['is_deleted'])

