"""This module holds the tests for the views of the ``assessments`` app"""

from bs4 import BeautifulSoup

from waffle.models import Switch

from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.common.tests.waffle_test_utils import override_switch
from mtp.apps.roleplays.factories import (
    AssessingRoleplayFactory,
    StepFactory,
)
from mtp.apps.roleplays.models import Criterion, Roleplay
from mtp.apps.trainings.factories import TrainingFactory
from mtp.apps.workshops.factories import (
    AssessingWorkshopFactory,
    WorkshopStepQuestionAnswerFactory,
    WorkshopStepQuestionUserAnswerFactory
)
from mtp.apps.workshops.models import WorkshopStepQuestion
from mtp.discrete_apps.lockable.models import Lockable
from mtp.discrete_apps.lockable.utils import _no_fields_locking

from ..factories.certifications import (
    CertificationCompetencyThresholdFactory,
    CertificationProgramFactory,
    CertificationProgramUserStatusFactory,
    CertificationStatusFactory,
)
from ..factories.competencies import (
    CompetencyFactory,
    CompetencyTreeUserScoreFactory,
    CompetencyUserScoreFactory,
    DimensionFactory,
    DimensionUserScoreFactory,
)
from ..factories.recommendations import (
    CurriculumFactory,
    LessonFactory,
    LessonDimensionWeightFactory,
    LessonRecommendationThresholdFactory,
    RecommendationLevelFactory,
    TopicFactory,
)

from ..models import (
    CompetencyTree,
    CompetencyTreeUserScore,
    CompetencyUserScore,
    DimensionUserScore,
)


class MTPBaseAssessmentTestCase(MTPTestCase):
    """Base test case to tests assessments.

    It provides some default test data useful in a lot of tests.

    """
    JOB_PROFILE_NAME = 'Techies'

    def _parse_url(self, url):
        """Helper: load the specified URL, parse the response with BeautifulSoup."""

        response = self.client.get(url, follow=True)
        self.assertEqual(response.status_code, 200)
        return BeautifulSoup(response.content)

    @classmethod
    def _record_global_score(cls, score):
        """DRYness helper method."""

        CompetencyTreeUserScore.objects.filter(
            tree=cls.competency_tree,
            user=cls.trainee,
            session=cls.training_session,
        ).delete()

        return CompetencyTreeUserScoreFactory(
            tree=cls.competency_tree,
            user=cls.trainee,
            session=cls.training_session,
            score=score,
        )

    @classmethod
    def _record_competency_score(cls, competency, score):
        """DRYness helper method."""

        return CompetencyUserScoreFactory(
            competency=competency,
            user=cls.trainee,
            session=cls.training_session,
            score=score,
        )

    @classmethod
    def _record_competency_scores(cls, scores):
        """DRYness helper method."""

        CompetencyUserScore.objects.filter(
            competency__tree=cls.competency_tree,
            session=cls.training_session,
            user=cls.trainee,
        ).delete()

        return [
            cls._record_competency_score(competency, scores[i])
            for i, competency in enumerate(cls.competencies)
        ]

    @classmethod
    def _record_dimension_score(cls, dimension, score):
        """DRYness helper method."""

        return DimensionUserScoreFactory(
            dimension=dimension,
            user=cls.trainee,
            session=cls.training_session,
            score=score,
        )

    @classmethod
    def _record_dimension_scores(cls, scores):
        """DRYness helper method."""

        DimensionUserScore.objects.filter(
            dimension__competency__tree=cls.competency_tree,
            session=cls.training_session,
            user=cls.trainee,
        ).delete()

        return [
            cls._record_dimension_score(dimension, scores[i])
            for i, dimension in enumerate(cls.dimensions)
        ]

    @classmethod
    def _compute_certification_status(cls):
        """DRYness helper method."""

        return cls.certification_program.compute_trainee_status(
            cls.trainee, cls.training_session,
        )

    @classmethod
    def _compute_certification_status_name(cls):
        """DRYness helper method."""

        return cls._compute_certification_status().status.name

    @classmethod
    def _unpack_recommendations(cls, recommendations):
        """DRYness helper method."""

        return [(rec.lesson.name, rec.level.name) for rec in recommendations]

    @classmethod
    def _compute_recommendations(cls, unpack=True):
        """DRYness helper method."""

        recommendations = cls.curriculum.compute_trainee_recommendations(
            cls.trainee, cls.training_session
        )
        if unpack:
            return cls._unpack_recommendations(recommendations)
        else:
            return recommendations

    @classmethod
    def _compute_recommendation_levels(cls):
        """DRYness helper method."""

        return [level_name for __, level_name in cls._compute_recommendations()]

    @classmethod
    def setUpTestData(cls):

        super().setUpTestData()

        # We need to enable the ``assessments`` feature with a Waffle switch.
        Switch.objects.update_or_create(name='enable_assessments', defaults={
            'active': True,
        })

        # We need a job profile for our training and trainee
        with cls.training.override_version_state(cls.training.VERSION_STATES.DRAFT):
            cls.job_profile.name = cls.JOB_PROFILE_NAME
            cls.job_profile.save()

        for trainee in cls.trainees:
            trainee.job_profile = cls.job_profile
            trainee.save()
            cls.training_session.users.add(trainee)

        # We had another training linked to the same job profile, just because
        # it should work.
        another_training = TrainingFactory()
        another_training.job_profile = cls.job_profile
        another_training.save()

        # A competency tree should have been created for the job profile.
        # If not, this will fail.
        cls.competency_tree = CompetencyTree.objects.get(name=cls.JOB_PROFILE_NAME)


class MTPSimpleAssessmentTestCase(MTPBaseAssessmentTestCase):

    @classmethod
    def setUpTestData(cls):

        super().setUpTestData()

        # We add just two competencies...
        cls.competencies = [
            CompetencyFactory(
                name='C%d' % (i + 1),
                tree=cls.competency_tree,
                sort_order=(i + 1),
            )
            for i in range(2)
        ]

        # With two dimensions each.
        cls.dimensions = []
        for i, competency in enumerate(cls.competencies):
            cls.dimensions.extend([
                DimensionFactory(
                    name='C%dD%d' % ((i + 1), (j + 1)),
                    default_weight=1,
                    sort_order=(j + 1),
                    competency=competency,
                )
                for j in range(2)
            ])

        with _no_fields_locking(Lockable):
            # Now, just one quiz...
            cls.quiz = AssessingWorkshopFactory(
                training=cls.training,
                weight=1,
                sort_order=1
            )

            # ... with only one question...
            cls.question = WorkshopStepQuestion.objects.create(
                workshop=cls.quiz,
                is_assessing=True,
                activity=cls.quiz,
                is_multiple_choices=False
            )
            cls.question.dimensions.add(cls.dimensions[0])

            # ... and three answers.
            cls.answers = [
                WorkshopStepQuestionAnswerFactory(
                    question=cls.question,
                    sort_order=(i + 1),
                    label='answer-%d' % i,
                    points=(i + 1),
                )
                for i in range(3)
            ]


class MTPComplexAssessmentTestCase(MTPBaseAssessmentTestCase):

    @classmethod
    def setUpTestData(cls):

        super().setUpTestData()

        # Disable the lockable globally
        with _no_fields_locking(Lockable):

            # We add competencies...
            cls.competencies = [
                CompetencyFactory(name='Development', tree=cls.competency_tree, sort_order=10),
                CompetencyFactory(name='Teamwork', tree=cls.competency_tree, sort_order=20),
            ]
            cls.development_competency = cls.competencies[0]
            cls.teamwork_competency = cls.competencies[1]

            # ... and dimensions for each of them.
            cls.dimensions = [
                DimensionFactory(
                    name='Code', default_weight=3, sort_order=10,
                    competency=cls.development_competency,
                ),
                DimensionFactory(
                    name='Tests', default_weight=2, sort_order=20,
                    competency=cls.development_competency,
                ),
                DimensionFactory(
                    name='Documentation', default_weight=2, sort_order=30,
                    competency=cls.development_competency,
                ),
                DimensionFactory(
                    name='Respect', default_weight=3, sort_order=10,
                    competency=cls.teamwork_competency,
                ),
                DimensionFactory(
                    name='Reliability', default_weight=2, sort_order=20,
                    competency=cls.teamwork_competency,
                ),
                DimensionFactory(
                    name='Autonomy', default_weight=1, sort_order=30,
                    competency=cls.teamwork_competency,
                ),
            ]

            # Now our training needs assessing activities!

            # First, a quiz. With 6 questions (one per dimension), each one
            # having 3 possible answers.
            cls.quiz = AssessingWorkshopFactory(
                training=cls.training,
                weight=3,
                sort_order=10
            )

            cls.quiz_questions = []
            for idx, dimension in enumerate(cls.dimensions):
                question = WorkshopStepQuestion.objects.create(
                    workshop=cls.quiz,

                    is_assessing=True,
                    activity=cls.quiz,

                    # The first 3 questions are single-choice,
                    # the next 3 are multiple-choice.
                    is_multiple_choices=idx > 2,
                )
                question.dimensions.add(dimension)
                cls.quiz_questions.append(question)

            cls.quiz_answers = []
            for i, question in enumerate(cls.quiz_questions):
                for j in range(3):
                    answer = WorkshopStepQuestionAnswerFactory(
                        question=question,
                        sort_order=i,

                        label='answer-%d-%d' % (i, j),

                        # Answers are worth (0, 1, 2) points for the first question,
                        # (3, 4, 5) points, for the second one, and so on...
                        points=i * 3 + j,

                        # The third answer is the wrong one.
                        is_correct=True if j % 3 else False

                    )
                    cls.quiz_answers.append(answer)

            # Then, a roleplay. With 6 criteria, one per dimension as well, split into 2 steps.
            cls.roleplay = AssessingRoleplayFactory(
                training=cls.training, weight=2, sort_order=20,
                criteria_rating_scale=6,
                criteria_rating_type=Roleplay.RATING_TYPES.LETTERS,
                rating_scale={
                    'A': 100, 'B': 90, 'C': 75, 'D': 60, 'E': 45, 'F': 30
                },
            )
            cls.roleplay_steps = StepFactory.create_batch(2, roleplay=cls.roleplay)

            cr_types = Criterion.CRITERION_TYPES
            cls.roleplay_criteria = []
            for i, step in enumerate(cls.roleplay_steps):
                for j in range(3):
                    criterion = Criterion.objects.create(
                        step=step,

                        name='crit-%d-%d' % (i, j),
                        description='desc crit-%d-%d' % (i, j),

                        is_assessing=True,
                        activity=cls.roleplay,

                        # The first two criteria are quantitative,
                        # the third one is qualitative.
                        criterion_type=(cr_types.QUALITATIVE if j == 2
                                            else cr_types.QUANTITATIVE),
                    )
                    cls.roleplay_criteria.append(criterion)

            for idx, criterion in enumerate(cls.roleplay_criteria):
                criterion.dimensions.add(cls.dimensions[idx])

            # OK. We have activities. Time to add a certification program!
            cls.certification_program = CertificationProgramFactory(
                tree=cls.competency_tree)

            # We create three certification statuses.
            # Each one has a global threshold, and a threshold for
            # each of the competencies defined above.
            cls.certification_statuses = []
            for i in range(1, 4):
                name = 'Level %d' % i
                threshold = 25 * i  # 25%, 50%, 75%
                certification_status = CertificationStatusFactory(
                    name=name,
                    program=cls.certification_program,
                    global_threshold=threshold,
                )
                cls.certification_statuses.append(certification_status)
                for competency in cls.competencies:
                    CertificationCompetencyThresholdFactory(
                        status=certification_status,
                        competency=competency,
                        threshold=threshold,
                    )

            # How about some recommendations now?
            cls.curriculum = CurriculumFactory(job_profile=cls.job_profile)

            cls.LEVEL_MANDATORY = 'Mandatory'
            cls.LEVEL_RECOMMENDED = 'Recommended'
            cls.LEVEL_OPTIONAL = 'Optional'
            cls.LEVEL_NAMES = (
                cls.LEVEL_MANDATORY,
                cls.LEVEL_RECOMMENDED,
                cls.LEVEL_OPTIONAL,
            )
            cls.levels = [RecommendationLevelFactory(name=name)
                for name in cls.LEVEL_NAMES]

            cls.topic = TopicFactory(curriculum=cls.curriculum)

            cls.LESSON_NAMES = (
                "Web development 101",
                "Best practices for today's developers",
                "Understanding diversity in a development team",
            )
            cls.lessons = [LessonFactory(name=name, curriculum=cls.curriculum)
                for name in cls.LESSON_NAMES]

            # Recommendation data...
            recommendation_data = [

                # For "Web development 101"
                {
                    'dimensions': [
                        {'dimension': cls.dimensions[0], 'weight': 3},
                        {'dimension': cls.dimensions[1], 'weight': 1},
                        {'dimension': cls.dimensions[2], 'weight': 1},
                    ],
                },

                # For "Best practices for today's developers"
                {
                    'dimensions': [
                        {'dimension': cls.dimensions[0], 'weight': 1},
                        {'dimension': cls.dimensions[1], 'weight': 3},
                        {'dimension': cls.dimensions[2], 'weight': 2},
                    ],
                },

                # For "Understanding diversity in a development team"
                {
                    'dimensions': [
                        {'dimension': cls.dimensions[3], 'weight': 5},
                        {'dimension': cls.dimensions[4], 'weight': 1},
                        {'dimension': cls.dimensions[5], 'weight': 1},
                    ],
                },
            ]

            # For each lesson, we create...
            for lesson, rec_data in zip(cls.lessons, recommendation_data):
                dimensions_data = rec_data.get('dimensions')

                for i in range(3):
                    dimension_data = dimensions_data[i]

                    # ... dimension weights...
                    LessonDimensionWeightFactory(
                        lesson=lesson,
                        dimension=dimension_data.get('dimension'),
                        weight=dimension_data.get('weight'),
                    )

                    # ... and recommendation thresholds.
                    LessonRecommendationThresholdFactory(
                        lesson=lesson,
                        level=cls.levels[i],
                        threshold=(i + 1) * 25,
                    )

        # We should now be ready to TEST ALL THE THINGS!


class RecordScoreTestCase(MTPComplexAssessmentTestCase):
    """A test case for `WorkshopStepQuestionUserAnswer` model based on `BaseWorkshopTestCase`
    and `MTPTestCase`.

    """

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        cls.question = cls.quiz.steps.instance_of(WorkshopStepQuestion).first()

        training = cls.question.workshop.training
        with training.override_version_state(training.VERSION_STATES.DRAFT):

            cls.good_answers = cls.question.answers.filter(is_correct=True)
            cls.bad_answers = cls.question.answers.filter(is_correct=False)

    def _record_user_answer(self, answer):
        """Allow to record an user answer.

        Parameters
        ----------
        answer: WorkshopStepQuestionAnswer
            The answer of default trainee.

        Returns
        -------
        int
            Return user score for the answer question.

        """

        with override_switch('enable_assessments', active=True):

            WorkshopStepQuestionUserAnswerFactory(
                question=answer.question,
                answer=answer,
                user=self.trainee,
                training_session=self.training_session
            )

            return self.question.user_scores.get().points


class CertificationResultsTestCase(MTPComplexAssessmentTestCase):
    """Test case to test the certification results."""

    def setUp(self):
        """Set up."""

        super().setUp()

        self.user_certifications = CertificationProgramUserStatusFactory.create_batch(
            5,
            program=self.certification_program,
            status=self.certification_statuses[0],
            session=self.training_session
        )
