"""
Test signal handlers for program_enrollments
"""


from unittest import mock

import pytest
from django.core.cache import cache
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.keys import CourseKey
from organizations.tests.factories import OrganizationFactory
from social_django.models import UserSocialAuth
from testfixtures import LogCapture

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollmentException
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.third_party_auth.models import SAMLProviderConfig
from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory
from lms.djangoapps.program_enrollments.signals import _listen_for_lms_retire, logger
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL
from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_completed_retirement
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase


class ProgramEnrollmentRetireSignalTests(ModuleStoreTestCase):
    """
    Test the _listen_for_lms_retire signal
    """

    def create_enrollment_and_history(self, user=None, external_user_key='defaultExternalKey'):
        """
        Create ProgramEnrollment and several History entries
        """
        if user:
            enrollment = ProgramEnrollmentFactory(user=user, external_user_key=external_user_key)
        else:
            enrollment = ProgramEnrollmentFactory(external_user_key=external_user_key)
        for status in ['pending', 'suspended', 'canceled', 'enrolled']:
            enrollment.status = status
            enrollment.save()
        return enrollment

    def assert_enrollment_and_history_retired(self, enrollment):
        """
        Assert that for the enrollment and all histories, external key is None
        """
        enrollment.refresh_from_db()
        assert enrollment.external_user_key is not None
        assert enrollment.external_user_key.startswith('retired_external_key')
        for history_record in enrollment.historical_records.all():
            assert history_record.external_user_key is not None
            assert history_record.external_user_key.startswith('retired_external_key')

    def test_retire_enrollment(self):
        """
        Test basic retirement of program enrollment
        """
        enrollment = self.create_enrollment_and_history()
        _listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
        self.assert_enrollment_and_history_retired(enrollment)

    def test_retire_enrollment_multiple(self):
        """
        Test basic retirement of user with multiple program enrollments
        """
        enrollment = self.create_enrollment_and_history()
        enrollment2 = self.create_enrollment_and_history(user=enrollment.user)
        enrollment3 = self.create_enrollment_and_history(user=enrollment.user)
        _listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
        self.assert_enrollment_and_history_retired(enrollment)
        self.assert_enrollment_and_history_retired(enrollment2)
        self.assert_enrollment_and_history_retired(enrollment3)

    def test_success_no_enrollment(self):
        """
        Basic success path for users who have no enrollments, should simply not error
        """
        user = UserFactory()
        _listen_for_lms_retire(sender=self.__class__, user=user)

    def test_idempotent(self):
        """
        Tests that running a retirement multiple times does not throw an error
        """
        enrollment = self.create_enrollment_and_history()

        # Run twice to make sure no errors are raised
        _listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
        fake_completed_retirement(enrollment.user)
        _listen_for_lms_retire(sender=self.__class__, user=enrollment.user)

        self.assert_enrollment_and_history_retired(enrollment)


class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
    """
    Test post-save handler on UserSocialAuth
    """
    ENABLED_CACHES = ['default']

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

        cls.external_id = '0000'
        cls.provider_slug = 'uox'
        cls.course_keys = [
            CourseKey.from_string('course-v1:edX+DemoX+Test_Course'),
            CourseKey.from_string('course-v1:edX+DemoX+Another_Test_Course'),
        ]
        cls.organization = OrganizationFactory.create(
            short_name='UoX'
        )
        cls.user = UserFactory.create()

        for course_key in cls.course_keys:
            CourseOverviewFactory(id=course_key)
        cls.provider_config = SAMLProviderConfigFactory.create(
            organization=cls.organization, slug=cls.provider_slug
        )

    def setUp(self):
        super().setUp()
        RequestCache.clear_all_namespaces()
        catalog_org = CatalogOrganizationFactory.create(key=self.organization.short_name)
        self.program_uuid = self._create_catalog_program(catalog_org)['uuid']

    def _create_catalog_program(self, catalog_org):
        """ helper method to create a cached catalog program """
        program = ProgramFactory.create(
            authoring_organizations=[catalog_org]
        )
        cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']), program, None)
        return program

    def _create_waiting_program_enrollment(self):
        """ helper method to create a waiting program enrollment """
        return ProgramEnrollmentFactory.create(
            user=None,
            external_user_key=self.external_id,
            program_uuid=self.program_uuid,
        )

    def _create_waiting_course_enrollments(self, program_enrollment):
        """ helper method to create waiting course enrollments """
        return [
            ProgramCourseEnrollmentFactory(
                program_enrollment=program_enrollment,
                course_enrollment=None,
                course_key=course_key,
            )
            for course_key in self.course_keys
        ]

    def _assert_program_enrollment_user(self, program_enrollment, user):
        """ validate program enrollment has a user """
        program_enrollment.refresh_from_db()
        assert program_enrollment.user == user

    def _assert_program_course_enrollment(self, program_course_enrollment, mode=CourseMode.MASTERS):
        """ validate program course enrollment has a valid course enrollment """
        program_course_enrollment.refresh_from_db()
        student_course_enrollment = program_course_enrollment.course_enrollment
        assert student_course_enrollment.user == self.user
        assert student_course_enrollment.course.id == program_course_enrollment.course_key
        assert student_course_enrollment.mode == mode

    def test_update_social_auth(self):
        """
        Makes sure we can update a social_auth row to trigger the same program enrollments
        """
        program_enrollment = self._create_waiting_program_enrollment()
        program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)

        user_social_auth = UserSocialAuth.objects.create(
            user=self.user,
            uid='{}:{}'.format(self.provider_slug, 'gobbledegook')
        )

        # Not yet a thing, didn't match
        program_enrollment.refresh_from_db()
        assert program_enrollment.user is None

        user_social_auth.uid = f'{self.provider_slug}:{self.external_id}'
        user_social_auth.save()

        # now we see the enrollments realized
        self._assert_program_enrollment_user(program_enrollment, self.user)
        for program_course_enrollment in program_course_enrollments:
            self._assert_program_course_enrollment(program_course_enrollment)

    def test_waiting_course_enrollments_completed(self):
        program_enrollment = self._create_waiting_program_enrollment()
        program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)

        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{self.provider_slug}:{self.external_id}'
        )

        self._assert_program_enrollment_user(program_enrollment, self.user)
        for program_course_enrollment in program_course_enrollments:
            self._assert_program_course_enrollment(program_course_enrollment)

    def test_same_user_key_in_multiple_organizations(self):
        uox_program_enrollment = self._create_waiting_program_enrollment()

        second_organization = OrganizationFactory.create()
        SAMLProviderConfigFactory.create(organization=second_organization, slug='aiu')
        catalog_org = CatalogOrganizationFactory.create(key=second_organization.short_name)
        program_uuid = self._create_catalog_program(catalog_org)['uuid']

        # aiu enrollment with the same student key as our uox user
        aiu_program_enrollment = ProgramEnrollmentFactory.create(
            user=None,
            external_user_key=self.external_id,
            program_uuid=program_uuid
        )

        UserSocialAuth.objects.create(
            user=UserFactory.create(),
            uid='{}:{}'.format('not_used', self.external_id),
        )

        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{self.provider_slug}:{self.external_id}',
        )
        self._assert_program_enrollment_user(uox_program_enrollment, self.user)

        aiu_user = UserFactory.create()
        UserSocialAuth.objects.create(
            user=aiu_user,
            uid='{}:{}'.format('aiu', self.external_id),
        )
        self._assert_program_enrollment_user(aiu_program_enrollment, aiu_user)

    def test_only_active_saml_config_used(self):
        """ makes sure only the active row in SAMLProvider config is used """
        program_enrollment = self._create_waiting_program_enrollment()

        # update will create a second record
        self.provider_config.organization = None
        self.provider_config.save()
        assert len(SAMLProviderConfig.objects.all()) == 2

        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{self.provider_slug}:{self.external_id}'
        )
        program_enrollment.refresh_from_db()
        assert program_enrollment.user is None

    def test_learner_already_enrolled_in_course(self):
        course_key = self.course_keys[0]
        course = CourseOverview.objects.get(id=course_key)
        CourseEnrollmentFactory(user=self.user, course=course, mode=CourseMode.VERIFIED)

        program_enrollment = self._create_waiting_program_enrollment()
        program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)

        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{self.provider_slug}:{self.external_id}'
        )

        self._assert_program_enrollment_user(program_enrollment, self.user)

        duplicate_program_course_enrollment = program_course_enrollments[0]
        self._assert_program_course_enrollment(
            duplicate_program_course_enrollment, CourseMode.VERIFIED
        )

        program_course_enrollment = program_course_enrollments[1]
        self._assert_program_course_enrollment(program_course_enrollment)

    def test_enrolled_with_no_course_enrollments(self):
        program_enrollment = self._create_waiting_program_enrollment()

        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{self.provider_slug}:{self.external_id}'
        )

        self._assert_program_enrollment_user(program_enrollment, self.user)

    def test_create_social_auth_with_no_waiting_enrollments(self):
        """
        No exceptions should be raised if there are no enrollments to update
        """
        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{self.provider_slug}:{self.external_id}'
        )

    def test_create_social_auth_provider_has_no_organization(self):
        """
        No exceptions should be raised if provider is not linked to an organization
        """
        provider = SAMLProviderConfigFactory.create()
        UserSocialAuth.objects.create(
            user=self.user,
            uid=f'{provider.slug}:{self.external_id}'
        )

    def test_create_social_auth_non_saml_provider(self):
        """
        No exceptions should be raised for a non-SAML uid
        """
        UserSocialAuth.objects.create(
            user=self.user,
            uid='google-oauth-user@gmail.com'
        )
        UserSocialAuth.objects.create(
            user=self.user,
            uid='123:123:123'
        )

    def test_saml_provider_not_found(self):
        """
        An error should be logged for incoming social auth entries with a saml id but
        no matching saml configuration exists
        """
        with LogCapture(logger.name) as log:
            UserSocialAuth.objects.create(
                user=self.user,
                uid='abc:123456'
            )
            log.check_present(
                (
                    logger.name,
                    'WARNING',
                    'Got incoming social auth for provider={} but no such provider exists'.format('abc')
                )
            )

    def test_cannot_find_catalog_program(self):
        """
        An error should be logged if a program enrollment exists but a matching catalog
        program cannot be found
        """
        self.program_uuid = self._create_catalog_program(None)['uuid']
        self._create_waiting_program_enrollment()

        with LogCapture(logger.name) as log:
            UserSocialAuth.objects.create(
                user=self.user,
                uid=f'{self.provider_slug}:{self.external_id}'
            )
            error_template = (
                'Failed to complete waiting enrollments for organization={}.'
                ' No catalog programs with matching authoring_organization exist.'
            )
            log.check_present(
                (
                    logger.name,
                    'WARNING',
                    error_template.format('UoX')
                )
            )

    def test_exception_on_enrollment_failure(self):
        program_enrollment = self._create_waiting_program_enrollment()
        self._create_waiting_course_enrollments(program_enrollment)

        with mock.patch('common.djangoapps.student.models.CourseEnrollment.enroll') as enrollMock:
            enrollMock.side_effect = CourseEnrollmentException('something has gone wrong')
            with pytest.raises(CourseEnrollmentException):
                UserSocialAuth.objects.create(
                    user=self.user,
                    uid=f'{self.provider_slug}:{self.external_id}'
                )

    def test_log_on_unexpected_exception(self):
        """
        unexpected errors as part of the account linking process should be logged and re-raised
        """
        program_enrollment = self._create_waiting_program_enrollment()
        self._create_waiting_course_enrollments(program_enrollment)

        with mock.patch('lms.djangoapps.program_enrollments.api.linking.enroll_in_masters_track') as enrollMock:
            enrollMock.side_effect = Exception('unexpected error')
            with LogCapture(logger.name) as log:
                with self.assertRaisesRegex(Exception, 'unexpected error'):
                    UserSocialAuth.objects.create(
                        user=self.user,
                        uid=f'{self.provider_slug}:{self.external_id}',
                    )
                error_template = 'Unable to link waiting enrollments for user {}, social auth creation failed: {}'
                log.check_present(
                    (
                        logger.name,
                        'WARNING',
                        error_template.format(self.user.id, 'unexpected error')
                    )
                )
