<?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 tool_dataprivacy;

/**
 * Expired contexts tests.
 *
 * @package    tool_dataprivacy
 * @copyright  2018 David Monllao
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
final class expired_contexts_test extends \advanced_testcase {

    /**
     * Setup the basics with the specified retention period.
     *
     * @param   string  $system Retention policy for the system.
     * @param   string  $user Retention policy for users.
     * @param   string  $course Retention policy for courses.
     * @param   string  $activity Retention policy for activities.
     */
    protected function setup_basics(string $system, string $user, ?string $course = null, ?string $activity = null): \stdClass {
        $this->resetAfterTest();

        $purposes = (object) [
            'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM),
            'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER),
        ];

        if (null !== $course) {
            $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
        }

        if (null !== $activity) {
            $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
        }

        return $purposes;
    }

    /**
     * Create a retention period and set it for the specified context level.
     *
     * @param   string  $retention
     * @param   int     $contextlevel
     * @return  purpose
     */
    protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel): purpose {
        $purpose = new purpose(0, (object) [
            'name' => 'Test purpose ' . rand(1, 1000),
            'retentionperiod' => $retention,
            'lawfulbases' => 'gdpr_art_6_1_a',
        ]);
        $purpose->create();

        $cat = new category(0, (object) ['name' => 'Test category']);
        $cat->create();

        if ($contextlevel <= CONTEXT_USER) {
            $record = (object) [
                'purposeid'     => $purpose->get('id'),
                'categoryid'    => $cat->get('id'),
                'contextlevel'  => $contextlevel,
            ];
            api::set_contextlevel($record);
        } else {
            list($purposevar, ) = data_registry::var_names_from_context(
                    \context_helper::get_class_for_level($contextlevel)
                );
            set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
        }

        return $purpose;
    }

    /**
     * Ensure that a user with no lastaccess is not flagged for deletion.
     */
    public function test_flag_not_setup(): void {
        $this->resetAfterTest();

        $user = $this->getDataGenerator()->create_user();

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a user with no lastaccess is not flagged for deletion.
     */
    public function test_flag_user_no_lastaccess(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user();

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a user with a recent lastaccess is not flagged for deletion.
     */
    public function test_flag_user_recent_lastaccess(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a user with a lastaccess in the past is flagged for deletion.
     */
    public function test_flag_user_past_lastaccess(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        // Although there is a block in the user context, everything in the user context is regarded as one.
        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);
    }

    /**
     * Ensure that a user with a lastaccess in the past but active enrolments is not flagged for deletion.
     */
    public function test_flag_user_past_lastaccess_still_enrolled(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enddate' => time() + YEARSECS]);
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $otheruser = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion.
     */
    public function test_flag_user_update_existing(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'P5Y');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'defaultexpired' => 0,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredcontext->save();
        $this->assertEquals(0, $expiredcontext->get('defaultexpired'));

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);

        // The user context will now have expired.
        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(1, $updatedcontext->get('defaultexpired'));
    }

    /**
     * Ensure that a user with a lastaccess in the past and expired enrolments.
     */
    public function test_flag_user_past_lastaccess_unexpired_past_enrolment(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'P1Y');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $otheruser = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a user with a lastaccess in the past and expired enrolments.
     */
    public function test_flag_user_past_override_role(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);

        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->user->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();
        role_assign($role->id, $user->id, $systemcontext->id);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        $expiredrecord = expired_context::get_record(['contextid' => $usercontext->id]);
        $this->assertFalse($expiredrecord);
    }

    /**
     * Ensure that a user with a lastaccess in the past and expired enrolments.
     */
    public function test_flag_user_past_lastaccess_expired_enrolled(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $otheruser = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(1, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);
    }

    /**
     * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
     * correctly.
     */
    public function test_flag_user_past_lastaccess_missing_enddate_required(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course();
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $otheruser = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Ensure that course end dates are not required.
        set_config('requireallenddatesforuserdeletion', 1, 'tool_dataprivacy');

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
     * correctly when the end date is not required.
     */
    public function test_flag_user_past_lastaccess_missing_enddate_not_required(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course();
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $otheruser = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Ensure that course end dates are required.
        set_config('requireallenddatesforuserdeletion', 0, 'tool_dataprivacy');

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);
    }

    /**
     * Ensure that a user with a recent lastaccess is not flagged for deletion.
     */
    public function test_flag_user_recent_lastaccess_existing_record(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
        $usercontext = \context_user::instance($user->id);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredcontext->save();

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        $this->expectException('dml_missing_record_exception');
        new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a user with a recent lastaccess is not flagged for deletion.
     */
    public function test_flag_user_retention_changed(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);

        $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
        $this->assertNotFalse($expiredcontext);

        // Increase the retention period to 5 years.
        $purposes->user->set('retentionperiod', 'P5Y');
        $purposes->user->save();

        // Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased.
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        // The expiry record will now have been removed.
        $this->expectException('dml_missing_record_exception');
        new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a user with a historically expired expired block record child is cleaned up.
     */
    public function test_flag_user_historic_block_unapproved(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $blockcontext = \context_block::instance($block->instance->id);
        $this->setUser();

        // Create an existing expired_context which has not been approved for the block.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $blockcontext->id,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredcontext->save();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);

        $expiredblockcontext = expired_context::get_record(['contextid' => $blockcontext->id]);
        $this->assertFalse($expiredblockcontext);

        $expiredusercontext = expired_context::get_record(['contextid' => $usercontext->id]);
        $this->assertNotFalse($expiredusercontext);
    }

    /**
     * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
     */
    public function test_flag_user_historic_unexpired_child(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');
        $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $blockcontext = \context_block::instance($block->instance->id);
        $this->setUser();

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(1, $flaggedusers);

        $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
        $this->assertNotFalse($expiredcontext);
    }

    /**
     * Ensure that a course with no end date is not flagged.
     */
    public function test_flag_course_no_enddate(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course();
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a course with an end date in the distant past, but a child which is unexpired is not flagged.
     */
    public function test_flag_course_past_enddate_future_child(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'P5Y');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a course with an end date in the distant past is flagged.
     */
    public function test_flag_course_past_enddate(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(2, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a course with an end date in the distant past is flagged.
     */
    public function test_flag_course_past_enddate_multiple(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course1 = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);

        $course2 = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(4, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a course with an end date in the future is not flagged.
     */
    public function test_flag_course_future_enddate(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a course with an end date in the future is not flagged.
     */
    public function test_flag_course_recent_unexpired_enddate(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);
    }

    /**
     * Ensure that a course with an end date in the distant past is flagged, taking into account any purpose override
     */
    public function test_flag_course_past_enddate_with_override_unexpired_role(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $role = $DB->get_record('role', ['shortname' => 'editingteacher']);

        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * DAYSECS),
                'enddate' => time() - DAYSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(1, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
        $this->assertEmpty($expiredrecord->get('expiredroles'));

        $unexpiredroles = $expiredrecord->get('unexpiredroles');
        $this->assertCount(1, $unexpiredroles);
        $this->assertContainsEquals($role->id, $unexpiredroles);
    }

    /**
     * Ensure that a course with an end date in the distant past is flagged, and any expired role is ignored.
     */
    public function test_flag_course_past_enddate_with_override_expired_role(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $role = $DB->get_record('role', ['shortname' => 'student']);

        // The role has a much shorter retention, but both should match.
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1M',
            ]);
        $override->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * DAYSECS),
                'enddate' => time() - DAYSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(1, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
        $this->assertEmpty($expiredrecord->get('expiredroles'));
        $this->assertEmpty($expiredrecord->get('unexpiredroles'));
        $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
    }

    /**
     * Ensure that where a course has explicitly expired one role, but that role is explicitly not expired in a child
     * context, does not have the parent context role expired.
     */
    public function test_flag_course_override_expiredwith_override_unexpired_on_child(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('P1Y', 'P1Y', 'P1Y');

        $role = $DB->get_record('role', ['shortname' => 'editingteacher']);

        (new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1S',
            ]))->save();

        $modpurpose = new purpose(0, (object) [
            'name' => 'Module purpose',
            'retentionperiod' => 'PT1S',
            'lawfulbases' => 'gdpr_art_6_1_a',
        ]);
        $modpurpose->create();

        (new purpose_override(0, (object) [
                'purposeid' => $modpurpose->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]))->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * DAYSECS),
                'enddate' => time() - DAYSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);

        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);

        api::set_context_instance((object) [
                'contextid' => $forumcontext->id,
                'purposeid' => $modpurpose->get('id'),
                'categoryid' => 0,
            ]);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(1, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        // The course will not be expired as the default expiry has not passed, and the explicit role override has been
        // removed due to the child non-expiry.
        $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
        $this->assertFalse($expiredrecord);

        // The forum will have an expiry for all _but_ the overridden role.
        $expiredrecord = expired_context::get_record(['contextid' => $forumcontext->id]);
        $this->assertEmpty($expiredrecord->get('expiredroles'));

        // The teacher is not expired.
        $unexpiredroles = $expiredrecord->get('unexpiredroles');
        $this->assertCount(1, $unexpiredroles);
        $this->assertContainsEquals($role->id, $unexpiredroles);
        $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
    }

    /**
     * Ensure that a user context previously flagged as approved is not removed if the user has any unexpired roles.
     */
    public function test_process_user_context_with_override_unexpired_role(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);

        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->user->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();
        role_assign($role->id, $user->id, $systemcontext->id);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'defaultexpired' => 1,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->add_unexpiredroles([$role->id]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_users_in_context',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        $manager->set_progress(new \null_progress_trace());
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $this->expectException('dml_missing_record_exception');
        $updatedcontext = new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a module context previously flagged as approved is removed with appropriate unexpiredroles kept.
     */
    public function test_process_course_context_with_override_unexpired_role(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $role = $DB->get_record('role', ['shortname' => 'editingteacher']);

        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');

        $student = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $student->id,
        ]);

        $teacher = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $teacher->id,
        ]);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $forumcontext->id,
                'defaultexpired' => 1,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->add_unexpiredroles([$role->id]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_users_in_context',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
        $mockprivacymanager
            ->expects($this->once())
            ->method('delete_data_for_users_in_context')
            ->with($this->callback(function($userlist) use ($student, $teacher) {
                $forumlist = $userlist->get_userlist_for_component('mod_forum');
                $userids = $forumlist->get_userids();
                $this->assertCount(1, $userids);
                $this->assertContainsEquals($student->id, $userids);
                $this->assertNotContainsEquals($teacher->id, $userids);
                return true;
            }));

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        $manager->set_progress(new \null_progress_trace());
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
     */
    public function test_process_course_context_with_override_expired_role(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');

        $role = $DB->get_record('role', ['shortname' => 'student']);

        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1M',
            ]);
        $override->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');

        $student = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $student->id,
        ]);

        $teacher = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $teacher->id,
        ]);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $forumcontext->id,
                'defaultexpired' => 0,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->add_expiredroles([$role->id]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_users_in_context',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
        $mockprivacymanager
            ->expects($this->once())
            ->method('delete_data_for_users_in_context')
            ->with($this->callback(function($userlist) use ($student, $teacher) {
                $forumlist = $userlist->get_userlist_for_component('mod_forum');
                $userids = $forumlist->get_userids();
                $this->assertCount(1, $userids);
                $this->assertContainsEquals($student->id, $userids);
                $this->assertNotContainsEquals($teacher->id, $userids);
                return true;
            }));

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        $manager->set_progress(new \null_progress_trace());
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
     */
    public function test_process_course_context_with_user_in_both_lists(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');

        $role = $DB->get_record('role', ['shortname' => 'student']);

        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1M',
            ]);
        $override->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');

        $teacher = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $teacher->id,
        ]);

        $student = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $student->id,
        ]);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $forumcontext->id,
                'defaultexpired' => 0,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->add_expiredroles([$role->id]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_users_in_context',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
        $mockprivacymanager
            ->expects($this->once())
            ->method('delete_data_for_users_in_context')
            ->with($this->callback(function($userlist) use ($student, $teacher) {
                $forumlist = $userlist->get_userlist_for_component('mod_forum');
                $userids = $forumlist->get_userids();
                $this->assertCount(1, $userids);
                $this->assertContainsEquals($student->id, $userids);
                $this->assertNotContainsEquals($teacher->id, $userids);
                return true;
            }));

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        $manager->set_progress(new \null_progress_trace());
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
     */
    public function test_process_course_context_with_user_in_both_lists_expired(): void {
        global $DB;
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');

        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $studentrole->id,
                'retentionperiod' => 'PT1M',
            ]);
        $override->save();

        $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $teacherrole->id,
                'retentionperiod' => 'PT1M',
            ]);
        $override->save();

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');

        $teacher = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $teacher->id,
        ]);

        $student = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
        $generator->create_discussion((object) [
            'course' => $forum->course,
            'forum' => $forum->id,
            'userid' => $student->id,
        ]);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $forumcontext->id,
                'defaultexpired' => 0,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->add_expiredroles([$studentrole->id, $teacherrole->id]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_users_in_context',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
        $mockprivacymanager
            ->expects($this->once())
            ->method('delete_data_for_users_in_context')
            ->with($this->callback(function($userlist) use ($student, $teacher) {
                $forumlist = $userlist->get_userlist_for_component('mod_forum');
                $userids = $forumlist->get_userids();
                $this->assertCount(2, $userids);
                $this->assertContainsEquals($student->id, $userids);
                $this->assertContainsEquals($teacher->id, $userids);
                return true;
            }));

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        $manager->set_progress(new \null_progress_trace());
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a site not setup will not process anything.
     */
    public function test_process_not_setup(): void {
        $this->resetAfterTest();

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);
    }

    /**
     * Ensure that a user with no lastaccess is not flagged for deletion.
     */
    public function test_process_none_approved(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);
    }

    /**
     * Ensure that a user with no lastaccess is not flagged for deletion.
     */
    public function test_process_no_context(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => -1,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $this->expectException('dml_missing_record_exception');
        new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a user context previously flagged as approved is removed.
     */
    public function test_process_user_context(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $blockcontext = \context_block::instance($block->instance->id);
        $this->setUser();

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
        $deleteinvocations = $this->exactly(2);
        $mockprivacymanager->expects($deleteinvocations)
            ->method('delete_data_for_all_users_in_context')
            ->willReturnCallback(function ($context) use (
                $deleteinvocations,
                $blockcontext,
                $usercontext,
            ): void {
                match (self::getInvocationCount($deleteinvocations)) {
                    1 => $this->assertEquals($blockcontext, $context),
                    2 => $this->assertEquals($usercontext, $context),
                    default => $this->fail('Unexpected invocation count'),
                };
            });

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(1, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));

        // Flag all expired contexts again.
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(0, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        // Ensure that the deleted context record is still present.
        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a course context previously flagged as approved is removed.
     */
    public function test_process_course_context(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $coursecontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->once())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a user context previously flagged as approved is not removed if the user then logs in.
     */
    public function test_process_user_context_logged_in_after_approval(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        // Now bump the user's last login time.
        $this->setUser($user);
        user_accesstime_log();
        $this->setUser();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $this->expectException('dml_missing_record_exception');
        new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a user context previously flagged as approved is not removed if the purpose has changed.
     */
    public function test_process_user_context_changed_after_approved(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $context = \context_block::instance($block->instance->id);
        $this->setUser();

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        // Now make the user a site admin.
        $admins = explode(',', get_config('moodle', 'siteadmins'));
        $admins[] = $user->id;
        set_config('siteadmins', implode(',', $admins));

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $this->expectException('dml_missing_record_exception');
        new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a user with a historically expired expired block record child is cleaned up.
     */
    public function test_process_user_historic_block_unapproved(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $blockcontext = \context_block::instance($block->instance->id);
        $this->setUser();

        // Create an expired_context for the user.
        $expiredusercontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredusercontext->save();

        // Create an existing expired_context which has not been approved for the block.
        $expiredblockcontext = new expired_context(0, (object) [
                'contextid' => $blockcontext->id,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredblockcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
        $deleteinvocations = $this->exactly(2);
        $mockprivacymanager->expects($deleteinvocations)
            ->method('delete_data_for_all_users_in_context')
            ->willReturnCallback(function ($context) use (
                $deleteinvocations,
                $blockcontext,
                $usercontext,
            ): void {
                match (self::getInvocationCount($deleteinvocations)) {
                    1 => $this->assertEquals($blockcontext, $context),
                    2 => $this->assertEquals($usercontext, $context),
                    default => $this->fail('Unexpected invocation count'),
                };
            });

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(1, $processedusers);

        $updatedcontext = new expired_context($expiredusercontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
     */
    public function test_process_user_historic_unexpired_child(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H');
        $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
        $usercontext = \context_user::instance($user->id);

        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $blockcontext = \context_block::instance($block->instance->id);
        $this->setUser();

        // Create an expired_context for the user.
        $expiredusercontext = new expired_context(0, (object) [
                'contextid' => $usercontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredusercontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
        $deleteinvocations = $this->exactly(2);
        $mockprivacymanager->expects($deleteinvocations)
            ->method('delete_data_for_all_users_in_context')
            ->willReturnCallback(function ($context) use (
                $deleteinvocations,
                $blockcontext,
                $usercontext,
            ): void {
                match (self::getInvocationCount($deleteinvocations)) {
                    1 => $this->assertEquals($blockcontext, $context),
                    2 => $this->assertEquals($usercontext, $context),
                    default => $this->fail('Unexpected invocation count'),
                };
            });

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(1, $processedusers);

        $updatedcontext = new expired_context($expiredusercontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
     * updated.
     */
    public function test_process_course_context_updated(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $coursecontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());
        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);

        // Changing the retention period to a longer period will remove the expired_context record.
        $purposes->activity->set('retentionperiod', 'P5Y');
        $purposes->activity->save();

        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $this->expectException('dml_missing_record_exception');
        $updatedcontext = new expired_context($expiredcontext->get('id'));
    }

    /**
     * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
     * updated.
     */
    public function test_process_course_context_outstanding_children(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        // Create an existing expired_context.
        $expiredcontext = new expired_context(0, (object) [
                'contextid' => $coursecontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcontext->get('id'));

        // No change - we just can't process it until the children have finished.
        $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
     * updated.
     */
    public function test_process_course_context_pending_children(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);

        // Create an existing expired_context for the course.
        $expiredcoursecontext = new expired_context(0, (object) [
                'contextid' => $coursecontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcoursecontext->save();

        // And for the forum.
        $expiredforumcontext = new expired_context(0, (object) [
                'contextid' => $forumcontext->id,
                'status' => expired_context::STATUS_EXPIRED,
            ]);
        $expiredforumcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(0, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredcoursecontext->get('id'));

        // No change - we just can't process it until the children have finished.
        $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
    }

    /**
     * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
     * updated.
     */
    public function test_process_course_context_approved_children(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $coursecontext = \context_course::instance($course->id);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);

        // Create an existing expired_context for the course.
        $expiredcoursecontext = new expired_context(0, (object) [
                'contextid' => $coursecontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredcoursecontext->save();

        // And for the forum.
        $expiredforumcontext = new expired_context(0, (object) [
                'contextid' => $forumcontext->id,
                'status' => expired_context::STATUS_APPROVED,
            ]);
        $expiredforumcontext->save();

        $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
            ->onlyMethods([
                'delete_data_for_user',
                'delete_data_for_all_users_in_context',
            ])
            ->getMock();
        $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
        $deleteinvocations = $this->exactly(2);
        $mockprivacymanager->expects($deleteinvocations)
            ->method('delete_data_for_all_users_in_context')
            ->willReturnCallback(function ($context) use (
                $deleteinvocations,
                $forumcontext,
                $coursecontext,
            ): void {
                match (self::getInvocationCount($deleteinvocations)) {
                    1 => $this->assertEquals($forumcontext, $context),
                    2 => $this->assertEquals($coursecontext, $context),
                    default => $this->fail('Unexpected invocation count'),
                };
            });

        $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
            ->onlyMethods(['get_privacy_manager'])
            ->getMock();
        $manager->set_progress(new \null_progress_trace());

        $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);

        // Initially only the forum will be processed.
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        $updatedcontext = new expired_context($expiredforumcontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));

        // The course won't have been processed yet.
        $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
        $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));

        // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts.
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);
        $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
        $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
    }

    /**
     * Test that the can_process_deletion function returns expected results.
     *
     * @dataProvider    can_process_deletion_provider
     * @param       int     $status
     * @param       bool    $expected
     */
    public function test_can_process_deletion($status, $expected): void {
        $purpose = new expired_context(0, (object) [
            'status' => $status,

            'contextid' => \context_system::instance()->id,
        ]);

        $this->assertEquals($expected, $purpose->can_process_deletion());
    }

    /**
     * Data provider for the can_process_deletion tests.
     *
     * @return  array
     */
    public static function can_process_deletion_provider(): array {
        return [
            'Pending' => [
                expired_context::STATUS_EXPIRED,
                false,
            ],
            'Approved' => [
                expired_context::STATUS_APPROVED,
                true,
            ],
            'Complete' => [
                expired_context::STATUS_CLEANED,
                false,
            ],
        ];
    }

    /**
     * Test that the is_complete function returns expected results.
     *
     * @dataProvider        is_complete_provider
     * @param       int     $status
     * @param       bool    $expected
     */
    public function test_is_complete($status, $expected): void {
        $purpose = new expired_context(0, (object) [
            'status' => $status,
            'contextid' => \context_system::instance()->id,
        ]);

        $this->assertEquals($expected, $purpose->is_complete());
    }

    /**
     * Data provider for the is_complete tests.
     *
     * @return  array
     */
    public static function is_complete_provider(): array {
        return [
            'Pending' => [
                expired_context::STATUS_EXPIRED,
                false,
            ],
            'Approved' => [
                expired_context::STATUS_APPROVED,
                false,
            ],
            'Complete' => [
                expired_context::STATUS_CLEANED,
                true,
            ],
        ];
    }

    /**
     * Test that the is_fully_expired function returns expected results.
     *
     * @dataProvider        is_fully_expired_provider
     * @param       array   $record
     * @param       bool    $expected
     */
    public function test_is_fully_expired($record, $expected): void {
        $purpose = new expired_context(0, (object) $record);

        $this->assertEquals($expected, $purpose->is_fully_expired());
    }

    /**
     * Data provider for the is_fully_expired tests.
     *
     * @return  array
     */
    public static function is_fully_expired_provider(): array {
        return [
            'Fully expired' => [
                [
                    'status' => expired_context::STATUS_APPROVED,
                    'defaultexpired' => 1,
                ],
                true,
            ],
            'Unexpired roles present' => [
                [
                    'status' => expired_context::STATUS_APPROVED,
                    'defaultexpired' => 1,
                    'unexpiredroles' => json_encode([1]),
                ],
                false,
            ],
            'Only some expired roles present' => [
                [
                    'status' => expired_context::STATUS_APPROVED,
                    'defaultexpired' => 0,
                    'expiredroles' => json_encode([1]),
                ],
                false,
            ],
        ];
    }

    /**
     * Ensure that any orphaned records are removed once the context has been removed.
     */
    public function test_orphaned_records_are_cleared(): void {
        $this->resetAfterTest();

        $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');

        $course = $this->getDataGenerator()->create_course([
                'startdate' => time() - (2 * YEARSECS),
                'enddate' => time() - YEARSECS,
            ]);
        $context = \context_course::instance($course->id);

        // Flag all expired contexts.
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        $manager->set_progress(new \null_progress_trace());
        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();

        $this->assertEquals(1, $flaggedcourses);
        $this->assertEquals(0, $flaggedusers);

        // Ensure that the record currently exists.
        $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
        $this->assertNotFalse($expiredcontext);

        // Approve it.
        $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save();

        // Process deletions.
        list($processedcourses, $processedusers) = $manager->process_approved_deletions();

        $this->assertEquals(1, $processedcourses);
        $this->assertEquals(0, $processedusers);

        // Ensure that the record still exists.
        $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
        $this->assertNotFalse($expiredcontext);

        // Remove the actual course.
        delete_course($course->id, false);

        // The record will still exist until we flag it again.
        $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
        $this->assertNotFalse($expiredcontext);

        list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
        $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
        $this->assertFalse($expiredcontext);
    }

    /**
     * Ensure that the progres tracer works as expected out of the box.
     */
    public function test_progress_tracer_default(): void {
        $manager = new \tool_dataprivacy\expired_contexts_manager();

        $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
        $rcm = $rc->getMethod('get_progress');

        $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager));
    }

    /**
     * Ensure that the progres tracer works as expected when given a specific traer.
     */
    public function test_progress_tracer_set(): void {
        $manager = new \tool_dataprivacy\expired_contexts_manager();
        $mytrace = new \null_progress_trace();
        $manager->set_progress($mytrace);

        $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
        $rcm = $rc->getMethod('get_progress');

        $this->assertSame($mytrace, $rcm->invoke($manager));
    }

    /**
     * Creates an HTML block on a user.
     *
     * @param   string  $title
     * @param   string  $body
     * @param   string  $format
     * @return  \block_instance
     */
    protected function create_user_block($title, $body, $format) {
        global $USER;

        $configdata = (object) [
            'title' => $title,
            'text' => [
                'itemid' => 19,
                'text' => $body,
                'format' => $format,
            ],
        ];

        $this->create_block($this->construct_user_page($USER));
        $block = $this->get_last_block_on_page($this->construct_user_page($USER));
        $block = block_instance('html', $block->instance);
        $block->instance_config_save((object) $configdata);

        return $block;
    }

    /**
     * Creates an HTML block on a page.
     *
     * @param \page $page Page
     */
    protected function create_block($page) {
        $page->blocks->add_block_at_end_of_default_region('html');
    }

    /**
     * Constructs a Page object for the User Dashboard.
     *
     * @param   \stdClass       $user User to create Dashboard for.
     * @return  \moodle_page
     */
    protected function construct_user_page(\stdClass $user) {
        $page = new \moodle_page();
        $page->set_context(\context_user::instance($user->id));
        $page->set_pagelayout('mydashboard');
        $page->set_pagetype('my-index');
        $page->blocks->load_blocks();
        return $page;
    }

    /**
     * Get the last block on the page.
     *
     * @param \page $page Page
     * @return \block_html Block instance object
     */
    protected function get_last_block_on_page($page) {
        $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
        $block = end($blocks);

        return $block;
    }

    /**
     * Test the is_context_expired functions when supplied with the system context.
     */
    public function test_is_context_expired_system(): void {
        $this->resetAfterTest();
        $this->setup_basics('PT1H', 'PT1H', 'P1D');
        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);

        $this->assertFalse(expired_contexts_manager::is_context_expired(\context_system::instance()));
        $this->assertFalse(
                expired_contexts_manager::is_context_expired_or_unprotected_for_user(\context_system::instance(), $user));
    }

    /**
     * Test the is_context_expired functions when supplied with a block in the user context.
     *
     * Children of a user context always follow the user expiry rather than any context level defaults (e.g. at the
     * block level.
     */
    public function test_is_context_expired_user_block(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
        $purposes->block = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $this->setUser($user);
        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
        $blockcontext = \context_block::instance($block->instance->id);
        $this->setUser();

        // Protected flags have no bearing on expiry of user subcontexts.
        $this->assertTrue(expired_contexts_manager::is_context_expired($blockcontext));

        $purposes->block->set('protected', 1)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));

        $purposes->block->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
    }

    /**
     * Test the is_context_expired functions when supplied with the front page course.
     */
    public function test_is_context_expired_frontpage(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');

        $frontcourse = get_site();
        $frontcoursecontext = \context_course::instance($frontcourse->id);

        $sitenews = $this->getDataGenerator()->create_module('forum', ['course' => $frontcourse->id]);
        $cm = get_coursemodule_from_instance('forum', $sitenews->id);
        $sitenewscontext = \context_module::instance($cm->id);

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);

        $this->assertFalse(expired_contexts_manager::is_context_expired($frontcoursecontext));
        $this->assertFalse(expired_contexts_manager::is_context_expired($sitenewscontext));

        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));

        // Protecting the course contextlevel does not impact the front page.
        $purposes->course->set('protected', 1)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));

        // Protecting the system contextlevel affects the front page, too.
        $purposes->system->set('protected', 1)->save();
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
    }

    /**
     * Test the is_context_expired functions when supplied with an expired course.
     */
    public function test_is_context_expired_course_expired(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
        $coursecontext = \context_course::instance($course->id);

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));

        $purposes->course->set('protected', 1)->save();
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->course->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
    }

    /**
     * Test the is_context_expired functions when supplied with an unexpired course.
     */
    public function test_is_context_expired_course_unexpired(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $this->assertTrue(expired_contexts_manager::is_context_expired($coursecontext));

        $purposes->course->set('protected', 1)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->course->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
    }

    /**
     * Test the is_context_expired functions when supplied with an unexpired course and a child context in the course which is protected.
     *
     * When a child context has a specific purpose set, then that purpose should be respected with respect to the
     * course.
     *
     * If the course is still within the expiry period for the child context, then that child's protected flag should be
     * respected, even when the course may have expired.
     */
    public function test_is_child_context_expired_course_unexpired_with_child(): void {
        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D', 'P1D');
        $purposes->course->set('protected', 0)->save();
        $purposes->activity->set('protected', 1)->save();

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() + WEEKSECS]);
        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);

        $coursecontext = \context_course::instance($course->id);
        $cm = get_coursemodule_from_instance('forum', $forum->id);
        $forumcontext = \context_module::instance($cm->id);

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
        $this->assertFalse(expired_contexts_manager::is_context_expired($forumcontext));

        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user));

        $purposes->activity->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user));
    }

    /**
     * Test the is_context_expired functions when supplied with an expired course which has role overrides.
     */
    public function test_is_context_expired_course_expired_override(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();
        role_assign($role->id, $user->id, $systemcontext->id);

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));

        $purposes->course->set('protected', 1)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->course->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
    }

    /**
     * Test the is_context_expired functions when supplied with an expired course which has role overrides.
     */
    public function test_is_context_expired_course_expired_override_parent(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->system->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();
        role_assign($role->id, $user->id, $systemcontext->id);

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));

        // The user override applies to this user. THIs means that the default expiry has no effect.
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->system->set('protected', 1)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->system->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $override->set('protected', 1)->save();
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->system->set('protected', 1)->save();
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

        $purposes->system->set('protected', 0)->save();
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));

    }

    /**
     * Test the is_context_expired functions when supplied with an expired course which has role overrides but the user
     * does not hold the role.
     */
    public function test_is_context_expired_course_expired_override_parent_no_role(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1H', 'PT1H');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->system->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'P5Y',
            ]);
        $override->save();

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        // This context is not _fully _ expired.
        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
    }

    /**
     * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
     */
    public function test_is_context_expired_course_expired_override_inverse(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('P1Y', 'P1Y');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'student']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->system->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1S',
            ]);
        $override->save();

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');

        // This context is not _fully _ expired.
        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
    }

    /**
     * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
     */
    public function test_is_context_expired_course_expired_override_inverse_parent(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('P1Y', 'P1Y');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->system->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1S',
            ]);
        $override->save();

        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
        role_assign($role->id, $user->id, $systemcontext->id);

        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
        role_unassign($studentrole->id, $user->id, $coursecontext->id);

        // This context is not _fully _ expired.
        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
    }

    /**
     * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
     */
    public function test_is_context_expired_course_expired_override_inverse_parent_not_assigned(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('P1Y', 'P1Y');

        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $role = $DB->get_record('role', ['shortname' => 'manager']);
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->system->get('id'),
                'roleid' => $role->id,
                'retentionperiod' => 'PT1S',
            ]);
        $override->save();

        // Enrol the user in the course without any role.
        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
        role_unassign($studentrole->id, $user->id, $coursecontext->id);

        // This context is not _fully _ expired.
        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
    }

    /**
     * Ensure that context expired checks for a specific user taken into account roles.
     */
    public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('PT1S', 'PT1S', 'PT1S');

        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $roles['manager'],
                'retentionperiod' => 'P1W',
                'protected' => 1,
            ]);
        $override->save();

        $s = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');

        $t = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');

        $sm = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
        role_assign($roles['manager'], $sm->id, $coursecontext->id);

        $m = $this->getDataGenerator()->create_user();
        role_assign($roles['manager'], $m->id, $coursecontext->id);

        $tm = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
        role_assign($roles['manager'], $tm->id, $coursecontext->id);

        // The context should only be expired for users who are not a manager.
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));

        $override->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
    }

    /**
     * Ensure that context expired checks for a specific user taken into account roles when retention is inversed.
     */
    public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected_inverse(): void {
        global $DB;

        $this->resetAfterTest();

        $purposes = $this->setup_basics('P5Y', 'P5Y', 'P5Y');

        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
        $coursecontext = \context_course::instance($course->id);
        $systemcontext = \context_system::instance();

        $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
        $override = new purpose_override(0, (object) [
                'purposeid' => $purposes->course->get('id'),
                'roleid' => $roles['student'],
                'retentionperiod' => 'PT1S',
            ]);
        $override->save();

        $s = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');

        $t = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');

        $sm = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
        role_assign($roles['manager'], $sm->id, $coursecontext->id);

        $m = $this->getDataGenerator()->create_user();
        role_assign($roles['manager'], $m->id, $coursecontext->id);

        $tm = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
        role_assign($roles['manager'], $tm->id, $coursecontext->id);

        // The context should only be expired for users who are only a student.
        $purposes->course->set('protected', 1)->save();
        $override->set('protected', 1)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));

        $purposes->course->set('protected', 0)->save();
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
    }
}
