# -*- coding: utf-8 -*-
"""
Miscellaneous tests for the student app.
"""
import logging
import unittest
from datetime import datetime, timedelta
from urllib import quote

import ddt
import pytz
from config_models.models import cache
from django.conf import settings
from django.contrib.auth.models import AnonymousUser, User
from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings
from django.test.client import Client
from markupsafe import escape
from mock import Mock, patch
from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import CourseLocator
from pyquery import PyQuery as pq

import shoppingcart  # pylint: disable=import-error
from bulk_email.models import Optout  # pylint: disable=import-error
from certificates.models import CertificateStatuses  # pylint: disable=import-error
from certificates.tests.factories import GeneratedCertificateFactory  # pylint: disable=import-error
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory, generate_course_run_key
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from student.models import (
    CourseEnrollment,
    LinkedInAddToProfileConfiguration,
    UserAttribute,
    anonymous_id_for_user,
    unique_id_for_user,
    user_by_anonymous_id
)
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from student.views import _cert_info, complete_course_mode_info, process_survey_link
from util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
from util.testing import EventTestMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreEnum, ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls

log = logging.getLogger(__name__)


@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class CourseEndingTest(TestCase):
    """Test things related to course endings: certificates, surveys, etc"""

    def test_process_survey_link(self):
        username = "fred"
        user = Mock(username=username)
        user_id = unique_id_for_user(user)
        link1 = "http://www.mysurvey.com"
        self.assertEqual(process_survey_link(link1, user), link1)

        link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}"
        link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=user_id)
        self.assertEqual(process_survey_link(link2, user), link2_expected)

    @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
    def test_cert_info(self):
        user = Mock(username="fred", id="1")
        survey_url = "http://a_survey.com"
        course = Mock(
            end_of_course_survey_url=survey_url,
            certificates_display_behavior='end',
            id=CourseLocator(org="x", course="y", run="z"),
        )
        course_mode = 'honor'

        self.assertEqual(
            _cert_info(user, course, None, course_mode),
            {
                'status': 'processing',
                'show_disabled_download_button': False,
                'certificate_message_viewable': False,
                'show_download_url': False,
                'show_survey_button': False,
                'can_unenroll': True,
            }
        )

        cert_status = {'status': 'unavailable'}
        self.assertEqual(
            _cert_info(user, course, cert_status, course_mode),
            {
                'status': 'processing',
                'show_disabled_download_button': False,
                'certificate_message_viewable': True,
                'show_download_url': False,
                'show_survey_button': False,
                'mode': None,
                'linked_in_url': None,
                'can_unenroll': True,
            }
        )

        cert_status = {'status': 'generating', 'grade': '0.67', 'mode': 'honor'}
        with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
            patch_persisted_grade.return_value = Mock(percent=1.0)
            self.assertEqual(
                _cert_info(user, course, cert_status, course_mode),
                {
                    'status': 'generating',
                    'show_disabled_download_button': True,
                    'certificate_message_viewable': True,
                    'show_download_url': False,
                    'show_survey_button': True,
                    'survey_url': survey_url,
                    'grade': '1.0',
                    'mode': 'honor',
                    'linked_in_url': None,
                    'can_unenroll': False,
                }
            )

        cert_status = {'status': 'generating', 'grade': '0.67', 'mode': 'honor'}
        self.assertEqual(
            _cert_info(user, course, cert_status, course_mode),
            {
                'status': 'generating',
                'show_disabled_download_button': True,
                'certificate_message_viewable': True,
                'show_download_url': False,
                'show_survey_button': True,
                'survey_url': survey_url,
                'grade': '0.67',
                'mode': 'honor',
                'linked_in_url': None,
                'can_unenroll': False,
            }
        )

        download_url = 'http://s3.edx/cert'
        cert_status = {
            'status': 'downloadable',
            'grade': '0.67',
            'download_url': download_url,
            'mode': 'honor'
        }

        self.assertEqual(
            _cert_info(user, course, cert_status, course_mode),
            {
                'status': 'downloadable',
                'show_disabled_download_button': False,
                'certificate_message_viewable': True,
                'show_download_url': True,
                'download_url': download_url,
                'show_survey_button': True,
                'survey_url': survey_url,
                'grade': '0.67',
                'mode': 'honor',
                'linked_in_url': None,
                'can_unenroll': False,
            }
        )

        cert_status = {
            'status': 'notpassing', 'grade': '0.67',
            'download_url': download_url,
            'mode': 'honor'
        }
        self.assertEqual(
            _cert_info(user, course, cert_status, course_mode),
            {
                'status': 'notpassing',
                'show_disabled_download_button': False,
                'certificate_message_viewable': True,
                'show_download_url': False,
                'show_survey_button': True,
                'survey_url': survey_url,
                'grade': '0.67',
                'mode': 'honor',
                'linked_in_url': None,
                'can_unenroll': True,
            }
        )

        # Test a course that doesn't have a survey specified
        course2 = Mock(end_of_course_survey_url=None, id=CourseLocator(org="a", course="b", run="c"))
        cert_status = {
            'status': 'notpassing', 'grade': '0.67',
            'download_url': download_url, 'mode': 'honor'
        }
        self.assertEqual(
            _cert_info(user, course2, cert_status, course_mode),
            {
                'status': 'notpassing',
                'show_disabled_download_button': False,
                'certificate_message_viewable': True,
                'show_download_url': False,
                'show_survey_button': False,
                'grade': '0.67',
                'mode': 'honor',
                'linked_in_url': None,
                'can_unenroll': True,
            }
        )

        # test when the display is unavailable or notpassing, we get the correct results out
        course2.certificates_display_behavior = 'early_no_info'
        cert_status = {'status': 'unavailable'}
        self.assertEqual(_cert_info(user, course2, cert_status, course_mode), {})

        cert_status = {
            'status': 'notpassing', 'grade': '0.67',
            'download_url': download_url,
            'mode': 'honor'
        }
        self.assertEqual(_cert_info(user, course2, cert_status, course_mode), {})

    @ddt.data(
        (0.70, 0.60),
        (0.60, 0.70),
        (None, 0.70),
        (None, 0.0),
        (0.70, None),
        (0.0, None),
        (0.70, 0.0),
        (0.0, 0.70),
    )
    @ddt.unpack
    def test_cert_grade(self, persisted_grade, cert_grade):
        """
        Tests that the higher of the persisted grade and the grade
        from the certs table is used on the learner dashboard.
        """
        expected_grade = max(persisted_grade, cert_grade)
        user = Mock(username="fred", id="1")
        survey_url = "http://a_survey.com"
        course = Mock(
            end_of_course_survey_url=survey_url,
            certificates_display_behavior='end',
            id=CourseLocator(org="x", course="y", run="z"),
        )
        course_mode = 'honor'

        if cert_grade is not None:
            cert_status = {'status': 'generating', 'grade': unicode(cert_grade), 'mode': 'honor'}
        else:
            cert_status = {'status': 'generating', 'mode': 'honor'}

        with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
            patch_persisted_grade.return_value = Mock(percent=persisted_grade)
            self.assertEqual(
                _cert_info(user, course, cert_status, course_mode),
                {
                    'status': 'generating',
                    'show_disabled_download_button': True,
                    'certificate_message_viewable': True,
                    'show_download_url': False,
                    'show_survey_button': True,
                    'survey_url': survey_url,
                    'grade': unicode(expected_grade),
                    'mode': 'honor',
                    'linked_in_url': None,
                    'can_unenroll': False,
                }
            )

    def test_cert_grade_no_grades(self):
        """
        Tests that the default cert info is returned
        when the learner has no persisted grade or grade
        in the certs table.
        """
        user = Mock(username="fred", id="1")
        survey_url = "http://a_survey.com"
        course = Mock(
            end_of_course_survey_url=survey_url,
            certificates_display_behavior='end',
            id=CourseLocator(org="x", course="y", run="z"),
        )
        course_mode = 'honor'
        cert_status = {'status': 'generating', 'mode': 'honor'}

        with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
            patch_persisted_grade.return_value = None
            self.assertEqual(
                _cert_info(user, course, cert_status, course_mode),
                {
                    'status': 'processing',
                    'certificate_message_viewable': False,
                    'show_disabled_download_button': False,
                    'show_download_url': False,
                    'show_survey_button': False,
                    'can_unenroll': True,
                }
            )


@ddt.ddt
class DashboardTest(ModuleStoreTestCase):
    """
    Tests for dashboard utility functions
    """
    ENABLED_SIGNALS = ['course_published']

    def setUp(self):
        super(DashboardTest, self).setUp()
        self.course = CourseFactory.create()
        self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
        self.client = Client()
        cache.clear()

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def _check_verification_status_on(self, mode, value):
        """
        Check that the css class and the status message are in the dashboard html.
        """
        CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
        CourseEnrollment.enroll(self.user, self.course.location.course_key, mode=mode)

        if mode == 'verified':
            # Simulate a successful verification attempt
            attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
            attempt.mark_ready()
            attempt.submit()
            attempt.approve()

        response = self.client.get(reverse('dashboard'))
        if mode in ['professional', 'no-id-professional']:
            self.assertContains(response, 'class="course professional"')
        else:
            self.assertContains(response, 'class="course {0}"'.format(mode))
        self.assertContains(response, value)

    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': True})
    def test_verification_status_visible(self):
        """
        Test that the certificate verification status for courses is visible on the dashboard.
        """
        self.client.login(username="jack", password="test")
        self._check_verification_status_on('verified', 'You&#39;re enrolled as a verified student')
        self._check_verification_status_on('honor', 'You&#39;re enrolled as an honor code student')
        self._check_verification_status_off('audit', '')
        self._check_verification_status_on('professional', 'You&#39;re enrolled as a professional education student')
        self._check_verification_status_on(
            'no-id-professional',
            'You&#39;re enrolled as a professional education student',
        )

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def _check_verification_status_off(self, mode, value):
        """
        Check that the css class and the status message are not in the dashboard html.
        """
        CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
        CourseEnrollment.enroll(self.user, self.course.location.course_key, mode=mode)

        if mode == 'verified':
            # Simulate a successful verification attempt
            attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
            attempt.mark_ready()
            attempt.submit()
            attempt.approve()

        response = self.client.get(reverse('dashboard'))

        if mode == 'audit':
            # Audit mode does not have a banner.  Assert no banner element.
            self.assertEqual(pq(response.content)(".sts-enrollment").length, 0)
        else:
            self.assertNotContains(response, "class=\"course {0}\"".format(mode))
            self.assertNotContains(response, value)

    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': False})
    def test_verification_status_invisible(self):
        """
        Test that the certificate verification status for courses is not visible on the dashboard
        if the verified certificates setting is off.
        """
        self.client.login(username="jack", password="test")
        self._check_verification_status_off('verified', 'You\'re enrolled as a verified student')
        self._check_verification_status_off('honor', 'You\'re enrolled as an honor code student')
        self._check_verification_status_off('audit', '')

    def test_course_mode_info(self):
        verified_mode = CourseModeFactory.create(
            course_id=self.course.id,
            mode_slug='verified',
            mode_display_name='Verified',
            expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
        )
        enrollment = CourseEnrollment.enroll(self.user, self.course.id)
        course_mode_info = complete_course_mode_info(self.course.id, enrollment)
        self.assertTrue(course_mode_info['show_upsell'])
        self.assertEquals(course_mode_info['days_for_upsell'], 1)

        verified_mode.expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=-1)
        verified_mode.save()
        course_mode_info = complete_course_mode_info(self.course.id, enrollment)
        self.assertFalse(course_mode_info['show_upsell'])
        self.assertIsNone(course_mode_info['days_for_upsell'])

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @patch('courseware.views.index.log.warning')
    @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
    def test_blocked_course_scenario(self, log_warning):

        self.client.login(username="jack", password="test")

        #create testing invoice 1
        sale_invoice_1 = shoppingcart.models.Invoice.objects.create(
            total_amount=1234.32, company_name='Test1', company_contact_name='Testw',
            company_contact_email='test1@test.com', customer_reference_number='2Fwe23S',
            recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A",
            course_id=self.course.id, is_valid=False
        )
        invoice_item = shoppingcart.models.CourseRegistrationCodeInvoiceItem.objects.create(
            invoice=sale_invoice_1,
            qty=1,
            unit_price=1234.32,
            course_id=self.course.id
        )
        course_reg_code = shoppingcart.models.CourseRegistrationCode(
            code="abcde",
            course_id=self.course.id,
            created_by=self.user,
            invoice=sale_invoice_1,
            invoice_item=invoice_item,
            mode_slug=CourseMode.DEFAULT_MODE_SLUG
        )
        course_reg_code.save()

        cart = shoppingcart.models.Order.get_cart_for_user(self.user)
        shoppingcart.models.PaidCourseRegistration.add_to_order(cart, self.course.id)
        resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': course_reg_code.code})
        self.assertEqual(resp.status_code, 200)

        redeem_url = reverse('register_code_redemption', args=[course_reg_code.code])
        response = self.client.get(redeem_url)
        self.assertEquals(response.status_code, 200)
        # check button text
        self.assertIn('Activate Course Enrollment', response.content)

        #now activate the user by enrolling him/her to the course
        response = self.client.post(redeem_url)
        self.assertEquals(response.status_code, 200)
        response = self.client.get(reverse('dashboard'))
        self.assertIn('You can no longer access this course because payment has not yet been received', response.content)
        optout_object = Optout.objects.filter(user=self.user, course_id=self.course.id)
        self.assertEqual(len(optout_object), 1)

        # Direct link to course redirect to user dashboard
        self.client.get(reverse('courseware', kwargs={"course_id": self.course.id.to_deprecated_string()}))
        log_warning.assert_called_with(
            u'User %s cannot access the course %s because payment has not yet been received',
            self.user,
            unicode(self.course.id),
        )

        # Now re-validating the invoice
        invoice = shoppingcart.models.Invoice.objects.get(id=sale_invoice_1.id)
        invoice.is_valid = True
        invoice.save()

        response = self.client.get(reverse('dashboard'))
        self.assertNotIn('You can no longer access this course because payment has not yet been received', response.content)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_linked_in_add_to_profile_btn_not_appearing_without_config(self):
        # Without linked-in config don't show Add Certificate to LinkedIn button
        self.client.login(username="jack", password="test")

        CourseModeFactory.create(
            course_id=self.course.id,
            mode_slug='verified',
            mode_display_name='verified',
            expiration_datetime=datetime.now(pytz.UTC) - timedelta(days=1)
        )

        CourseEnrollment.enroll(self.user, self.course.id, mode='honor')

        self.course.start = datetime.now(pytz.UTC) - timedelta(days=2)
        self.course.end = datetime.now(pytz.UTC) - timedelta(days=1)
        self.course.display_name = u"Omega"
        self.course = self.update_course(self.course, self.user.id)

        download_url = 'www.edx.org'
        GeneratedCertificateFactory.create(
            user=self.user,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            mode='honor',
            grade='67',
            download_url=download_url
        )
        response = self.client.get(reverse('dashboard'))

        self.assertEquals(response.status_code, 200)
        self.assertNotIn('Add Certificate to LinkedIn', response.content)

        response_url = 'http://www.linkedin.com/profile/add?_ed='
        self.assertNotContains(response, escape(response_url))

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
    def test_linked_in_add_to_profile_btn_with_certificate(self):
        # If user has a certificate with valid linked-in config then Add Certificate to LinkedIn button
        # should be visible. and it has URL value with valid parameters.
        self.client.login(username="jack", password="test")

        LinkedInAddToProfileConfiguration.objects.create(
            company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
            enabled=True
        )

        CourseModeFactory.create(
            course_id=self.course.id,
            mode_slug='verified',
            mode_display_name='verified',
            expiration_datetime=datetime.now(pytz.UTC) - timedelta(days=1)
        )

        self.course.certificate_available_date = datetime.now(pytz.UTC) - timedelta(days=1)
        CourseEnrollment.enroll(self.user, self.course.id, mode='honor')

        self.course.start = datetime.now(pytz.UTC) - timedelta(days=2)
        self.course.end = datetime.now(pytz.UTC) - timedelta(days=1)
        self.course.display_name = u"Omega"
        self.course = self.update_course(self.course, self.user.id)

        download_url = 'www.edx.org'
        GeneratedCertificateFactory.create(
            user=self.user,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            mode='honor',
            grade='67',
            download_url=download_url
        )
        response = self.client.get(reverse('dashboard'))

        self.assertEquals(response.status_code, 200)
        self.assertIn('Add Certificate to LinkedIn', response.content)

        expected_url = (
            u'http://www.linkedin.com/profile/add'
            u'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
            u'pfCertificationName={platform}+Honor+Code+Certificate+for+Omega&'
            u'pfCertificationUrl=www.edx.org&'
            u'source=o'
        ).format(platform=quote(settings.PLATFORM_NAME.encode('utf-8')))

        self.assertContains(response, escape(expected_url))

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
    def test_dashboard_metadata_caching(self, modulestore_type):
        """
        Check that the student dashboard makes use of course metadata caching.

        After creating a course, that course's metadata should be cached as a
        CourseOverview. The student dashboard should never have to make calls to
        the modulestore.

        Arguments:
            modulestore_type (ModuleStoreEnum.Type): Type of modulestore to create
                test course in.

        Note to future developers:
            If you break this test so that the "check_mongo_calls(0)" fails,
            please do NOT change it to "check_mongo_calls(n>1)". Instead, change
            your code to not load courses from the module store. This may
            involve adding fields to CourseOverview so that loading a full
            CourseDescriptor isn't necessary.
        """
        # Create a course and log in the user.
        # Creating a new course will trigger a publish event and the course will be cached
        test_course = CourseFactory.create(default_store=modulestore_type, emit_signals=True)
        self.client.login(username="jack", password="test")

        with check_mongo_calls(0):
            CourseEnrollment.enroll(self.user, test_course.id)

        # Subsequent requests will only result in SQL queries to load the
        # CourseOverview object that has been created.
        with check_mongo_calls(0):
            response_1 = self.client.get(reverse('dashboard'))
            self.assertEquals(response_1.status_code, 200)
            response_2 = self.client.get(reverse('dashboard'))
            self.assertEquals(response_2.status_code, 200)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_dashboard_header_nav_has_find_courses(self):
        self.client.login(username="jack", password="test")
        response = self.client.get(reverse("dashboard"))

        # "Explore courses" is shown in the side panel
        self.assertContains(response, "Explore courses")

        # But other links are hidden in the navigation
        self.assertNotContains(response, "How it Works")
        self.assertNotContains(response, "Schools & Partners")

    def test_course_mode_info_with_honor_enrollment(self):
        """It will be true only if enrollment mode is honor and course has verified mode."""
        course_mode_info = self._enrollment_with_complete_course('honor')
        self.assertTrue(course_mode_info['show_upsell'])
        self.assertEquals(course_mode_info['days_for_upsell'], 1)

    @ddt.data('verified', 'credit')
    def test_course_mode_info_with_different_enrollments(self, enrollment_mode):
        """If user enrollment mode is either verified or credit then show_upsell
        will be always false.
        """
        course_mode_info = self._enrollment_with_complete_course(enrollment_mode)
        self.assertFalse(course_mode_info['show_upsell'])
        self.assertIsNone(course_mode_info['days_for_upsell'])

    def _enrollment_with_complete_course(self, enrollment_mode):
        """"Dry method for course enrollment."""
        CourseModeFactory.create(
            course_id=self.course.id,
            mode_slug='verified',
            mode_display_name='Verified',
            expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
        )
        enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode=enrollment_mode)
        return complete_course_mode_info(self.course.id, enrollment)


@ddt.ddt
class DashboardTestsWithSiteOverrides(SiteMixin, ModuleStoreTestCase):
    """
    Tests for site settings overrides used when rendering the dashboard view
    """

    def setUp(self):
        super(DashboardTestsWithSiteOverrides, self).setUp()
        self.org = 'fakeX'
        self.course = CourseFactory.create(org=self.org)
        self.user = UserFactory.create(username='jack', email='jack@fake.edx.org', password='test')
        CourseModeFactory.create(mode_slug='no-id-professional', course_id=self.course.id)
        CourseEnrollment.enroll(self.user, self.course.location.course_key, mode='no-id-professional')
        cache.clear()

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': False})
    @ddt.data(
        ('testserver1.com', {'ENABLE_VERIFIED_CERTIFICATES': True}),
        ('testserver2.com', {'ENABLE_VERIFIED_CERTIFICATES': True, 'DISPLAY_COURSE_MODES_ON_DASHBOARD': True}),
    )
    @ddt.unpack
    def test_course_mode_visible(self, site_domain, site_configuration_values):
        """
        Test that the course mode for courses is visible on the dashboard
        when settings have been overridden by site configuration.
        """
        site_configuration_values.update({
            'SITE_NAME': site_domain,
            'course_org_filter': self.org
        })
        self.set_up_site(site_domain, site_configuration_values)
        self.client.login(username='jack', password='test')
        response = self.client.get(reverse('dashboard'))
        self.assertContains(response, 'class="course professional"')

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': False})
    @ddt.data(
        ('testserver3.com', {'ENABLE_VERIFIED_CERTIFICATES': False}),
        ('testserver4.com', {'DISPLAY_COURSE_MODES_ON_DASHBOARD': False}),
    )
    @ddt.unpack
    def test_course_mode_invisible(self, site_domain, site_configuration_values):
        """
        Test that the course mode for courses is invisible on the dashboard
        when settings have been overridden by site configuration.
        """
        site_configuration_values.update({
            'SITE_NAME': site_domain,
            'course_org_filter': self.org
        })
        self.set_up_site(site_domain, site_configuration_values)
        self.client.login(username='jack', password='test')
        response = self.client.get(reverse('dashboard'))
        self.assertNotContains(response, 'class="course professional"')


class UserSettingsEventTestMixin(EventTestMixin):
    """
    Mixin for verifying that user setting events were emitted during a test.
    """
    def setUp(self):
        super(UserSettingsEventTestMixin, self).setUp('util.model_utils.tracker')

    def assert_user_setting_event_emitted(self, **kwargs):
        """
        Helper method to assert that we emit the expected user settings events.

        Expected settings are passed in via `kwargs`.
        """
        if 'truncated' not in kwargs:
            kwargs['truncated'] = []
        self.assert_event_emitted(
            USER_SETTINGS_CHANGED_EVENT_NAME,
            table=self.table,
            user_id=self.user.id,
            **kwargs
        )


class EnrollmentEventTestMixin(EventTestMixin):
    """ Mixin with assertions for validating enrollment events. """
    def setUp(self):
        super(EnrollmentEventTestMixin, self).setUp('student.models.tracker')

    def assert_enrollment_mode_change_event_was_emitted(self, user, course_key, mode):
        """Ensures an enrollment mode change event was emitted"""
        self.mock_tracker.emit.assert_called_once_with(  # pylint: disable=maybe-no-member
            'edx.course.enrollment.mode_changed',
            {
                'course_id': course_key.to_deprecated_string(),
                'user_id': user.pk,
                'mode': mode
            }
        )
        self.mock_tracker.reset_mock()

    def assert_enrollment_event_was_emitted(self, user, course_key):
        """Ensures an enrollment event was emitted since the last event related assertion"""
        self.mock_tracker.emit.assert_called_once_with(  # pylint: disable=maybe-no-member
            'edx.course.enrollment.activated',
            {
                'course_id': course_key.to_deprecated_string(),
                'user_id': user.pk,
                'mode': CourseMode.DEFAULT_MODE_SLUG
            }
        )
        self.mock_tracker.reset_mock()

    def assert_unenrollment_event_was_emitted(self, user, course_key):
        """Ensures an unenrollment event was emitted since the last event related assertion"""
        self.mock_tracker.emit.assert_called_once_with(  # pylint: disable=maybe-no-member
            'edx.course.enrollment.deactivated',
            {
                'course_id': course_key.to_deprecated_string(),
                'user_id': user.pk,
                'mode': CourseMode.DEFAULT_MODE_SLUG
            }
        )
        self.mock_tracker.reset_mock()


class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
    """Tests enrolling and unenrolling in courses."""

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_enrollment(self):
        user = User.objects.create_user("joe", "joe@joe.com", "password")
        course_id = CourseKey.from_string("edX/Test101/2013")
        course_id_partial = CourseKey.from_string("edX/Test101/")

        # Test basic enrollment
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
        CourseEnrollment.enroll(user, course_id)
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
        self.assert_enrollment_event_was_emitted(user, course_id)

        # Enrolling them again should be harmless
        CourseEnrollment.enroll(user, course_id)
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
        self.assert_no_events_were_emitted()

        # Now unenroll the user
        CourseEnrollment.unenroll(user, course_id)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
        self.assert_unenrollment_event_was_emitted(user, course_id)

        # Unenrolling them again should also be harmless
        CourseEnrollment.unenroll(user, course_id)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
        self.assert_no_events_were_emitted()

        # The enrollment record should still exist, just be inactive
        enrollment_record = CourseEnrollment.objects.get(
            user=user,
            course_id=course_id
        )
        self.assertFalse(enrollment_record.is_active)

        # Make sure mode is updated properly if user unenrolls & re-enrolls
        enrollment = CourseEnrollment.enroll(user, course_id, "verified")
        self.assertEquals(enrollment.mode, "verified")
        CourseEnrollment.unenroll(user, course_id)
        enrollment = CourseEnrollment.enroll(user, course_id, "audit")
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assertEquals(enrollment.mode, "audit")

    def test_enrollment_non_existent_user(self):
        # Testing enrollment of newly unsaved user (i.e. no database entry)
        user = User(username="rusty", email="rusty@fake.edx.org")
        course_id = CourseLocator("edX", "Test101", "2013")

        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))

        # Unenroll does nothing
        CourseEnrollment.unenroll(user, course_id)
        self.assert_no_events_were_emitted()

        # Implicit save() happens on new User object when enrolling, so this
        # should still work
        CourseEnrollment.enroll(user, course_id)
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_enrollment_event_was_emitted(user, course_id)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_enrollment_by_email(self):
        user = User.objects.create(username="jack", email="jack@fake.edx.org")
        course_id = CourseLocator("edX", "Test101", "2013")

        CourseEnrollment.enroll_by_email("jack@fake.edx.org", course_id)
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_enrollment_event_was_emitted(user, course_id)

        # This won't throw an exception, even though the user is not found
        self.assertIsNone(
            CourseEnrollment.enroll_by_email("not_jack@fake.edx.org", course_id)
        )
        self.assert_no_events_were_emitted()

        self.assertRaises(
            User.DoesNotExist,
            CourseEnrollment.enroll_by_email,
            "not_jack@fake.edx.org",
            course_id,
            ignore_errors=False
        )
        self.assert_no_events_were_emitted()

        # Now unenroll them by email
        CourseEnrollment.unenroll_by_email("jack@fake.edx.org", course_id)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_unenrollment_event_was_emitted(user, course_id)

        # Harmless second unenroll
        CourseEnrollment.unenroll_by_email("jack@fake.edx.org", course_id)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_no_events_were_emitted()

        # Unenroll on non-existent user shouldn't throw an error
        CourseEnrollment.unenroll_by_email("not_jack@fake.edx.org", course_id)
        self.assert_no_events_were_emitted()

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_enrollment_multiple_classes(self):
        user = User(username="rusty", email="rusty@fake.edx.org")
        course_id1 = CourseLocator("edX", "Test101", "2013")
        course_id2 = CourseLocator("MITx", "6.003z", "2012")

        CourseEnrollment.enroll(user, course_id1)
        self.assert_enrollment_event_was_emitted(user, course_id1)
        CourseEnrollment.enroll(user, course_id2)
        self.assert_enrollment_event_was_emitted(user, course_id2)
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id1))
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id2))

        CourseEnrollment.unenroll(user, course_id1)
        self.assert_unenrollment_event_was_emitted(user, course_id1)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id2))

        CourseEnrollment.unenroll(user, course_id2)
        self.assert_unenrollment_event_was_emitted(user, course_id2)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id2))

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_activation(self):
        user = User.objects.create(username="jack", email="jack@fake.edx.org")
        course_id = CourseLocator("edX", "Test101", "2013")
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))

        # Creating an enrollment doesn't actually enroll a student
        # (calling CourseEnrollment.enroll() would have)
        enrollment = CourseEnrollment.get_or_create_enrollment(user, course_id)
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_no_events_were_emitted()

        # Until you explicitly activate it
        enrollment.activate()
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_enrollment_event_was_emitted(user, course_id)

        # Activating something that's already active does nothing
        enrollment.activate()
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_no_events_were_emitted()

        # Now deactive
        enrollment.deactivate()
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_unenrollment_event_was_emitted(user, course_id)

        # Deactivating something that's already inactive does nothing
        enrollment.deactivate()
        self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_no_events_were_emitted()

        # A deactivated enrollment should be activated if enroll() is called
        # for that user/course_id combination
        CourseEnrollment.enroll(user, course_id)
        self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
        self.assert_enrollment_event_was_emitted(user, course_id)

    def test_change_enrollment_modes(self):
        user = User.objects.create(username="justin", email="jh@fake.edx.org")
        course_id = CourseLocator("edX", "Test101", "2013")

        CourseEnrollment.enroll(user, course_id, "audit")
        self.assert_enrollment_event_was_emitted(user, course_id)

        CourseEnrollment.enroll(user, course_id, "honor")
        self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "honor")

        # same enrollment mode does not emit an event
        CourseEnrollment.enroll(user, course_id, "honor")
        self.assert_no_events_were_emitted()

        CourseEnrollment.enroll(user, course_id, "audit")
        self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "audit")


@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class ChangeEnrollmentViewTest(ModuleStoreTestCase):
    """Tests the student.views.change_enrollment view"""

    def setUp(self):
        super(ChangeEnrollmentViewTest, self).setUp()
        self.course = CourseFactory.create()
        self.user = UserFactory.create(password='secret')
        self.client.login(username=self.user.username, password='secret')
        self.url = reverse('change_enrollment')

    def _enroll_through_view(self, course):
        """ Enroll a student in a course. """
        response = self.client.post(
            reverse('change_enrollment'), {
                'course_id': course.id.to_deprecated_string(),
                'enrollment_action': 'enroll'
            }
        )
        return response

    def test_enroll_as_default(self):
        """Tests that a student can successfully enroll through this view"""
        response = self._enroll_through_view(self.course)
        self.assertEqual(response.status_code, 200)
        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            self.user, self.course.id
        )
        self.assertTrue(is_active)
        self.assertEqual(enrollment_mode, CourseMode.DEFAULT_MODE_SLUG)

    def test_cannot_enroll_if_already_enrolled(self):
        """
        Tests that a student will not be able to enroll through this view if
        they are already enrolled in the course
        """
        CourseEnrollment.enroll(self.user, self.course.id)
        self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
        # now try to enroll that student
        response = self._enroll_through_view(self.course)
        self.assertEqual(response.status_code, 400)

    def test_change_to_default_if_verified(self):
        """
        Tests that a student that is a currently enrolled verified student cannot
        accidentally change their enrollment mode
        """
        CourseEnrollment.enroll(self.user, self.course.id, mode=u'verified')
        self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
        # now try to enroll the student in the default mode:
        response = self._enroll_through_view(self.course)
        self.assertEqual(response.status_code, 400)
        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            self.user, self.course.id
        )
        self.assertTrue(is_active)
        self.assertEqual(enrollment_mode, u'verified')

    def test_change_to_default_if_verified_not_active(self):
        """
        Tests that one can renroll for a course if one has already unenrolled
        """
        # enroll student
        CourseEnrollment.enroll(self.user, self.course.id, mode=u'verified')
        # now unenroll student:
        CourseEnrollment.unenroll(self.user, self.course.id)
        # check that they are verified but inactive
        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            self.user, self.course.id
        )
        self.assertFalse(is_active)
        self.assertEqual(enrollment_mode, u'verified')
        # now enroll them through the view:
        response = self._enroll_through_view(self.course)
        self.assertEqual(response.status_code, 200)
        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            self.user, self.course.id
        )
        self.assertTrue(is_active)
        self.assertEqual(enrollment_mode, CourseMode.DEFAULT_MODE_SLUG)


class AnonymousLookupTable(ModuleStoreTestCase):
    """
    Tests for anonymous_id_functions
    """
    def setUp(self):
        super(AnonymousLookupTable, self).setUp()
        self.course = CourseFactory.create()
        self.user = UserFactory.create()
        CourseModeFactory.create(
            course_id=self.course.id,
            mode_slug='honor',
            mode_display_name='Honor Code',
        )
        patcher = patch('student.models.tracker')
        patcher.start()
        self.addCleanup(patcher.stop)

    def test_for_unregistered_user(self):  # same path as for logged out user
        self.assertEqual(None, anonymous_id_for_user(AnonymousUser(), self.course.id))
        self.assertIsNone(user_by_anonymous_id(None))

    def test_roundtrip_for_logged_user(self):
        CourseEnrollment.enroll(self.user, self.course.id)
        anonymous_id = anonymous_id_for_user(self.user, self.course.id)
        real_user = user_by_anonymous_id(anonymous_id)
        self.assertEqual(self.user, real_user)
        self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, self.course.id, save=False))

    def test_roundtrip_with_unicode_course_id(self):
        course2 = CourseFactory.create(display_name=u"Omega Course Ω")
        CourseEnrollment.enroll(self.user, course2.id)
        anonymous_id = anonymous_id_for_user(self.user, course2.id)
        real_user = user_by_anonymous_id(anonymous_id)
        self.assertEqual(self.user, real_user)
        self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))

    def test_secret_key_changes(self):
        """Test that a new anonymous id is returned when the secret key changes."""
        CourseEnrollment.enroll(self.user, self.course.id)
        anonymous_id = anonymous_id_for_user(self.user, self.course.id)
        with override_settings(SECRET_KEY='some_new_and_totally_secret_key'):
            # Recreate user object to clear cached anonymous id.
            self.user = User.objects.get(pk=self.user.id)
            new_anonymous_id = anonymous_id_for_user(self.user, self.course.id)
            self.assertNotEqual(anonymous_id, new_anonymous_id)
            self.assertEqual(self.user, user_by_anonymous_id(anonymous_id))
            self.assertEqual(self.user, user_by_anonymous_id(new_anonymous_id))


@attr(shard=3)
@skip_unless_lms
@patch('openedx.core.djangoapps.programs.utils.get_programs')
class RelatedProgramsTests(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
    """Tests verifying that related programs appear on the course dashboard."""
    maxDiff = None
    password = 'test'
    related_programs_preface = 'Related Programs'

    @classmethod
    def setUpClass(cls):
        super(RelatedProgramsTests, cls).setUpClass()

        cls.user = UserFactory()
        cls.course = CourseFactory()
        cls.enrollment = CourseEnrollmentFactory(user=cls.user, course_id=cls.course.id)  # pylint: disable=no-member

    def setUp(self):
        super(RelatedProgramsTests, self).setUp()

        self.url = reverse('dashboard')

        self.create_programs_config()
        self.client.login(username=self.user.username, password=self.password)

        course_run = CourseRunFactory(key=unicode(self.course.id))  # pylint: disable=no-member
        course = CatalogCourseFactory(course_runs=[course_run])
        self.programs = [ProgramFactory(courses=[course]) for __ in range(2)]

    def assert_related_programs(self, response, are_programs_present=True):
        """Assertion for verifying response contents."""
        assertion = getattr(self, 'assert{}Contains'.format('' if are_programs_present else 'Not'))

        for program in self.programs:
            assertion(response, self.expected_link_text(program))

        assertion(response, self.related_programs_preface)

    def expected_link_text(self, program):
        """Construct expected dashboard link text."""
        return u'{title} {type}'.format(title=program['title'], type=program['type'])

    def test_related_programs_listed(self, mock_get_programs):
        """Verify that related programs are listed when available."""
        mock_get_programs.return_value = self.programs

        response = self.client.get(self.url)
        self.assert_related_programs(response)

    def test_no_data_no_programs(self, mock_get_programs):
        """Verify that related programs aren't listed when none are available."""
        mock_get_programs.return_value = []

        response = self.client.get(self.url)
        self.assert_related_programs(response, are_programs_present=False)

    def test_unrelated_program_not_listed(self, mock_get_programs):
        """Verify that unrelated programs don't appear in the listing."""
        nonexistent_course_run_id = generate_course_run_key()

        course_run = CourseRunFactory(key=nonexistent_course_run_id)
        course = CatalogCourseFactory(course_runs=[course_run])
        unrelated_program = ProgramFactory(courses=[course])

        mock_get_programs.return_value = self.programs + [unrelated_program]

        response = self.client.get(self.url)
        self.assert_related_programs(response)
        self.assertNotContains(response, unrelated_program['title'])

    def test_program_title_unicode(self, mock_get_programs):
        """Verify that the dashboard can deal with programs whose titles contain Unicode."""
        self.programs[0]['title'] = u'Bases matemáticas para estudiar ingeniería'
        mock_get_programs.return_value = self.programs

        response = self.client.get(self.url)
        self.assert_related_programs(response)


class UserAttributeTests(TestCase):
    """Tests for the UserAttribute model."""

    def setUp(self):
        super(UserAttributeTests, self).setUp()
        self.user = UserFactory()
        self.name = 'test'
        self.value = 'test-value'

    def test_get_set_attribute(self):
        self.assertIsNone(UserAttribute.get_user_attribute(self.user, self.name))
        UserAttribute.set_user_attribute(self.user, self.name, self.value)
        self.assertEqual(UserAttribute.get_user_attribute(self.user, self.name), self.value)
        new_value = 'new_value'
        UserAttribute.set_user_attribute(self.user, self.name, new_value)
        self.assertEqual(UserAttribute.get_user_attribute(self.user, self.name), new_value)

    def test_unicode(self):
        UserAttribute.set_user_attribute(self.user, self.name, self.value)
        for field in (self.name, self.value, self.user.username):
            self.assertIn(field, unicode(UserAttribute.objects.get(user=self.user)))
