"""This modules holds certifications tests for the ``assessments`` app."""

from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse

from mtp.apps.assessments.factories.competencies import CompetencyUserScoreFactory

from ..forms import CertificationThresholdsForm
from ..models import CertificationStatus, CertificationCompetencyThreshold
from .test_cases import MTPComplexAssessmentTestCase


class CertificationThresholdsViewTestCase(MTPComplexAssessmentTestCase):
    """Test case that tests the certification program cms view."""

    def test_thresholds_form(self):

        self.login_as_admin()

        # First we load and parse the page.
        url = reverse(
            'assessments:certification-program', kwargs={
                'jobprofile_uuid': self.job_profile.id
            }
        )
        html = self._parse_url(url)

        # We check that all the expected form fields are present and properly
        # initialized.
        expected_fields = [
            {
                'name': 'global_%s' % status.id,
                'value': status.global_threshold,
            }
            for status in self.certification_statuses
        ]
        for competency in self.competencies:
            expected_fields.extend([
                {
                    'name': '%s_%s' % (competency.id, status.id),
                    'value': 25 * (i + 1),
                }
                for i, status in enumerate(self.certification_statuses)
            ])

        for field in expected_fields:
            expected_name = field.get('name')
            expected_value = field.get('value')
            actual_field = html.find('input', attrs={
                'type': 'number',
                'name': expected_name,
                'value': expected_value,
            })
            self.assertIsNotNone(actual_field)

        # Now we POST sensible values through the form.
        post_data = {
            field.get('name'): (5 * i)
            for i, field in enumerate(expected_fields)
        }
        response = self.client.post(url, post_data, follow=True)
        self.assertEqual(response.status_code, 200)

        # And we check that everything was recorded properly.
        for name, threshold in post_data.items():

            competency_id, status_id = name.split('_')

            # Trying to ``get`` the objects is enough.
            # An exception will be raised if things are broken.
            if competency_id == CertificationThresholdsForm.GLOBAL_COMPETENCY_ID:
                CertificationStatus.objects.get(
                    pk=status_id,
                    global_threshold=threshold,
                )
            else:
                CertificationCompetencyThreshold.objects.get(
                    competency__id=competency_id,
                    status__id=status_id,
                    threshold=threshold,
                )


class CertificationComputationsTestCase(MTPComplexAssessmentTestCase):
    """Test case that tests the certification status computation."""

    def test_no_global_score(self):
        """Test that the proper exception is raised if there is no global score."""

        with self.assertRaises(RuntimeError):
            self._compute_certification_status()

    def test_insufficient_global_score(self):
        """Test that an insufficient global score is enough to earn no status."""

        self._record_global_score(10)
        status = self._compute_certification_status()
        self.assertIsNone(status)

        self._record_competency_scores([80, 80])
        status = self._compute_certification_status()
        self.assertIsNone(status)

    def test_missing_competency_score(self):
        """Test that a missing competency score prevents earning a status."""

        self._record_global_score(50)
        self._record_competency_score(self.competencies[0], 80)
        status = self._compute_certification_status()
        self.assertIsNone(status)

    def test_all_scores_ok(self):
        """Test that high enough scores yield the highest status."""

        self._record_global_score(80)
        self._record_competency_scores([80, 80])
        status = self._compute_certification_status()
        self.assertEqual(status.status.name, 'Level 3')
        self.assertEqual(status.score, 80)

    def test_one_insufficient_score(self):
        """Test that one lower score is enough to get a lower status."""

        self._record_global_score(75)
        self._record_competency_scores([80, 55])
        status = self._compute_certification_status()
        self.assertEqual(status.status.name, 'Level 2')
        self.assertEqual(status.score, 75)

    def test_one_very_insufficient_score(self):
        """Test that one even lower score is enough to get an even lower status."""

        self._record_global_score(60)
        self._record_competency_scores([80, 30])
        status = self._compute_certification_status()
        self.assertEqual(status.status.name, 'Level 1')
        self.assertEqual(status.score, 60)

    def test_one_abysmal_score(self):
        """Test that one extremely low score yields no status at all."""

        self._record_global_score(80)
        self._record_competency_scores([80, 5])
        status = self._compute_certification_status()
        self.assertIsNone(status)

    def test_recomputation(self):
        """Test that the certification is correctly updated during recomputation."""

        self._record_global_score(75)
        self._record_competency_scores([80, 55])

        old_status = self._compute_certification_status()
        self.assertEqual(old_status.status.name, 'Level 2')
        self.assertEqual(old_status.score, 75)

        # We force the certification status
        old_status.is_forced = True
        old_status.save()

        self._record_global_score(80)
        self._record_competency_scores([80, 80])

        new_status = self._compute_certification_status()
        # We check the new_status is the same object but updated
        self.assertEqual(new_status.id, old_status.id)
        self.assertEqual(new_status.status.name, 'Level 2')  # Status forced, it don't change.
        self.assertEqual(new_status.score, 80)

        # We unforce the certification status
        new_status.is_forced = False
        new_status.save()

        new_status = self._compute_certification_status()
        # We check the new_status is the same object but updated
        self.assertEqual(new_status.id, old_status.id)
        self.assertEqual(new_status.status.name, 'Level 3')
        self.assertEqual(new_status.score, 80)


class CertificationStatusReferenceTestCase(MTPComplexAssessmentTestCase):
    """Test case that tests the certification status reference."""

    @classmethod
    def setUpTestData(cls):
        """Database initialization."""
        super().setUpTestData()

        # We flag a certification status as reference
        cls.certification_status = cls.certification_statuses[1]
        cls.certification_status.flag_as_reference()

    def setUp(self):
        """Refresh ``certification_status`` from database."""
        super().setUp()

        self.certification_status.refresh_from_db()

    def test_reference_manager(self):
        """Test reference manager."""

        references = CertificationStatus.references
        self.assertEqual(references.count(), 1)
        self.assertEqual(references.get(), self.certification_status)

    def test_get_reference_status_from_program(self):
        """Test get the reference status from a certification program."""

        program = self.certification_program
        self.assertEqual(program.reference_status, self.certification_status)

        self.certification_status.unflag_as_reference()
        del program.reference_status  # Clear the cached property

        # We check that there is no reference certification status
        self.assertEqual(CertificationStatus.references.count(), 0)

        self.assertIsNone(program.reference_status)

    def test_get_reference_certification_threshold_from_competency_user_score(self):
        """Test get the reference certification threshold from a competency user score."""

        competency = self.competencies[0]
        user_score = CompetencyUserScoreFactory(competency=competency, user=self.trainee)

        self.assertEqual(
            user_score.reference_certification_threshold.status,
            self.certification_status
        )

        self.certification_status.unflag_as_reference()
        del user_score.reference_certification_threshold  # Clear the cached property

        # We check that there is no reference certification status
        self.assertEqual(CertificationStatus.references.count(), 0)

        self.assertIsNone(user_score.reference_certification_threshold)

    def test_the_reference_uniqueness_in_same_program(self):
        """Test the uniqueness during reference status marking in same program."""

        references_count = CertificationStatus.references.count()
        self.assertEqual(references_count, 1)
        self.assertTrue(self.certification_status.is_reference)

        # We mark a other status as reference in same program
        other_status_in_same_program = self.certification_statuses[0]
        other_status_in_same_program.flag_as_reference()

        references_count = CertificationStatus.references.count()
        self.assertEqual(references_count, 1)
        self.assertTrue(other_status_in_same_program.is_reference)

    def test_validate_reference_uniqueness(self):
        """Test the ``validate_reference_uniqueness`` method."""

        # We mark manually another status as reference
        other_status_in_same_program = self.certification_statuses[0]
        other_status_in_same_program.is_reference = True

        # Impossible to set two references status in same program
        with self.assertRaises(ValidationError):
            other_status_in_same_program.validate_reference_uniqueness()

        other_status_in_same_program.unflag_all_instances()

        # Now, its works !
        other_status_in_same_program.validate_reference_uniqueness()
