"""
Tests for student enrollment.
"""
from mock import patch, Mock

import ddt
from django.core.cache import cache
from nose.tools import raises
import unittest
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings

from course_modes.models import CourseMode
from enrollment import api
from enrollment.errors import EnrollmentApiLoadError, EnrollmentNotFoundError, CourseModeNotFoundError
from enrollment.tests import fake_data_api
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase


@ddt.ddt
@override_settings(ENROLLMENT_DATA_API="enrollment.tests.fake_data_api")
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(CacheIsolationTestCase):
    """
    Test student enrollment, especially with different course modes.
    """
    USERNAME = "Bob"
    COURSE_ID = "some/great/course"

    ENABLED_CACHES = ['default']

    def setUp(self):
        super(EnrollmentTest, self).setUp()
        fake_data_api.reset()

    @ddt.data(
        # Default (no course modes in the database)
        # Expect automatically being enrolled as "honor".
        ([], 'honor'),

        # Audit / Verified / Honor
        # We should always go to the "choose your course" page.
        # We should also be enrolled as "honor" by default.
        (['honor', 'verified', 'audit'], 'honor'),

        # Check for professional ed happy path.
        (['professional'], 'professional'),
        (['no-id-professional'], 'no-id-professional')
    )
    @ddt.unpack
    def test_enroll(self, course_modes, mode):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
        # Enroll in the course and verify the URL we get sent to
        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode=mode)
        self.assertIsNotNone(result)
        self.assertEquals(result['student'], self.USERNAME)
        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
        self.assertEquals(result['mode'], mode)

        get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
        self.assertEquals(result, get_result)

    @ddt.data(
        ([CourseMode.DEFAULT_MODE_SLUG, 'verified', 'credit'], CourseMode.DEFAULT_MODE_SLUG),
        (['audit', 'verified', 'credit'], 'audit'),
        (['honor', 'verified', 'credit'], 'honor'),
    )
    @ddt.unpack
    def test_enroll_no_mode_success(self, course_modes, expected_mode):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
        with patch('enrollment.api.CourseMode.modes_for_course') as mock_modes_for_course:
            mock_course_modes = [Mock(slug=mode) for mode in course_modes]
            mock_modes_for_course.return_value = mock_course_modes
            # Enroll in the course and verify the URL we get sent to
            result = api.add_enrollment(self.USERNAME, self.COURSE_ID)
            self.assertIsNotNone(result)
            self.assertEquals(result['student'], self.USERNAME)
            self.assertEquals(result['course']['course_id'], self.COURSE_ID)
            self.assertEquals(result['mode'], expected_mode)

    @ddt.data(
        ['professional'],
        ['verified'],
        ['verified', 'professional'],
    )
    @raises(CourseModeNotFoundError)
    def test_enroll_no_mode_error(self, course_modes):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
        # Enroll in the course and verify that we raise CourseModeNotFoundError
        api.add_enrollment(self.USERNAME, self.COURSE_ID)

    @raises(CourseModeNotFoundError)
    def test_prof_ed_enroll(self):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=['professional'])
        # Enroll in the course and verify the URL we get sent to
        api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='verified')

    @ddt.data(
        # Default (no course modes in the database)
        # Expect that users are automatically enrolled as "honor".
        ([], 'honor'),

        # Audit / Verified / Honor
        # We should always go to the "choose your course" page.
        # We should also be enrolled as "honor" by default.
        (['honor', 'verified', 'audit'], 'honor'),

        # Check for professional ed happy path.
        (['professional'], 'professional'),
        (['no-id-professional'], 'no-id-professional')
    )
    @ddt.unpack
    def test_unenroll(self, course_modes, mode):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
        # Enroll in the course and verify the URL we get sent to
        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode=mode)
        self.assertIsNotNone(result)
        self.assertEquals(result['student'], self.USERNAME)
        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
        self.assertEquals(result['mode'], mode)
        self.assertTrue(result['is_active'])

        result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode=mode, is_active=False)
        self.assertIsNotNone(result)
        self.assertEquals(result['student'], self.USERNAME)
        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
        self.assertEquals(result['mode'], mode)
        self.assertFalse(result['is_active'])

    @raises(EnrollmentNotFoundError)
    def test_unenroll_not_enrolled_in_course(self):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor'])
        api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='honor', is_active=False)

    @ddt.data(
        # Simple test of honor and verified.
        ([
            {'course_id': 'the/first/course', 'course_modes': [], 'mode': 'honor'},
            {'course_id': 'the/second/course', 'course_modes': ['honor', 'verified'], 'mode': 'verified'}
        ]),

        # No enrollments
        ([]),

        # One Enrollment
        ([
            {'course_id': 'the/third/course', 'course_modes': ['honor', 'verified', 'audit'], 'mode': 'audit'}
        ]),
    )
    def test_get_all_enrollments(self, enrollments):
        for enrollment in enrollments:
            fake_data_api.add_course(enrollment['course_id'], course_modes=enrollment['course_modes'])
            api.add_enrollment(self.USERNAME, enrollment['course_id'], enrollment['mode'])
        result = api.get_enrollments(self.USERNAME)
        self.assertEqual(len(enrollments), len(result))
        for result_enrollment in result:
            self.assertIn(
                result_enrollment['course']['course_id'],
                [enrollment['course_id'] for enrollment in enrollments]
            )

    def test_update_enrollment(self):
        # Add fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
        # Enroll in the course and verify the URL we get sent to
        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
        get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
        self.assertEquals(result, get_result)

        result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='honor')
        self.assertEquals('honor', result['mode'])

        result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='verified')
        self.assertEquals('verified', result['mode'])

    def test_update_enrollment_attributes(self):
        # Add fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit', 'credit'])
        # Enroll in the course and verify the URL we get sent to
        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
        get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
        self.assertEquals(result, get_result)

        enrollment_attributes = [
            {
                "namespace": "credit",
                "name": "provider_id",
                "value": "hogwarts",
            }
        ]

        result = api.update_enrollment(
            self.USERNAME, self.COURSE_ID, mode='credit', enrollment_attributes=enrollment_attributes
        )
        self.assertEquals('credit', result['mode'])
        attributes = api.get_enrollment_attributes(self.USERNAME, self.COURSE_ID)
        self.assertEquals(enrollment_attributes[0], attributes[0])

    def test_get_course_details(self):
        # Add a fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
        result = api.get_course_enrollment_details(self.COURSE_ID)
        self.assertEquals(result['course_id'], self.COURSE_ID)
        self.assertEquals(3, len(result['course_modes']))

    @override_settings(ENROLLMENT_DATA_API='foo.bar.biz.baz')
    @raises(EnrollmentApiLoadError)
    def test_data_api_config_error(self):
        # Enroll in the course and verify the URL we get sent to
        api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')

    def test_caching(self):
        # Add fake course enrollment information to the fake data API
        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])

        # Hit the fake data API.
        details = api.get_course_enrollment_details(self.COURSE_ID)

        # Reset the fake data API, should rely on the cache.
        fake_data_api.reset()
        cached_details = api.get_course_enrollment_details(self.COURSE_ID)

        # The data matches
        self.assertEqual(len(details['course_modes']), 3)
        self.assertEqual(details, cached_details)

    def test_update_enrollment_expired_mode_with_error(self):
        """ Verify that if verified mode is expired and include expire flag is
        false then enrollment cannot be updated. """
        self.assert_add_modes_with_enrollment('audit')
        # On updating enrollment mode to verified it should the raise the error.
        with self.assertRaises(CourseModeNotFoundError):
            self.assert_update_enrollment(mode='verified', include_expired=False)

    def test_update_enrollment_with_expired_mode(self):
        """ Verify that if verified mode is expired then enrollment can be
        updated if include_expired flag is true."""
        self.assert_add_modes_with_enrollment('audit')
        # enrollment in verified mode will work fine with include_expired=True
        self.assert_update_enrollment(mode='verified', include_expired=True)

    @ddt.data(True, False)
    def test_unenroll_with_expired_mode(self, include_expired):
        """ Verify that un-enroll will work fine for expired courses whether include_expired
        is true or false."""
        self.assert_add_modes_with_enrollment('verified')
        self.assert_update_enrollment(mode='verified', is_active=False, include_expired=include_expired)

    def assert_add_modes_with_enrollment(self, enrollment_mode):
        """ Dry method for adding fake course enrollment information to fake
        data API and enroll the student in the course. """
        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode=enrollment_mode)
        get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
        self.assertEquals(result, get_result)
        # set the course verify mode as expire.
        fake_data_api.set_expired_mode(self.COURSE_ID)

    def assert_update_enrollment(self, mode, is_active=True, include_expired=False):
        """ Dry method for updating enrollment."""

        result = api.update_enrollment(
            self.USERNAME, self.COURSE_ID, mode=mode, is_active=is_active, include_expired=include_expired
        )
        self.assertEquals(mode, result['mode'])
        self.assertIsNotNone(result)
        self.assertEquals(result['student'], self.USERNAME)
        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
        self.assertEquals(result['mode'], mode)

        if is_active:
            self.assertTrue(result['is_active'])
        else:
            self.assertFalse(result['is_active'])
