"""This module holds the tests for the tasks of the saba discrete_app"""

import json
from datetime import timedelta
from mock import patch

from django.test.testcases import SimpleTestCase
from django.utils import timezone

from constance.test import override_config

from django_lockable.models import Lockable
from django_lockable.utils import _no_fields_locking

from mtp.apps.assessments.factories.certifications import CertificationProgramUserStatusFactory
from mtp.apps.assessments.models.certifications import CertificationProgramUserStatus
from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.celery import app
from mtp.custom.nissan.apps.import_export.saba import UserImporter

from .test_cases import BaseAssessmentsResultsToSabaTaskTestCase, MockedSabaApiClient
from ..factories import CertificationProgramUserStatusDjangoTaskFactory
from ..models import CertificationProgramUserStatusDjangoTask
from ..tasks import (
    AssessmentsResultsToSabaTask,
    CertificationProgramUserStatusTask,
    ExemptOrRecommendedCourseError,
    MissingKeyInDataError,
    MissingMTPDataError,
    MissingSabaDataError,
    import_users_from_saba,
    sync_assessments_to_saba,
)


@app.task(bind=True, base=CertificationProgramUserStatusTask)
def log_something(self, certification=None):
    """Simple task that log something in the tied django task instance"""

    django_task = self.get_django_task()
    django_task.logger.info('The task is executing...')

    return 'Hello %s' % certification


class CertificationProgramUserStatusTaskTestCase(MTPTestCase):
    """Test case for the ``CertificationProgramUserStatusTask`` celery task

    In the first two methods, we call directly the ``log_something``.
    Celery is preventing us to directly catch the exceptions raised
    in the ``async`` mode.

    """

    def test_call_task_without_certification_program_status_id_should_raise_an_exception(self):
        """The task should failed if the certification_program_status_id argument is not set"""

        with self.assertRaises(TypeError):
            log_something(self.admin)

    def test_call_task_with_wrong_certification_should_raise_an_exception(self):
        """The task should fail if the certification is of wrong type"""

        with self.assertRaises(ValueError):
            # pylint: disable=unexpected-keyword-arg
            log_something(
                self.admin,
                certification='XXX'
            )

    def test_call_task_with_a_valid_certification_should_work(self):
        """The task should work when called with a valid certification instance"""

        certification = CertificationProgramUserStatusFactory()

        # In this case, we need celery to correctly set an id for the task,
        # so we call it with "delay"
        async_result = log_something.delay(
            self.admin,
            certification=certification
        )

        # We should have a new ``CertificationProgramUserStatusDjangoTask`` instance
        certif_django_task = CertificationProgramUserStatusDjangoTask.objects.first()

        # The certification is correct
        self.assertEqual(
            certif_django_task.certification,
            certification
        )

        # The task id is correct
        self.assertEqual(
            str(certif_django_task.celery_task_id),
            async_result.id
        )

        # And a correct ``AsyncResult``...
        self.assertIsNotNone(certif_django_task.celery_async_result)


@override_config(
    SABA_API_USERNAME_INTERNAL_ID='XXX',
    SABA_API_BASE_URL='XXX',
)
class AssessmentsResultsToSabaTaskTestCase(BaseAssessmentsResultsToSabaTaskTestCase):
    """Tests for ``AssessmentsResultsToSabaTask``"""

    def test_parse_certification(self):
        """Test the `parse_certification` step"""

        # now attach the certification
        self.task.certification = self.certification_complete_with_data
        self.task._execute_step('parse_certification')

        self.assertEqual(
            self.task.data['executed_steps'],
            ['parse_certification: OK']
        )
        self.assertTrue(self.task.data['steps']['parse_certification']['success'])

        out_data = self.task.data['steps']['parse_certification']['out']

        # check recommendations
        self.assertEqual(len(out_data['mandatory_courses']), 2)
        self.assertEqual(len(out_data['exempted_courses']), 1)
        self.assertTrue('XXX' in [r['external_id'] for r in out_data['mandatory_courses']])
        self.assertTrue('XXX' in [e['external_id'] for e in out_data['exempted_courses']])

        # check other data
        self.assertEqual(out_data['certification_id'], self.task.certification.pk)
        self.assertEqual(out_data['saba_trainee_id'], self.task.certification.user.external_id)
        self.assertEqual(
            out_data['course_external_id'],
            self.task.certification.session.training.external_id
        )
        self.assertEqual(
            out_data['session_start_date'],
            str(self.task.certification.session.start_date.date())
        )
        self.assertEqual(out_data['session_status'], self.task.certification.status.name)
        self.assertEqual(out_data['session_score'], self.task.certification.score)

    def test_parse_certification_error_handling(self):
        """Test parse_certification error handling"""

        # linked test data from MTPComplexAssessmentTestCase
        certification = CertificationProgramUserStatusFactory(
            program=self.certification_program,
            status=self.certification_statuses[0],
            session=self.training_session
        )

        # test without certification.user.external_id (implicit)
        self.assertEqual(certification.user.external_id, '')
        self.task.certification = certification

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingMTPDataError,
            'does not seem to be linked to a SABA user'
        ):
            self.task._execute_step('parse_certification')

        # attach a saba external_id to the trainee
        certification.user.external_id = 'XXX'
        certification.user.save()
        del certification.user.original_or_self_external_id

        # test without certification.session
        certification.session = None
        self.task.certification = certification

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingMTPDataError,
            'No session linked to certification'
        ):
            self.task._execute_step('parse_certification')

        # get the certification.session back from DB
        certification.refresh_from_db()
        self.assertIsNotNone(certification.session)

        # test without certification.session.training.external_id
        with _no_fields_locking(Lockable):
            certification.session.training.external_id = ''

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingMTPDataError,
            'Missing or invalid external_id for training'
        ):
            self.task._execute_step('parse_certification')

    def test_get_trainee_details(self):
        """Test the `get_trainee_details` step"""

        # previous step results required for this step
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'saba_trainee_id': 'YYY'
                }
            }
        }

        # Execute the step
        self.task._execute_step('get_trainee_details')

        # Check that the out data is correct
        self.assertEqual(
            self.task.data['executed_steps'],
            ['get_trainee_details: OK']
        )
        self.assertTrue(self.task.data['steps']['get_trainee_details']['success'])
        self.assertEqual(
            self.task.data['steps']['get_trainee_details']['out'],
            self.mocked_saba_api_client.SABA_USER_DETAILS
        )

    def test_get_trainee_details_error_handling(self):
        """Test `get_trainee_details` error handling

        Notes
        -----
        We also test here that the ``user_message`` with a custom value when raising
        the Exception is working.

        """

        # previous step results required for this step
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'saba_trainee_id': 'give_me_a_wrong_result'
                }
            }
        }

        try:
            self.task._execute_step('get_trainee_details')
            # above line should raise an Exception
            self.fail()
        except MissingSabaDataError as exception:
            self.assertIn(
                'SABA did not reply to user_details with an id for user',
                str(exception)
            )
            self.assertIn('The user is missing in NAO', exception.user_message)

    def test_get_enrollment_details(self):
        """Test the `get_enrollment_details` step"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'session_start_date': str(timezone.now().date()),
                    'course_external_id': 'XXX',
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS
            },
        }

        self.task._execute_step('get_enrollment_details')

        self.assertEqual(
            self.task.data['executed_steps'],
            ['get_enrollment_details: OK']
        )
        self.assertTrue(self.task.data['steps']['get_enrollment_details']['success'])
        self.assertEqual(
            self.task.data['steps']['get_enrollment_details']['out'],
            self.mocked_saba_api_client.SABA_ENROLLMENT
        )

    def test_get_enrollment_details_error_handling(self):
        """Test the `get_enrollment_details` error handling"""

        # test with a wrong id
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'session_start_date': str(timezone.now().date()),
                    'course_external_id': 'XXX',
                }
            },
            'get_trainee_details': {
                'out': {
                    'id': 'give_me_a_wrong_result_for_id'
                }
            },
        }

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingSabaDataError,
            'SABA did not reply to get_enrollment with an id'
        ):
            self.task._execute_step('get_enrollment_details')

        # test with a wrong class_id.id
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'session_start_date': str(timezone.now().date()),
                    'course_external_id': 'XXX',
                }
            },
            'get_trainee_details': {
                'out': {
                    'id': 'give_me_a_wrong_result_for_class_id'
                }
            },
        }

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingSabaDataError,
            'enrollment.class_id.id'
        ):
            self.task._execute_step('get_enrollment_details')

        # test with a wrong status in enrollment details
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'session_start_date': str(timezone.now().date()),
                    'course_external_id': 'XXX',
                }
            },
            'get_trainee_details': {
                'out': {
                    'id': 'give_me_a_wrong_id_for_details'
                }
            },
        }

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingSabaDataError,
            'status is not valid'
        ):
            self.task._execute_step('get_enrollment_details')

    def test_set_registration(self):
        """Test the `set_registration` step"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'session_status': 'Referred',
                    'session_score': 0
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
            'get_enrollment_details': {
                'out': self.mocked_saba_api_client.SABA_ENROLLMENT,
            }
        }

        self.task._execute_step('set_registration')

        self.assertEqual(
            self.task.data['executed_steps'],
            ['set_registration: OK']
        )
        self.assertTrue(self.task.data['steps']['set_registration']['success'])

    def test_set_registration_error_handling(self):
        """Test the `set_registration` error_handling"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'session_status': 'UNKNOW_STATUS',
                    'session_score': 0
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
            'get_enrollment_details': {
                'out': self.mocked_saba_api_client.SABA_ENROLLMENT,
            }
        }

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            MissingMTPDataError,
            'Unknown status in db'
        ):
            self.task._execute_step('set_registration')

    def test__get_recommendations_initial_data(self):
        """Test the `_get_recommendations_initial_data` method"""

        recommendations = self.certification_complete_with_data.recommendations.all()

        output = self.task._get_recommendations_initial_data(recommendations)

        expected_output = [
            {
                'success': False,
                'external_id': reco.lesson.reference.external_id,
                'local_id': reco.lesson.id
            }
            for reco in recommendations
        ]

        self.assertCountEqual(output, expected_output)

    def test__get_recommendations_initial_data_error_handling(self):
        """Test the `_get_recommendations_initial_data` method's error handling"""

        recommendation = self.certification_complete_with_data.recommendations.first()
        recommendation.lesson.reference.external_id = ''
        recommendation.lesson.reference.save()

        output = self.task._get_recommendations_initial_data([recommendation])

        expected_output = [
            {
                'local_id': recommendation.lesson.id,
                'success': False,
                'external_id': None,
                'exception': 'No external_id for lesson %s (%s)' % (
                    recommendation.lesson.pk,
                    recommendation.lesson.name,
                )
            }
        ]

        self.assertListEqualUnordered(output, expected_output)

    def test__check_course_initial_status_should_not_raise_if_course_is_not_found_in_saba(self):
        """Test that this method should not raise if course not found in SABA"""

        course = {
            'external_id': 'give_me_a_wrong_result',
            'success': False,
        }

        is_ok, course = self.task._check_course_initial_status(course)

        # The course is not OK
        self.assertEqual(is_ok, False)

        # The course data was amended with the error message
        self.assertEqual(
            course['exception'],
            'The course id %s is not recognized in NAO' % course['external_id']
        )

    def test_set_exempt_courses(self):
        """Test the `set_exempt_courses` step"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'exempted_courses': [{
                        'external_id': 'XXX',
                        'success': False,
                    }, ],
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
        }

        self.task._execute_step('set_exempt_courses')

        self.assertEqual(
            self.task.data['executed_steps'],
            ['set_exempt_courses: OK']
        )
        self.assertTrue(self.task.data['steps']['set_exempt_courses']['success'])

    def test_set_exempt_courses_error_handling(self):
        """Test the `set_exempt_courses` error handling"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'exempted_courses': [{
                        'external_id': 'give_me_a_wrong_result',
                        'success': False,
                    }, ],
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
        }

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            ExemptOrRecommendedCourseError,
            'Course give_me_a_wrong_result: KO'
        ):
            self.task._execute_step('set_exempt_courses')

    def test_set_exempt_courses_saba_api_error_handling(self):
        """Test the `set_exempt_courses` SABA api error handling"""

        def reset_task_data():
            """Shortcut method to reset the task data"""

            self.task.data = {
                'steps': {
                    'parse_certification': {
                        'out': {
                            'exempted_courses': [
                                {
                                    'external_id': 'XXX',
                                    'success': False,
                                },
                            ],
                        }
                    },
                    'get_trainee_details': {
                        'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
                    }
                }
            }

        def failed_step_data_is_correct():
            """Shortcut method to check that the failed step data is correct"""

            # The data should contains the SABA response
            self.assertEqual(
                self.task.data['steps']['set_exempt_courses']['out'][0]['response'],
                saba_response
            )

            # The data should contains the exception raised
            self.assertEqual(
                self.task.data['steps']['set_exempt_courses']['out'][0]['exception'],
                'http status: 500 - Unable to create transcript for course SABA_COURSE_INTERNAL'
                '_ID. Unknown error from Saba'
            )

            # The step status should be set to KO
            self.assertEqual(
                self.task.data['executed_steps'],
                ['set_exempt_courses: KO']
            )

            # The step success should be set to False
            self.assertFalse(self.task.data['steps']['set_exempt_courses']['success'])

        saba_response = {
            'successCount': 1,
            'errorCount': 0,
            'successMap': {}
        }
        error_message = 'Course XXX: KO (http status: 500 - Unable to create transcript for' \
                        ' course SABA_COURSE_INTERNAL_ID. Unknown error from Saba'

        reset_task_data()
        with patch.object(MockedSabaApiClient, 'create_transcript', return_value=saba_response):
            self.task._execute_step('set_exempt_courses')

        # At first, the transcript must be set to OK (the SABA API answered correctly)
        self.assertEqual(
            self.task.data['executed_steps'],
            ['set_exempt_courses: OK']
        )
        self.assertTrue(self.task.data['steps']['set_exempt_courses']['success'])

        # "successCount" key equal to 0
        saba_response['successCount'] = 0
        reset_task_data()
        with patch.object(MockedSabaApiClient, 'create_transcript', return_value=saba_response):
            with self.assertRaisesMessage(ExemptOrRecommendedCourseError, error_message):
                self.task._execute_step('set_exempt_courses')

        failed_step_data_is_correct()

        # "errorCount" key equal to 1
        saba_response['successCount'] = 1
        saba_response['errorCount'] = 1
        reset_task_data()
        with patch.object(MockedSabaApiClient, 'create_transcript', return_value=saba_response):
            with self.assertRaisesMessage(ExemptOrRecommendedCourseError, error_message):
                self.task._execute_step('set_exempt_courses')

        failed_step_data_is_correct()

        # Delete the "successMap" key
        saba_response['successCount'] = 1
        saba_response['errorCount'] = 0
        del saba_response['successMap']
        reset_task_data()
        with patch.object(MockedSabaApiClient, 'create_transcript', return_value=saba_response):
            with self.assertRaisesMessage(ExemptOrRecommendedCourseError, error_message):
                self.task._execute_step('set_exempt_courses')

        failed_step_data_is_correct()

    def test_set_mandatory_courses(self):
        """Test the `set_mandatory_courses` step"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'mandatory_courses': [{
                        'external_id': 'XXX',
                        'success': False,
                    }, ],
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
        }

        self.task._execute_step('set_mandatory_courses')

        self.assertEqual(
            self.task.data['executed_steps'],
            ['set_mandatory_courses: OK']
        )
        self.assertTrue(self.task.data['steps']['set_mandatory_courses']['success'])

    def test_set_mandatory_courses_should_delete_the_previous_transcripts(self):
        """Test that the `set_mandatory_courses` step should delete the previous transcripts"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'mandatory_courses': [{
                        'external_id': 'XXX',
                        'success': False,
                    }, ],
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
        }

        with patch.object(MockedSabaApiClient, 'delete_transcript') as mocked:
            self.task._execute_step('set_mandatory_courses')

        mocked.assert_called_once_with('fake_id')

    def test_set_mandatory_courses_should_delete_the_previous_course_activity(self):
        """Test that the `set_mandatory_courses` step should delete the previous course activity"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'mandatory_courses': [{
                        'external_id': 'XXX',
                        'success': False,
                    }, ],
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
        }

        with patch.object(MockedSabaApiClient, 'delete_course_activity') as mocked:
            self.task._execute_step('set_mandatory_courses')

        mocked.assert_called_once_with('fake_id')  # The `MockedSabaApiClient` returned this id

    def test_set_mandatory_courses_error_handling(self):
        """Test the `set_mandatory_courses` error handling"""

        # previous data needed
        self.task.data['steps'] = {
            'parse_certification': {
                'out': {
                    'mandatory_courses': [{
                        'external_id': 'give_me_a_wrong_result',
                        'success': False,
                    }, ],
                }
            },
            'get_trainee_details': {
                'out': self.mocked_saba_api_client.SABA_USER_DETAILS,
            },
        }

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            ExemptOrRecommendedCourseError,
            'Course give_me_a_wrong_result: KO'
        ):
            self.task._execute_step('set_mandatory_courses')

    def test_step_can_be_skipped(self):
        """Test that a task can be skipped when process is resumed"""

        test_output = {
            'WHATEVER': 'THE OUTPUT'
        }

        for step in AssessmentsResultsToSabaTask.STEPS:
            # let's assume this step has already been completed successfully
            self.task.data['steps'] = {
                step: {
                    'success': True,
                    'out': test_output
                },
            }
            # let's execute it again
            self.task._execute_step(step)
            # output must remain untouched
            self.assertEqual(
                self.task.data['steps'][step]['out'],
                test_output
            )

    def test__get_step_data(self):
        """Test `_get_step_data` helper"""

        # no 'steps' in data
        self.task.data = {}
        with self.assertRaises(KeyError):
            self.task._get_step_data('XXX', 'XXX')

        # no step in data['steps']
        self.task.data = {
            'steps': {}
        }
        with self.assertRaises(KeyError):
            self.task._get_step_data('XXX', 'XXX')

        # no 'out' in data['steps'][step]
        self.task.data = {
            'steps': {'step1': {}}
        }
        with self.assertRaises(KeyError):
            self.task._get_step_data('step1', 'XXX')

        # no key in data['steps'][step]
        self.task.data = {
            'steps': {'step1': {'out': {}}}
        }
        with self.assertRaises(MissingKeyInDataError):
            self.task._get_step_data('step1', 'XXX')

        # correct data structure
        self.task.data = {
            'steps': {
                'step1': {
                    'out': {
                        'key1': 'value1'
                    }
                }
            }
        }
        self.assertEqual('value1', self.task._get_step_data('step1', 'key1'))

    def test__fill_step_data(self):
        """Test `_fill_step_data` helper"""

        # successful step
        self.task._fill_step_data('step1', {'key1': 'value1'})
        self.assertEqual(self.task._get_step_data('step1', 'key1'), 'value1')
        self.assertTrue(self.task._is_step_completed('step1'))

        # unsuccessful step
        self.task._fill_step_data('step2', {'key1': 'value1'}, success=False)
        self.assertEqual(self.task._get_step_data('step2', 'key1'), 'value1')
        self.assertFalse(self.task._is_step_completed('step2'))

    def test_certification_program_user_status_task_run(self):
        """Test the core part of ``CertificationProgramUserStatusTask``"""

        task_return = self.task.run(
            certification=self.certification_complete_with_data,
            saba_api_client=self.mocked_saba_api_client,
        )

        self.assertEqual(task_return, 'OK')

        # show the full dict diff if there is a mismatch
        self.maxDiff = None  # pylint: disable=invalid-name
        self.assertDictEqual(
            self.task.get_django_task().redis_instance.results,
            self.expected_return_ok
        )

    def test_certification_program_user_status_task_run_error_handling(self):
        """Test the error handling mechanism

        The error must be present in the redis-stored results of the task.
        """

        # This certification will trigger an error
        certification = CertificationProgramUserStatusFactory(
            program=self.certification_program,
            status=self.certification_statuses[0],
            session=self.training_session
        )
        # override the user so we can match ``expected_return_ko`` from test_case
        certification.user = self.trainee

        task_return = self.task.run(
            certification=certification,
            saba_api_client=self.mocked_saba_api_client
        )

        self.assertEqual(task_return, 'KO')

        self.assertDictEqual(
            self.task.get_django_task().redis_instance.results,
            self.expected_return_ko
        )

    def test_task_results_is_json_serializable(self):
        """Test that task results is JSON serializable, to be able to store in Redis for example"""

        try:
            json.dumps(self.expected_return_ok)
            json.dumps(self.expected_return_ko)
        except TypeError:
            self.fail('Json serialization failed.')

    def test_certification_program_user_status_task_run_resume(self):
        """Test the resume mecanism from a previous task

        The resume mecanims extracts results from the last previous run for the
        same certification and take its output as new run input. Furthermore, steps
        already successfully completed in the previous run are not executed again during
        the new run.

        """

        previous_run = CertificationProgramUserStatusDjangoTaskFactory(
            user=self.admin,
            certification=self.certification_complete_with_data,
            finished_at=timezone.now() - timedelta(minutes=5),
        )

        # lets attach the result of a successful step
        # with values different from the expected ones (see ``.test_cases``)
        expected_session_date = timezone.now()
        self.assertNotEqual(
            self.certification_complete_with_data.session.start_date.date(),
            expected_session_date
        )
        expected_score = 66
        self.assertNotEqual(self.certification_complete_with_data.score, expected_score)
        successful_step_result = {
            'executed_steps': [
                'parse_certification: OK',
                'get_trainee_details: OK',
                'get_enrollment_details: OK',
                'set_registration: OK',
                'set_exempt_courses: OK',
                'set_mandatory_courses: KO'
            ],
            'steps': {
                'parse_certification': {
                    'success': True,
                    'out': {
                        'session_score': expected_score,
                        'session_start_date': str(expected_session_date),
                        'exempted_courses': [{
                            'external_id': 'XXX',
                            'success': False,
                        }, ],
                        'mandatory_courses': [{
                            'external_id': 'XXX',
                            'success': False,
                        }, {
                            'external_id': 'XXX',
                            'success': False,
                        }],
                        'course_external_id': 'XXX',
                        'saba_trainee_id': 'XXX',
                        'certification_id': self.certification_complete_with_data.pk,
                        'session_status': 'Level 1'
                    }
                },
            },
            'exception_user_message': 'Something went wrong',
            'exception': 'Something went wrong'
        }
        previous_run.redis_instance.results = successful_step_result

        self.task.run(
            certification=self.certification_complete_with_data,
            saba_api_client=self.mocked_saba_api_client
        )

        # the successful step result has been left "untouched"
        self.assertDictEqual(
            self.task.data['steps']['parse_certification'],
            successful_step_result['steps']['parse_certification']
        )

        # The exceptions are "cleaned"
        self.assertEqual(self.task.data['exception_user_message'], '')
        self.assertEqual(self.task.data['exception'], '')

        # The last step was "KO", and is now "OK"
        self.assertNotEqual(
            successful_step_result['executed_steps'][-1],
            self.task.data['executed_steps'][-1]
        )
        self.assertEqual(
            self.task.data['executed_steps'][-1],
            'set_mandatory_courses: OK'
        )


@override_config(SABA_SYNCHRO_ENABLED=True)
class SyncAssessmentsToSabaTestCase(BaseAssessmentsResultsToSabaTaskTestCase):
    """Test the main task ``sync_assessments_to_saba``"""

    def test_sync_assessments_to_saba_with_queued_certification(self):
        """Test the main job with a queued certification

        We dont test the outcome of an assessment task here, just the wrapper around
        several of them.

        """

        # linked test data from MTPComplexAssessmentTestCase
        CertificationProgramUserStatusFactory(
            program=self.certification_program,
            status=self.certification_statuses[0],
            session=self.training_session,
            sync_status=CertificationProgramUserStatus.SYNC_STATUSES.QUEUED
        )

        # launch the main task
        # called with a mocked client to be injected into task to avoid hitting
        # the real Saba API
        sync_assessments_to_saba(saba_api_client=self.mocked_saba_api_client)

        # it should have created one DjangoTask per pending certif
        self.assertEqual(
            CertificationProgramUserStatusDjangoTask.objects.count(),
            1
        )

    def test_sync_assessments_to_saba_with_non_queued_certifications(self):
        """Test the main job with certifications.sync_status != 'QUEUED'"""

        sync_statuses = CertificationProgramUserStatus.SYNC_STATUSES

        # create a task for each sync_status excepted queued
        for status, __ in sync_statuses:
            if status != sync_statuses.QUEUED.value:
                CertificationProgramUserStatusFactory(
                    program=self.certification_program,
                    status=self.certification_statuses[0],
                    session=self.training_session,
                    sync_status=status
                )

        # launch the main task
        # called with a mocked client to be injected into task to avoid hitting
        # the real Saba API
        sync_assessments_to_saba(saba_api_client=self.mocked_saba_api_client)

        # it should not have created any Django Task
        self.assertEqual(
            CertificationProgramUserStatusDjangoTask.objects.count(),
            0
        )


class ImportUsersFromSabaTaskTestCase(SimpleTestCase):
    """Test case for the ``import_users_from_saba`` celery task"""

    @override_config(SABA_ENABLED=True)
    def test_task_should_call_the_user_importer(self):
        """Test that the task should call the ``import_users`` method on the importer"""

        with patch.object(UserImporter, 'import_users') as mocked:
            import_users_from_saba()

        mocked.assert_called_once_with()
