"""This module holds the tests for the models of the ``evaluations`` app."""

import os
from collections import defaultdict

from numpy import NaN
from pandas import Series
from pandas.util import testing as pdt

from django.conf import settings
from django.utils.translation import override, ugettext_lazy as _

from mtp.apps.accounts.constants import INTERNAL_GROUP_TRAINER, INTERNAL_GROUP_TRAINEE
from mtp.apps.accounts.models import Group
from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.data_export.constants import EXCEL_STYLE_CHOICES
from mtp.apps.data_export.exporters import ExcelDataExporter

from ..factories import CriterionFactory, EvaluationFactory, ExerciseFactory, RatingFactory
from ..models import Criterion, EvaluationGroupPermission

from .test_cases import EvaluationTestCase


class EvaluationModelTestCase(MTPTestCase):
    """Test for the ``Evaluation`` model"""

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

        super().setUpTestData()

        # Keep track of the groups
        cls.trainer_group = Group.objects.get(name=INTERNAL_GROUP_TRAINER)
        cls.trainee_group = Group.objects.get(name=INTERNAL_GROUP_TRAINEE)

        # New evaluation
        cls.evaluation = EvaluationFactory()

    def get_evaluation_permissions(self, evaluation):
        """Shortcut method to get the evaluation permissions as a un dict"""

        permissions_by_group = defaultdict(list)
        for permission in EvaluationGroupPermission.objects.filter(evaluation=evaluation):
            permissions_by_group[permission.group].append(permission.permission)

        return permissions_by_group

    def test_save_evaluation_should_correctly_set_the_roles(self):
        """test the save method from the evaluation"""

        # Per default, the evaluation is set as ``MAY_EVALUATE_OTHERS``
        self.assertEqual(self.evaluation.evaluation_type,
                         self.evaluation.EVALUATION_TYPES.MAY_EVALUATE_OTHERS)

        perms_list = EvaluationGroupPermission.PERMISSION_TYPES
        evaluations_perms = self.get_evaluation_permissions(self.evaluation)

        # We got the correct permissions for both the trainers, trainees
        self.assertCountEqual(
            evaluations_perms[self.trainer_group],
            [
                perms_list.MAY_MODERATE,
                perms_list.MAY_EVALUATE_SELF,
                perms_list.MAY_EVALUATE_OTHERS
            ]
        )
        self.assertCountEqual(
            evaluations_perms[self.trainee_group],
            [perms_list.MAY_EVALUATE_OTHERS, perms_list.MAY_PARTICIPATE]
        )

        # Change the evaluation with ``MAY_EVALUATE_SELF`` flag
        self.evaluation.evaluation_type = self.evaluation.EVALUATION_TYPES.MAY_EVALUATE_SELF
        self.evaluation.save()

        evaluations_perms = self.get_evaluation_permissions(self.evaluation)

        # We got the correct permissions for both the trainers, trainees
        self.assertCountEqual(
            evaluations_perms[self.trainer_group],
            [
                perms_list.MAY_MODERATE,
                perms_list.MAY_EVALUATE_SELF,
                perms_list.MAY_EVALUATE_OTHERS
            ]
        )
        self.assertCountEqual(
            evaluations_perms[self.trainee_group],
            [perms_list.MAY_EVALUATE_SELF, perms_list.MAY_PARTICIPATE]
        )

        # Change the evaluation with ``MAY_EVALUATE_ALL`` flag
        self.evaluation.evaluation_type = self.evaluation.EVALUATION_TYPES.MAY_EVALUATE_ALL
        self.evaluation.save()

        evaluations_perms = self.get_evaluation_permissions(self.evaluation)

        # We got the correct permissions for both the trainers, trainees
        self.assertCountEqual(
            evaluations_perms[self.trainer_group],
            [
                perms_list.MAY_MODERATE,
                perms_list.MAY_EVALUATE_SELF,
                perms_list.MAY_EVALUATE_OTHERS
            ]
        )
        self.assertCountEqual(
            evaluations_perms[self.trainee_group],
            [
                perms_list.MAY_EVALUATE_SELF,
                perms_list.MAY_EVALUATE_OTHERS,
                perms_list.MAY_PARTICIPATE
            ]
        )


class CriterionModelTestCase(EvaluationTestCase):
    """Test case for the ``Criterion`` model"""

    def test_get_session_user_texts_should_return_the_textual_ratings(self):
        """Test that the ``get_session_user_texts`` should return the textual ratings"""

        # At first, no textual ratings
        self.assertIsNone(
            self.qualitative_criteria[0].get_session_user_texts(
                self.training_session,
                self.trainee
            )
        )

        # New textual rating on this criterion
        RatingFactory(
            criterion=self.qualitative_criteria[0],
            session=self.training_session,
            trainee=self.trainee,
            text='michel'
        )

        self.assertEqual(
            self.qualitative_criteria[0].get_session_user_texts(
                self.training_session,
                self.trainee
            ),
            'michel'
        )

        # Another rating !
        RatingFactory(
            criterion=self.qualitative_criteria[0],
            session=self.training_session,
            trainee=self.trainee,
            text='jean'
        )

        self.assertEqual(
            self.qualitative_criteria[0].get_session_user_texts(
                self.training_session,
                self.trainee
            ),
            'jean\n + michel'
        )

    def test_get_session_user_texts_should_raise_if_called_on_quantitative_criterion(self):
        """Test that the ``get_session_user_texts`` should raise if wrong type of criterion"""

        with self.assertRaisesMessage(ValueError,
                                      'Cannot have texts if criterion is not a "QUALITATIVE" one'):
            self.quantitative_criteria[0].get_session_user_texts(
                self.training_session,
                self.trainee
            )

    def test_get_session_user_score_should_return_the_average_of_the_scores(self):
        """Test that the ``get_session_user_score`` should return the average of the scores"""

        # At first, no numeric ratings
        self.assertIsNone(
            self.quantitative_criteria[0].get_session_user_score(
                self.training_session,
                self.trainee
            )
        )

        # New numeric rating on this criterion
        RatingFactory(
            criterion=self.quantitative_criteria[0],
            session=self.training_session,
            trainee=self.trainee,
            score=1
        )

        self.assertEqual(
            self.quantitative_criteria[0].get_session_user_score(
                self.training_session,
                self.trainee
            ),
            1
        )

        # Another rating for this criterion
        RatingFactory(
            criterion=self.quantitative_criteria[0],
            session=self.training_session,
            trainee=self.trainee,
            score=2
        )

        self.assertEqual(
            self.quantitative_criteria[0].get_session_user_score(
                self.training_session,
                self.trainee
            ),
            1.5
        )

    def test_get_session_user_score_should_raise_if_wrong_type_of_criterion(self):
        """Test that the ``get_session_user_score`` should raise if wrong type of criterion"""

        with self.assertRaisesMessage(ValueError,
                                      'Cannot have a rating if criterion is not a '
                                      '"QUANTITATIVE" one'):
            self.qualitative_criteria[0].get_session_user_score(
                self.training_session,
                self.trainee
            )


class EvaluationResultsExcelDataTestCase(MTPTestCase):
    """Test case for the ``get_results_excel_data`` method on the ``Evaluation`` model"""

    @classmethod
    def setUpTestData(cls):
        """Create a new evaluation with some data"""

        super().setUpTestData()

        cls.evaluation = EvaluationFactory(training=cls.training)
        cls.exercise = ExerciseFactory(evaluation=cls.evaluation)
        cls.qualitative_criterion = CriterionFactory(
            exercise=cls.exercise,
            criterion_type=Criterion.CRITERION_TYPES.QUALITATIVE
        )
        cls.quantitative_criterion = CriterionFactory(
            exercise=cls.exercise,
            criterion_type=Criterion.CRITERION_TYPES.QUANTITATIVE
        )

    @staticmethod
    def get_user_index_name(user):
        """Shortcut method to get the user index name

        Parameters
        ----------
        user: MTPUser
            The user we want the index name for

        Returns
        -------
        str

        """

        return '%(last_name)s %(first_name)s (%(username)s)' % {
            'last_name': user.last_name,
            'first_name': user.first_name,
            'username': user.username
        }

    @override('en')
    def check_data_frame_values(self, data_frame, scores, mean_points, max_ranges, totals):
        """Shortcut method to check the content of the provided data frame

        Parameters
        ----------
        data_frame: pandas.DataFrame
            The data frame we want to check
        scores: dict
            A dict containing the expected scores by trainee
        mean_points: list
            The list of expected values for the "Mean" index
        max_ranges: list
            The list of expected values for the "Maximum range" index
        totals: list
            The list of expected values for the "Total of ratings" index

        """

        for trainee, score in scores.items():
            self.assertEqual(list(data_frame.loc[trainee].values), score)

        self.assertEqual(list(data_frame.loc[str(_('Mean'))].values), mean_points)
        self.assertEqual(list(data_frame.loc[str(_('Maximum range'))].values), max_ranges)
        self.assertEqual(list(data_frame.loc[str(_('Total of ratings'))].values), totals)

    def test_is_exportable(self):
        """Test that the ``is_exportable`` method is working"""

        # Set the training in DRAFT mode
        self.training.version_state = self.training.VERSION_STATES.DRAFT
        self.training.save()

        # We soft-delete all the exercises on the evaluation
        for exercise in self.evaluation.exercises.all():
            exercise.remove()

        # We shouldn't be able to export
        self.assertFalse(self.evaluation.is_exportable(self.training_session))

        # Add one exercise, without criteria
        exercise = ExerciseFactory(evaluation=self.evaluation)

        # We shouldn't be able to export
        self.assertFalse(self.evaluation.is_exportable(self.training_session))

        # Add one criterion
        qualitative_criterion = CriterionFactory(
            exercise=exercise,
            criterion_type=Criterion.CRITERION_TYPES.QUALITATIVE
        )

        # We shouldn't be able to export
        self.assertFalse(self.evaluation.is_exportable(self.training_session))

        # Finally, add one rating for this criterion
        rating = RatingFactory(
            trainee=self.trainee,
            session=self.training_session,
            criterion=qualitative_criterion,
            text='test'
        )

        # We should be able to export
        self.assertTrue(self.evaluation.is_exportable(self.training_session))

        # If we soft-delete the rating, we shouldn't be able to export
        rating.remove()

        self.assertFalse(self.evaluation.is_exportable(self.training_session))

        # If we soft-delete our criterion, we shouldn't be able to export
        rating.unremove()
        qualitative_criterion.remove()

        self.assertFalse(self.evaluation.is_exportable(self.training_session))

        # If we soft-delete our exercise, we shouldn't be able to export
        qualitative_criterion.unremove()
        exercise.remove()

        self.assertFalse(self.evaluation.is_exportable(self.training_session))

    def test_evaluation_get_results_excel_data_header_styles(self):
        """Test the "header_styles" from the ``get_results_excel_data`` of the ``Evaluation``"""

        header_styles = self.evaluation.get_results_excel_data(
            self.training_session
        )['header_styles']

        expected_header_styles = [
            EXCEL_STYLE_CHOICES.WORKSHOP_STEP
        ] + [EXCEL_STYLE_CHOICES.DEFAULT] * 2

        # One style for each exercise + one style for each criterion
        self.assertEqual(header_styles, expected_header_styles)

    @override('en')
    def test_evaluation_get_results_excel_data(self):
        """Test the "data" key from the ``get_results_excel_data`` of the ``Evaluation`` model"""

        data_frame = self.evaluation.get_results_excel_data(self.training_session)['data']

        # Check the indexes of the data frame
        indexes = [
            self.get_user_index_name(self.trainee),
            str(_('Mean')),
            str(_('Maximum range')),
            str(_('Total of ratings'))
        ]
        self.assertEqual(list(data_frame.index.values), indexes)

        # Check the columns of the data frame
        expected_columns = [
            '%s (%s)' % (self.exercise.name, str(_('Mean'))),
            '%d - %s' % (1, self.qualitative_criterion.description),
            '%d - %s' % (2, self.quantitative_criterion.description)
        ]

        self.assertEqual(list(data_frame.columns.values), expected_columns)

        # New rating for the qualitative criterion
        RatingFactory(
            trainee=self.trainee,
            session=self.training_session,
            criterion=self.qualitative_criterion,
            text='test'
        )

        # New rating for the quantitative criterion
        RatingFactory(
            trainee=self.trainee,
            session=self.training_session,
            criterion=self.quantitative_criterion,
            score=1
        )

        # Check the values
        data_frame = self.evaluation.get_results_excel_data(self.training_session)['data']
        self.check_data_frame_values(
            data_frame,
            {
                self.get_user_index_name(self.trainee): [1, 'test', 1]
            },
            [1, NaN, 1],
            [5.0, NaN, 5.0],
            [1.0, 1.0, 1.0]
        )

        # Another trainee
        self.training_session.users.add(self.trainees[1])

        # Ratings for this trainee
        RatingFactory(
            trainee=self.trainees[1],
            session=self.training_session,
            criterion=self.qualitative_criterion,
            text='test1'
        )
        RatingFactory(
            trainee=self.trainees[1],
            session=self.training_session,
            criterion=self.quantitative_criterion,
            score=2
        )

        data_frame = self.evaluation.get_results_excel_data(self.training_session)['data']
        self.check_data_frame_values(
            data_frame,
            {
                self.get_user_index_name(self.trainee): [1, 'test', 1],
                self.get_user_index_name(self.trainees[1]): [2, 'test1', 2]
            },
            [1.5, NaN, 1.5],
            [5.0, NaN, 5.0],
            [2.0, 2.0, 2.0]
        )

    def test_criteria_with_same_name(self):
        """Test that we should be able to export if we got some duplicated criteria names"""

        # New exercise with the same name and criteria with the same name
        new_exercise = ExerciseFactory(evaluation=self.evaluation, name=self.exercise.name)
        CriterionFactory(
            exercise=new_exercise,
            criterion_type=Criterion.CRITERION_TYPES.QUALITATIVE,
            description=self.qualitative_criterion.description
        )
        CriterionFactory(
            exercise=new_exercise,
            criterion_type=Criterion.CRITERION_TYPES.QUANTITATIVE,
            description=self.quantitative_criterion.description
        )

        # We should be able to export (this will raise if this not the case)
        self.evaluation.get_results_excel_data(self.training_session)

    def test_export_excel_from_data_results(self):
        """Test that exporting an excel file from the ``get_data_results`` should work"""

        path = os.path.join(settings.MEDIA_ROOT, 'test.xlsx')
        exporter = ExcelDataExporter(path)
        results = self.evaluation.get_results_excel_data(self.training_session)
        results['sheet_name'] = 'test'

        exporter.add_sheet(**results)
        exporter.save()

        self.assertTrue(os.path.isfile(path))


class ExercisesResultsExcelDataTestCase(EvaluationTestCase):
    """Test case for the ``get_results_excel_data`` method on the ``Exercise`` model"""

    def get_series_and_styles(self, exercise):
        """Shortcut method to get the data and the styles as two distinct lists

        Parameters
        ----------
        exercise: Exercise
            The exercise we want the results for

        Returns
        -------
        tuple
            A tuple with two entries:
            - series_list: A list of pandas Series
            - styles: A list of ``EXCEL_STYLE_CHOICES``

        """

        results = exercise.get_results_excel_data(self.training_session)
        series_list, styles = map(list, zip(*results))  # pylint: disable=bad-builtin

        return series_list, styles

    def check_series(self, series_to_check, expected_series):
        """Shortcut method to check that two list of series are equals

        Parameters
        ----------
        series_to_check: list
            A list of ``pandas.Series`` we to check
        expected_series: list
            A list of ``pandas.Series`` to check against

        """

        for index, series in enumerate(series_to_check):
            with self.subTest(series.name):
                pdt.assert_series_equal(series, expected_series[index])

    def test_exercise_get_results_excel_data_for_quantitative_criteria(self):
        """Test the ``get_results_excel_data`` output for the quantitative criteria"""

        # One trainee, no ratings given
        series_list, __ = self.get_series_and_styles(self.exercises[0])

        # Test the results for the quantitative criteria (the last 6)
        expected_quantitative_series = [
            Series(
                [NaN, 0, 5],  # No numeric rating for the user, number of ratings, max range
                name='%d - %s' % (index, criterion.description)
            )
            for index, criterion in enumerate(
                self.exercises[0].criteria.quantitative().non_deleted(),
                start=7
            )
        ]
        self.check_series(series_list[6:], expected_quantitative_series)

        # Add a numeric rating for the first trainee, first qualitative criterion
        first_quantitative_criterion = self.exercises[0].criteria.quantitative().first()

        RatingFactory(
            trainee=self.trainee,
            session=self.training_session,
            criterion=first_quantitative_criterion,
            score=1
        )

        expected_quantitative_series[0] = Series(
            [1, 1.0, 5],
            name='%d - %s' % (7, first_quantitative_criterion.description)
        )
        series_list, __ = self.get_series_and_styles(self.exercises[0])
        self.check_series(series_list[6:], expected_quantitative_series)

        # Another rating for the same user, same criterion
        RatingFactory(
            trainee=self.trainee,
            session=self.training_session,
            criterion=first_quantitative_criterion,
            score=2
        )

        expected_quantitative_series[0] = Series(
            [1.5, 2.0, 5],
            name='%d - %s' % (7, first_quantitative_criterion.description)
        )

        series_list, __ = self.get_series_and_styles(self.exercises[0])
        self.check_series(series_list[6:], expected_quantitative_series)

    def test_exercise_get_results_excel_data_for_qualitative_criteria(self):
        """Test the ``get_results_excel_data`` output for the qualitative criteria"""

        # One trainee, no ratings given
        series_list, __ = self.get_series_and_styles(self.exercises[0])

        # Test the results for the qualitative criteria (the first 6)
        expected_qualitative_series = [
            Series(
                ['', 0, NaN],  # No textual rating for the user, number of ratings, no max range
                name='%d - %s' % (index, criterion.description)
            )
            for index, criterion in enumerate(
                self.exercises[0].criteria.qualitative().non_deleted(),
                start=1
            )
        ]
        self.check_series(series_list[:5], expected_qualitative_series)

        # Add a textual rating for the first trainee, first qualitative criterion
        first_qualitative_criterion = self.exercises[0].criteria.qualitative().first()

        RatingFactory(
            trainee=self.trainee,
            session=self.training_session,
            criterion=first_qualitative_criterion,
            text='test'
        )

        expected_qualitative_series[0] = Series(
            ['test', 1, NaN],
            name='%d - %s' % (1, first_qualitative_criterion.description)
        )
        series_list, __ = self.get_series_and_styles(self.exercises[0])
        self.check_series(series_list[:5], expected_qualitative_series)
