"""Test cases for the ``workshops`` models."""

from mock import patch

from django.db import transaction, IntegrityError

from mtp.apps.workshops.models import (
    WorkshopStepQuestion,
    WorkshopStepQuestionUserAnswer
)

from ..factories import WorkshopStepQuestionUserAnswerFactory

from .test_cases import WorkshopTestCase


class WorkshopModelTestCase(WorkshopTestCase):
    """Test cases for the ``Workshop`` model."""

    def get_trainee_score(self):
        """Shortcut method to get the score fo the trainee for the current workshop"""

        try:
            # Delete the ``@cached_property`` from the workshop, that's necessary
            # to get the right parts attributes
            del self.workshop.assessing_parts
        except AttributeError:
            pass  # Do nothing, the attribute might not be set

        return self.workshop.get_trainee_session_score(
            session=self.training_session, trainee=self.trainee)

    def test_compute_challenge_score_single_choice_with_valid_answer(self):
        """Test the ``compute_challenge_score`` method with a single choice question"""

        answer = self.workshop_question.answers.filter(is_correct=True).first()

        # Add a valid user answer
        self.workshop_question.save_user_answers(
            self.trainee,
            self.training_session,
            [answer]
        )

        # The trainee got the points for this question
        self.assertEqual(self.get_trainee_score(), 1)

    def test_compute_challenge_score_single_choice_with_invalid_answer(self):
        """Test the ``compute_challenge_score`` method with a single choice question"""

        answer = self.workshop_question.answers.filter(is_correct=False).first()

        # Add a invalid user answer
        self.workshop_question.save_user_answers(
            self.trainee,
            self.training_session,
            [answer]
        )

        # The trainee got the points for this question (0)
        self.assertEqual(self.get_trainee_score(), 0)

    def test_compute_challenge_score_multiple_choice_answer_all_valid(self):
        """Test the ``compute_challenge_score`` method with a multiple choices question"""

        # The trainee found all (and only) the valid answers
        answers = self.workshop_question_mc.answers.filter(is_correct=True)

        # Add all valid answers
        self.workshop_question_mc.save_user_answers(
            self.trainee,
            self.training_session,
            answers
        )

        # The trainee got the points for this question (1+1)
        self.assertEqual(self.get_trainee_score(), 2)

    def test_compute_challenge_score_multiple_choice_answer_one_invalid(self):
        """Test the ``compute_challenge_score`` method with a multiple choices question

        In this test, the ``is_all_or_nothing`` flag is set to True.

        """

        # The trainee found all the answers (valid AND invalid ones)
        answers = self.workshop_question_mc.answers.all()

        # Add all answers
        self.workshop_question_mc.save_user_answers(
            self.trainee,
            self.training_session,
            answers
        )

        # The trainee got nothing since it's a ``all_or_nothing`` question type
        self.assertEqual(self.get_trainee_score(), 0)

    def test_compute_challenge_score_multiple_choice_answer_one_invalid_all_or_nothing_false(self):
        """Test the ``compute_challenge_score`` method with a multiple choices question

        In this test, the ``is_all_or_nothing`` flag is set to False.

        """

        # Deactivate the ``is_all_or_nothing`` flag
        training = self.workshop_question_mc.workshop.training

        with training.override_version_state(training.VERSION_STATES.DRAFT):
            self.workshop_question_mc.is_all_or_nothing = False
            self.workshop_question_mc.save()

        # The trainee found all the answers (valid AND invalid ones)
        answers = self.workshop_question_mc.answers.all()

        # Add all answers
        self.workshop_question_mc.save_user_answers(
            self.trainee,
            self.training_session,
            answers
        )

        # The trainee got 2 points (for the two valid answers)
        self.assertEqual(self.get_trainee_score(), 2)

    def test_media_files(self):
        """Test that the ``media_files`` property should return a list of media path"""

        self.assertEqual(
            self.workshop.media_files,
            [self.workshop_question_mc_image.image.name]
        )

    def test_get_export_url(self):
        """Test the ``get_export_url`` method"""

        # Since the workshop have an "image" step, the url should be None
        self.assertIsNone(self.workshop.get_export_url())

        # We delete the "image" step
        self.workshop_question_mc_image.delete()

        # The export url is not None anymore
        self.assertIsNotNone(self.workshop.get_export_url())


class WorkshopStepQuestionModelTestCase(WorkshopTestCase):
    """Test cases for the ``WorkshopStepQuestion`` model."""

    @classmethod
    def setUpTestData(cls):
        """Set up the test data"""

        super().setUpTestData()

        cls.valid_answers = cls.workshop_question_mc.answers.filter(is_correct=True)
        cls.invalid_answers = cls.workshop_question_mc.answers.filter(is_correct=False)

    def question_is_valid(self):
        """Shortcut method to test the validity of the question"""

        return self.workshop_question_mc.is_valid_for_trainee(
            self.trainee, self.training_session)

    def test_is_valid_for_trainee_with_all_or_nothing(self):
        """Test the ``is_valid_for_trainee`` method when ``is_all_or_nothing`` flag is active."""

        # Add a valid user answer
        self.add_user_answers([self.valid_answers.first()])

        # Since the question is a multiple choice one, with the ``is_all_or_nothing`` flag,
        # the question is not valid yet
        self.assertFalse(self.question_is_valid())

        # Add another valid user answer
        self.add_user_answers([self.valid_answers.last()])

        # Since the trainee got all the good answers, the question is valid now
        self.assertTrue(self.question_is_valid())

        # Add an invalid answer
        self.add_user_answers([self.invalid_answers.first()])

        # The question should now be invalid
        self.assertFalse(self.question_is_valid())

    def test_is_valid_for_trainee_without_all_or_nothing(self):
        """Test the ``is_valid_for_trainee`` method when ``is_all_or_nothing`` is not active."""

        # Deactivate the ``is_all_or_nothing`` flag
        training = self.workshop_question_mc.workshop.training
        with training.override_version_state(training.VERSION_STATES.DRAFT):

            self.workshop_question_mc.is_all_or_nothing = False
            self.workshop_question_mc.save()

        # Add an invalid answer
        self.add_user_answers([self.invalid_answers.first()])

        # The question should be invalid
        self.assertFalse(self.question_is_valid())

        # Add a valid answer
        self.add_user_answers([self.valid_answers.first()])

        # With at last one good answer, the question is valid
        self.assertTrue(self.question_is_valid())

        # Soft-delete the trainee's answer
        WorkshopStepQuestionUserAnswer.objects.get(
            answer=self.valid_answers.first(),
            user=self.trainee
        ).remove()

        # The question is not valid anymore
        self.assertFalse(self.question_is_valid())

    def test_save_user_answers(self):
        """Test the ``save_user_answers``method."""

        self.workshop_question_mc.save_user_answers(
            self.trainee,
            self.training_session,
            self.answers,
        )

        user_answers_count = WorkshopStepQuestionUserAnswer.objects.filter(
            user=self.trainee,
            training_session=self.training_session,
            question=self.workshop_question_mc,
        ).count()
        self.assertEqual(len(self.answers), user_answers_count)

    def test_reset_user_answers(self):
        """Test the ``reset_user_answers``method."""

        # We save answers for trainee and trainee1
        self.workshop_question_mc.save_user_answers(
            self.trainee,
            self.training_session,
            self.answers,
        )

        self.workshop_question_mc.save_user_answers(
            self.trainees[1],
            self.training_session,
            self.answers,
        )

        # We reset answers of trainee
        self.workshop_question_mc.reset_user_answers(
            self.trainee,
            self.training_session
        )

        # The trainee should have 0 registered answers
        trainee_answers_count = WorkshopStepQuestionUserAnswer.objects.non_deleted().filter(
            user=self.trainee,
            training_session=self.training_session,
            question=self.workshop_question_mc,
        ).count()
        self.assertEqual(trainee_answers_count, 0)

        # The trainee1 should keep this registered answers
        trainee1_answers_count = WorkshopStepQuestionUserAnswer.objects.non_deleted().filter(
            user=self.trainees[1],
            training_session=self.training_session,
            question=self.workshop_question_mc,
        ).count()
        self.assertEqual(trainee1_answers_count, len(self.answers))

    def test_is_open_property(self):
        """Test that the ``is_open`` property is return correct values"""

        # Since we got multiple answers set on the question, it's not an "open" one
        self.assertTrue(self.workshop_question.answers.non_deleted().exists())
        self.assertFalse(self.workshop_question.is_open)

        # We delete the answers, so the question is now an "open" one
        self.workshop_question.answers.non_deleted().delete()
        self.assertFalse(self.workshop_question.answers.non_deleted().exists())

        del self.workshop_question.is_open
        self.assertTrue(self.workshop_question.is_open)

    def test_get_training_session_average_score(self):
        """Test that the ``get_training_session_average_score`` method"""

        # The average must be 0.0, since we got no answers yet
        # 3 queries since the recompute is done
        with self.assertNumQueries(3):
            self.assertEqual(
                self.workshop_question_mc.get_session_average_score(
                    session=self.training_session
                ),
                0.0
            )

        # The trainee answer all the valid answers
        self.workshop_question_mc.save_user_answers(
            self.trainee,
            self.training_session,
            self.valid_answers
        )

        # Now, only 1 query
        with self.assertNumQueries(1):
            self.assertEqual(
                self.workshop_question_mc.get_session_average_score(
                    session=self.training_session
                ),
                2.0
            )

        # Add right answers for another user
        self.training_session.users.add(self.trainees[1])
        self.workshop_question_mc.save_user_answers(
            self.trainees[1],
            self.training_session,
            self.valid_answers
        )

        self.assertEqual(
            self.workshop_question_mc.get_session_average_score(
                session=self.training_session
            ),
            2.0  # The average of 2 and 2 is 2 (no shit)
        )

        # Add wrong answers for another user
        self.training_session.users.add(self.trainees[2])
        self.workshop_question_mc.save_user_answers(
            self.trainees[2],
            self.training_session,
            self.invalid_answers
        )

        self.assertEqual(
            round(self.workshop_question_mc.get_session_average_score(
                session=self.training_session
            ), 1),
            1.3  # The average of 2 and 2 and 0 is 1.3
        )

        # If we remove the user from the session, the method still work
        self.training_session.users.remove(self.trainee, self.trainees[1], self.trainees[2])
        self.assertEqual(self.training_session.users.count(), 0)

        self.workshop_question_mc.get_session_average_score(
            session=self.training_session
        )

    def test_get_trainee_session_score_should_compute_if_score_is_none(self):
        """Test that the ``get_trainee_session_score`` should compute if the score is None"""

        # First call to the score, this should compute
        with patch.object(WorkshopStepQuestion, 'compute_trainee_session_score') as mocked:
            self.workshop_question.get_trainee_session_score(
                self.trainee,
                self.training_session
            )

        mocked.assert_called_once_with(self.trainee, self.training_session)

        # Second call, the score is already computed
        with patch.object(WorkshopStepQuestion, 'compute_trainee_session_score') as mocked:
            self.workshop_question.get_trainee_session_score(
                self.trainee,
                self.training_session
            )

        mocked.assert_not_called()

    def test_update_existing_open_question(self):
        """The answer of an open question can be updated"""

        # answer a first time
        self.workshop_open_question.save_user_answers(
            user=self.trainee,
            session=self.training_session,
            answers=['foo'],
        )

        answers = self.workshop_open_question.user_answers.all()
        self.assertEqual(len(answers), 1)
        first_answer = answers.first()
        self.assertEqual(first_answer.text, 'foo')

        # answer a second time
        self.workshop_open_question.save_user_answers(
            user=self.trainee,
            session=self.training_session,
            answers=['bar'],
        )

        answers = self.workshop_open_question.user_answers.all()
        self.assertEqual(len(answers), 1)
        second_answer = answers.first()
        self.assertEqual(second_answer.id, first_answer.id)
        self.assertEqual(second_answer.text, 'bar')

    def test_max_points_yield_property_should_return_the_sum_of_answers_points(self):
        """Test that the ``max_points_yield`` should return the sum of answers points"""

        # The question is a multiple choices one, the maximum number of points is the sum
        # of the points of all the answers
        self.assertEqual(
            sum(
                self.workshop_question_mc.answers.non_deleted().values_list(
                    'points',
                    flat=True
                )
            ),
            self.workshop_question_mc.max_points_yield
        )

        # Soft-delete the first answer, the property still work
        with self.training.override_version_state(self.training.VERSION_STATES.DRAFT):
            self.workshop_question_mc.answers.first().remove()

        del self.workshop_question_mc.max_points_yield

        self.assertEqual(
            sum(
                self.workshop_question_mc.answers.non_deleted().values_list(
                    'points',
                    flat=True
                )
            ),
            self.workshop_question_mc.max_points_yield
        )

        # For a single choice question, the property is now equal to the max of the answers points
        self.assertEqual(
            max(
                self.workshop_question.answers.non_deleted().values_list(
                    'points',
                    flat=True
                )
            ),
            self.workshop_question.max_points_yield
        )


class WorkshopStepQuestionUserAnswerModelTestCase(WorkshopTestCase):
    """Test case for ``WorkshopStepQuestionUserAnswer`` model."""

    def test_uniqueness(self):
        """Check we can't have 2 user answers for the same user, session, question and answer."""

        WorkshopStepQuestionUserAnswerFactory(
            user=self.trainee,
            training_session=self.training_session,
            question=self.workshop_question_mc,
            answer=self.answers[0],
        )

        with transaction.atomic():
            with self.assertRaises(IntegrityError):
                WorkshopStepQuestionUserAnswerFactory(
                    user=self.trainee,
                    training_session=self.training_session,
                    question=self.workshop_question_mc,
                    answer=self.answers[0],
                )


class WorkshopStepQuestionAnswerTestCase(WorkshopTestCase):
    """Test cases for the ``WorkshopStepQuestionAnswer`` model"""

    def test_answer_is_selected(self):
        """Test the ``is_selected`` method"""

        first_answer = self.workshop_question_mc.answers.first()

        # The trainee didn't select this answer
        self.assertFalse(
            first_answer.is_selected(
                trainee=self.trainee,
                training_session=self.training_session
            )
        )

        # Mark this answer as "selected"
        WorkshopStepQuestionUserAnswerFactory(
            user=self.trainee,
            training_session=self.training_session,
            question=self.workshop_question_mc,
            answer=first_answer
        )

        # The trainee did select our answer
        self.assertTrue(
            first_answer.is_selected(
                trainee=self.trainee,
                training_session=self.training_session
            )
        )

    def test_session_rate(self):
        """Test the ``session_rate`` method"""

        first_answer = self.workshop_question_mc.answers.first()
        last_answer = self.workshop_question_mc.answers.last()

        # The rate on the answer must be 0, since we don't have any user answers yet
        self.assertEqual(
            first_answer.session_rate(self.training_session),
            0
        )

        # The trainee select the answer
        WorkshopStepQuestionUserAnswerFactory(
            user=self.trainee,
            training_session=self.training_session,
            question=self.workshop_question_mc,
            answer=first_answer
        )

        # The rate on the answer must be 100 (the trainee is alone in this session)
        self.assertEqual(
            first_answer.session_rate(self.training_session),
            100
        )

        # If we add another trainee, without an answer, the rate must still be 100
        self.training_session.users.add(self.trainees[2])
        self.assertEqual(
            first_answer.session_rate(self.training_session),
            100
        )

        # The second trainee select the last answer
        WorkshopStepQuestionUserAnswerFactory(
            user=self.trainees[2],
            training_session=self.training_session,
            question=self.workshop_question_mc,
            answer=last_answer
        )

        # The rates are correct on both the answers
        self.assertEqual(
            first_answer.session_rate(self.training_session),
            50
        )
        self.assertEqual(
            last_answer.session_rate(self.training_session),
            50
        )
