<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace enrol_lti\local\ltiadvantage\task;

use enrol_lti\helper;
use enrol_lti\local\ltiadvantage\entity\user;
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
use enrol_lti\local\ltiadvantage\repository\user_repository;

defined('MOODLE_INTERNAL') || die();

require_once(__DIR__ . '/../lti_advantage_testcase.php');

/**
 * Tests for the enrol_lti\local\ltiadvantage\task\sync_members scheduled task.
 *
 * @package enrol_lti
 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @coversDefaultClass \enrol_lti\local\ltiadvantage\task\sync_members
 */
final class sync_members_test extends \lti_advantage_testcase {

    /**
     * Verify the user's profile picture has been set, which is useful to verify picture syncs.
     *
     * @param int $userid the id of the Moodle user.
     * @param bool $match true to verify a match, false to verify a non-match.
     */
    protected function verify_user_profile_image(int $userid, bool $match = true): void {
        global $CFG;
        $user = \core_user::get_user($userid);
        $usercontext = \context_user::instance($user->id);
        $expected = $CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev='. $user->picture;

        $page = new \moodle_page();
        $page->set_url('/user/profile.php');
        $page->set_context(\context_system::instance());
        $renderer = $page->get_renderer('core');
        $userpicture = new \user_picture($user);
        if ($match) {
            $this->assertEquals($expected, $userpicture->get_url($page, $renderer)->out(false));
        } else {
            $this->assertNotEquals($expected, $userpicture->get_url($page, $renderer)->out(false));
        }

    }

    /**
     * Helper to get a list of mocked member entries for use in the mocked sync task.
     *
     * @param array $userids the array of lti user ids to use.
     * @param array|null $legacyuserids legacy user ids for the lti11_legacy_user_id property, null if not desired.
     * @param bool $names whether to include names in the user data or not.
     * @param bool $emails whether to include email in the user data or not.
     * @param bool $linklevel whether to mock the user return data at link-level (true) or context-level (false).
     * @param bool $picture whether to mock a user's picture field in the return data.
     * @param array $roles an array of IMS roles to include with each member which, if empty, defaults to just the learner role.
     * @return array the array of users.
     * @throws \Exception if the legacyuserids array doesn't contain the correct number of ids.
     */
    protected static function get_mock_members_with_ids(
        array $userids,
        ?array $legacyuserids = null,
        $names = true,
        $emails = true,
        bool $linklevel = true,
        bool $picture = false,
        array $roles = [],
    ): array {

        if (!is_null($legacyuserids) && count($legacyuserids) != count($userids)) {
            throw new \Exception('legacyuserids must contain the same number of ids as $userids.');
        }

        if (empty($roles)) {
            $roles = ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'];
        }

        $users = [];
        foreach ($userids as $userid) {
            $user = ['user_id' => (string) $userid, 'roles' => $roles];
            if ($picture) {
                $user['picture'] = static::getExternalTestFileUrl('/test.jpg', false);
            }
            if ($names) {
                $user['given_name'] = 'Firstname' . $userid;
                $user['family_name'] = 'Surname' . $userid;
            }
            if ($emails) {
                $user['email'] = "firstname.surname{$userid}@lms.example.org";
            }
            if ($legacyuserids) {
                $user['lti11_legacy_user_id'] = array_shift($legacyuserids);
            }
            if ($linklevel) {
                // Link-level memberships also include a message property.
                $user['message'] = [
                    'https://purl.imsglobal.org/spec/lti/claim/message_type' => 'LtiResourceLinkRequest'
                ];
            }
            $users[] = $user;
        }
        return $users;
    }

    /**
     * Gets a task mocked to only support resource-link-level memberships request.
     *
     * @param array $resourcelinks array for stipulating per link users, containing list of [resourcelink, members].
     * @return sync_members|\PHPUnit\Framework\MockObject\MockObject
     */
    protected function get_mock_task_resource_link_level(array $resourcelinks = []) {
        $mocktask = $this->getMockBuilder(sync_members::class)
            ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members'])
            ->getMock();
        $mocktask->expects($this->any())
            ->method('get_context_level_members')
            ->will($this->returnCallback(function() {
                return false;
            }));
        $expectedcount = !empty($resourcelinks) ? count($resourcelinks) : 1;
        $mocktask->expects($this->exactly($expectedcount))
            ->method('get_resource_link_level_members')
            ->will($this->returnCallback(function ($nrpsinfo, $serviceconnector, $registration, $reslink) use ($resourcelinks) {
                if ($resourcelinks) {
                    foreach ($resourcelinks as $rl) {
                        if ($reslink->get_resourcelinkid() === $rl[0]->get_resourcelinkid()) {
                            return $rl[1];
                        }
                    }
                } else {
                    return self::get_mock_members_with_ids(range(1, 2));
                }
            }));
        return $mocktask;
    }

    /**
     * Gets a task mocked to only support context-level memberships request.
     *
     * @return sync_members|\PHPUnit\Framework\MockObject\MockObject
     */
    protected function get_mock_task_context_level() {
        $mocktask = $this->getMockBuilder(sync_members::class)
            ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members'])
            ->getMock();
        $mocktask->expects($this->any())
            ->method('get_resource_link_level_members')
            ->will($this->returnCallback(function() {
                // An exception is what the service code will throw if the resource link level service isn't available.
                throw new \Exception();
            }));
        $mocktask->expects($this->any())
            ->method('get_context_level_members')
            ->will($this->returnCallback(function() {
                return self::get_mock_members_with_ids(range(1, 3), null, true, true, false);
            }));;
        return $mocktask;
    }

    /**
     * Gets a sync task, with the remote calls mocked to return the supplied users.
     *
     * See get_mock_members_with_ids() for generating the users for input.
     *
     * @param array $users a list of users, the result of a call to get_mock_members_with_ids().
     * @return \PHPUnit\Framework\MockObject\MockObject the mock task.
     */
    protected function get_mock_task_with_users(array $users) {
        $mocktask = $this->getMockBuilder(sync_members::class)
            ->onlyMethods(['get_resource_link_level_members', 'get_context_level_members'])
            ->getMock();
        $mocktask->expects($this->any())
            ->method('get_context_level_members')
            ->will($this->returnCallback(function() {
                return false;
            }));
        $mocktask->expects($this->any())
            ->method('get_resource_link_level_members')
            ->will($this->returnCallback(function () use ($users) {
                return $users;
            }));
        return $mocktask;
    }

    /**
     * Check that all the given ltiusers are enrolled in the course.
     *
     * @param \stdClass $course the course instance.
     * @param user[] $ltiusers array of lti user instances.
     */
    protected function verify_course_enrolments(\stdClass $course, array $ltiusers) {
        global $CFG;
        require_once($CFG->libdir . '/enrollib.php');
        $enrolledusers = get_enrolled_users(\context_course::instance($course->id));
        $this->assertCount(count($ltiusers), $enrolledusers);
        $enrolleduserids = array_map(function($stringid) {
            return (int) $stringid;
        }, array_column($enrolledusers, 'id'));
        foreach ($ltiusers as $ltiuser) {
            $this->assertContains($ltiuser->get_localid(), $enrolleduserids);
        }
    }

    /**
     * Test confirming task name.
     *
     * @covers ::get_name
     */
    public function test_get_name(): void {
        $this->assertEquals(get_string('tasksyncmembers', 'enrol_lti'), (new sync_members())->get_name());
    }

    /**
     * Test a resource-link-level membership sync, confirming that all relevant domain objects are updated properly.
     *
     * @covers ::execute
     */
    public function test_resource_link_level_sync(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]);
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice = $this->get_tool_launch_service();
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Sync members.
        $task = $this->get_mock_task_resource_link_level();
        $task->execute();

        // Verify 2 users and their corresponding course enrolments exist.
        $this->expectOutputRegex(
            "/Completed - Synced members for tool '$resource->id' in the course '$course->id'. ".
            "Processed 2 users; enrolled 2 members; unenrolled 0 members./"
        );
        $userrepo = new user_repository();
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(2, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);
    }

    /**
     * Test a resource-link-level membership sync when there are more than one resource links for the resource.
     *
     * @covers ::execute
     */
    public function test_resource_link_level_sync_multiple_resource_links(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment();

        // Launch twice - once from each resource link in the platform.
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0], '123');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0], '456');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Now, grab the resource links.
        $rlrepo = new resource_link_repository();
        $reslinks = $rlrepo->find_by_resource($resource->id);
        $mockmembers = self::get_mock_members_with_ids(range(1, 10));
        $mockusers1 = array_slice($mockmembers, 0, 6);
        $mockusers2 = array_slice($mockmembers, 6);
        $resourcelinks = [
            [$reslinks[0], $mockusers1],
            [$reslinks[1], $mockusers2]
        ];

        // Sync the members, using the mock task set up to sync different sets of users for each resource link.
        $task = $this->get_mock_task_resource_link_level($resourcelinks);
        ob_start();
        $task->execute();
        $output = ob_get_contents();
        ob_end_clean();

        // Verify 10 users and their corresponding course enrolments exist.
        $userrepo = new user_repository();
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(10, $ltiusers);
        $this->assertStringContainsString("Completed - Synced 6 members for the resource link", $output);
        $this->assertStringContainsString("Completed - Synced 4 members for the resource link", $output);
        $this->assertStringContainsString("Completed - Synced members for tool '$resource->id' in the course '".
            "$resource->courseid'. Processed 10 users; enrolled 10 members; unenrolled 0 members.\n", $output);
        $this->verify_course_enrolments($course, $ltiusers);
    }

    /**
     * Verify the task will update users' profile pictures if the 'picture' member field is provided.
     *
     * @covers ::execute
     */
    public function test_user_profile_image_sync(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Sync members.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(['1'], null, true, true, true, true));
        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify 1 users and their corresponding course enrolments exist.
        $userrepo = new user_repository();
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(1, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);

        // Verify user profile image has been updated.
        $this->verify_user_profile_image($ltiusers[0]->get_localid());
    }

    /**
     * Test a context-level membership sync, confirming that all relevant domain objects are updated properly.
     *
     * @covers ::execute
     */
    public function test_context_level_sync(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Sync members.
        $task = $this->get_mock_task_context_level();
        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify 3 users and their corresponding course enrolments exist.
        $userrepo = new user_repository();
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(3, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);
    }

    /**
     * Test verifying the sync task handles the omission/inclusion of PII information for users.
     *
     * @covers ::execute
     */
    public function test_sync_user_data(): void {
        $this->resetAfterTest();
        [$course, $resource, $resource2, $resource3, $appreg] = $this->create_test_environment();
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids(['1'])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Sync members.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(1, 5), null, false, false));

        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify 5 users and their corresponding course enrolments exist.
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(5, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);

        // Since user data wasn't included in the response, the users will have been synced using fallbacks,
        // so verify these.
        foreach ($ltiusers as $ltiuser) {
            $user = \core_user::get_user($ltiuser->get_localid());
            // Firstname falls back to sourceid.
            $this->assertEquals($ltiuser->get_sourceid(), $user->firstname);

            // Lastname falls back to resource context id.
            $this->assertEquals($appreg->get_platformid(), $user->lastname);

            // Email falls back to example.com.
            $issuersubhash = sha1($appreg->get_platformid() . '_' . $ltiuser->get_sourceid());
            $this->assertEquals("enrol_lti_13_{$issuersubhash}@example.com", $user->email);
        }

        // Sync again, this time with user data included.
        $mockmembers = self::get_mock_members_with_ids(range(1, 5));
        $task = $this->get_mock_task_with_users($mockmembers);

        ob_start();
        $task->execute();
        ob_end_clean();

        // User data was included in the response and should have been updated.
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(5, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);
        foreach ($ltiusers as $ltiuser) {
            $user = \core_user::get_user($ltiuser->get_localid());
            $mockmemberindex = array_search($ltiuser->get_sourceid(), array_column($mockmembers, 'user_id'));
            $mockmember = $mockmembers[$mockmemberindex];
            $this->assertEquals($mockmember['given_name'], $user->firstname);
            $this->assertEquals($mockmember['family_name'], $user->lastname);
            $this->assertEquals($mockmember['email'], $user->email);
        }
    }

    /**
     * Test verifying the task won't sync members for shared resources having member sync disabled.
     *
     * @covers ::execute
     */
    public function test_membership_sync_disabled(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment(true, true, false);

        // Launch the tool for a user.
        $mockuser = self::get_mock_launch_users_with_ids(['1'])[0];
        $mocklaunch = $this->get_mock_launch($resource, $mockuser);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Sync members.
        $task = $this->get_mock_task_with_users(self::get_mock_launch_users_with_ids(range(1, 4)));
        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify no users were added or removed.
        // A single user (the user who launched the resource link) is expected.
        $userrepo = new user_repository();
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(1, $ltiusers);
        $this->assertEquals($mockuser['user_id'], $ltiusers[0]->get_sourceid());
        $this->verify_course_enrolments($course, $ltiusers);
    }

    /**
     * Test verifying the sync task for resources configured as 'helper::MEMBER_SYNC_ENROL_AND_UNENROL'.
     *
     * @covers ::execute
     */
    public function test_sync_mode_enrol_and_unenrol(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment();
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mockuser = self::get_mock_launch_users_with_ids(['1'])[0];
        $mocklaunch = $this->get_mock_launch($resource, $mockuser);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);

        // Sync members.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(1, 3)));

        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify 3 users and their corresponding course enrolments exist.
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(3, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);

        // Now, simulate a subsequent sync in which 1 existing user maintains access,
        // 2 existing users are unenrolled and 3 new users are enrolled.
        $task2 = $this->get_mock_task_with_users(self::get_mock_members_with_ids(['1', '4', '5', '6']));
        ob_start();
        $task2->execute();
        ob_end_clean();

        // Verify the missing users have been unenrolled and new users enrolled.
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(4, $ltiusers);
        $unenrolleduserids = ['2', '3'];
        $enrolleduserids = ['1', '4', '5', '6'];
        foreach ($ltiusers as $ltiuser) {
            $this->assertNotContains($ltiuser->get_sourceid(), $unenrolleduserids);
            $this->assertContains($ltiuser->get_sourceid(), $enrolleduserids);
        }
        $this->verify_course_enrolments($course, $ltiusers);
    }

    /**
     * Confirm the sync task operation for resources configured as 'helper::MEMBER_SYNC_UNENROL_MISSING'.
     *
     * @covers ::execute
     */
    public function test_sync_mode_unenrol_missing(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_UNENROL_MISSING);
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));

        // Sync members using a payload which doesn't include the original launch user (User id = 1).
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 3)));

        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify the original user (launching user) has been unenrolled and that no new members have been enrolled.
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(0, $ltiusers);
    }

    /**
     * Confirm the sync task operation for resources configured as 'helper::MEMBER_SYNC_ENROL_NEW'.
     *
     * @covers ::execute
     */
    public function test_sync_mode_enrol_new(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_NEW);
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));

        // Sync members using a payload which includes two new members only (i.e. not the original launching user).
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 3)));

        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify we now have 3 enrolments. The original user (who was not unenrolled) and the 2 new users.
        $ltiusers = $userrepo->find_by_resource($resource->id);
        $this->assertCount(3, $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);
    }

    /**
     * Test confirming that no changes take place if the auth_lti plugin is not enabled.
     *
     * @covers ::execute
     */
    public function test_sync_auth_disabled(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment(false);
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));

        // If the task were to run, this would trigger 1 unenrolment (the launching user) and 3 enrolments.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 2)));
        $task->execute();

        // Verify that the sync didn't take place.
        $this->expectOutputRegex("/Skipping task - Authentication plugin 'LTI' is not enabled/");
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));
    }

    /**
     * Test confirming that no sync takes place when the enrol_lti plugin is not enabled.
     *
     * @covers ::execute
     */
    public function test_sync_enrol_disabled(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment(true, false);
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mocklaunch = $this->get_mock_launch($resource, self::get_mock_launch_users_with_ids([1])[0]);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));

        // If the task were to run, this would trigger 1 unenrolment of the launching user and enrolment of 3 users.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 2)));
        $task->execute();

        // Verify that the sync didn't take place.
        $this->expectOutputRegex("/Skipping task - The 'Publish as LTI tool' plugin is disabled/");
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));
    }

    /**
     * Test syncing members when the enrolment instance is disabled.
     *
     * @covers ::execute
     */
    public function test_sync_members_disabled_instance(): void {
        $this->resetAfterTest();
        global $DB;

        [$course, $resource, $resource2, $resource3] = $this->create_test_environment();
        $userrepo = new user_repository();

        // Disable resource 1.
        $enrol = (object) ['id' => $resource->enrolid, 'status' => ENROL_INSTANCE_DISABLED];
        $DB->update_record('enrol', $enrol);

        // Delete the activity being shared by resource2, leaving resource 2 disabled as a result.
        $modcontext = \context::instance_by_id($resource2->contextid);
        course_delete_module($modcontext->instanceid);

        // Only the enabled resource 3 should sync members.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(1, 1)));
        $task->execute();

        $this->expectOutputRegex(
            "/^Starting - Member sync for published resource '$resource3->id' for course '$course->id'.\n".
            "Completed - Synced members for tool '$resource3->id' in the course '$course->id'. Processed 0 users; ".
            "enrolled 0 members; unenrolled 0 members.\n$/"
        );
        $this->assertCount(0, $userrepo->find_by_resource($resource->id));
    }

    /**
     * Test syncing members for a membersync-enabled resource when the launch omits the NRPS service endpoints.
     *
     * @covers ::execute
     */
    public function test_sync_no_nrps_support(): void {
        $this->resetAfterTest();
        [$course, $resource] = $this->create_test_environment();
        $userrepo = new user_repository();

        // Launch the tool for a user.
        $mockinstructor = self::get_mock_launch_users_with_ids([1])[0];
        $mocklaunch = $this->get_mock_launch($resource, $mockinstructor, null, null, false);
        $launchservice = $this->get_tool_launch_service();
        $instructoruser = $this->lti_advantage_user_authenticates('1');
        $launchservice->user_launches_tool($instructoruser, $mocklaunch);
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));

        // The task would sync an additional 2 users if the link had NRPS service support.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(range(2, 2)));

        // We expect the task to report that it is skipping the resource due to a lack of NRPS support.
        $task->execute();

        // Verify no enrolments or unenrolments.
        $this->expectOutputRegex(
            "/Skipping - No names and roles service found.\n".
            "Completed - Synced members for tool '{$resource->id}' in the course '{$course->id}'. ".
            "Processed 0 users; enrolled 0 members; unenrolled 0 members./"
        );
        $this->assertCount(1, $userrepo->find_by_resource($resource->id));
    }

    /**
     * Test confirming that preexisting, non-lti user accounts do not have their profiles or pictures updated during sync.
     *
     * @covers ::execute
     */
    public function test_sync_non_lti_linked_user(): void {
        $this->resetAfterTest();

        // Set up the environment.
        [$course, $resource] = $this->create_test_environment();

        // Fake an auth - making sure it's a manual account.
        $authenticateduser = $this->lti_advantage_user_authenticates('123');
        $authenticateduser->auth = 'manual';
        $authenticateduser->password = '1234abcD*';
        user_update_user($authenticateduser);
        $authenticateduser = \core_user::get_user($authenticateduser->id);

        // Mock the launch for the specified user.
        $mocklaunchuser = self::get_mock_launch_users_with_ids([$authenticateduser->id])[0];
        $mocklaunch = $this->get_mock_launch($resource, $mocklaunchuser);
        $this->get_tool_launch_service()->user_launches_tool($authenticateduser, $mocklaunch);

        // Prepare the sync task, with a stubbed list of members.
        $task = $this->get_mock_task_with_users(self::get_mock_members_with_ids(['123'], null, true, true, true, true));

        // Run the member sync.
        $this->expectOutputRegex(
            "/Skipped profile sync for user '$authenticateduser->id'. The user does not belong to the LTI auth method.\n" .
            "Skipped picture sync for user '$authenticateduser->id'. The user does not belong to the LTI auth method/"
        );
        $task->execute();

        $updateduser = \core_user::get_user($authenticateduser->id);
        $this->assertEquals($authenticateduser->firstname, $updateduser->firstname);
        $this->assertEquals($authenticateduser->lastname, $updateduser->lastname);
        $this->assertEquals($authenticateduser->email, $updateduser->email);
        $this->verify_user_profile_image($authenticateduser->id, false);
    }

    /**
     * Test the member sync for a range of scenarios including migrated tools, unlaunched tools, provisioning methods.
     *
     * @dataProvider member_sync_data_provider
     * @param array|null $legacydata array detailing what legacy information to create, or null if not required.
     * @param array|null $resourceconfig array detailing config values to be used when creating the test enrol_lti instances.
     * @param array $launchdata array containing details of the launch, including user and migration claim.
     * @param array|null $syncmembers the members to use in the mock sync.
     * @param array $expected the array detailing expectations.
     * @covers ::execute
     */
    public function test_sync_enrolments_and_migration(?array $legacydata, ?array $resourceconfig, array $launchdata,
            ?array $syncmembers, array $expected): void {

        $this->resetAfterTest();

        // Set up the environment.
        [$course, $resource] = $this->create_test_environment(true, true, true, helper::MEMBER_SYNC_ENROL_AND_UNENROL, true, false,
            0, $resourceconfig['provisioningmodeinstructor'] ?? 0, $resourceconfig['provisioningmodelearner'] ?? 0);

        // Set up legacy tool and user data.
        if ($legacydata) {
            [$legacytools, $legacyconsumerrecord, $legacyusers] = $this->setup_legacy_data($course, $legacydata);
        }

        // Mock the launch for the specified user.
        $mocklaunch = $this->get_mock_launch($resource, $launchdata['user'], null, [], true,
            $launchdata['launch_migration_claim']);

        // Perform the launch.
        $instructoruser = $this->lti_advantage_user_authenticates(
            $launchdata['user']['user_id'],
            $launchdata['launch_migration_claim'] ?? []
        );
        $this->get_tool_launch_service()->user_launches_tool($instructoruser, $mocklaunch);

        // Prepare the sync task, with a stubbed list of members.
        $task = $this->get_mock_task_with_users($syncmembers);

        // Run the member sync.
        ob_start();
        $task->execute();
        ob_end_clean();

        // Verify enrolments.
        $ltiusers = (new user_repository())->find_by_resource($resource->id);
        $enrolled = array_filter($expected['enrolments'], function($user) {
            return $user['is_enrolled'];
        });
        $this->assertCount(count($enrolled), $ltiusers);
        $this->verify_course_enrolments($course, $ltiusers);

        // Verify migration, if expected.
        if ($legacydata) {
            $legacyuserids = array_column($legacyusers, 'id');
            foreach ($ltiusers as $ltiuser) {
                $this->assertArrayHasKey($ltiuser->get_sourceid(), $expected['enrolments']);
                if (!$expected['enrolments'][$ltiuser->get_sourceid()]['is_migrated']) {
                    // Those members who hadn't launched over 1p1 prior will have new lti user records created.
                    $this->assertNotContains((string)$ltiuser->get_localid(), $legacyuserids);
                } else {
                    // Those members who were either already migrated during launch, or were migrated during the sync,
                    // will be mapped to their legacy user accounts.
                    $this->assertContains((string)$ltiuser->get_localid(), $legacyuserids);
                }
            }
        }
    }

    /**
     * Data provider for member syncs.
     *
     * @return array[] the array of test data.
     */
    public static function member_sync_data_provider(): array {
        global $CFG;
        require_once($CFG->dirroot . '/auth/lti/auth.php');
        return [
            'Migrated tool, user ids changed, new and existing users present in sync' => [
                'legacydata' => [
                    'users' => [
                        ['user_id' => '1'],
                        ['user_id' => '2'],
                    ],
                    'consumer_key' => 'CONSUMER_1',
                    'tools' => [
                        ['secret' => 'toolsecret1'],
                        ['secret' => 'toolsecret2'],
                    ]
                ],
                'resourceconfig' => null,
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => [
                        'consumer_key' => 'CONSUMER_1',
                        'signing_secret' => 'toolsecret1',
                        'user_id' => '1',
                        'context_id' => 'd345b',
                        'tool_consumer_instance_guid' => '12345-123',
                        'resource_link_id' => '4b6fa'
                    ],
                ],
                'syncmembers' => [
                    self::get_mock_members_with_ids(['1p3_1'], ['1'])[0],
                    self::get_mock_members_with_ids(['1p3_2'], ['2'])[0],
                    self::get_mock_members_with_ids(['1p3_3'], ['3'])[0],
                    self::get_mock_members_with_ids(['1p3_4'], ['4'])[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true,
                            'is_migrated' => true,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => true,
                            'is_migrated' => true,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'Migrated tool, no change in user ids, new and existing users present in sync' => [
                'legacydata' => [
                    'users' => [
                        ['user_id' => '1'],
                        ['user_id' => '2'],
                    ],
                    'consumer_key' => 'CONSUMER_1',
                    'tools' => [
                        ['secret' => 'toolsecret1'],
                        ['secret' => 'toolsecret2'],
                    ]
                ],
                'resourceconfig' => null,
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1'])[0],
                    'launch_migration_claim' => [
                        'consumer_key' => 'CONSUMER_1',
                        'signing_secret' => 'toolsecret1',
                        'context_id' => 'd345b',
                        'tool_consumer_instance_guid' => '12345-123',
                        'resource_link_id' => '4b6fa'
                    ],
                ],
                'syncmembers' => [
                    self::get_mock_members_with_ids(['1'], null)[0],
                    self::get_mock_members_with_ids(['2'], null)[0],
                    self::get_mock_members_with_ids(['3'], null)[0],
                    self::get_mock_members_with_ids(['4'], null)[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1' => [
                            'is_enrolled' => true,
                            'is_migrated' => true,
                        ],
                        '2' => [
                            'is_enrolled' => true,
                            'is_migrated' => true,
                        ],
                        '3' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '4' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'New tool, no launch migration claim, change in user ids, new and existing users present in sync' => [
                'legacydata' => [
                    'users' => [
                        ['user_id' => '1'],
                        ['user_id' => '2'],
                    ],
                    'consumer_key' => 'CONSUMER_1',
                    'tools' => [
                        ['secret' => 'toolsecret1'],
                        ['secret' => 'toolsecret2'],
                    ]
                ],
                'resourceconfig' => null,
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => null,
                ],
                'syncmembers' => [
                    self::get_mock_members_with_ids(['1p3_1'], null)[0],
                    self::get_mock_members_with_ids(['1p3_2'], null)[0],
                    self::get_mock_members_with_ids(['1p3_3'], null)[0],
                    self::get_mock_members_with_ids(['1p3_4'], null)[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'New tool, no launch migration claim, no change in user ids, new and existing users present in sync' => [
                'legacydata' => [
                    'users' => [
                        ['user_id' => '1'],
                        ['user_id' => '2'],
                    ],
                    'consumer_key' => 'CONSUMER_1',
                    'tools' => [
                        ['secret' => 'toolsecret1'],
                        ['secret' => 'toolsecret2'],
                    ]
                ],
                'resourceconfig' => null,
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1'])[0],
                    'launch_migration_claim' => null,
                ],
                'syncmembers' => [
                    self::get_mock_members_with_ids(['1'], null)[0],
                    self::get_mock_members_with_ids(['2'], null)[0],
                    self::get_mock_members_with_ids(['3'], null)[0],
                    self::get_mock_members_with_ids(['4'], null)[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '2' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '3' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '4' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'New tool, migration only via member sync, no launch claim, new and existing users present in sync' => [
                'legacydata' => [
                    'users' => [
                        ['user_id' => '1'],
                        ['user_id' => '2'],
                    ],
                    'consumer_key' => 'CONSUMER_1',
                    'tools' => [
                        ['secret' => 'toolsecret1'],
                        ['secret' => 'toolsecret2'],
                    ]
                ],
                'resourceconfig' => null,
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => null,
                ],
                'syncmembers' => [
                    self::get_mock_members_with_ids(['1p3_1'], ['1'])[0],
                    self::get_mock_members_with_ids(['1p3_2'], ['2'])[0],
                    self::get_mock_members_with_ids(['1p3_3'], ['3'])[0],
                    self::get_mock_members_with_ids(['1p3_4'], ['4'])[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => true,
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'Default provisioning modes, mixed bag of users and roles' => [
                'legacydata' => null,
                'resourceconfig' => [
                    'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY,
                    'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING
                ],
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => null,
                ],
                'syncmembers' => [
                    // This user is just an instructor but is also the user who is already linked, via the launch above.
                    self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                    ])[0],
                    // This user is just a learner.
                    self::get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is also a learner.
                    self::get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is both an instructor and a learner.
                    self::get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked).
                            'is_migrated' => false,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode.
                            'is_migrated' => false,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode.
                            'is_migrated' => false,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => false,  // Both roles - not enrolled due to instructor's 'prompt' provisioning mode.
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'All automatic provisioning, mixed bag of users and roles' => [
                'legacydata' => null,
                'resourceconfig' => [
                    'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY,
                    'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY
                ],
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => null,
                ],
                'syncmembers' => [
                    // This user is just an instructor but is also the user who is already linked, via the launch above.
                    self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                    ])[0],
                    // This user is just a learner.
                    self::get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is also a learner.
                    self::get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is both an instructor and a learner.
                    self::get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked).
                            'is_migrated' => false,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode.
                            'is_migrated' => false,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode.
                            'is_migrated' => false,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => true, // Both roles - enrolled due to instructor's 'auto' provisioning mode.
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'All prompt provisioning, mixed bag of users and roles' => [
                'legacydata' => null,
                'resourceconfig' => [
                    'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING,
                    'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_PROMPT_NEW_EXISTING
                ],
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => null,
                ],
                'syncmembers' => [
                    // This user is just an instructor but is also the user who is already linked, via the launch above.
                    self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                    ])[0],
                    // This user is just a learner.
                    self::get_mock_members_with_ids(['1p3_2'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is also a learner.
                    self::get_mock_members_with_ids(['1p3_3'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is both an instructor and a learner.
                    self::get_mock_members_with_ids(['1p3_4'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked).
                            'is_migrated' => false,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => false, // Learner - not enrolled due to 'prompt' provisioning mode.
                            'is_migrated' => false,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => false, // Learner - not enrolled due to 'prompt' provisioning mode.
                            'is_migrated' => false,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => false, // Both roles - not enrolled due to instructor's 'prompt' provisioning mode.
                            'is_migrated' => false,
                        ]
                    ]
                ]
            ],
            'All automatic provisioning, with legacy data and migration claim, mixed bag of users and roles' => [
                'legacydata' => [
                    'users' => [
                        ['user_id' => '2'],
                        ['user_id' => '3'],
                        ['user_id' => '4'],
                        ['user_id' => '5']
                    ],
                    'consumer_key' => 'CONSUMER_1',
                    'tools' => [
                        ['secret' => 'toolsecret1'],
                        ['secret' => 'toolsecret2'],
                    ]
                ],
                'resourceconfig' => [
                    'provisioningmodelearner' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY,
                    'provisioningmodeinstructor' => \auth_plugin_lti::PROVISIONING_MODE_AUTO_ONLY
                ],
                'launchdata' => [
                    'user' => self::get_mock_launch_users_with_ids(['1p3_1'])[0],
                    'launch_migration_claim' => [
                        'consumer_key' => 'CONSUMER_1',
                        'signing_secret' => 'toolsecret1',
                        'context_id' => 'd345b',
                        'tool_consumer_instance_guid' => '12345-123',
                        'resource_link_id' => '4b6fa'
                    ],
                ],
                'syncmembers' => [
                    // This user is just an instructor but is also the user who is already linked, via the launch above.
                    self::get_mock_members_with_ids(['1p3_1'], null, true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                    ])[0],
                    // This user is just a learner.
                    self::get_mock_members_with_ids(['1p3_2'], ['2'], true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is also a learner.
                    self::get_mock_members_with_ids(['1p3_3'], ['3'], true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is both an instructor and a learner.
                    self::get_mock_members_with_ids(['1p3_4'], ['4'], true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
                    ])[0],
                    // This user is just an instructor who hasn't launched before (unlike the first user here).
                    self::get_mock_members_with_ids(['1p3_5'], ['5'], true, true, true, false, [
                        'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
                    ])[0],
                ],
                'expected' => [
                    'enrolments' => [
                        '1p3_1' => [
                            'is_enrolled' => true, // Instructor - enrolled because they are also the launch user (already linked).
                            'is_migrated' => false,
                        ],
                        '1p3_2' => [
                            'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode.
                            'is_migrated' => true,
                        ],
                        '1p3_3' => [
                            'is_enrolled' => true, // Learner - enrolled due to 'auto' provisioning mode.
                            'is_migrated' => true,
                        ],
                        '1p3_4' => [
                            'is_enrolled' => true, // Both roles - enrolled due to instructor's 'auto' provisioning mode.
                            'is_migrated' => true
                        ],
                        '1p3_5' => [
                            'is_enrolled' => true, // Instructor role only - enrolled due to instructor's 'auto' provisioning mode.
                            'is_migrated' => true
                        ]
                    ]
                ]
            ],
        ];
    }
}
