<?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/>.

/**
 * Unit tests for (some of) mod/assign/locallib.php.
 *
 * @package    mod_assign
 * @category   test
 * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
namespace mod_assign;

use mod_assign_grade_form;
use mod_assign_test_generator;
use mod_assign_testable_assign;

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

global $CFG;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
require_once($CFG->dirroot . '/mod/assign/tests/generator.php');

/**
 * Unit tests for (some of) mod/assign/locallib.php.
 *
 * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
final class locallib_test extends \advanced_testcase {
    // Use the generator helper.
    use mod_assign_test_generator;

    /** @var array */
    public $extrastudents;

    /** @var array */
    public $extrasuspendedstudents;

    public function test_return_links(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();

        $assign = $this->create_instance($course);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        $assign->register_return_link('RETURNACTION', ['param' => 1]);
        $this->assertEquals('RETURNACTION', $assign->get_return_action());
        $this->assertEquals(['param' => 1], $assign->get_return_params());
    }

    public function test_get_feedback_plugins(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $this->setUser($teacher);
        $assign = $this->create_instance($course);
        $installedplugins = array_keys(\core_component::get_plugin_list('assignfeedback'));

        foreach ($assign->get_feedback_plugins() as $plugin) {
            $this->assertContains($plugin->get_type(), $installedplugins, 'Feedback plugin not in list of installed plugins');
        }
    }

    public function test_get_submission_plugins(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $this->setUser($teacher);
        $assign = $this->create_instance($course);
        $installedplugins = array_keys(\core_component::get_plugin_list('assignsubmission'));

        foreach ($assign->get_submission_plugins() as $plugin) {
            $this->assertContains($plugin->get_type(), $installedplugins, 'Submission plugin not in list of installed plugins');
        }
    }

    public function test_is_blind_marking(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['blindmarking' => 1]);
        $this->assertEquals(true, $assign->is_blind_marking());

        // Test cannot see student names.
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));

        // Test students cannot reveal identities.
        $nopermission = false;
        $student->ignoresesskey = true;
        $this->setUser($student);
        $this->expectException('required_capability_exception');
        $assign->reveal_identities();
        $student->ignoresesskey = false;

        // Test teachers cannot reveal identities.
        $nopermission = false;
        $teacher->ignoresesskey = true;
        $this->setUser($teacher);
        $this->expectException('required_capability_exception');
        $assign->reveal_identities();
        $teacher->ignoresesskey = false;

        // Test sesskey is required.
        $this->setUser($teacher);
        $this->expectException('moodle_exception');
        $assign->reveal_identities();

        // Test editingteacher can reveal identities if sesskey is ignored.
        $teacher->ignoresesskey = true;
        $this->setUser($teacher);
        $assign->reveal_identities();
        $this->assertEquals(false, $assign->is_blind_marking());
        $teacher->ignoresesskey = false;

        // Test student names are visible.
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertEquals(false, strpos($output, get_string('hiddenuser', 'assign')));

        // Set this back to default.
        $teacher->ignoresesskey = false;
    }

    /**
     * Data provider for test_get_assign_perpage
     *
     * @return array[] Provider data
     */
    public static function get_assign_perpage_provider(): array {
        return [
            [
                'maxperpage' => -1,
                'userprefs' => [
                    -1 => -1,
                    10 => 10,
                    20 => 20,
                    50 => 50,
                ],
            ],
            [
                'maxperpage' => 15,
                'userprefs' => [
                    -1 => 15,
                    10 => 10,
                    20 => 15,
                    50 => 15,
                ],
            ],
        ];
    }

    /**
     * Test maxperpage
     *
     * @dataProvider get_assign_perpage_provider
     * @param integer $maxperpage site config value
     * @param array $userprefs Array of user preferences and expected page sizes
     */
    public function test_get_assign_perpage($maxperpage, $userprefs): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course);

        set_config('maxperpage', $maxperpage, 'assign');
        set_user_preference('assign_perpage', null);
        $this->assertEquals(10, $assign->get_assign_perpage());
        foreach ($userprefs as $pref => $perpage) {
            set_user_preference('assign_perpage', $pref);
            $this->assertEquals($perpage, $assign->get_assign_perpage());
        }
    }

    /**
     * Test filter by requires grading.
     *
     * This is specifically checking an assignment with no grade to make sure we do not
     * get an exception thrown when rendering the grading table for this type of assignment.
     */
    public function test_gradingtable_filter_by_requiresgrading_no_grade(): void {
        global $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
                'assignsubmission_onlinetext_enabled' => 1,
                'assignfeedback_comments_enabled' => 0,
                'grade' => GRADE_TYPE_NONE,
            ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', [
            'id' => $assign->get_course_module()->id,
            'action' => 'grading',
        ]));

        // Render the table with the requires grading filter.
        $gradingtable = new \assign_grading_table($assign, 1, ASSIGN_FILTER_REQUIRE_GRADING, 0, true);
        $output = $assign->get_renderer()->render($gradingtable);

        // Test that the filter function does not throw errors for assignments with no grade.
        $this->assertStringContainsString(get_string('nothingtodisplay'), $output);
    }


    /**
     * Test submissions with extension date.
     */
    public function test_gradingtable_extension_due_date(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Setup the assignment.
        $this->setUser($teacher);
        $time = time();
        $assign = $this->create_instance($course, [
                'assignsubmission_onlinetext_enabled' => 1,
                'duedate' => time() - (4 * DAYSECS),
            ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', [
            'id' => $assign->get_course_module()->id,
            'action' => 'grading',
        ]));

        // Check that the assignment is late.
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS))), $output);

        // Grant an extension.
        $extendedtime = $time + (2 * DAYSECS);
        $assign->testable_save_user_extension($student->id, $extendedtime);
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);

        // Simulate a submission.
        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, false);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE,
        ];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);

        // Verify output.
        $this->setUser($teacher);
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_submitted', 'assign'), $output);
        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
    }

    /**
     * Test that late submissions with extension date calculate correctly.
     */
    public function test_gradingtable_extension_date_calculation_for_lateness(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Setup the assignment.
        $this->setUser($teacher);
        $time = time();
        $assign = $this->create_instance($course, [
                'assignsubmission_onlinetext_enabled' => 1,
                'duedate' => time() - (4 * DAYSECS),
            ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', [
            'id' => $assign->get_course_module()->id,
            'action' => 'grading',
        ]));

        // Check that the assignment is late.
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
        $difftime = time() - $time;
        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);

        // Grant an extension that is in the past.
        $assign->testable_save_user_extension($student->id, $time - (2 * DAYSECS));
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($time - (2 * DAYSECS))), $output);
        $difftime = time() - $time;
        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);

        // Simulate a submission.
        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, false);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE,
        ];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);
        $submittedtime = time();

        // Verify output.
        $this->setUser($teacher);
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_submitted', 'assign'), $output);
        $this->assertStringContainsString(get_string('userextensiondate', 'assign', userdate($time - (2 * DAYSECS))), $output);

        $difftime = $submittedtime - $time;
        $this->assertStringContainsString(
            get_string('submittedlateshort', 'assign', format_time((2 * DAYSECS) + $difftime)),
            $output
        );
    }

    public function test_gradingtable_status_rendering(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Setup the assignment.
        $this->setUser($teacher);
        $time = time();
        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'duedate' => $time - (4 * DAYSECS),
         ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', [
            'id' => $assign->get_course_module()->id,
            'action' => 'grading',
        ]));

        // Check that the assignment is late.
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);
        $difftime = time() - $time;
        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);

        // Simulate a student viewing the assignment without submitting.
        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
        $assign->testable_update_submission($submission, $student->id, true, false);
        $submittedtime = time();

        // Verify output.
        $this->setUser($teacher);
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $difftime = $submittedtime - $time;
        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((4 * DAYSECS) + $difftime)), $output);

        $document = new \DOMDocument();
        @$document->loadHTML($output);
        $xpath = new \DOMXPath($document);
        $this->assertEmpty($xpath->evaluate('string(//td[@id="mod_assign_grading-' . $assign->get_context()->id . '_r0_c6"])'));
    }

    /**
     * Check that group submission information is rendered correctly in the
     * grading table.
     */
    public function test_gradingtable_group_submissions_rendering(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        groups_add_member($group, $teacher);

        $students = [];

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $students[] = $student;
        groups_add_member($group, $student);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $students[] = $student;
        groups_add_member($group, $student);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $students[] = $student;
        groups_add_member($group, $student);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $students[] = $student;
        groups_add_member($group, $student);

        // Verify group assignments.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
            'requireallteammemberssubmit' => 0,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', [
            'id' => $assign->get_course_module()->id,
            'action' => 'grading',
        ]));

        // Add a submission.
        $this->setUser($student);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE,
        ];
        $notices = [];
        $assign->save_submission($data, $notices);

        $submission = $assign->get_group_submission($student->id, 0, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, true);

        // Check output.
        $this->setUser($teacher);
        $gradingtable = new \assign_grading_table($assign, 4, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $document = new \DOMDocument();
        @$document->loadHTML($output);
        $xpath = new \DOMXPath($document);

        // The XPath expression is based on the unique ID of the table.
        $xpathuniqueidroot = 'mod_assign_grading-' . $assign->get_context()->id;

        // Check status.
        $this->assertSame(
            get_string('submissionstatus_submitted', 'assign'),
            $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c3"]//div[@class="submissionstatussubmitted"])')
        );
        $this->assertSame(
            get_string('submissionstatus_submitted', 'assign'),
            $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c3"]//div[@class="submissionstatussubmitted"])')
        );

        // Check submission last modified date.
        $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c6"])')));
        $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c6"])')));

        // Check group.
        $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c4"])'));
        $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c4"])'));

        // Check submission text.
        $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c7"]/div/div)'));
        $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c7"]/div/div)'));

        // Check comments can be made.
        $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r0_c8"]//textarea)'));
        $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r3_c8"]//textarea)'));
    }

    public function test_show_intro(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        // Test whether we are showing the intro at the correct times.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['alwaysshowdescription' => 1]);

        $this->assertEquals(true, $assign->testable_show_intro());

        $tomorrow = time() + DAYSECS;

        $assign = $this->create_instance($course, [
                'alwaysshowdescription' => 0,
                'allowsubmissionsfromdate' => $tomorrow,
            ]);
        $this->assertEquals(false, $assign->testable_show_intro());
        $yesterday = time() - DAYSECS;
        $assign = $this->create_instance($course, [
                'alwaysshowdescription' => 0,
                'allowsubmissionsfromdate' => $yesterday,
            ]);
        $this->assertEquals(true, $assign->testable_show_intro());
    }

    public function test_has_submissions_or_grades(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
        $instance = $assign->get_instance();

        // Should start empty.
        $this->assertEquals(false, $assign->has_submissions_or_grades());

        // Simulate a submission.
        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);

        // The submission is still new.
        $this->assertEquals(false, $assign->has_submissions_or_grades());

        // Submit the submission.
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, false);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);

        // Now test again.
        $this->assertEquals(true, $assign->has_submissions_or_grades());
    }

    public function test_delete_grades(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course);

        // Simulate adding a grade.
        $this->setUser($teacher);
        $data = new \stdClass();
        $data->grade = '50.0';
        $assign->testable_apply_grade_to_user($data, $student->id, 0);

        // Now see if the data is in the gradebook.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id);

        $this->assertNotEquals(0, count($gradinginfo->items));

        $assign->testable_delete_grades();
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id);

        $this->assertEquals(0, count($gradinginfo->items));
    }

    public function test_delete_instance(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);

        // Simulate adding a grade.
        $this->setUser($teacher);
        $data = new \stdClass();
        $data->grade = '50.0';
        $assign->testable_apply_grade_to_user($data, $student->id, 0);

        // Simulate a submission.
        $this->add_submission($student, $assign);

        // Now try and delete.
        $this->setUser($teacher);
        $this->assertEquals(true, $assign->delete_instance());
    }

    /**
     * @covers ::assign_reset_userdata
     */
    public function test_reset_userdata(): void {
        global $DB;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $now = time();
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'allowsubmissionsfromdate' => $now - HOURSECS,
            'duedate' => $now,
            'cutoffdate' => $now + HOURSECS,
            'gradingduedate' => $now + DAYSECS,
        ]);

        // Simulate adding a grade.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0);

        // Simulate a submission.
        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);

        $this->assertEquals(true, $assign->has_submissions_or_grades());
        // Now try and reset.
        $data = new \stdClass();
        $data->reset_assign_submissions = 1;
        $data->reset_gradebook_grades = 1;
        $data->reset_assign_user_overrides = 1;
        $data->reset_assign_group_overrides = 1;
        $data->courseid = $course->id;
        $data->timeshift = DAYSECS;
        $this->setUser($teacher);
        $assign->reset_userdata($data);
        $this->assertEquals(false, $assign->has_submissions_or_grades());

        // Reload the instance data.
        $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
        $this->assertEquals($now - HOURSECS + DAYSECS, $instance->allowsubmissionsfromdate);
        $this->assertEquals($now + DAYSECS, $instance->duedate);
        $this->assertEquals($now + HOURSECS + DAYSECS, $instance->cutoffdate);
        $this->assertEquals($now + DAYSECS + DAYSECS, $instance->gradingduedate);

        // Test reset using assign_reset_userdata().
        $assignduedate = $instance->duedate; // Keep old updated value for comparison.
        $data->timeshift = (2 * DAYSECS);
        assign_reset_userdata($data);
        $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
        $this->assertEquals($assignduedate + (2 * DAYSECS), $instance->duedate);

        // Create one more assignment and reset, make sure time shifted for previous assignment is not changed.
        $assign2 = $this->create_instance($course, [
                'assignsubmission_onlinetext_enabled' => 1,
                'duedate' => $now,
            ]);
        $assignduedate = $instance->duedate;
        $data->timeshift = 3 * DAYSECS;
        $assign2->reset_userdata($data);
        $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
        $this->assertEquals($assignduedate, $instance->duedate);
        $instance2 = $DB->get_record('assign', ['id' => $assign2->get_instance()->id]);
        $this->assertEquals($now + 3 * DAYSECS, $instance2->duedate);

        // Reset both assignments using assign_reset_userdata() and make sure both assignments have same date.
        $assignduedate = $instance->duedate;
        $assign2duedate = $instance2->duedate;
        $data->timeshift = (4 * DAYSECS);
        assign_reset_userdata($data);
        $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
        $this->assertEquals($assignduedate + (4 * DAYSECS), $instance->duedate);
        $instance2 = $DB->get_record('assign', ['id' => $assign2->get_instance()->id]);
        $this->assertEquals($assign2duedate + (4 * DAYSECS), $instance2->duedate);
    }

    public function test_plugin_settings(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $now = time();
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
                'assignsubmission_file_enabled' => 1,
                'assignsubmission_file_maxfiles' => 12,
                'assignsubmission_file_maxsizebytes' => 10,
            ]);

        $plugin = $assign->get_submission_plugin_by_type('file');
        $this->assertEquals('12', $plugin->get_config('maxfilesubmissions'));
    }

    public function test_update_calendar(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $this->setUser($teacher);
        $userctx = \context_user::instance($teacher->id)->id;

        // Hack to pretend that there was an editor involved. We need both $_POST and $_REQUEST, and a sesskey.
        $draftid = file_get_unused_draft_itemid();
        $_REQUEST['introeditor'] = $draftid;
        $_POST['introeditor'] = $draftid;
        $_POST['sesskey'] = sesskey();

        // Write links to a draft area.
        $fakearealink1 = file_rewrite_pluginfile_urls(
            '<a href="@@PLUGINFILE@@/pic.gif">link</a>',
            'draftfile.php',
            $userctx,
            'user',
            'draft',
            $draftid
        );
        $fakearealink2 = file_rewrite_pluginfile_urls(
            '<a href="@@PLUGINFILE@@/pic.gif">new</a>',
            'draftfile.php',
            $userctx,
            'user',
            'draft',
            $draftid
        );

        // Create a new \assignment with links to a draft area.
        $now = time();
        $assign = $this->create_instance($course, [
                'duedate' => $now,
                'intro' => $fakearealink1,
                'introformat' => FORMAT_HTML,
            ]);

        // See if there is an event in the calendar.
        $params = ['modulename' => 'assign', 'instance' => $assign->get_instance()->id];
        $event = $DB->get_record('event', $params);
        $this->assertNotEmpty($event);
        $this->assertSame('link', $event->description);     // The pluginfile links are removed.

        // Make sure the same works when updating the assignment.
        $instance = $assign->get_instance();
        $instance->instance = $instance->id;
        $instance->intro = $fakearealink2;
        $instance->introformat = FORMAT_HTML;
        $assign->update_instance($instance);
        $params = ['modulename' => 'assign', 'instance' => $assign->get_instance()->id];
        $event = $DB->get_record('event', $params);
        $this->assertNotEmpty($event);
        $this->assertSame('new', $event->description);     // The pluginfile links are removed.

        // Create an assignment with a description that should be hidden.
        $assign = $this->create_instance($course, [
                'duedate' => $now + 160,
                'alwaysshowdescription' => false,
                'allowsubmissionsfromdate' => $now + 60,
                'intro' => 'Some text',
            ]);

        // Get the event from the calendar.
        $params = ['modulename' => 'assign', 'instance' => $assign->get_instance()->id];
        $event = $DB->get_record('event', [
            'modulename' => 'assign',
            'instance' => $assign->get_instance()->id,
        ]);

        $this->assertEmpty($event->description);

        // Change the allowsubmissionfromdate to the past - do this directly in the DB
        // because if we call the assignment update method - it will update the calendar
        // and we want to test that this works from cron.
        $DB->set_field('assign', 'allowsubmissionsfromdate', $now - 60, ['id' => $assign->get_instance()->id]);
        // Run cron to update the event in the calendar.
        \assign::cron();
        $event = $DB->get_record('event', $params);

        $this->assertStringContainsString('Some text', $event->description);
    }

    public function test_update_instance(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);

        $now = time();
        $instance = $assign->get_instance();
        $instance->duedate = $now;
        $instance->instance = $instance->id;
        $instance->assignsubmission_onlinetext_enabled = 1;

        $assign->update_instance($instance);

        $instance = $DB->get_record('assign', ['id' => $assign->get_instance()->id]);
        $this->assertEquals($now, $instance->duedate);
    }

    public function test_cannot_submit_empty(): void {
        global $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, ['submissiondrafts' => 1]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Test you cannot see the submit button for an offline assignment regardless.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringNotContainsString(
            get_string('submitassignment', 'assign'),
            $output,
            'Can submit empty offline assignment'
        );
    }

    public function test_cannot_submit_empty_no_submission(): void {
        global $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Test you cannot see the submit button for an online text assignment with no submission.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringNotContainsString(
            get_string('submitassignment', 'assign'),
            $output,
            'Cannot submit empty onlinetext assignment'
        );
    }

    public function test_can_submit_with_submission(): void {
        global $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Add a draft.
        $this->add_submission($student, $assign);

        // Test you can see the submit button for an online text assignment with a submission.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertStringContainsString(
            get_string('submitassignment', 'assign'),
            $output,
            'Can submit non empty onlinetext assignment'
        );
    }

    /**
     * Test new_submission_empty
     *
     * We only test combinations of plugins here. Individual plugins are tested
     * in their respective test files.
     *
     * @dataProvider new_submission_empty_testcases
     * @param string $data The file submission data
     * @param bool $expected The expected return value
     */
    public function test_new_submission_empty($data, $expected): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
                'assignsubmission_file_enabled' => 1,
                'assignsubmission_file_maxfiles' => 12,
                'assignsubmission_file_maxsizebytes' => 10,
                'assignsubmission_onlinetext_enabled' => 1,
            ]);
        $this->setUser($student);
        $submission = new \stdClass();

        if ($data['file'] && isset($data['file']['filename'])) {
            $itemid = file_get_unused_draft_itemid();
            $submission->files_filemanager = $itemid;
            $data['file'] += ['contextid' => \context_user::instance($student->id)->id, 'itemid' => $itemid];
            $fs = get_file_storage();
            $fs->create_file_from_string((object)$data['file'], 'Content of ' . $data['file']['filename']);
        }

        if ($data['onlinetext']) {
            $submission->onlinetext_editor = ['text' => $data['onlinetext']];
        }

        $result = $assign->new_submission_empty($submission);
        $this->assertTrue($result === $expected);
    }

    /**
     * Dataprovider for the test_new_submission_empty testcase
     *
     * @return array[] An array of testcases
     */
    public static function new_submission_empty_testcases(): array {
        return [
            'With file and onlinetext' => [
                [
                    'file' => [
                        'component' => 'user',
                        'filearea' => 'draft',
                        'filepath' => '/',
                        'filename' => 'not_a_virus.exe',
                    ],
                    'onlinetext' => 'Balin Fundinul Uzbadkhazaddumu',
                ],
                false,
            ],
        ];
    }

    public function test_list_participants(): void {
        global $CFG;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        // Create 10 students.
        for ($i = 0; $i < 10; $i++) {
            $this->getDataGenerator()->create_and_enrol($course, 'student');
        }

        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['grade' => 100]);

        $this->assertCount(10, $assign->list_participants(null, true));
    }

    public function test_list_participants_activeenrol(): void {
        global $CFG, $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        // Create 10 students.
        for ($i = 0; $i < 10; $i++) {
            $this->getDataGenerator()->create_and_enrol($course, 'student');
        }

        // Create 10 suspended students.
        for ($i = 0; $i < 10; $i++) {
            $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
        }

        $this->setUser($teacher);
        set_user_preference('grade_report_showonlyactiveenrol', false);
        $assign = $this->create_instance($course, ['grade' => 100]);

        $this->assertCount(10, $assign->list_participants(null, true));
    }

    public function test_list_participants_with_group_restriction(): void {
        global $CFG;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $unrelatedstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Turn on availability and a group restriction, and check that it doesn't show users who aren't in the group.
        $CFG->enableavailability = true;

        $specialgroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $assign = $this->create_instance($course, [
            'grade' => 100,
            'availability' => json_encode(
                \core_availability\tree::get_root_json([\availability_group\condition::get_json($specialgroup->id)])
            ),
        ]);

        groups_add_member($specialgroup, $student);
        groups_add_member($specialgroup, $otherstudent);
        $this->assertEquals(2, count($assign->list_participants(null, true)));
    }

    public function test_get_participant_user_not_exist(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();

        $assign = $this->create_instance($course);
        $this->assertNull($assign->get_participant('-1'));
    }

    public function test_get_participant_not_enrolled(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);

        $user = $this->getDataGenerator()->create_user();
        $this->assertNull($assign->get_participant($user->id));
    }

    public function test_get_participant_no_submission(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $participant = $assign->get_participant($student->id);

        $this->assertEquals($student->id, $participant->id);
        $this->assertFalse($participant->submitted);
        $this->assertFalse($participant->requiregrading);
        $this->assertFalse($participant->grantedextension);
    }

    public function test_get_participant_granted_extension(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign->save_user_extension($student->id, time());
        $participant = $assign->get_participant($student->id);

        $this->assertEquals($student->id, $participant->id);
        $this->assertFalse($participant->submitted);
        $this->assertFalse($participant->requiregrading);
        $this->assertTrue($participant->grantedextension);
    }

    public function test_get_participant_with_ungraded_submission(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Simulate a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        $participant = $assign->get_participant($student->id);

        $this->assertEquals($student->id, $participant->id);
        $this->assertTrue($participant->submitted);
        $this->assertTrue($participant->requiregrading);
        $this->assertFalse($participant->grantedextension);
    }

    /**
     * Tests that if a student with no submission who can no longer submit is not a participant.
     */
    public function test_get_participant_with_no_submission_no_capability(): void {
        global $DB;
        $this->resetAfterTest();
        $course = self::getDataGenerator()->create_course();
        $coursecontext = \context_course::instance($course->id);
        $assign = $this->create_instance($course);
        $teacher = self::getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = self::getDataGenerator()->create_and_enrol($course, 'student');

        // Remove the students capability to submit.
        $role = $DB->get_field('role', 'id', ['shortname' => 'student']);
        assign_capability('mod/assign:submit', CAP_PROHIBIT, $role, $coursecontext);

        $participant = $assign->get_participant($student->id);

        self::assertNull($participant);
    }

    /**
     * Tests that if a student that has submitted but can no longer submit is a participant.
     */
    public function test_get_participant_with_submission_no_capability(): void {
        global $DB;
        $this->resetAfterTest();
        $course = self::getDataGenerator()->create_course();
        $coursecontext = \context_course::instance($course->id);
        $assign = $this->create_instance($course);
        $teacher = self::getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = self::getDataGenerator()->create_and_enrol($course, 'student');

        // Simulate a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Remove the students capability to submit.
        $role = $DB->get_field('role', 'id', ['shortname' => 'student']);
        assign_capability('mod/assign:submit', CAP_PROHIBIT, $role, $coursecontext);

        $participant = $assign->get_participant($student->id);

        self::assertNotNull($participant);
        self::assertEquals($student->id, $participant->id);
        self::assertTrue($participant->submitted);
        self::assertTrue($participant->requiregrading);
        self::assertFalse($participant->grantedextension);
    }

    public function test_get_participant_with_graded_submission(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Simulate a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        $this->mark_submission($teacher, $assign, $student, 50.0);

        $data = new \stdClass();
        $data->grade = '50.0';
        $assign->testable_apply_grade_to_user($data, $student->id, 0);

        $participant = $assign->get_participant($student->id);

        $this->assertEquals($student->id, $participant->id);
        $this->assertTrue($participant->submitted);
        $this->assertFalse($participant->requiregrading);
        $this->assertFalse($participant->grantedextension);
    }

    /**
     * No active group and non-group submissions disallowed => 2 groups.
     */
    public function test_count_teams_no_active_non_group_allowed(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_add_member($group1, $student1);
        groups_add_member($group2, $student2);

        $this->setUser($teacher);
        $assign = $this->create_instance($course, ['teamsubmission' => 1]);

        $this->assertEquals(2, $assign->count_teams());
    }

    /**
     * No active group and non group submissions allowed => 2 groups + the default one.
     */
    public function test_count_teams_non_group_allowed(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $this->getDataGenerator()->create_grouping_group(['groupid' => $group1->id, 'groupingid' => $grouping->id]);
        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $this->getDataGenerator()->create_grouping_group(['groupid' => $group2->id, 'groupingid' => $grouping->id]);

        groups_add_member($group1, $student1);
        groups_add_member($group2, $student2);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'teamsubmissiongroupingid' => $grouping->id,
            'preventsubmissionnotingroup' => false,
        ]);

        $this->setUser($teacher);
        $this->assertEquals(3, $assign->count_teams());

        // Active group only.
        $this->assertEquals(1, $assign->count_teams($group1->id));
        $this->assertEquals(1, $assign->count_teams($group2->id));
    }

    /**
     * Active group => just selected one.
     */
    public function test_count_teams_no_active_group(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $this->getDataGenerator()->create_grouping_group(['groupid' => $group1->id, 'groupingid' => $grouping->id]);
        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $this->getDataGenerator()->create_grouping_group(['groupid' => $group2->id, 'groupingid' => $grouping->id]);

        groups_add_member($group1, $student1);
        groups_add_member($group2, $student2);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'preventsubmissionnotingroup' => true,
        ]);

        $this->setUser($teacher);
        $this->assertEquals(2, $assign->count_teams());

        // Active group only.
        $this->assertEquals(1, $assign->count_teams($group1->id));
        $this->assertEquals(1, $assign->count_teams($group2->id));
    }

    /**
     * Active group => just selected one.
     */
    public function test_count_teams_groups_only(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'teamsubmissiongroupingid' => $grouping->id,
            'preventsubmissionnotingroup' => false,
        ]);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_add_member($group1, $student1);

        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_add_member($group2, $student2);

        $this->getDataGenerator()->create_grouping_group(['groupid' => $group1->id, 'groupingid' => $grouping->id]);
        $this->getDataGenerator()->create_grouping_group(['groupid' => $group2->id, 'groupingid' => $grouping->id]);

        $this->setUser($teacher);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'preventsubmissionnotingroup' => true,
        ]);
        $this->assertEquals(2, $assign->count_teams());
    }

    public function test_submit_to_default_group(): void {
        global $DB, $SESSION;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 0,
            'groupmode' => VISIBLEGROUPS,
        ]);

        $usergroup = $assign->get_submission_group($student->id);
        $this->assertFalse($usergroup, 'New student is in default group');

        // Add a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Set active groups to all groups.
        $this->setUser($teacher);
        $SESSION->activegroup[$course->id]['aag'][0] = 0;
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));

        // Set an active group.
        $SESSION->activegroup[$course->id]['aag'][0] = (int) $group->id;
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
    }

    public function test_count_submissions_no_draft(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $assign->get_user_submission($student->id, true);

        // Note: Drafts count as a submission.
        $this->assertEquals(0, $assign->count_grades());
        $this->assertEquals(0, $assign->count_submissions());
        $this->assertEquals(1, $assign->count_submissions(true));
        $this->assertEquals(0, $assign->count_submissions_need_grading());
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
    }

    public function test_count_submissions_draft(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $this->add_submission($student, $assign);

        // Note: Drafts count as a submission.
        $this->assertEquals(0, $assign->count_grades());
        $this->assertEquals(1, $assign->count_submissions());
        $this->assertEquals(1, $assign->count_submissions(true));
        $this->assertEquals(0, $assign->count_submissions_need_grading());
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
    }

    public function test_count_submissions_submitted(): void {
        global $SESSION;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        $this->assertEquals(0, $assign->count_grades());
        $this->assertEquals(1, $assign->count_submissions());
        $this->assertEquals(1, $assign->count_submissions(true));
        $this->assertEquals(1, $assign->count_submissions_need_grading());
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
    }

    public function test_count_submissions_graded(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0);

        // Although it has been graded, it is still marked as submitted.
        $this->assertEquals(1, $assign->count_grades());
        $this->assertEquals(1, $assign->count_submissions());
        $this->assertEquals(1, $assign->count_submissions(true));
        $this->assertEquals(0, $assign->count_submissions_need_grading());
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_NEW));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_REOPENED));
    }

    public function test_count_submissions_graded_group(): void {
        global $SESSION;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $othergroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_add_member($group, $student);

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'groupmode' => VISIBLEGROUPS,
        ]);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // The user should still be listed when fetching all groups.
        $this->setUser($teacher);
        $SESSION->activegroup[$course->id]['aag'][0] = 0;
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));

        // The user should still be listed when fetching just their group.
        $SESSION->activegroup[$course->id]['aag'][0] = $group->id;
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));

        // The user should still be listed when fetching just their group.
        $SESSION->activegroup[$course->id]['aag'][0] = $othergroup->id;
        $this->assertEquals(0, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
    }

    // TODO
    public function x_test_count_submissions_for_team() {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $othergroup = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_add_member($group, $student);

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'teamsubmission' => 1,
        ]);

        // Add a graded submission.
        $this->add_submission($student, $assign);

        // Simulate adding a grade.
        $this->setUser($teacher);
        $data = new \stdClass();
        $data->grade = '50.0';
        $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id, 0);

        // Simulate a submission.
        $this->setUser($this->extrastudents[1]);
        $submission = $assign->get_group_submission($this->extrastudents[1]->id, $groupid, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $this->extrastudents[1]->id, true, false);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);

        // Simulate a submission.
        $this->setUser($this->extrastudents[2]);
        $submission = $assign->get_group_submission($this->extrastudents[2]->id, $groupid, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $this->extrastudents[2]->id, true, false);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);

        // Simulate a submission.
        $this->setUser($this->extrastudents[3]);
        $submission = $assign->get_group_submission($this->extrastudents[3]->id, $groupid, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $this->extrastudents[3]->id, true, false);
        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text',
            'format' => FORMAT_MOODLE];
        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
        $plugin->save($submission, $data);

        // Simulate adding a grade.
        $this->setUser($teacher);
        $data = new \stdClass();
        $data->grade = '50.0';
        $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
        $assign->testable_apply_grade_to_user($data, $this->extrasuspendedstudents[0]->id, 0);

        // Create a new submission with status NEW.
        $this->setUser($this->extrastudents[4]);
        $submission = $assign->get_group_submission($this->extrastudents[4]->id, $groupid, true);

        $this->assertEquals(2, $assign->count_grades());
        $this->assertEquals(4, $assign->count_submissions());
        $this->assertEquals(5, $assign->count_submissions(true));
        $this->assertEquals(3, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
    }

    public function test_get_grading_userid_list_only_active(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
            $course,
            'student',
            null,
            'manual',
            0,
            0,
            ENROL_USER_SUSPENDED
        );

        $this->setUser($teacher);

        $assign = $this->create_instance($course);
        $this->assertCount(1, $assign->testable_get_grading_userid_list());
    }

    public function test_get_grading_userid_list_all(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
            $course,
            'student',
            null,
            'manual',
            0,
            0,
            ENROL_USER_SUSPENDED
        );

        $this->setUser($teacher);
        set_user_preference('grade_report_showonlyactiveenrol', false);

        $assign = $this->create_instance($course);
        $this->assertCount(2, $assign->testable_get_grading_userid_list());
    }

    public function test_cron(): void {
        global $PAGE;
        $this->resetAfterTest();

        // First run cron so there are no messages waiting to be sent (from other tests).
        \core\cron::setup_user();
        \assign::cron();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Now create an assignment and add some feedback.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'sendstudentnotifications' => 1,
        ]);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0);

        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
        \core\cron::setup_user();
        $sink = $this->redirectMessages();
        \assign::cron();
        $messages = $sink->get_messages();

        $this->assertEquals(1, count($messages));
        $this->assertEquals(1, $messages[0]->notification);
        $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
        // Test customdata.
        $customdata = json_decode($messages[0]->customdata);
        $this->assertEquals($assign->get_course_module()->id, $customdata->cmid);
        $this->assertEquals($assign->get_instance()->id, $customdata->instance);
        $this->assertEquals('feedbackavailable', $customdata->messagetype);
        $userpicture = new \user_picture($teacher);
        $userpicture->size = 1; // Use f1 size.
        $this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationiconurl);
        $this->assertEquals(0, $customdata->uniqueidforuser);   // Not used in this case.
        $this->assertFalse($customdata->blindmarking);
    }

    public function test_cron_without_notifications(): void {
        $this->resetAfterTest();

        // First run cron so there are no messages waiting to be sent (from other tests).
        \core\cron::setup_user();
        \assign::cron();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Now create an assignment and add some feedback.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'sendstudentnotifications' => 1,
        ]);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'sendstudentnotifications' => 0,
        ]);

        \core\cron::setup_user();
        $sink = $this->redirectMessages();
        \assign::cron();
        $messages = $sink->get_messages();

        $this->assertEquals(0, count($messages));
    }

    public function test_cron_regraded(): void {
        $this->resetAfterTest();

        // First run cron so there are no messages waiting to be sent (from other tests).
        \core\cron::setup_user();
        \assign::cron();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Now create an assignment and add some feedback.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'sendstudentnotifications' => 1,
        ]);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0);

        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
        \core\cron::setup_user();
        \assign::cron();

        // Regrade.
        $this->mark_submission($teacher, $assign, $student, 50.0);

        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
        \core\cron::setup_user();
        $sink = $this->redirectMessages();
        \assign::cron();
        $messages = $sink->get_messages();

        $this->assertEquals(1, count($messages));
        $this->assertEquals(1, $messages[0]->notification);
        $this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
    }

    /**
     * Test delivery of grade notifications as controlled by marking workflow.
     */
    public function test_markingworkflow_cron(): void {
        $this->resetAfterTest();

        // First run cron so there are no messages waiting to be sent (from other tests).
        \core\cron::setup_user();
        \assign::cron();

        $course = $this->getDataGenerator()->create_course(['shortname' => 'E&U']);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Now create an assignment and add some feedback.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'name' => 'Escaping & Unescaping',
            'sendstudentnotifications' => 1,
            'markingworkflow' => 1,
        ]);

        // Mark a submission but set the workflowstate to an unreleased state.
        // This should not trigger a notification.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'sendstudentnotifications' => 1,
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE,
        ]);

        \core\cron::setup_user();
        $sink = $this->redirectMessages();
        \assign::cron();
        $messages = $sink->get_messages();

        $this->assertEquals(0, count($messages));

        // Transition to the released state.
        $this->setUser($teacher);
        $submission = $assign->get_user_submission($student->id, true);
        $submission->workflowstate = ASSIGN_MARKING_WORKFLOW_STATE_RELEASED;
        $assign->testable_apply_grade_to_user($submission, $student->id, 0);

        // Now run cron and see that one message was sent.
        \core\cron::setup_user();
        $sink = $this->redirectMessages();
        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
        \assign::cron();
        $messages = $sink->get_messages();

        $this->assertEquals(1, count($messages));

        // Get some bits we will need to verify the content of the message.
        $assignname = $assign->get_instance()->name;
        $assignurl = (new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]))->out();
        $teachername = fullname($teacher);
        $assignsurl = (new \moodle_url('/mod/assign/index.php', ['id' => $course->id]))->out();
        $courseurl = (new \moodle_url('/course/view.php', ['id' => $course->id]))->out();

        $message = $messages[0];
        $this->assertEquals(1, $message->notification);
        $this->assertEquals(format_string($assign->get_instance()->name), $message->contexturlname);
        $this->assertEquals("$teachername has given feedback for assignment $assignname", $message->subject, );
        $this->assertEquals("E&U -> Assignment -> Escaping & Unescaping
---------------------------------------------------------------------
$teachername has posted some feedback on your
assignment submission for 'Escaping & Unescaping'

You can see it appended to your assignment submission:

    $assignurl

---------------------------------------------------------------------
",
            $message->fullmessage,
        );
        $this->assertEquals(
            '<p><font face="sans-serif">
    <a href="' . $courseurl . '">E&amp;U</a> ->
    <a href="' . $assignsurl . '">Assignment</a> ->
    <a href="' . $assignurl . '">Escaping &amp; Unescaping</a>
</font></p>
<hr>
<font face="sans-serif"><p>' . $teachername . ' has posted some feedback on your ' .
            'assignment submission for \'Escaping &amp; Unescaping\'.
You can see it appended to your <a href="' . $assignurl .
            '">assignment submission</a>.</p></font>
<hr>',
            $message->fullmessagehtml
        );
    }

    public function test_cron_message_includes_courseid(): void {
        $this->resetAfterTest();

        // First run cron so there are no messages waiting to be sent (from other tests).
        \core\cron::setup_user();
        \assign::cron();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Now create an assignment and add some feedback.
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'sendstudentnotifications' => 1,
        ]);

        // Mark a submission but set the workflowstate to an unreleased state.
        // This should not trigger a notification.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student);
        \phpunit_util::stop_message_redirection();

        // Now run cron and see that one message was sent.
        \core\cron::setup_user();
        $this->preventResetByRollback();
        $sink = $this->redirectEvents();
        $this->expectOutputRegex('/Done processing 1 assignment submissions/');
        \assign::cron();

        $events = $sink->get_events();
        $event = reset($events);
        $this->assertInstanceOf('\core\event\notification_sent', $event);
        $this->assertEquals($assign->get_course()->id, $event->other['courseid']);
        $sink->close();
    }

    public function test_is_graded(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course);

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, 50.0);

        $this->setUser($teacher);
        $this->assertEquals(true, $assign->testable_is_graded($student->id));
        $this->assertEquals(false, $assign->testable_is_graded($otherstudent->id));
    }

    public function test_can_grade(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course);

        $this->setUser($student);
        $this->assertEquals(false, $assign->can_grade());

        $this->setUser($teacher);
        $this->assertEquals(true, $assign->can_grade());

        // Test the viewgrades capability for other users.
        $this->setUser();
        $this->assertTrue($assign->can_grade($teacher->id));
        $this->assertFalse($assign->can_grade($student->id));

        // Test the viewgrades capability - without mod/assign:grade.
        $this->setUser($student);

        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
        assign_capability('mod/assign:viewgrades', CAP_ALLOW, $studentrole->id, $assign->get_context()->id);
        $this->assertEquals(false, $assign->can_grade());
    }

    public function test_can_view_submission(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
            $course,
            'student',
            null,
            'manual',
            0,
            0,
            ENROL_USER_SUSPENDED
        );

        $assign = $this->create_instance($course);

        $this->setUser($student);
        $this->assertEquals(true, $assign->can_view_submission($student->id));
        $this->assertEquals(false, $assign->can_view_submission($otherstudent->id));
        $this->assertEquals(false, $assign->can_view_submission($teacher->id));

        $this->setUser($teacher);
        $this->assertEquals(true, $assign->can_view_submission($student->id));
        $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
        $this->assertEquals(true, $assign->can_view_submission($teacher->id));
        $this->assertEquals(false, $assign->can_view_submission($suspendedstudent->id));

        $this->setUser($editingteacher);
        $this->assertEquals(true, $assign->can_view_submission($student->id));
        $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
        $this->assertEquals(true, $assign->can_view_submission($teacher->id));
        $this->assertEquals(true, $assign->can_view_submission($suspendedstudent->id));

        // Test the viewgrades capability - without mod/assign:grade.
        $this->setUser($student);
        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
        assign_capability('mod/assign:viewgrades', CAP_ALLOW, $studentrole->id, $assign->get_context()->id);
        $this->assertEquals(true, $assign->can_view_submission($student->id));
        $this->assertEquals(true, $assign->can_view_submission($otherstudent->id));
        $this->assertEquals(true, $assign->can_view_submission($teacher->id));
        $this->assertEquals(false, $assign->can_view_submission($suspendedstudent->id));
    }

    public function test_update_submission(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course);

        $this->add_submission($student, $assign);
        $submission = $assign->get_user_submission($student->id, 0);
        $assign->testable_update_submission($submission, $student->id, true, true);

        $this->setUser($teacher);

        // Verify the gradebook update.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);

        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
        $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
    }

    public function test_update_submission_team(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group, $student);

        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group, $otherstudent);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
        ]);

        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
        $this->assertNull($gradinginfo->items[0]->grades[$student->id]->usermodified);

        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $otherstudent->id);
        $this->asserttrue(isset($gradinginfo->items[0]->grades[$otherstudent->id]));
        $this->assertNull($gradinginfo->items[0]->grades[$otherstudent->id]->usermodified);

        $this->add_submission($student, $assign);
        $submission = $assign->get_group_submission($student->id, 0, true);
        $assign->testable_update_submission($submission, $student->id, true, true);

        // Verify the gradebook update for the student.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);

        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
        $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);

        // Verify the gradebook update for the other student.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $otherstudent->id);

        $this->assertTrue(isset($gradinginfo->items[0]->grades[$otherstudent->id]));
        $this->assertEquals($otherstudent->id, $gradinginfo->items[0]->grades[$otherstudent->id]->usermodified);
    }

    public function test_update_submission_suspended(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', 0, 0, ENROL_USER_SUSPENDED);

        $assign = $this->create_instance($course);

        $this->add_submission($student, $assign);
        $submission = $assign->get_user_submission($student->id, 0);
        $assign->testable_update_submission($submission, $student->id, true, false);

        $this->setUser($teacher);

        // Verify the gradebook update.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);

        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
        $this->assertEquals($student->id, $gradinginfo->items[0]->grades[$student->id]->usermodified);
    }

    public function test_update_submission_blind(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'blindmarking' => 1,
        ]);

        $this->add_submission($student, $assign);
        $submission = $assign->get_user_submission($student->id, 0);
        $assign->testable_update_submission($submission, $student->id, true, false);

        // Verify the gradebook update.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);

        // The usermodified is not set because this is blind marked.
        $this->assertTrue(isset($gradinginfo->items[0]->grades[$student->id]));
        $this->assertNull($gradinginfo->items[0]->grades[$student->id]->usermodified);
    }

    public function test_group_submissions_submit_for_marking_requireallteammemberssubmit(): void {
        global $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group, $student);

        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group, $otherstudent);

        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
            'requireallteammemberssubmit' => 1,
        ]);

        // Now verify group assignments.
        $this->setUser($teacher);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Add a submission.
        $this->add_submission($student, $assign);

        // Check we can see the submit button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);

        $submission = $assign->get_group_submission($student->id, 0, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, true);

        // Check that the student does not see "Submit" button.
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);

        // Change to another user in the same group.
        $this->setUser($otherstudent);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
        $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);

        $submission = $assign->get_group_submission($otherstudent->id, 0, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $otherstudent->id, true, true);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
    }

    public function test_group_submissions_submit_for_marking(): void {
        global $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group, $student);

        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group, $otherstudent);

        // Now verify group assignments.
        $this->setUser($teacher);
        $time = time();
        $assign = $this->create_instance($course, [
            'teamsubmission' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
            'requireallteammemberssubmit' => 0,
            'duedate' => $time - (2 * DAYSECS),
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Add a submission.
        $this->add_submission($student, $assign);

        // Check we can see the submit button.
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertStringContainsString(get_string('submitassignment', 'assign'), $output);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringContainsString(get_string('timeremaining', 'assign'), $output);
        $difftime = time() - $time;
        $this->assertStringContainsString(get_string('overdue', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);

        $submission = $assign->get_group_submission($student->id, 0, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, true);

        // Check that the student does not see "Submit" button.
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);

        // Change to another user in the same group.
        $this->setUser($otherstudent);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);

        // Check that time remaining is not overdue.
        $output = $assign->view_student_summary($otherstudent, true);
        $this->assertStringContainsString(get_string('timeremaining', 'assign'), $output);
        $difftime = time() - $time;
        $this->assertStringContainsString(get_string('submittedlate', 'assign', format_time((2 * DAYSECS) + $difftime)), $output);

        $submission = $assign->get_group_submission($otherstudent->id, 0, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $otherstudent->id, true, true);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $otherstudent);
        $this->assertStringNotContainsString(get_string('submitassignment', 'assign'), $output);
    }

    public function test_submissions_open(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $suspendedstudent = $this->getDataGenerator()->create_and_enrol(
            $course,
            'student',
            null,
            'manual',
            0,
            0,
            ENROL_USER_SUSPENDED
        );

        $this->setAdminUser();

        $now = time();
        $tomorrow = $now + DAYSECS;
        $oneweek = $now + WEEKSECS;
        $yesterday = $now - DAYSECS;

        $assign = $this->create_instance($course);
        $this->assertEquals(true, $assign->testable_submissions_open($student->id));

        $assign = $this->create_instance($course, ['duedate' => $tomorrow]);
        $this->assertEquals(true, $assign->testable_submissions_open($student->id));

        $assign = $this->create_instance($course, ['duedate' => $yesterday]);
        $this->assertEquals(true, $assign->testable_submissions_open($student->id));

        $assign = $this->create_instance($course, ['duedate' => $yesterday, 'cutoffdate' => $tomorrow]);
        $this->assertEquals(true, $assign->testable_submissions_open($student->id));

        $assign = $this->create_instance($course, ['duedate' => $yesterday, 'cutoffdate' => $yesterday]);
        $this->assertEquals(false, $assign->testable_submissions_open($student->id));

        $assign->testable_save_user_extension($student->id, $tomorrow);
        $this->assertEquals(true, $assign->testable_submissions_open($student->id));

        $assign = $this->create_instance($course, ['submissiondrafts' => 1]);
        $this->assertEquals(true, $assign->testable_submissions_open($student->id));

        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student->id, true, false);

        $this->setUser($teacher);
        $this->assertEquals(false, $assign->testable_submissions_open($student->id));
    }

    public function test_get_graders(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setAdminUser();

        // Create an assignment with no groups.
        $assign = $this->create_instance($course);
        $this->assertCount(2, $assign->testable_get_graders($student->id));
    }

    public function test_get_graders_separate_groups(): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_add_member($group1, $student);

        $this->setAdminUser();

        // Force create an assignment with SEPARATEGROUPS.
        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);

        $assign = $this->create_instance($course, [
            'groupingid' => $grouping->id,
            'groupmode' => SEPARATEGROUPS,
        ]);

        $this->assertCount(4, $assign->testable_get_graders($student->id));

        // Note the second student is in a group that is not in the grouping.
        // This means that we get all graders that are not in a group in the grouping.
        $this->assertCount(4, $assign->testable_get_graders($otherstudent->id));
    }

    public function test_get_notified_users(): void {
        global $CFG, $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $this->getDataGenerator()->create_grouping_group(['groupid' => $group1->id, 'groupingid' => $grouping->id]);

        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        groups_add_member($group1, $teacher);

        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        groups_add_member($group1, $editingteacher);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group1, $student);

        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $capability = 'mod/assign:receivegradernotifications';
        $coursecontext = \context_course::instance($course->id);
        $role = $DB->get_record('role', ['shortname' => 'teacher']);

        $this->setUser($teacher);

        // Create an assignment with no groups.
        $assign = $this->create_instance($course);

        $this->assertCount(3, $assign->testable_get_notifiable_users($student->id));

        // Change nonediting teachers role to not receive grader notifications.
        assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);

        // Only the editing teachers will be returned.
        $this->assertCount(1, $assign->testable_get_notifiable_users($student->id));

        // Note the second student is in a group that is not in the grouping.
        // This means that we get all graders that are not in a group in the grouping.
        $this->assertCount(1, $assign->testable_get_notifiable_users($otherstudent->id));
    }

    public function test_get_notified_users_in_grouping(): void {
        global $CFG, $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $this->getDataGenerator()->create_grouping_group(['groupid' => $group1->id, 'groupingid' => $grouping->id]);

        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        groups_add_member($group1, $teacher);

        $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        groups_add_member($group1, $editingteacher);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group1, $student);

        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        // Force create an assignment with SEPARATEGROUPS.
        $assign = $this->create_instance($course, [
            'groupingid' => $grouping->id,
            'groupmode' => SEPARATEGROUPS,
        ]);

        // Student is in a group - only the tacher and editing teacher in the group shoudl be present.
        $this->setUser($student);
        $this->assertCount(2, $assign->testable_get_notifiable_users($student->id));

        // Note the second student is in a group that is not in the grouping.
        // This means that we get all graders that are not in a group in the grouping.
        $this->assertCount(1, $assign->testable_get_notifiable_users($otherstudent->id));

        // Change nonediting teachers role to not receive grader notifications.
        $capability = 'mod/assign:receivegradernotifications';
        $coursecontext = \context_course::instance($course->id);
        $role = $DB->get_record('role', ['shortname' => 'teacher']);
        assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);

        // Only the editing teachers will be returned.
        $this->assertCount(1, $assign->testable_get_notifiable_users($student->id));

        // Note the second student is in a group that is not in the grouping.
        // This means that we get all graders that are not in a group in the grouping.
        // Unfortunately there are no editing teachers who are not in a group.
        $this->assertCount(0, $assign->testable_get_notifiable_users($otherstudent->id));
    }

    public function test_group_members_only(): void {
        global $CFG;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $this->getDataGenerator()->create_grouping_group([
            'groupid' => $group1->id,
            'groupingid' => $grouping->id,
        ]);

        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $this->getDataGenerator()->create_grouping_group([
            'groupid' => $group2->id,
            'groupingid' => $grouping->id,
        ]);

        $group3 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        // Add users in the following groups
        // - Teacher - Group 1.
        // - Student - Group 1.
        // - Student - Group 2.
        // - Student - Unrelated Group
        // - Student - No group.
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        groups_add_member($group1, $teacher);

        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group1, $student);

        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group2, $otherstudent);

        $yetotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group2, $otherstudent);

        $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setAdminUser();

        $CFG->enableavailability = true;
        $assign = $this->create_instance($course, [], [
            'availability' => json_encode(
                \core_availability\tree::get_root_json([\availability_grouping\condition::get_json()])
            ),
            'groupingid' => $grouping->id,
        ]);

        // The two students in groups should be returned, but not the teacher in the group, or the student not in the
        // group, or the student in an unrelated group.
        $this->setUser($teacher);
        $participants = $assign->list_participants(0, true);
        $this->assertCount(2, $participants);
        $this->assertTrue(isset($participants[$student->id]));
        $this->assertTrue(isset($participants[$otherstudent->id]));
    }

    public function test_get_uniqueid_for_user(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $students = [];
        for ($i = 0; $i < 10; $i++) {
            $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
            $students[$student->id] = $student;
        }

        $this->setUser($teacher);
        $assign = $this->create_instance($course);

        foreach ($students as $student) {
            $uniqueid = $assign->get_uniqueid_for_user($student->id);
            $this->assertEquals($student->id, $assign->get_user_id_for_uniqueid($uniqueid));
        }
    }

    public function test_show_student_summary(): void {
        global $CFG, $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $this->setUser($teacher);
        $assign = $this->create_instance($course);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // No feedback should be available because this student has not been graded.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output, 'Do not show feedback if there is no grade');

        // Simulate adding a grade.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student);

        // Now we should see the feedback.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertMatchesRegularExpression('/Feedback/', $output, 'Show feedback if there is a grade');

        // Now hide the grade in gradebook.
        $this->setUser($teacher);
        require_once($CFG->libdir . '/gradelib.php');
        $gradeitem = new \grade_item([
            'itemtype'      => 'mod',
            'itemmodule'    => 'assign',
            'iteminstance'  => $assign->get_instance()->id,
            'courseid'      => $course->id]);

        $gradeitem->set_hidden(1, false);

        // No feedback should be available because the grade is hidden.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertDoesNotMatchRegularExpression(
            '/Feedback/',
            $output,
            'Do not show feedback if the grade is hidden in the gradebook'
        );

        // Freeze the context.
        $this->setAdminUser();
        $context = $assign->get_context();
        $CFG->contextlocking = true;
        $context->set_locked(true);

        // No feedback should be available because the grade is hidden.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertDoesNotMatchRegularExpression(
            '/Feedback/',
            $output,
            'Do not show feedback if the grade is hidden in the gradebook'
        );

        // Show the feedback again - it should still be visible even in a frozen context.
        $this->setUser($teacher);
        $gradeitem->set_hidden(0, false);

        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertMatchesRegularExpression('/Feedback/', $output, 'Show feedback if there is a grade');
    }

    public function test_show_student_summary_with_feedback(): void {
        global $CFG, $PAGE;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'assignfeedback_comments_enabled' => 1,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // No feedback should be available because this student has not been graded.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertDoesNotMatchRegularExpression('/Feedback/', $output);

        // Simulate adding a grade.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);
        $this->mark_submission($teacher, $assign, $student, null, [
            'assignfeedbackcomments_editor' => [
                'text' => 'Tomato sauce',
                'format' => FORMAT_MOODLE,
            ],
        ]);

        // Should have feedback but no grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertMatchesRegularExpression('/Feedback/', $output);
        $this->assertMatchesRegularExpression('/Tomato sauce/', $output);
        $this->assertDoesNotMatchRegularExpression('/Grade/', $output, 'Do not show grade when there is no grade.');
        $this->assertDoesNotMatchRegularExpression('/Graded on/', $output, 'Do not show graded date when there is no grade.');

        // Add a grade now.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'assignfeedbackcomments_editor' => [
                'text' => 'Bechamel sauce',
                'format' => FORMAT_MOODLE,
            ],
        ]);

        // Should have feedback but no grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertDoesNotMatchRegularExpression('/Tomato sauce/', $output);
        $this->assertMatchesRegularExpression('/Bechamel sauce/', $output);
        $this->assertMatchesRegularExpression('/Grade/', $output);
        $this->assertMatchesRegularExpression('/Graded on/', $output);

        // Now hide the grade in gradebook.
        $this->setUser($teacher);
        $gradeitem = new \grade_item([
            'itemtype'      => 'mod',
            'itemmodule'    => 'assign',
            'iteminstance'  => $assign->get_instance()->id,
            'courseid'      => $course->id]);

        $gradeitem->set_hidden(1, false);

        // No feedback should be available because the grade is hidden.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertDoesNotMatchRegularExpression(
            '/Feedback/',
            $output,
            'Do not show feedback if the grade is hidden in the gradebook'
        );
    }

    /**
     * Test reopen behavior when in "Manual" mode.
     */
    public function test_attempt_reopen_method_manual(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
            'maxattempts' => 3,
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Student should be able to see an add submission button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Add a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Verify the student cannot make changes to the submission.
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Mark the submission.
        $this->mark_submission($teacher, $assign, $student);

        // Check the student can see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertNotEquals(false, strpos($output, '50.0'));

        // Allow the student another attempt.
        $teacher->ignoresesskey = true;
        $this->setUser($teacher);
        $result = $assign->testable_process_add_attempt($student->id);
        $this->assertEquals(true, $result);

        // Check that the previous attempt is now in the submission history table.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        // Need a better check.
        $this->assertNotEquals(false, strpos($output, 'Submission text'), 'Contains: Submission text');

        // Check that the student now has a submission history.
        $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));

        // Check that the student now does not have a button for Submit.
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));

        // Check that the student now has a button for Add a new attempt".
        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));

        $this->setUser($teacher);
        // Check that the grading table loads correctly and contains this user.
        // This is also testing that we do not get duplicate rows in the grading table.
        $gradingtable = new \assign_grading_table($assign, 100, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertEquals(true, strpos($output, $student->lastname));

        // Should be 1 not 2.
        $this->assertEquals(1, $assign->count_submissions());
        $this->assertEquals(1, $assign->count_submissions_with_status('reopened'));
        $this->assertEquals(0, $assign->count_submissions_need_grading());
        $this->assertEquals(1, $assign->count_grades());

        // Change max attempts to unlimited.
        $formdata = clone($assign->get_instance());
        $formdata->maxattempts = ASSIGN_UNLIMITED_ATTEMPTS;
        $formdata->instance = $formdata->id;
        $assign->update_instance($formdata);

        // Mark the submission again.
        $this->mark_submission($teacher, $assign, $student, 60.0, [], 1);

        // Check the grade exists.
        $this->setUser($teacher);
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEquals(60, (int) $grades[$student->id]->rawgrade);

        // Check we can reopen still.
        $result = $assign->testable_process_add_attempt($student->id);
        $this->assertEquals(true, $result);

        // Should no longer have a grade because there is no grade for the latest attempt.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEmpty($grades);
    }

    /**
     * Test reopen behavior when in "Reopen until pass" mode.
     */
    public function test_attempt_reopen_method_untilpass(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
            'maxattempts' => 3,
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Set grade to pass to 80.
        $gradeitem = $assign->get_grade_item();
        $gradeitem->gradepass = '80.0';
        $gradeitem->update();

        // Student should be able to see an add submission button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Add a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Verify the student cannot make a new attempt.
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));

        // Mark the submission as non-passing.
        $this->mark_submission($teacher, $assign, $student, 50.0);

        // Check the student can see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertNotEquals(false, strpos($output, '50.0'));

        // Check that the student now has a submission history.
        $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));

        // Check that the student now does not have a button for Submit.
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));

        // Check that the student now has a button for Add a new attempt.
        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));

        // Add a second submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Mark the second submission as passing.
        $this->mark_submission($teacher, $assign, $student, 80.0, [], 1);

        // Check that the student does not have a button for Add a new attempt.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));

        // Re-mark the second submission as not passing.
        $this->mark_submission($teacher, $assign, $student, 40.0, [], 1);

        // Check that the student now has a button for Add a new attempt.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertMatchesRegularExpression('/' . get_string('addnewattempt', 'assign') . '/', $output);
        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
    }

    public function test_attempt_reopen_method_untilpass_passing(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
            'maxattempts' => 3,
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Set grade to pass to 80.
        $gradeitem = $assign->get_grade_item();
        $gradeitem->gradepass = '80.0';
        $gradeitem->update();

        // Student should be able to see an add submission button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Add a submission as a student.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Mark the submission as passing.
        $this->mark_submission($teacher, $assign, $student, 100.0);

        // Check the student can see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertNotEquals(false, strpos($output, '100.0'));

        // Check that the student does not have a button for Add a new attempt.
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
    }

    public function test_attempt_reopen_method_untilpass_no_passing_requirement(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS,
            'maxattempts' => 3,
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Set grade to pass to 0, so that no attempts should reopen.
        $gradeitem = $assign->get_grade_item();
        $gradeitem->gradepass = '0';
        $gradeitem->update();

        // Student should be able to see an add submission button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Add a submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Mark the submission with any grade.
        $this->mark_submission($teacher, $assign, $student, 0.0);

        // Check the student can see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertNotEquals(false, strpos($output, '0.0'));

        // Check that the student does not have a button for Add a new attempt.
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
    }

    /**
     * Test reopen behavior when in "Automatic" mode.
     *
     * @coversNothing
     */
    public function test_attempt_reopen_method_automatic(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC,
            'maxattempts' => 3,
            'submissiondrafts' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Set grade to pass to 80.
        $gradeitem = $assign->get_grade_item();
        $gradeitem->gradepass = '80.0';
        $gradeitem->update();

        // Student should be able to see an add submission button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Add a submission as a student.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Verify the student cannot make a new attempt.
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, get_string('addnewattempt', 'assign')));

        // Mark the submission as non-passing.
        $this->mark_submission($teacher, $assign, $student, 50.0);

        // Check the student now has a button for Add a new attempt.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));

        // Add a second submission.
        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign);

        // Mark the submission as passing.
        $this->mark_submission($teacher, $assign, $student, 80.0, [], 1);

        // Check the student now has a button for Add a new attempt.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
    }

    /**
     * Test student visibility for each stage of the marking workflow.
     */
    public function test_markingworkflow(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'markingworkflow' => 1,
        ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Mark the submission and set to notmarked.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED,
        ]);

        // Check the student can't see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, '50.0'));

        // Make sure the grade isn't pushed to the gradebook.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEmpty($grades);

        // Mark the submission and set to inmarking.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_INMARKING,
        ]);

        // Check the student can't see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, '50.0'));

        // Make sure the grade isn't pushed to the gradebook.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEmpty($grades);

        // Mark the submission and set to readyforreview.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW,
        ]);

        // Check the student can't see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, '50.0'));

        // Make sure the grade isn't pushed to the gradebook.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEmpty($grades);

        // Mark the submission and set to inreview.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW,
        ]);

        // Check the student can't see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, '50.0'));

        // Make sure the grade isn't pushed to the gradebook.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEmpty($grades);

        // Mark the submission and set to readyforrelease.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE,
        ]);

        // Check the student can't see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertEquals(false, strpos($output, '50.0'));

        // Make sure the grade isn't pushed to the gradebook.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEmpty($grades);

        // Mark the submission and set to released.
        $this->mark_submission($teacher, $assign, $student, 50.0, [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
        ]);

        // Check the student can see the grade.
        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertNotEquals(false, strpos($output, '50.0'));

        // Make sure the grade is pushed to the gradebook.
        $grades = $assign->get_user_grades_for_gradebook($student->id);
        $this->assertEquals(50, (int)$grades[$student->id]->rawgrade);
    }

    /**
     * Test that a student allocated a specific marker is only shown to that marker.
     */
    public function test_markerallocation(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course, [
            'markingworkflow' => 1,
            'markingallocation' => 1,
        ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Allocate marker to submission.
        $this->mark_submission($teacher, $assign, $student, null, [
            'allocatedmarker' => $teacher->id,
        ]);

        // Check the allocated marker can view the submission.
        $this->setUser($teacher);
        $users = $assign->list_participants(0, true);
        $this->assertEquals(1, count($users));
        $this->assertTrue(isset($users[$student->id]));

        $cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id);
        $context = \context_module::instance($cm->id);
        $assign = new mod_assign_testable_assign($context, $cm, $course);

        // Check that other teachers can't view this submission.
        $this->setUser($otherteacher);
        $users = $assign->list_participants(0, true);
        $this->assertEquals(0, count($users));
    }

    /**
     * Ensure that a teacher cannot submit for students as standard.
     */
    public function test_teacher_submit_for_student(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
        ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Add a submission but do not submit.
        $this->add_submission($student, $assign, 'Student submission text');

        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringContainsString('Student submission text', $output, 'Contains student submission text');

        // Check that a teacher can not edit the submission as they do not have the capability.
        $this->setUser($teacher);
        $this->expectException('moodle_exception');
        $this->expectExceptionMessage('error/nopermission');
        $this->add_submission($student, $assign, 'Teacher edited submission text', false);
    }

    /**
     * Ensure that a teacher with the editothersubmission capability can submit on behalf of a student.
     */
    public function test_teacher_submit_for_student_with_capability(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
        ]);

        // Add the required capability.
        $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
        role_assign($roleid, $teacher->id, $assign->get_context()->id);
        accesslib_clear_all_caches_for_unit_testing();

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Add a submission but do not submit.
        $this->add_submission($student, $assign, 'Student submission text');

        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringContainsString('Student submission text', $output, 'Contains student submission text');

        // Check that a teacher can edit the submission.
        $this->setUser($teacher);
        $this->add_submission($student, $assign, 'Teacher edited submission text', false);

        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringNotContainsString('Student submission text', $output, 'Contains student submission text');
        $this->assertStringContainsString('Teacher edited submission text', $output, 'Contains teacher edited submission text');

        // Check that the teacher can submit the students work.
        $this->setUser($teacher);
        $this->submit_for_grading($student, $assign, [], false);

        // Revert to draft so the student can edit it.
        $assign->revert_to_draft($student->id);

        $this->setUser($student);

        // Check that the submission text was saved.
        $output = $assign->view_student_summary($student, true);
        $this->assertStringContainsString('Teacher edited submission text', $output, 'Contains student submission text');

        // Check that the student can submit their work.
        $this->submit_for_grading($student, $assign, []);

        $output = $assign->view_student_summary($student, true);
        $this->assertStringNotContainsString(get_string('addsubmission', 'assign'), $output);

        // An editing teacher without the extra role should still be able to revert to draft.
        $this->setUser($otherteacher);

        // Revert to draft so the submission is editable.
        $assign->revert_to_draft($student->id);
    }

    /**
     * Ensure that disabling submit after the cutoff date works as expected.
     */
    public function test_disable_submit_after_cutoff_date(): void {
        global $PAGE;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $now = time();
        $tomorrow = $now + DAYSECS;
        $lastweek = $now - (7 * DAYSECS);
        $yesterday = $now - DAYSECS;

        $this->setAdminUser();
        $assign = $this->create_instance($course, [
            'duedate' => $yesterday,
            'cutoffdate' => $tomorrow,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Student should be able to see an add submission button.
        $this->setUser($student);
        $output = $assign->view_submission_action_bar($assign->get_instance(), $student);
        $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));

        // Add a submission but don't submit now.
        $this->add_submission($student, $assign);

        // Create another instance with cut-off and due-date already passed.
        $this->setAdminUser();
        $assign = $this->create_instance($course, [
            'duedate' => $lastweek,
            'cutoffdate' => $yesterday,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $this->setUser($student);
        $output = $assign->view_student_summary($student, true);
        $this->assertStringNotContainsString(
            $output,
            get_string('editsubmission', 'assign'),
            'Should not be able to edit after cutoff date.'
        );
        $this->assertStringNotContainsString(
            $output,
            get_string('submitassignment', 'assign'),
            'Should not be able to submit after cutoff date.'
        );
    }

    /**
     * Testing for submission comment plugin settings.
     *
     * @dataProvider submission_plugin_settings_provider
     * @param   bool    $globalenabled
     * @param   array   $instanceconfig
     * @param   bool    $isenabled
     */
    public function test_submission_comment_plugin_settings($globalenabled, $instanceconfig, $isenabled): void {
        global $CFG;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();

        $CFG->usecomments = $globalenabled;
        $assign = $this->create_instance($course, $instanceconfig);
        $plugin = $assign->get_submission_plugin_by_type('comments');
        $this->assertEquals($isenabled, (bool) $plugin->is_enabled('enabled'));
    }

    /**
     * Data provider for test_submission_comment_plugin_settings.
     *
     * @return array[]
     */
    public static function submission_plugin_settings_provider(): array {
        return [
            'CFG->usecomments true, empty config => Enabled by default' => [
                true,
                [],
                true,
            ],
            'CFG->usecomments true, config enabled => Comments enabled' => [
                true,
                [
                    'assignsubmission_comments_enabled' => 1,
                ],
                true,
            ],
            'CFG->usecomments true, config idisabled => Comments enabled' => [
                true,
                [
                    'assignsubmission_comments_enabled' => 0,
                ],
                true,
            ],
            'CFG->usecomments false, empty config => Disabled by default' => [
                false,
                [],
                false,
            ],
            'CFG->usecomments false, config enabled => Comments disabled' => [
                false,
                [
                    'assignsubmission_comments_enabled' => 1,
                ],
                false,
            ],
            'CFG->usecomments false, config disabled => Comments disabled' => [
                false,
                [
                    'assignsubmission_comments_enabled' => 0,
                ],
                false,
            ],
        ];
    }

    /**
     * Testing for comment inline settings
     */
    public function test_feedback_comment_commentinline(): void {
        global $CFG, $USER;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $sourcetext = "Hello!

I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.

URL outside a tag: https://moodle.org/logo/logo-240x60.gif
Plugin url outside a tag: @@PLUGINFILE@@/logo-240x60.gif

External link 1:<img src='https://moodle.org/logo/logo-240x60.gif' alt='Moodle'/>
External link 2:<img alt=\"Moodle\" src=\"https://moodle.org/logo/logo-240x60.gif\"/>
Internal link 1:<img src='@@PLUGINFILE@@/logo-240x60.gif' alt='Moodle'/>
Internal link 2:<img alt=\"Moodle\" src=\"@@PLUGINFILE@@logo-240x60.gif\"/>
Anchor link 1:<a href=\"@@PLUGINFILE@@logo-240x60.gif\" alt=\"bananas\">Link text</a>
Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
";

        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'assignfeedback_comments_enabled' => 1,
            'assignfeedback_comments_commentinline' => 1,
        ]);

        $this->setUser($student);

        // Add a submission but don't submit now.
        $this->add_submission($student, $assign, $sourcetext);

        $this->setUser($teacher);

        $data = new \stdClass();
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
        $pagination = [
            'userid' => $student->id,
            'rownum' => 0,
            'last' => true,
            'useridlistid' => $assign->get_useridlist_key_id(),
            'attemptnumber' => 0,
        ];
        $formparams = [$assign, $data, $pagination];
        $mform = new mod_assign_grade_form(null, [$assign, $data, $pagination]);

        // We need to get the URL these will be transformed to.
        $context = \context_user::instance($USER->id);
        $itemid = $data->assignfeedbackcomments_editor['itemid'];
        $url = $CFG->wwwroot . '/draftfile.php/' . $context->id . '/user/draft/' . $itemid;

        // Note the internal images have been stripped and the html is purified (quotes fixed in this case).
        $filteredtext = "Hello!

I'm writing to you from the Moodle Majlis in Muscat, Oman, where we just had several days of Moodle community goodness.

URL outside a tag: https://moodle.org/logo/logo-240x60.gif
Plugin url outside a tag: $url/logo-240x60.gif

External link 1:<img src=\"https://moodle.org/logo/logo-240x60.gif\" alt=\"Moodle\" />
External link 2:<img alt=\"Moodle\" src=\"https://moodle.org/logo/logo-240x60.gif\" />
Internal link 1:<img src=\"$url/logo-240x60.gif\" alt=\"Moodle\" />
Internal link 2:<img alt=\"Moodle\" src=\"@@PLUGINFILE@@logo-240x60.gif\" />
Anchor link 1:<a href=\"@@PLUGINFILE@@logo-240x60.gif\">Link text</a>
Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
";

        $this->assertEquals($filteredtext, $data->assignfeedbackcomments_editor['text']);
    }

    /**
     * Testing for feedback comment plugin settings.
     *
     * @dataProvider feedback_plugin_settings_provider
     * @param   array   $instanceconfig
     * @param   bool    $isenabled
     */
    public function test_feedback_plugin_settings($instanceconfig, $isenabled): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();

        $assign = $this->create_instance($course, $instanceconfig);
        $plugin = $assign->get_feedback_plugin_by_type('comments');
        $this->assertEquals($isenabled, (bool) $plugin->is_enabled('enabled'));
    }

    /**
     * Data provider for test_feedback_plugin_settings.
     *
     * @return array[]
     */
    public static function feedback_plugin_settings_provider(): array {
        return [
            'No configuration => disabled' => [
                [],
                false,
            ],
            'Actively disabled' => [
                [
                    'assignfeedback_comments_enabled' => 0,
                ],
                false,
            ],
            'Actively enabled' => [
                [
                    'assignfeedback_comments_enabled' => 1,
                ],
                true,
            ],
        ];
    }

    /**
     * Testing if gradebook feedback plugin is enabled.
     */
    public function test_is_gradebook_feedback_enabled(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $adminconfig = get_config('assign');
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;

        // Create assignment with gradebook feedback enabled and grade = 0.
        $assign = $this->create_instance($course, [
            "{$gradebookplugin}_enabled" => 1,
            'grades' => 0,
        ]);

        // Get gradebook feedback plugin.
        $gradebookplugintype = str_replace('assignfeedback_', '', $gradebookplugin);
        $plugin = $assign->get_feedback_plugin_by_type($gradebookplugintype);
        $this->assertEquals(1, $plugin->is_enabled('enabled'));
        $this->assertEquals(1, $assign->is_gradebook_feedback_enabled());
    }

    /**
     * Testing if gradebook feedback plugin is disabled.
     */
    public function test_is_gradebook_feedback_disabled(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $adminconfig = get_config('assign');
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;

        // Create assignment with gradebook feedback disabled and grade = 0.
        $assign = $this->create_instance($course, [
            "{$gradebookplugin}_enabled" => 0,
            'grades' => 0,
        ]);

        $gradebookplugintype = str_replace('assignfeedback_', '', $gradebookplugin);
        $plugin = $assign->get_feedback_plugin_by_type($gradebookplugintype);
        $this->assertEquals(0, $plugin->is_enabled('enabled'));
    }

    /**
     * Testing can_edit_submission.
     */
    public function test_can_edit_submission(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
        ]);

        // Check student can edit their own submission.
        $this->assertTrue($assign->can_edit_submission($student->id, $student->id));

        // Check student cannot edit others submission.
        $this->assertFalse($assign->can_edit_submission($otherstudent->id, $student->id));

        // Check teacher cannot (by default) edit a students submission.
        $this->assertFalse($assign->can_edit_submission($student->id, $teacher->id));
    }

    /**
     * Testing can_edit_submission with the editothersubmission capability.
     */
    public function test_can_edit_submission_with_editothersubmission(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
        ]);

        // Add the required capability to edit a student submission.
        $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
        role_assign($roleid, $teacher->id, $assign->get_context()->id);
        accesslib_clear_all_caches_for_unit_testing();

        // Check student can edit their own submission.
        $this->assertTrue($assign->can_edit_submission($student->id, $student->id));

        // Check student cannot edit others submission.
        $this->assertFalse($assign->can_edit_submission($otherstudent->id, $student->id));

        // Retest - should now have access.
        $this->assertTrue($assign->can_edit_submission($student->id, $teacher->id));
    }

    /**
     * Testing can_edit_submission
     */
    public function test_can_edit_submission_separategroups(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group1->id);
        groups_add_member($group1, $student1);
        groups_add_member($group1, $student2);

        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group2->id);
        groups_add_member($group2, $student3);
        groups_add_member($group2, $student4);

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
            'groupingid' => $grouping->id,
            'groupmode' => SEPARATEGROUPS,
        ]);

        // Verify a student does not have the ability to edit submissions for other users.
        $this->assertTrue($assign->can_edit_submission($student1->id, $student1->id));
        $this->assertFalse($assign->can_edit_submission($student2->id, $student1->id));
        $this->assertFalse($assign->can_edit_submission($student3->id, $student1->id));
        $this->assertFalse($assign->can_edit_submission($student4->id, $student1->id));
    }

    /**
     * Testing can_edit_submission
     */
    public function test_can_edit_submission_separategroups_with_editothersubmission(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student5 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group1->id);
        groups_add_member($group1, $student1);
        groups_add_member($group1, $student2);

        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group2->id);
        groups_add_member($group2, $student3);
        groups_add_member($group2, $student4);

        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
            'submissiondrafts' => 1,
            'groupingid' => $grouping->id,
            'groupmode' => SEPARATEGROUPS,
            'preventsubmissionnotingroup' => 0,
        ]);

        // Add the capability to the new \assignment for student 1.
        $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
        role_assign($roleid, $student1->id, $assign->get_context()->id);
        accesslib_clear_all_caches_for_unit_testing();

        // Verify student1 has the ability to edit submissions for other users in their group, but not other groups.
        $this->assertTrue($assign->can_edit_submission($student1->id, $student1->id));
        $this->assertTrue($assign->can_edit_submission($student2->id, $student1->id));
        $this->assertFalse($assign->can_edit_submission($student3->id, $student1->id));
        $this->assertFalse($assign->can_edit_submission($student4->id, $student1->id));
        $this->assertFalse($assign->can_edit_submission($student5->id, $student1->id));

        // Verify other students do not have the ability to edit submissions for other users.
        $this->assertTrue($assign->can_edit_submission($student2->id, $student2->id));
        $this->assertFalse($assign->can_edit_submission($student1->id, $student2->id));
        $this->assertFalse($assign->can_edit_submission($student3->id, $student2->id));
        $this->assertFalse($assign->can_edit_submission($student4->id, $student2->id));
        $this->assertFalse($assign->can_edit_submission($student5->id, $student2->id));

        // Add the required capability to edit other submissions and to view all groups to the teacher.
        $roleid = create_role('Dummy role 2', 'dummyrole2', 'dummy role description');
        assign_capability('mod/assign:editothersubmission', CAP_ALLOW, $roleid, $assign->get_context()->id);
        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $assign->get_context()->id);
        role_assign($roleid, $teacher->id, $assign->get_context()->id);

        // Verify the teacher has the ability to edit submissions for other users including users not in a group.
        $this->assertTrue($assign->can_edit_submission($student1->id, $teacher->id));
        $this->assertTrue($assign->can_edit_submission($student2->id, $teacher->id));
        $this->assertTrue($assign->can_edit_submission($student3->id, $teacher->id));
        $this->assertTrue($assign->can_edit_submission($student4->id, $teacher->id));
        $this->assertTrue($assign->can_edit_submission($student5->id, $teacher->id));
    }

    /**
     * Test if the view blind details capability works
     */
    public function test_can_view_blind_details(): void {
        global $DB;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'blindmarking' => 1,
        ]);

        $this->assertTrue($assign->is_blind_marking());

        // Test student names are hidden to teacher.
        $this->setUser($teacher);
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));    // "Participant" is somewhere on the page.
        $this->assertEquals(false, strpos($output, fullname($student)));    // Students full name doesn't appear.

        // Test student names are visible to manager.
        $this->setUser($manager);
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertEquals(true, strpos($output, get_string('hiddenuser', 'assign')));
        $this->assertEquals(true, strpos($output, fullname($student)));
    }

    /**
     * Testing get_shared_group_members
     */
    public function test_get_shared_group_members(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group1->id);
        groups_add_member($group1, $student1);
        groups_add_member($group1, $student2);

        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group2->id);
        groups_add_member($group2, $student3);
        groups_add_member($group2, $student4);

        $assign = $this->create_instance($course, [
            'groupingid' => $grouping->id,
            'groupmode' => SEPARATEGROUPS,
        ]);

        $cm = $assign->get_course_module();

        // Get shared group members for students 0 and 1.
        $groupmembers = $assign->get_shared_group_members($cm, $student1->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student1->id, $groupmembers);
        $this->assertContainsEquals($student2->id, $groupmembers);

        $groupmembers = $assign->get_shared_group_members($cm, $student2->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student1->id, $groupmembers);
        $this->assertContainsEquals($student2->id, $groupmembers);

        $groupmembers = $assign->get_shared_group_members($cm, $student3->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student3->id, $groupmembers);
        $this->assertContainsEquals($student4->id, $groupmembers);

        $groupmembers = $assign->get_shared_group_members($cm, $student4->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student3->id, $groupmembers);
        $this->assertContainsEquals($student4->id, $groupmembers);
    }

    /**
     * Testing get_shared_group_members
     */
    public function test_get_shared_group_members_override(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group1->id);
        groups_add_member($group1, $student1);
        groups_add_member($group1, $student2);

        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        groups_assign_grouping($grouping->id, $group2->id);
        groups_add_member($group2, $student3);
        groups_add_member($group2, $student4);

        $assign = $this->create_instance($course, [
            'groupingid' => $grouping->id,
            'groupmode' => SEPARATEGROUPS,
        ]);

        $cm = $assign->get_course_module();

        // Add the capability to access allgroups for one of the students.
        $roleid = create_role('Access all groups role', 'accessallgroupsrole', '');
        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $assign->get_context()->id);
        role_assign($roleid, $student1->id, $assign->get_context()->id);
        accesslib_clear_all_caches_for_unit_testing();

        // Get shared group members for students 0 and 1.
        $groupmembers = $assign->get_shared_group_members($cm, $student1->id);
        $this->assertCount(4, $groupmembers);
        $this->assertContainsEquals($student1->id, $groupmembers);
        $this->assertContainsEquals($student2->id, $groupmembers);
        $this->assertContainsEquals($student3->id, $groupmembers);
        $this->assertContainsEquals($student4->id, $groupmembers);

        $groupmembers = $assign->get_shared_group_members($cm, $student2->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student1->id, $groupmembers);
        $this->assertContainsEquals($student2->id, $groupmembers);

        $groupmembers = $assign->get_shared_group_members($cm, $student3->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student3->id, $groupmembers);
        $this->assertContainsEquals($student4->id, $groupmembers);

        $groupmembers = $assign->get_shared_group_members($cm, $student4->id);
        $this->assertCount(2, $groupmembers);
        $this->assertContainsEquals($student3->id, $groupmembers);
        $this->assertContainsEquals($student4->id, $groupmembers);
    }

    /**
     * Test get plugins file areas
     */
    public function test_get_plugins_file_areas(): void {
        global $DB;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $assign = $this->create_instance($course);

        // Test that all the submission and feedback plugins are returning the expected file aras.
        $usingfilearea = 0;
        $coreplugins = \core_plugin_manager::standard_plugins_list('assignsubmission');
        foreach ($assign->get_submission_plugins() as $plugin) {
            $type = $plugin->get_type();
            if (!in_array($type, $coreplugins)) {
                continue;
            }
            $fileareas = $plugin->get_file_areas();

            if ($type == 'onlinetext') {
                $this->assertEquals(['submissions_onlinetext' => 'Online text'], $fileareas);
                $usingfilearea++;
            } else if ($type == 'file') {
                $this->assertEquals(['submission_files' => 'File submissions'], $fileareas);
                $usingfilearea++;
            } else {
                $this->assertEmpty($fileareas);
            }
        }
        $this->assertEquals(2, $usingfilearea);

        $usingfilearea = 0;
        $coreplugins = \core_plugin_manager::standard_plugins_list('assignfeedback');
        foreach ($assign->get_feedback_plugins() as $plugin) {
            $type = $plugin->get_type();
            if (!in_array($type, $coreplugins)) {
                continue;
            }
            $fileareas = $plugin->get_file_areas();

            if ($type == 'editpdf') {
                $checkareas = [
                    'download' => 'Annotate PDF',
                    'combined' => 'Annotate PDF',
                    'partial' => 'Annotate PDF',
                    'importhtml' => 'Annotate PDF',
                    'pages' => 'Annotate PDF',
                    'readonlypages' => 'Annotate PDF',
                    'stamps' => 'Annotate PDF',
                    'tmp_jpg_to_pdf' => 'Annotate PDF',
                    'tmp_rotated_jpg' => 'Annotate PDF',
                ];
                $this->assertEquals($checkareas, $fileareas);
                $usingfilearea++;
            } else if ($type == 'file') {
                $this->assertEquals(['feedback_files' => 'Feedback files'], $fileareas);
                $usingfilearea++;
            } else if ($type == 'comments') {
                $this->assertEquals(['feedback' => 'Feedback comments'], $fileareas);
                $usingfilearea++;
            } else {
                $this->assertEmpty($fileareas);
            }
        }
        $this->assertEquals(3, $usingfilearea);
    }

    /**
     * Test override exists
     *
     * This function needs to obey the group override logic as per the assign grading table and
     * the overview block.
     */
    public function test_override_exists(): void {
        global $DB;

        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
        $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        // Data:
        // - student1 => group A only
        // - student2 => group B only
        // - student3 => Group A + Group B (No user override)
        // - student4 => Group A + Group B (With user override)
        // - student4 => No groups.
        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group1, $student1);

        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group2, $student2);

        $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group1, $student3);
        groups_add_member($group2, $student3);

        $student4 = $this->getDataGenerator()->create_and_enrol($course, 'student');
        groups_add_member($group1, $student4);
        groups_add_member($group2, $student4);

        $student5 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course);
        $instance = $assign->get_instance();

        // Overrides for each of the groups, and a user override.
        $overrides = [
            (object) [
                // Override for group 1, highest priority (numerically lowest sortorder).
                'assignid' => $instance->id,
                'groupid' => $group1->id,
                'userid' => null,
                'sortorder' => 1,
                'allowsubmissionsfromdate' => 1,
                'duedate' => 2,
                'cutoffdate' => 3,
                'timelimit' => null,
            ],
            (object) [
                // Override for group 2, lower priority (numerically higher sortorder).
                'assignid' => $instance->id,
                'groupid' => $group2->id,
                'userid' => null,
                'sortorder' => 2,
                'allowsubmissionsfromdate' => 5,
                'duedate' => 6,
                'cutoffdate' => 6,
                'timelimit' => null,
            ],
            (object) [
                // User override.
                'assignid' => $instance->id,
                'groupid' => null,
                'userid' => $student3->id,
                'sortorder' => null,
                'allowsubmissionsfromdate' => 7,
                'duedate' => 8,
                'cutoffdate' => 9,
                'timelimit' => null,
            ],
        ];

        foreach ($overrides as &$override) {
            $override->id = $DB->insert_record('assign_overrides', $override);
        }

        // User only in group 1 should see the group 1 override.
        $this->assertEquals($overrides[0], $assign->override_exists($student1->id));

        // User only in group 2 should see the group 2 override.
        $this->assertEquals($overrides[1], $assign->override_exists($student2->id));

        // User only in both groups with an override should see the user override as it has higher priority.
        $this->assertEquals($overrides[2], $assign->override_exists($student3->id));

        // User only in both groups with no override should see the group 1 override as it has higher priority.
        $this->assertEquals($overrides[0], $assign->override_exists($student4->id));

        // User with no overrides shoudl get nothing.
        $override = $assign->override_exists($student5->id);
        $this->assertNull($override->duedate);
        $this->assertNull($override->cutoffdate);
        $this->assertNull($override->allowsubmissionsfromdate);
    }

    /**
     * Test the quicksave grades processor
     */
    public function test_process_save_quick_grades(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $teacher->ignoresesskey = true;
        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
                'maxattempts' => ASSIGN_UNLIMITED_ATTEMPTS,
                'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
            ]);

        // Initially grade the user.
        $grade = (object) [
            'attemptnumber' => '',
            'timemodified' => '',
        ];
        $data = [
            "grademodified_{$student->id}" => $grade->timemodified,
            "gradeattempt_{$student->id}" => $grade->attemptnumber,
            "quickgrade_{$student->id}" => '60.0',
        ];

        $result = $assign->testable_process_save_quick_grades($data);
        $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
        $grade = $assign->get_user_grade($student->id, false);
        $this->assertEquals(60.0, $grade->grade);

        // Attempt to grade with a past attempts grade info.
        $assign->testable_process_add_attempt($student->id);
        $data = [
            'grademodified_' . $student->id => $grade->timemodified,
            'gradeattempt_' . $student->id => $grade->attemptnumber,
            'quickgrade_' . $student->id => '50.0',
        ];
        $result = $assign->testable_process_save_quick_grades($data);
        $this->assertStringContainsString(get_string('errorrecordmodified', 'assign'), $result);
        $grade = $assign->get_user_grade($student->id, false);
        $this->assertFalse($grade);

        // Attempt to grade a the attempt.
        $submission = $assign->get_user_submission($student->id, false);
        $data = [
            'grademodified_' . $student->id => '',
            'gradeattempt_' . $student->id => $submission->attemptnumber,
            'quickgrade_' . $student->id => '40.0',
        ];
        $result = $assign->testable_process_save_quick_grades($data);
        $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
        $grade = $assign->get_user_grade($student->id, false);
        $this->assertEquals(40.0, $grade->grade);

        // Catch grade update conflicts.
        // Save old data for later.
        $pastdata = $data;
        // Update the grade the 'good' way.
        $data = [
            'grademodified_' . $student->id => $grade->timemodified,
            'gradeattempt_' . $student->id => $grade->attemptnumber,
            'quickgrade_' . $student->id => '30.0',
        ];
        $result = $assign->testable_process_save_quick_grades($data);
        $this->assertStringContainsString(get_string('quickgradingchangessaved', 'assign'), $result);
        $grade = $assign->get_user_grade($student->id, false);
        $this->assertEquals(30.0, $grade->grade);

        // Now update using 'old' data. Should fail.
        $result = $assign->testable_process_save_quick_grades($pastdata);
        $this->assertStringContainsString(get_string('errorrecordmodified', 'assign'), $result);
        $grade = $assign->get_user_grade($student->id, false);
        $this->assertEquals(30.0, $grade->grade);
    }

    /**
     * Test updating activity completion when submitting an assessment.
     */
    public function test_update_activity_completion_records_solitary_submission(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'grade' => 100,
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
            'requireallteammemberssubmit' => 0,
        ]);
        $cm = $assign->get_course_module();

        // Submit the assignment as the student.
        $this->add_submission($student, $assign);

        // Check that completion is not met yet.
        $completion = new \completion_info($course);
        $completiondata = $completion->get_data($cm, false, $student->id);
        $this->assertEquals(0, $completiondata->completionstate);

        // Update to mark as complete.
        $submission = $assign->get_user_submission($student->id, true);
        $assign->testable_update_activity_completion_records(
            0,
            0,
            $submission,
            $student->id,
            COMPLETION_COMPLETE,
            $completion
        );

        // Completion should now be met.
        $completiondata = $completion->get_data($cm, false, $student->id);
        $this->assertEquals(1, $completiondata->completionstate);
    }

    /**
     * Test updating activity completion when submitting an assessment.
     */
    public function test_update_activity_completion_records_team_submission(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        groups_add_member($group1, $student);
        groups_add_member($group1, $otherstudent);

        $assign = $this->create_instance($course, [
            'grade' => 100,
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
            'teamsubmission' => 1,
        ]);

        $cm = $assign->get_course_module();

        $this->add_submission($student, $assign);
        $this->submit_for_grading($student, $assign, ['groupid' => $group1->id]);

        $completion = new \completion_info($course);

        // Check that completion is not met yet.
        $completiondata = $completion->get_data($cm, false, $student->id);
        $this->assertEquals(0, $completiondata->completionstate);

        $completiondata = $completion->get_data($cm, false, $otherstudent->id);
        $this->assertEquals(0, $completiondata->completionstate);

        $submission = $assign->get_user_submission($student->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $submission->groupid = $group1->id;

        $assign->testable_update_activity_completion_records(1, 0, $submission, $student->id, COMPLETION_COMPLETE, $completion);

        // Completion should now be met.
        $completiondata = $completion->get_data($cm, false, $student->id);
        $this->assertEquals(1, $completiondata->completionstate);

        $completiondata = $completion->get_data($cm, false, $otherstudent->id);
        $this->assertEquals(1, $completiondata->completionstate);
    }

    /**
     * Test updating activity completion when submitting an assessment for MDL-67126.
     */
    public function test_update_activity_completion_records_team_submission_new(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
        $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
        $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);

        groups_add_member($group1, $student);
        groups_add_member($group1, $otherstudent);

        $assign = $this->create_instance($course, [
            'submissiondrafts' => 0,
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
            'completionsubmit' => 1,
            'teamsubmission' => 1,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $cm = $assign->get_course_module();

        $this->add_submission($student, $assign);

        $completion = new \completion_info($course);

        // Completion should now be met.
        $completiondata = $completion->get_data($cm, false, $student->id);
        $this->assertEquals(1, $completiondata->completionstate);

        $completiondata = $completion->get_data($cm, false, $otherstudent->id);
        $this->assertEquals(1, $completiondata->completionstate);
    }

    /**
     * Data provider for test_fix_null_grades
     * @return array[] Test data for test_fix_null_grades. Each element should contain grade, expectedcount and gradebookvalue
     */
    public static function fix_null_grades_provider(): array {
        return [
            'Negative less than one is errant' => [
                'grade' => -0.64,
                'gradebookvalue' => null,
            ],
            'Negative more than one is errant' => [
                'grade' => -30.18,
                'gradebookvalue' => null,
            ],
            'Negative one exactly is not errant, but shouldn\'t be pushed to gradebook' => [
                'grade' => ASSIGN_GRADE_NOT_SET,
                'gradebookvalue' => null,
            ],
            'Positive grade is not errant' => [
                'grade' => 1,
                'gradebookvalue' => 1,
            ],
            'Large grade is not errant' => [
                'grade' => 100,
                'gradebookvalue' => 100,
            ],
            'Zero grade is not errant' => [
                'grade' => 0,
                'gradebookvalue' => 0,
            ],
        ];
    }

    /**
     * Test fix_null_grades
     * @param number $grade The grade we should set in the assign grading table.
     * @param number $expectedcount The finalgrade we expect in the gradebook after fixing the grades.
     * @dataProvider fix_null_grades_provider
     */
    public function test_fix_null_grades($grade, $gradebookvalue): void {
        global $DB;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course);

        // Try getting a student's grade. This will give a grade of -1.
        // Then we can override it with a bad negative grade.
        $assign->get_user_grade($student->id, true);

        // Set the grade to something errant.
        // We don't set the grader here, so we expect it to be -1 as a result.
        $DB->set_field(
            'assign_grades',
            'grade',
            $grade,
            [
                'userid' => $student->id,
                'assignment' => $assign->get_instance()->id,
            ]
        );
        $assign->grade = $grade;
        $assigntemp = clone $assign->get_instance();
        $assigntemp->cmidnumber = $assign->get_course_module()->idnumber;
        assign_update_grades($assigntemp);

        // Check that the gradebook was updated with the assign grade. So we can guarentee test results later on.
        $expectedgrade = $grade == -1 ? null : $grade; // Assign sends null to the gradebook for -1 grades.
        $gradegrade = \grade_grade::fetch(['userid' => $student->id, 'itemid' => $assign->get_grade_item()->id]);
        $this->assertEquals(-1, $gradegrade->usermodified);
        $this->assertEquals($expectedgrade, $gradegrade->rawgrade);

        // Call fix_null_grades().
        $method = new \ReflectionMethod(\assign::class, 'fix_null_grades');
        $result = $method->invoke($assign);

        $this->assertSame(true, $result);

        $gradegrade = \grade_grade::fetch(['userid' => $student->id, 'itemid' => $assign->get_grade_item()->id]);

        $this->assertEquals(-1, $gradegrade->usermodified);
        $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);

        // Check that the grade was updated in the gradebook by fix_null_grades.
        $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
    }

    /**
     * Test grade override displays 'Graded' for students
     */
    public function test_grade_submission_override(): void {
        global $DB, $PAGE, $OUTPUT;

        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $this->setUser($teacher);
        $assign = $this->create_instance($course, [
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // Simulate adding a grade.
        $this->setUser($teacher);
        $data = new \stdClass();
        $data->grade = '50.0';
        $assign->testable_apply_grade_to_user($data, $student->id, 0);

        // Set grade override.
        $gradegrade = \grade_grade::fetch([
            'userid' => $student->id,
            'itemid' => $assign->get_grade_item()->id,
        ]);

        // Check that grade submission is not overridden yet.
        $this->assertEquals(false, $gradegrade->is_overridden());

        // Simulate a submission.
        $this->setUser($student);
        $submission = $assign->get_user_submission($student->id, true);

        $PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));

        // Set override grade grade, and check that grade submission has been overridden.
        $gradegrade->set_overridden(true);
        $this->assertEquals(true, $gradegrade->is_overridden());

        // Check that submissionslocked message 'This assignment is not accepting submissions' does not appear for student.
        $gradingtable = new \assign_grading_table($assign, 1, '', 0, true);
        $output = $assign->get_renderer()->render($gradingtable);
        $this->assertStringContainsString(get_string('submissionstatus_', 'assign'), $output);

        $assignsubmissionstatus = $assign->get_assign_submission_status_renderable($student, true);
        $output2 = $assign->get_renderer()->render($assignsubmissionstatus);

        // Check that submissionslocked 'This assignment is not accepting submissions' message does not appear for student.
        $this->assertStringNotContainsString(get_string('submissionslocked', 'assign'), $output2);
        // Check that submissionstatus_marked 'Graded' message does appear for student.
        $this->assertStringContainsString(get_string('submissionstatus_marked', 'assign'), $output2);
    }

    /**
     * Test the result of get_filters is consistent.
     */
    public function test_get_filters(): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);
        $valid = $assign->get_filters();

        $this->assertEquals(count($valid), 7);
    }

    /**
     * Test assign->get_instance() for a number of cases, as defined in the data provider.
     *
     * @dataProvider assign_get_instance_provider
     * @param array $courseconfig the config to use when creating the course.
     * @param array $assignconfig the config to use when creating the assignment.
     * @param array $enrolconfig the config to use when enrolling the user (this will be the active user).
     * @param array $expectedproperties an map containing the expected names and values for the assign instance data.
     */
    public function test_assign_get_instance(
        array $courseconfig,
        array $assignconfig,
        array $enrolconfig,
        array $expectedproperties
    ): void {
        $this->resetAfterTest();

        set_config('enablecourserelativedates', true); // Enable relative dates at site level.

        $course = $this->getDataGenerator()->create_course($courseconfig);
        $assign = $this->create_instance($course, $assignconfig);
        $user = $this->getDataGenerator()->create_and_enrol($course, ...array_values($enrolconfig));

        $instance = $assign->get_instance($user->id);

        foreach ($expectedproperties as $propertyname => $propertyval) {
            $this->assertEquals($propertyval, $instance->$propertyname);
        }
    }

    /**
     * The test_assign_get_instance data provider.
     *
     * @return array[]
     */
    public static function assign_get_instance_provider(): array {
        $timenow = time();

        // The get_default_instance() method shouldn't calculate any properties per-user. It should just return the record data.
        // We'll confirm this works for a few different user types anyway, just like we do for get_instance().
        return [
            'Teacher whose enrolment starts after the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 6 * DAYSECS],
            ],
            'Teacher whose enrolment starts before the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 12 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
            'Teacher whose enrolment starts after the course start date, relative dates mode disabled' => [
                'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
            'Student whose enrolment starts after the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 6 * DAYSECS],
            ],
            'Student whose enrolment starts before the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 12 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
            'Student whose enrolment starts after the course start date, relative dates mode disabled' => [
                'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
        ];
    }

    /**
     * Test assign->get_default_instance() for a number of cases, as defined in the date provider.
     *
     * @dataProvider assign_get_default_instance_provider
     * @param array $courseconfig the config to use when creating the course.
     * @param array $assignconfig the config to use when creating the assignment.
     * @param array $enrolconfig the config to use when enrolling the user (this will be the active user).
     * @param array $expectedproperties an map containing the expected names and values for the assign instance data.
     */
    public function test_assign_get_default_instance(
        array $courseconfig,
        array $assignconfig,
        array $enrolconfig,
        array $expectedproperties
    ): void {
        $this->resetAfterTest();

        set_config('enablecourserelativedates', true); // Enable relative dates at site level.

        $course = $this->getDataGenerator()->create_course($courseconfig);
        $assign = $this->create_instance($course, $assignconfig);
        $user = $this->getDataGenerator()->create_and_enrol($course, ...array_values($enrolconfig));

        $this->setUser($user);
        $defaultinstance = $assign->get_default_instance();

        foreach ($expectedproperties as $propertyname => $propertyval) {
            $this->assertEquals($propertyval, $defaultinstance->$propertyname);
        }
    }

    /**
     * The test_assign_get_default_instance data provider.
     *
     * @return array[]
     */
    public static function assign_get_default_instance_provider(): array {
        $timenow = time();

        // The get_default_instance() method shouldn't calculate any properties per-user. It should just return the record data.
        // We'll confirm this works for a few different user types anyway, just like we do for get_instance().
        return [
            'Teacher whose enrolment starts after the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
            'Teacher whose enrolment starts before the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 12 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
            'Teacher whose enrolment starts after the course start date, relative dates mode disabled' => [
                'courseconfig' => ['relativedatesmode' => false, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'teacher', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
            'Student whose enrolment starts after the course start date, relative dates mode enabled' => [
                'courseconfig' => ['relativedatesmode' => true, 'startdate' => $timenow - 10 * DAYSECS],
                'assignconfig' => ['duedate' => $timenow + 4 * DAYSECS],
                'enrolconfig' => ['shortname' => 'student', 'userparams' => null, 'method' => 'manual',
                    'startdate' => $timenow - 8 * DAYSECS],
                'expectedproperties' => ['duedate' => $timenow + 4 * DAYSECS],
            ],
        ];
    }

    /**
     * Test that cron task uses task API to get its last run time.
     */
    public function test_cron_use_task_api_to_get_lastruntime(): void {
        global $DB;
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();

        // Create an assignment which allows submissions from 3 days ago.
        $assign1 = $this->create_instance($course, [
            'duedate' => time() + DAYSECS,
            'alwaysshowdescription' => 0,
            'allowsubmissionsfromdate' => time() - 3 * DAYSECS,
            'intro' => 'This one should not be re-created',
        ]);

        // Create an assignment which allows submissions from 1 day ago.
        $assign2 = $this->create_instance($course, [
            'duedate' => time() + DAYSECS,
            'alwaysshowdescription' => 0,
            'allowsubmissionsfromdate' => time() - DAYSECS,
            'intro' => 'This one should be re-created',
        ]);

        // Set last run time 2 days ago.
        $DB->set_field('task_scheduled', 'lastruntime', time() - 2 * DAYSECS, ['classname' => '\mod_assign\task\cron_task']);

        // Remove events to make sure cron will update calendar and re-create one of them.
        $params = ['modulename' => 'assign', 'instance' => $assign1->get_instance()->id];
        $DB->delete_records('event', $params);
        $params = ['modulename' => 'assign', 'instance' => $assign2->get_instance()->id];
        $DB->delete_records('event', $params);

        // Run cron.
        \assign::cron();

        // Assert that calendar hasn't been updated for the first assignment as it's supposed to be
        // updated as part of previous cron runs (allowsubmissionsfromdate is less than lastruntime).
        $params = ['modulename' => 'assign', 'instance' => $assign1->get_instance()->id];
        $event1 = $DB->get_record('event', $params);
        $this->assertEmpty($event1);

        // Assert that calendar has been updated for the second assignment
        // because its allowsubmissionsfromdate is greater than lastruntime.
        $params = ['modulename' => 'assign', 'instance' => $assign2->get_instance()->id];
        $event2 = $DB->get_record('event', $params);
        $this->assertNotEmpty($event2);
        $this->assertSame('This one should be re-created', $event2->description);
    }

    /**
     * Test that attachments should not be provided if \assign->show_intro returns false.
     *
     * @covers \assign::should_provide_intro_attachments
     */
    public function test_should_provide_intro_attachments_with_show_intro_disabled(): void {
        $this->resetAfterTest();
        $futuredate = time() + 300;
        [$assign, $instance, $student] = $this->create_submission([
            'alwaysshowdescription' => '0',
            'allowsubmissionsfromdate' => $futuredate,
        ]);
        $this->assertFalse($assign->should_provide_intro_attachments($student->id));
    }

    /**
     * Test that attachments should be provided if user has capability to manage activity.
     *
     * @covers \assign::should_provide_intro_attachments
     */
    public function test_should_provide_intro_attachments_with_bypass_capability(): void {
        $this->resetAfterTest();
        [$assign, $instance, $student] = $this->create_submission([
            'submissionattachments' => 1,
        ]);
        // Provide teaching role to student1 so they are able to bypass time limit restrictions on viewing attachments.
        $this->getDataGenerator()->enrol_user($student->id, $instance->course, 'editingteacher');
        $this->assertTrue($assign->should_provide_intro_attachments($student->id));
    }

    /**
     * Test that attachments should be provided if submissionattachments is disabled.
     *
     * @covers \assign::should_provide_intro_attachments
     */
    public function test_should_provide_intro_attachments_with_submissionattachments_disabled(): void {
        $this->resetAfterTest();
        [$assign, $instance, $student] = $this->create_submission();
        $this->assertTrue($assign->should_provide_intro_attachments($student->id));
    }

    /**
     * Test that attachments should not be provided if submissionattachments is enabled with no open submission.
     *
     * @covers \assign::should_provide_intro_attachments
     */
    public function test_should_provide_intro_attachments_with_submissionattachments_enabled_and_submissions_closed(): void {
        $this->resetAfterTest();
        // Set cut-off date to the past.
        [$assign, $instance, $student] = $this->create_submission([
            'timelimit' => '300',
            'submissionattachments' => 1,
            'cutoffdate' => time() - 300,
        ]);
        $this->assertFalse($assign->should_provide_intro_attachments($student->id));
    }

    /**
     * Test that attachments should be provided if submissionattachments is enabled with an open submission.
     *
     * @covers \assign::should_provide_intro_attachments
     */
    public function test_should_provide_intro_attachments_submissionattachments_enabled_and_an_open_submission(): void {
        $this->resetAfterTest();
        set_config('enabletimelimit', '1', 'assign');
        [$assign, $instance, $student] = $this->create_submission([
            'timelimit' => '300',
            'submissionattachments' => 1,
        ]);

        // Open a submission.
        $assign->get_user_submission($student->id, true);

        $this->assertTrue($assign->should_provide_intro_attachments($student->id));
    }

    /**
     * Test that a submission using a time limit is currently open.
     *
     * @covers \assign::is_attempt_in_progress
     */
    public function test_is_attempt_in_progress_with_open_submission(): void {
        global $DB;
        $this->resetAfterTest();
        set_config('enabletimelimit', '1', 'assign');
        [$assign, $instance, $student] = $this->create_submission([
            'timelimit' => '300',
        ]);
        $submission = $assign->get_user_submission($student->id, true);
        // Set a timestarted.
        $submission->timestarted = time() - 300;
        $DB->update_record('assign_submission', $submission);
        $this->assertTrue($assign->is_attempt_in_progress());
    }

    /**
     * Test that a submission using a time limit is started without a start time.
     *
     * @covers \assign::is_attempt_in_progress
     */
    public function test_is_attempt_in_progress_with_open_submission_and_no_timestarted(): void {
        $this->resetAfterTest();
        set_config('enabletimelimit', '1', 'assign');
        [$assign, $instance, $student] = $this->create_submission([
            'timelimit' => '300',
        ]);
        $assign->get_user_submission($student->id, true);
        $this->assertFalse($assign->is_attempt_in_progress());
    }

    /**
     * Test that a submission using a time limit is currently not open.
     *
     * @covers \assign::is_attempt_in_progress
     */
    public function test_is_attempt_in_progress_with_no_open_submission(): void {
        global $DB;
        $this->resetAfterTest();
        set_config('enabletimelimit', '1', 'assign');
        [$assign, $instance, $student] = $this->create_submission([
            'timelimit' => '300',
        ]);
        // Clear all current submissions.
        $DB->delete_records('assign_submission', ['assignment' => $instance->id]);
        $this->assertFalse($assign->is_attempt_in_progress());
    }

    /**
     * Create a submission for testing.
     * @param  array $params Optional params to use for creating assignment instance.
     * @return array an array containing all the required data for testing
     */
    protected function create_submission(array $params = []) {
        global $DB;

        // Create a course and assignment and users.
        $course = self::getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]);

        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
        $params = array_merge([
            'course' => $course->id,
            'assignsubmission_file_maxfiles' => 1,
            'assignsubmission_file_maxsizebytes' => 1024 * 1024,
            'assignsubmission_onlinetext_enabled' => 1,
            'assignsubmission_file_enabled' => 1,
            'submissiondrafts' => 1,
            'assignfeedback_file_enabled' => 1,
            'assignfeedback_comments_enabled' => 1,
            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
            'sendnotifications' => 0,
        ], $params);

        set_config('submissionreceipts', 0, 'assign');

        $instance = $generator->create_instance($params);
        $cm = get_coursemodule_from_instance('assign', $instance->id);
        $context = \context_module::instance($cm->id);

        $assign = new \mod_assign_testable_assign($context, $cm, $course);

        $student = self::getDataGenerator()->create_user();
        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);

        $this->setUser($student);

        // Create a student1 with an online text submission.
        // Simulate a submission.
        $submission = $assign->get_user_submission($student->id, true);

        $data = new \stdClass();
        $data->onlinetext_editor = [
            'itemid' => file_get_unused_draft_itemid(),
            'text' => 'Submission text with a <a href="@@PLUGINFILE@@/intro.txt">link</a>',
            'format' => FORMAT_MOODLE];

        $draftidfile = file_get_unused_draft_itemid();
        $usercontext = \context_user::instance($student->id);
        $filerecord = [
            'contextid' => $usercontext->id,
            'component' => 'user',
            'filearea'  => 'draft',
            'itemid'    => $draftidfile,
            'filepath'  => '/',
            'filename'  => 't.txt',
        ];
        $fs = get_file_storage();
        $fs->create_file_from_string($filerecord, 'text contents');

        $data->files_filemanager = $draftidfile;

        $notices = [];
        $assign->save_submission($data, $notices);

        return [$assign, $instance, $student];
    }

    /**
     * Test user filtering by First name, Last name and Submission status.
     *
     * @covers \assign::is_userid_filtered
     */
    public function test_is_userid_filtered(): void {
        $this->resetAfterTest();

        // Generate data and simulate student submissions.
        $course = $this->getDataGenerator()->create_course();
        $params1 = ['firstname' => 'Valentin', 'lastname' => 'Ivanov'];
        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student', $params1);
        $params2 = ['firstname' => 'Nikolay', 'lastname' => 'Petrov'];
        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student', $params2);
        $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]);
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $this->setUser($student1);
        $submission = $assign->get_user_submission($student1->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
        $assign->testable_update_submission($submission, $student1->id, true, false);
        $this->setUser($student2);
        $submission = $assign->get_user_submission($student2->id, true);
        $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
        $assign->testable_update_submission($submission, $student2->id, true, false);
        $this->setUser($teacher);

        // By default, both users should match filters.
        $this->AssertTrue($assign->is_userid_filtered($student1->id));
        $this->AssertTrue($assign->is_userid_filtered($student2->id));

        // Filter by First name starting with V.
        $_GET['tifirst'] = 'V';
        $this->AssertTrue($assign->is_userid_filtered($student1->id));
        $this->AssertFalse($assign->is_userid_filtered($student2->id));

        // Add Last name to filter out both users.
        $_GET['tilast'] = 'G';
        $this->AssertFalse($assign->is_userid_filtered($student1->id));
        $this->AssertFalse($assign->is_userid_filtered($student2->id));

        // Unsetting variables doesn't change behaviour because filters are stored in user preferences.
        unset($_GET['tifirst']);
        unset($_GET['tilast']);
        $this->AssertFalse($assign->is_userid_filtered($student1->id));
        $this->AssertFalse($assign->is_userid_filtered($student2->id));

        // Reset table preferences.
        $_GET['treset'] = '1';
        $this->AssertTrue($assign->is_userid_filtered($student1->id));
        $this->AssertTrue($assign->is_userid_filtered($student2->id));

        // Display users with submitted submissions only.
        set_user_preference('assign_filter', ASSIGN_SUBMISSION_STATUS_SUBMITTED);
        $this->AssertTrue($assign->is_userid_filtered($student1->id));
        $this->AssertFalse($assign->is_userid_filtered($student2->id));

        // Display users with drafts.
        set_user_preference('assign_filter', ASSIGN_SUBMISSION_STATUS_DRAFT);
        $this->AssertFalse($assign->is_userid_filtered($student1->id));
        $this->AssertTrue($assign->is_userid_filtered($student2->id));

        // Reset the filter.
        set_user_preference('assign_filter', '');
        $this->AssertTrue($assign->is_userid_filtered($student1->id));
        $this->AssertTrue($assign->is_userid_filtered($student2->id));
    }

    /**
     * Test get_error_messages like a public function.
     *
     * @covers \assign::get_error_messages
     */
    public function test_get_error_messages(): void {
        $this->resetAfterTest();

        // Generate data.
        $course = $this->getDataGenerator()->create_course();
        $assign = $this->create_instance($course);

        // Get the empty error message list.
        $result = $assign->get_error_messages();
        $this->assertIsArray($result);
        $this->assertEmpty($result);

        // Generate users.
        $teacher  = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');

        // Set the teacher as user and try to delete the user's submission to create the error.
        $this->setUser($teacher);
        $assign->remove_submission($student1->id);

        // Get the created error messages.
        $result = $assign->get_error_messages();
        $this->assertIsArray($result);
        $this->assertNotEmpty($result);
    }

    /**
     * Test that assignment grades are pushed to the gradebook when anonymous
     * submissions and marking workflow are enabled (MDL-83195).
     * @covers \assign::gradebook_item_update
     */
    public function test_release_grade_anon(): void {
        $this->resetAfterTest();
        $course = $this->getDataGenerator()->create_course();
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');

        $assign = $this->create_instance($course, [
            'blindmarking' => 1,
            'markingworkflow' => 1,
            'markinganonymous' => 1,
        ]);

        // Add a grade and change the workflow status to "Released".
        $this->mark_submission($teacher, $assign, $student, 50.0,  [
            'workflowstate' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
        ]);

        // Make sure the grade has been pushed to the gradebook.
        $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assign->get_instance()->id, $student->id);
        $this->assertEquals(50, (int)$gradinginfo->items[0]->grades[$student->id]->grade);
    }
}
