"""Tests for per-course verification status on the dashboard. """
import unittest
from datetime import datetime, timedelta

import ddt
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import override_settings
from mock import patch
from nose.plugins.attrib import attr
from pytz import UTC

from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from student.helpers import (
    VERIFY_STATUS_APPROVED,
    VERIFY_STATUS_MISSED_DEADLINE,
    VERIFY_STATUS_NEED_TO_REVERIFY,
    VERIFY_STATUS_NEED_TO_VERIFY,
    VERIFY_STATUS_RESUBMITTED,
    VERIFY_STATUS_SUBMITTED
)
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory


@attr(shard=3)
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
    """Tests for per-course verification status on the dashboard. """

    PAST = 'past'
    FUTURE = 'future'
    DATES = {
        PAST: datetime.now(UTC) - timedelta(days=5),
        FUTURE: datetime.now(UTC) + timedelta(days=5),
        None: None,
    }

    URLCONF_MODULES = ['verify_student.urls']

    def setUp(self):
        # Invoke UrlResetMixin
        super(TestCourseVerificationStatus, self).setUp()

        self.user = UserFactory(password="edx")
        self.course = CourseFactory.create()
        success = self.client.login(username=self.user.username, password="edx")
        self.assertTrue(success, msg="Did not log in successfully")
        self.dashboard_url = reverse('dashboard')

    def test_enrolled_as_non_verified(self):
        self._setup_mode_and_enrollment(None, "audit")

        # Expect that the course appears on the dashboard
        # without any verification messaging
        self._assert_course_verification_status(None)

    def test_no_verified_mode_available(self):
        # Enroll the student in a verified mode, but don't
        # create any verified course mode.
        # This won't happen unless someone deletes a course mode,
        # but if so, make sure we handle it gracefully.
        CourseEnrollmentFactory(
            course_id=self.course.id,
            user=self.user,
            mode="verified"
        )

        # Continue to show the student as needing to verify.
        # The student is enrolled as verified, so we might as well let them
        # complete verification.  We'd need to change their enrollment mode
        # anyway to ensure that the student is issued the correct kind of certificate.
        self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)

    def test_need_to_verify_no_expiration(self):
        self._setup_mode_and_enrollment(None, "verified")

        # Since the student has not submitted a photo verification,
        # the student should see a "need to verify" message
        self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)

        # Start the photo verification process, but do not submit
        # Since we haven't submitted the verification, we should still
        # see the "need to verify" message
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)

        # Upload images, but don't submit to the verification service
        # We should still need to verify
        attempt.mark_ready()
        self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)

    def test_need_to_verify_expiration(self):
        self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")
        response = self.client.get(self.dashboard_url)
        self.assertContains(response, self.BANNER_ALT_MESSAGES[VERIFY_STATUS_NEED_TO_VERIFY])
        self.assertContains(response, "You only have 4 days left to verify for this course.")

    @ddt.data(None, FUTURE)
    def test_waiting_approval(self, expiration):
        self._setup_mode_and_enrollment(self.DATES[expiration], "verified")

        # The student has submitted a photo verification
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()

        # Now the student should see a "verification submitted" message
        self._assert_course_verification_status(VERIFY_STATUS_SUBMITTED)

    @ddt.data(None, FUTURE)
    def test_fully_verified(self, expiration):
        self._setup_mode_and_enrollment(self.DATES[expiration], "verified")

        # The student has an approved verification
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.approve()

        # Expect that the successfully verified message is shown
        self._assert_course_verification_status(VERIFY_STATUS_APPROVED)

        # Check that the "verification good until" date is displayed
        response = self.client.get(self.dashboard_url)
        self.assertContains(response, attempt.expiration_datetime.strftime("%m/%d/%Y"))

    def test_missed_verification_deadline(self):
        # Expiration date in the past
        self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")

        # The student does NOT have an approved verification
        # so the status should show that the student missed the deadline.
        self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE)

    def test_missed_verification_deadline_verification_was_expired(self):
        # Expiration date in the past
        self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")

        # Create a verification, but the expiration date of the verification
        # occurred before the deadline.
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.approve()
        attempt.created_at = self.DATES[self.PAST] - timedelta(days=900)
        attempt.save()

        # The student didn't have an approved verification at the deadline,
        # so we should show that the student missed the deadline.
        self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE)

    def test_missed_verification_deadline_but_later_verified(self):
        # Expiration date in the past
        self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")

        # Successfully verify, but after the deadline has already passed
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.approve()
        attempt.created_at = self.DATES[self.PAST] - timedelta(days=900)
        attempt.save()

        # The student didn't have an approved verification at the deadline,
        # so we should show that the student missed the deadline.
        self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE)

    def test_verification_denied(self):
        # Expiration date in the future
        self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")

        # Create a verification with the specified status
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.deny("Not valid!")

        # Since this is not a status we handle, don't display any
        # messaging relating to verification
        self._assert_course_verification_status(None)

    def test_verification_error(self):
        # Expiration date in the future
        self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")

        # Create a verification with the specified status
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.status = "must_retry"
        attempt.system_error("Error!")

        # Since this is not a status we handle, don't display any
        # messaging relating to verification
        self._assert_course_verification_status(None)

    @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
    def test_verification_will_expire_by_deadline(self):
        # Expiration date in the future
        self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")

        # Create a verification attempt that:
        # 1) Is current (submitted in the last year)
        # 2) Will expire by the deadline for the course
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.approve()
        attempt.save()

        # Verify that learner can submit photos if verification is set to expire soon.
        self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY)

    @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
    def test_reverification_submitted_with_current_approved_verificaiton(self):
        # Expiration date in the future
        self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")

        # Create a verification attempt that is approved but expiring soon
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.approve()
        attempt.save()

        # Verify that learner can submit photos if verification is set to expire soon.
        self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY)

        # Submit photos for reverification
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()

        # Expect that learner has submitted photos for reverfication and his/her
        # previous verification is set to expired soon.
        self._assert_course_verification_status(VERIFY_STATUS_RESUBMITTED)

    def test_verification_occurred_after_deadline(self):
        # Expiration date in the past
        self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified")

        # The deadline has passed, and we've asked the student
        # to reverify (through the support team).
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()

        # Expect that the user's displayed enrollment mode is verified.
        self._assert_course_verification_status(VERIFY_STATUS_APPROVED)

    def test_with_two_verifications(self):
        # checking if a user has two verification and but most recent verification course deadline is expired

        self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified")

        # The student has an approved verification
        attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt.mark_ready()
        attempt.submit()
        attempt.approve()
        # Making created at to previous date to differentiate with 2nd attempt.
        attempt.created_at = datetime.now(UTC) - timedelta(days=1)
        attempt.save()

        # Expect that the successfully verified message is shown
        self._assert_course_verification_status(VERIFY_STATUS_APPROVED)

        # Check that the "verification good until" date is displayed
        response = self.client.get(self.dashboard_url)
        self.assertContains(response, attempt.expiration_datetime.strftime("%m/%d/%Y"))

        # Adding another verification with different course.
        # Its created_at is greater than course deadline.
        course2 = CourseFactory.create()
        CourseModeFactory.create(
            course_id=course2.id,
            mode_slug="verified",
            expiration_datetime=self.DATES[self.PAST]
        )
        CourseEnrollmentFactory(
            course_id=course2.id,
            user=self.user,
            mode="verified"
        )

        # The student has an approved verification
        attempt2 = SoftwareSecurePhotoVerification.objects.create(user=self.user)
        attempt2.mark_ready()
        attempt2.submit()
        attempt2.approve()
        attempt2.save()

        # Mark the attemp2 as approved so its date will appear on dasboard.
        self._assert_course_verification_status(VERIFY_STATUS_APPROVED)
        response2 = self.client.get(self.dashboard_url)
        self.assertContains(response2, attempt2.expiration_datetime.strftime("%m/%d/%Y"))
        self.assertEqual(response2.content.count(attempt2.expiration_datetime.strftime("%m/%d/%Y")), 2)

    def _setup_mode_and_enrollment(self, deadline, enrollment_mode):
        """Create a course mode and enrollment.

        Arguments:
            deadline (datetime): The deadline for submitting your verification.
            enrollment_mode (str): The mode of the enrollment.

        """
        CourseModeFactory.create(
            course_id=self.course.id,
            mode_slug="verified",
            expiration_datetime=deadline
        )
        CourseEnrollmentFactory(
            course_id=self.course.id,
            user=self.user,
            mode=enrollment_mode
        )
        VerificationDeadline.set_deadline(self.course.id, deadline)

    BANNER_ALT_MESSAGES = {
        VERIFY_STATUS_NEED_TO_VERIFY: "ID verification pending",
        VERIFY_STATUS_SUBMITTED: "ID verification pending",
        VERIFY_STATUS_APPROVED: "ID Verified Ribbon/Badge",
    }

    NOTIFICATION_MESSAGES = {
        VERIFY_STATUS_NEED_TO_VERIFY: [
            "You still need to verify for this course.",
            "Verification not yet complete"
        ],
        VERIFY_STATUS_SUBMITTED: ["You have submitted your verification information."],
        VERIFY_STATUS_RESUBMITTED: ["You have submitted your reverification information."],
        VERIFY_STATUS_APPROVED: ["You have successfully verified your ID with edX"],
        VERIFY_STATUS_NEED_TO_REVERIFY: ["Your current verification will expire soon."]
    }

    MODE_CLASSES = {
        None: "audit",
        VERIFY_STATUS_NEED_TO_VERIFY: "verified",
        VERIFY_STATUS_SUBMITTED: "verified",
        VERIFY_STATUS_APPROVED: "verified",
        VERIFY_STATUS_MISSED_DEADLINE: "audit",
        VERIFY_STATUS_NEED_TO_REVERIFY: "audit",
        VERIFY_STATUS_RESUBMITTED: "audit"
    }

    def _assert_course_verification_status(self, status):
        """Check whether the specified verification status is shown on the dashboard.

        Arguments:
            status (str): One of the verification status constants.
                If None, check that *none* of the statuses are displayed.

        Raises:
            AssertionError

        """
        response = self.client.get(self.dashboard_url)

        # Sanity check: verify that the course is on the page
        self.assertContains(response, unicode(self.course.id))

        # Verify that the correct banner is rendered on the dashboard
        alt_text = self.BANNER_ALT_MESSAGES.get(status)
        if alt_text:
            self.assertContains(response, alt_text)

        # Verify that the correct banner color is rendered
        self.assertContains(
            response,
            "<article class=\"course {}\">".format(self.MODE_CLASSES[status])
        )

        # Verify that the correct copy is rendered on the dashboard
        if status is not None:
            if status in self.NOTIFICATION_MESSAGES:
                # Different states might have different messaging
                # so in some cases we check several possibilities
                # and fail if none of these are found.
                found_msg = False
                for message in self.NOTIFICATION_MESSAGES[status]:
                    if message in response.content:
                        found_msg = True
                        break

                fail_msg = "Could not find any of these messages: {expected}".format(
                    expected=self.NOTIFICATION_MESSAGES[status]
                )
                self.assertTrue(found_msg, msg=fail_msg)
        else:
            # Combine all possible messages into a single list
            all_messages = []
            for msg_group in self.NOTIFICATION_MESSAGES.values():
                all_messages.extend(msg_group)

            # Verify that none of the messages are displayed
            for msg in all_messages:
                self.assertNotContains(response, msg)
