# pylint: disable=missing-docstring
from django.core.cache import cache
from django.test.utils import override_settings
# Will also run default tests for IDTokens and UserInfo
from edx_oauth2_provider.tests import IDTokenTestCase, UserInfoTestCase

from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from student.models import UserProfile, anonymous_id_for_user
from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff, OrgInstructorRole, OrgStaffRole
from student.tests.factories import UserFactory, UserProfileFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls


class BaseTestMixin(ModuleStoreTestCase):
    profile = None
    ENABLED_SIGNALS = ['course_published']

    def setUp(self):
        super(BaseTestMixin, self).setUp()
        self.course_key = CourseFactory.create(emit_signals=True).id
        self.course_id = unicode(self.course_key)
        self.user_factory = UserFactory
        self.set_user(self.make_user())

    def set_user(self, user):
        super(BaseTestMixin, self).set_user(user)
        self.profile = UserProfileFactory(user=self.user)


class IDTokenTest(BaseTestMixin, IDTokenTestCase):
    def setUp(self):
        super(IDTokenTest, self).setUp()

        # CourseAccessHandler uses the application cache.
        cache.clear()

    def test_sub_claim(self):
        scopes, claims = self.get_id_token_values('openid')
        self.assertIn('openid', scopes)

        sub = claims['sub']

        expected_sub = anonymous_id_for_user(self.user, None)
        self.assertEqual(sub, expected_sub)

    def test_user_name_claim(self):
        _scopes, claims = self.get_id_token_values('openid profile')
        claim_name = claims['name']

        user_profile = UserProfile.objects.get(user=self.user)
        user_name = user_profile.name

        self.assertEqual(claim_name, user_name)

    @override_settings(LANGUAGE_CODE='en')
    def test_user_without_locale_claim(self):
        scopes, claims = self.get_id_token_values('openid profile')
        self.assertIn('profile', scopes)
        self.assertEqual(claims['locale'], 'en')

    def test_user_with_locale_claim(self):
        language = 'en'
        set_user_preference(self.user, LANGUAGE_KEY, language)
        scopes, claims = self.get_id_token_values('openid profile')

        self.assertIn('profile', scopes)

        locale = claims['locale']
        self.assertEqual(language, locale)

    def test_user_tracking_id_claim(self):
        scopes, claims = self.get_id_token_values('openid profile')
        self.assertIn('profile', scopes)
        self.assertEqual(claims['user_tracking_id'], self.user.id)

    def test_no_special_course_access(self):
        with check_mongo_calls(0):
            scopes, claims = self.get_id_token_values('openid course_instructor course_staff')
        self.assertNotIn('course_staff', scopes)
        self.assertNotIn('staff_courses', claims)

        self.assertNotIn('course_instructor', scopes)
        self.assertNotIn('instructor_courses', claims)

    def test_course_staff_courses(self):
        CourseStaffRole(self.course_key).add_users(self.user)
        with check_mongo_calls(0):
            scopes, claims = self.get_id_token_values('openid course_staff')

        self.assertIn('course_staff', scopes)
        self.assertNotIn('staff_courses', claims)  # should not return courses in id_token

    def test_course_instructor_courses(self):
        with check_mongo_calls(0):
            CourseInstructorRole(self.course_key).add_users(self.user)

        scopes, claims = self.get_id_token_values('openid course_instructor')

        self.assertIn('course_instructor', scopes)
        self.assertNotIn('instructor_courses', claims)  # should not return courses in id_token

    def test_course_staff_courses_with_claims(self):
        CourseStaffRole(self.course_key).add_users(self.user)

        course_id = unicode(self.course_key)

        nonexistent_course_id = 'some/other/course'

        claims = {
            'staff_courses': {
                'values': [course_id, nonexistent_course_id],
                'essential': True,
            }
        }

        with check_mongo_calls(0):
            scopes, claims = self.get_id_token_values(scope='openid course_staff', claims=claims)

        self.assertIn('course_staff', scopes)
        self.assertIn('staff_courses', claims)
        self.assertEqual(len(claims['staff_courses']), 1)
        self.assertIn(course_id, claims['staff_courses'])
        self.assertNotIn(nonexistent_course_id, claims['staff_courses'])

    def test_permissions_scope(self):
        scopes, claims = self.get_id_token_values('openid profile permissions')
        self.assertIn('permissions', scopes)
        self.assertFalse(claims['administrator'])

        self.user.is_staff = True
        self.user.save()
        _scopes, claims = self.get_id_token_values('openid profile permissions')
        self.assertTrue(claims['administrator'])


class UserInfoTest(BaseTestMixin, UserInfoTestCase):
    def setUp(self):
        super(UserInfoTest, self).setUp()
        # create another course in the DB that only global staff have access to
        CourseFactory.create(emit_signals=True)

    def token_for_scope(self, scope):
        full_scope = 'openid %s' % scope
        self.set_access_token_scope(full_scope)

        token = self.access_token.token  # pylint: disable=no-member
        return full_scope, token

    def get_with_scope(self, scope):
        scope, token = self.token_for_scope(scope)
        result, claims = self.get_userinfo(token, scope)
        self.assertEqual(result.status_code, 200)

        return claims

    def get_with_claim_value(self, scope, claim, values):
        _full_scope, token = self.token_for_scope(scope)

        result, claims = self.get_userinfo(
            token,
            claims={claim: {'values': values}}
        )

        self.assertEqual(result.status_code, 200)
        return claims

    def _assert_role_using_scope(self, scope, claim, assert_one_course=True):
        with check_mongo_calls(0):
            claims = self.get_with_scope(scope)
        self.assertEqual(len(claims), 2)
        courses = claims[claim]
        self.assertIn(self.course_id, courses)
        if assert_one_course:
            self.assertEqual(len(courses), 1)

    def test_request_global_staff_courses_using_scope(self):
        GlobalStaff().add_users(self.user)
        self._assert_role_using_scope('course_staff', 'staff_courses', assert_one_course=False)

    def test_request_org_staff_courses_using_scope(self):
        OrgStaffRole(self.course_key.org).add_users(self.user)
        self._assert_role_using_scope('course_staff', 'staff_courses')

    def test_request_org_instructor_courses_using_scope(self):
        OrgInstructorRole(self.course_key.org).add_users(self.user)
        self._assert_role_using_scope('course_instructor', 'instructor_courses')

    def test_request_staff_courses_using_scope(self):
        CourseStaffRole(self.course_key).add_users(self.user)
        self._assert_role_using_scope('course_staff', 'staff_courses')

    def test_request_instructor_courses_using_scope(self):
        CourseInstructorRole(self.course_key).add_users(self.user)
        self._assert_role_using_scope('course_instructor', 'instructor_courses')

    def _assert_role_using_claim(self, scope, claim):
        values = [self.course_id, 'some_invalid_course']
        with check_mongo_calls(0):
            claims = self.get_with_claim_value(scope, claim, values)
        self.assertEqual(len(claims), 2)

        courses = claims[claim]
        self.assertIn(self.course_id, courses)
        self.assertEqual(len(courses), 1)

    def test_request_global_staff_courses_with_claims(self):
        GlobalStaff().add_users(self.user)
        self._assert_role_using_claim('course_staff', 'staff_courses')

    def test_request_org_staff_courses_with_claims(self):
        OrgStaffRole(self.course_key.org).add_users(self.user)
        self._assert_role_using_claim('course_staff', 'staff_courses')

    def test_request_org_instructor_courses_with_claims(self):
        OrgInstructorRole(self.course_key.org).add_users(self.user)
        self._assert_role_using_claim('course_instructor', 'instructor_courses')

    def test_request_staff_courses_with_claims(self):
        CourseStaffRole(self.course_key).add_users(self.user)
        self._assert_role_using_claim('course_staff', 'staff_courses')

    def test_request_instructor_courses_with_claims(self):
        CourseInstructorRole(self.course_key).add_users(self.user)
        self._assert_role_using_claim('course_instructor', 'instructor_courses')

    def test_permissions_scope(self):
        claims = self.get_with_scope('permissions')
        self.assertIn('administrator', claims)
        self.assertFalse(claims['administrator'])

        self.user.is_staff = True
        self.user.save()
        claims = self.get_with_scope('permissions')
        self.assertTrue(claims['administrator'])

    def test_profile_scope(self):
        claims = self.get_with_scope('profile')
        self.assertEqual(claims['name'], UserProfile.objects.get(user=self.user).name)
        self.assertEqual(claims['user_tracking_id'], self.user.id)
