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

namespace mod_assign;

use core\task\task_trait;
use mod_assign\task\queue_assignment_due_digest_notification_tasks_for_users;

/**
 * Test class for the assignment notification_helper.
 *
 * @package    mod_assign
 * @category   test
 * @copyright  2024 David Woloszyn <david.woloszyn@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @covers \mod_assign\notification_helper
 */
final class notification_helper_test extends \advanced_testcase {

    use task_trait;

    /**
     * Run all the tasks related to the 'due soon' notifications.
     */
    protected function run_due_soon_notification_helper_tasks(): void {
        $task = \core\task\manager::get_scheduled_task(\mod_assign\task\queue_all_assignment_due_soon_notification_tasks::class);
        $task->execute();
        $clock = \core\di::get(\core\clock::class);

        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        if ($adhoctask) {
            $this->assertInstanceOf(\mod_assign\task\queue_assignment_due_soon_notification_tasks_for_users::class, $adhoctask);
            $adhoctask->execute();
            \core\task\manager::adhoc_task_complete($adhoctask);
        }

        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        if ($adhoctask) {
            $this->assertInstanceOf(\mod_assign\task\send_assignment_due_soon_notification_to_user::class, $adhoctask);
            $adhoctask->execute();
            \core\task\manager::adhoc_task_complete($adhoctask);
        }
    }

    /**
     * Test getting due soon assignments.
     */
    public function test_get_due_soon_assignments(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $helper = \core\di::get(notification_helper::class);
        $clock = $this->mock_clock_with_frozen();

        // Create an assignment with a due date < 48 hours.
        $course = $generator->create_course();
        $generator->create_module('assign', ['course' => $course->id, 'duedate' => $clock->time() + DAYSECS]);

        // Check that we have a result returned.
        $result = $helper::get_due_soon_assignments();
        $this->assertTrue($result->valid());
        $result->close();

        // Time travel 3 days into the future. We should have no assignments in range.
        $clock->bump(DAYSECS * 3);
        $result = $helper::get_due_soon_assignments();
        $this->assertFalse($result->valid());
        $result->close();
    }

    /**
     * Test getting users within an assignment that have a due date soon.
     */
    public function test_get_due_soon_users_within_assignment(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $helper = \core\di::get(notification_helper::class);
        $clock = $this->mock_clock_with_frozen();

        // Create a course and enrol some users.
        $course = $generator->create_course(['enablecompletion' => 1]);
        $user1 = $generator->create_user();
        $user2 = $generator->create_user();
        $user3 = $generator->create_user();
        $user4 = $generator->create_user();
        $user5 = $generator->create_user();
        $user6 = $generator->create_user();
        $user7 = $generator->create_user();
        $user8 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');
        $generator->enrol_user($user2->id, $course->id, 'student');
        $generator->enrol_user($user3->id, $course->id, 'student');
        $generator->enrol_user($user4->id, $course->id, 'student');
        $generator->enrol_user($user5->id, $course->id, 'student');
        $generator->enrol_user($user6->id, $course->id, 'student');
        $generator->enrol_user($user7->id, $course->id, 'student');
        $generator->enrol_user($user8->id, $course->id, 'teacher');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an assignment with a due date < 48 hours.
        $duedate = $clock->time() + DAYSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ], [
            'completion' => \COMPLETION_TRACKING_AUTOMATIC,
            'completionview' => 1,
        ]);

        // User1 will have a user override, giving them an extra 1 hour for 'duedate'.
        $userduedate = $duedate + HOURSECS;
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'userid' => $user1->id,
            'duedate' => $userduedate,
        ]);

        // User2 and user3 will have a group override, giving them an extra 2 hours for 'duedate'.
        $groupduedate = $duedate + (HOURSECS * 2);
        $group = $generator->create_group(['courseid' => $course->id]);
        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user2->id]);
        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user3->id]);
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'groupid' => $group->id,
            'duedate' => $groupduedate,
        ]);

        // User4 will have a user override of one extra week, excluding them from the results.
        $userduedate = $duedate + WEEKSECS;
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'userid' => $user4->id,
            'duedate' => $userduedate,
        ]);

        $assignmentgenerator->create_submission([
            'userid' => $user5->id,
            'cmid' => $assignment->cmid,
            'status' => 'submitted',
            'timemodified' => $clock->time(),
            'onlinetext' => 'Some text',
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // User6 will exclude themselves by meeting completion conditions.
        $completion = new \completion_info($course);
        $assigncm = get_coursemodule_from_instance('assign', $assignment->id);
        $completion->set_module_viewed($assigncm, $user6->id);

        // User7 will exclude themselves b/c they already have a grade.
        $context = \context_module::instance($assigncm->id);
        $assign = new \assign($context, $assigncm, $course);
        $this->setUser($user8);
        $gradedata = new \stdClass();
        $gradedata->grade = '80.0';
        $gradedata->attemptnumber = 1;
        $assign->save_grade($user7->id, $gradedata);

        $this->setUser();

        // There should be 3 users with the teacher excluded.
        $users = $helper::get_users_within_assignment($assignment->id, $helper::TYPE_DUE_SOON);
        $this->assertCount(3, $users);
        $this->assertArrayHasKey($user1->id, $users);
        $this->assertArrayHasKey($user2->id, $users);
        $this->assertArrayHasKey($user3->id, $users);
    }

    /**
     * Test sending the assignment due soon notification to a user.
     */
    public function test_send_due_soon_notification_to_user(): void {
        global $DB;
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $helper = \core\di::get(notification_helper::class);
        $clock = $this->mock_clock_with_frozen();
        $sink = $this->redirectMessages();

        // Create a course and enrol a user.
        $course = $generator->create_course();
        $user1 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');

        // Suspended user, should not receive notification.
        $user2 = $generator->create_user(['suspended' => 1]);
        $generator->enrol_user($user2->id, $course->id, 'student');

        // Nologin user, should not receive notification.
        $user3 = $generator->create_user(['auth' => 'nologin']);
        $generator->enrol_user($user3->id, $course->id, 'student');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an assignment with a due date < 48 hours.
        $duedate = $clock->time() + DAYSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $clock->bump(5);

        // Run the tasks.
        $this->run_due_soon_notification_helper_tasks();

        // Get the assignment object.
        [$course, $assigncm] = get_course_and_cm_from_instance($assignment->id, 'assign');
        $cmcontext = \context_module::instance($assigncm->id);
        $assignmentobj = new \assign($cmcontext, $assigncm, $course);
        $duedate = $assignmentobj->get_instance($user1->id)->duedate;

        // Get the notifications that should have been created during the adhoc task.
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);

        // Check the subject matches.
        $message = reset($messages);
        $stringparams = [
            'duedate' => userdate($duedate),
            'assignmentname' => $assignment->name,
        ];
        $expectedsubject = get_string('assignmentduesoonsubject', 'mod_assign', $stringparams);
        $this->assertEquals($expectedsubject, $message->subject);

        // Clear sink.
        $sink->clear();

        // Run the tasks again.
        $this->run_due_soon_notification_helper_tasks();

        // There should be no notification because nothing has changed.
        $this->assertEmpty($sink->get_messages_by_component('mod_assign'));

        // Let's modify the 'duedate' for the assignment (it will still be within the 48 hour range).
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->duedate = $duedate + HOURSECS;
        $DB->update_record('assign', $updatedata);

        // Run the tasks again.
        $this->run_due_soon_notification_helper_tasks();

        // There should be a new notification because the 'duedate' has been updated.
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);
        $message = reset($messages);
        $stringparams = [
            'duedate' => userdate($updatedata->duedate),
            'assignmentname' => $assignment->name,
        ];
        $expectedsubject = get_string('assignmentduesoonsubject', 'mod_assign', $stringparams);
        $this->assertEquals($expectedsubject, $message->subject);

        // Clear sink.
        $sink->clear();

        // Let's update the assignment visibility.
        $DB->set_field('course_modules', 'visible', 0, ['id' => $assigncm->id]);

        // Update the duedate to force a new notification.
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->duedate = $duedate + HOURSECS * 3;
        $DB->update_record('assign', $updatedata);

        // Run the tasks again.
        $this->run_due_soon_notification_helper_tasks();

        // There should not be a new notification the assignmnet is not visible.
        $this->assertEmpty($sink->get_messages_by_component('mod_assign'));

        // Update the visibility back to visible before the next assert.
        $DB->set_field('course_modules', 'visible', 1, ['id' => $assigncm->id]);

        // Clear sink.
        $sink->clear();

        // Let's modify the 'duedate' one more time.
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->duedate = $duedate + (HOURSECS * 2);
        $DB->update_record('assign', $updatedata);

        // This time, the user will submit the assignment.
        $assignmentgenerator->create_submission([
            'userid' => $user1->id,
            'cmid' => $assignment->cmid,
            'status' => 'submitted',
            'timemodified' => $clock->time(),
            'onlinetext' => 'Some text',
        ]);
        $clock->bump(5);

        // Run the tasks again.
        $this->run_due_soon_notification_helper_tasks();

        // There should only be one notifcation for submission (not for due soon).
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);
        $expectedsubject = get_string('submissionreceiptsmall', 'mod_assign', ['assignment' => $assignment->name]);
        $this->assertEquals($expectedsubject, reset($messages)->subject);

        // Clear sink.
        $sink->clear();
    }

    /**
     * Test that we do not fail on deleted assignments with due soon notifications to a user.
     */
    public function test_not_to_fail_on_deleted_assigment_with_due_soon_notifications_to_user(): void {
        global $DB;
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $clock = $this->mock_clock_with_frozen();

        // Create a course and enrol a user.
        $course = $generator->create_course();
        $user1 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an assignment with a due date < 48 hours.
        $duedate = $clock->time() + DAYSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $clock->bump(5);

        // Run the scheduled and ad-hoc task to queue the notifications.
        $task = \core\task\manager::get_scheduled_task(\mod_assign\task\queue_all_assignment_due_soon_notification_tasks::class);
        $task->execute();

        $clock->bump(5);
        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        $this->assertInstanceOf(\mod_assign\task\queue_assignment_due_soon_notification_tasks_for_users::class, $adhoctask);
        $adhoctask->execute();
        \core\task\manager::adhoc_task_complete($adhoctask);

        // Delete the assignment.
        $DB->delete_records('assign', ['id' => $assignment->id]);

        // Try to run the ad-hoc task to send the notifications.
        $clock->bump(5);
        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        $this->assertInstanceOf(\mod_assign\task\send_assignment_due_soon_notification_to_user::class, $adhoctask);

        ob_start();
        $adhoctask->execute();
        $output = ob_get_clean();

        \core\task\manager::adhoc_task_complete($adhoctask);

        // The ad-hoc task should be deleted.
        $this->assertNull(\core\task\manager::get_next_adhoc_task($clock->time()));
        $this->assertStringContainsString(
            needle: "No notification send as the assignment $assignment->id can no longer be found in the database.",
            haystack: $output
        );
    }

    /**
     * Run all the tasks related to the 'overdue' notifications.
     */
    protected function run_overdue_notification_helper_tasks(): void {
        $task = \core\task\manager::get_scheduled_task(\mod_assign\task\queue_all_assignment_overdue_notification_tasks::class);
        $task->execute();
        $clock = \core\di::get(\core\clock::class);

        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        if ($adhoctask) {
            $this->assertInstanceOf(\mod_assign\task\queue_assignment_overdue_notification_tasks_for_users::class, $adhoctask);
            $adhoctask->execute();
            \core\task\manager::adhoc_task_complete($adhoctask);
        }

        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        if ($adhoctask) {
            $this->assertInstanceOf(\mod_assign\task\send_assignment_overdue_notification_to_user::class, $adhoctask);
            $adhoctask->execute();
            \core\task\manager::adhoc_task_complete($adhoctask);
        }
    }

    /**
     * Test getting overdue assignments.
     */
    public function test_get_overdue_assignments(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $helper = \core\di::get(notification_helper::class);
        $clock = $this->mock_clock_with_frozen();

        // Create an overdue assignment.
        $course = $generator->create_course();
        $generator->create_module('assign', ['course' => $course->id, 'duedate' => $clock->time() - HOURSECS]);

        // Check that we have a result returned.
        $result = $helper::get_overdue_assignments();
        $this->assertTrue($result->valid());
        $result->close();

        // Time travel 2 hours into the future.
        // We should have no assignments found as we are only getting overdue assignments within a 2 hour window.
        $clock->bump(HOURSECS * 2);
        $result = $helper::get_overdue_assignments();
        $this->assertFalse($result->valid());
        $result->close();
    }

    /**
     * Test getting users within an assignment that is overdue.
     */
    public function test_get_overdue_users_within_assignment(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $helper = \core\di::get(notification_helper::class);
        $clock = $this->mock_clock_with_frozen();

        // Create a course and enrol some users.
        $course = $generator->create_course(['enablecompletion' => 1]);
        $user1 = $generator->create_and_enrol($course, 'student');
        $user2 = $generator->create_and_enrol($course, 'student');
        $user3 = $generator->create_and_enrol($course, 'student');
        $user4 = $generator->create_and_enrol($course, 'student');
        $user5 = $generator->create_and_enrol($course, 'student');
        $user6 = $generator->create_and_enrol($course, 'student');
        $user7 = $generator->create_and_enrol($course, 'teacher');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an overdue assignment.
        $duedate = $clock->time() - HOURSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // User1 will have a user override, giving them an extra minute for 'duedate'.
        $userduedate = $duedate + MINSECS;
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'userid' => $user1->id,
            'duedate' => $userduedate,
        ]);

        // User2 and user3 will have a group override, giving them an extra minute for 'duedate'.
        $groupduedate = $duedate + MINSECS;
        $group = $generator->create_group(['courseid' => $course->id]);
        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user2->id]);
        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user3->id]);
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'groupid' => $group->id,
            'duedate' => $groupduedate,
        ]);

        // User4 will have a user override of one extra week, excluding them from the results.
        $userduedate = $duedate + WEEKSECS;
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'userid' => $user4->id,
            'duedate' => $userduedate,
        ]);

        // User5 will submit the assignment, excluding them from the results.
        $assignmentgenerator->create_submission([
            'userid' => $user5->id,
            'cmid' => $assignment->cmid,
            'status' => 'submitted',
            'timemodified' => $clock->time(),
            'onlinetext' => 'Some text',
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // User6 will have a cut-off date override that has already lapsed, excluding them from the results.
        $usercutoffdate = $clock->time() - MINSECS;
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'userid' => $user6->id,
            'cutoffdate' => $usercutoffdate,
        ]);

        // There should be 3 users with the teacher excluded.
        $users = $helper::get_users_within_assignment($assignment->id, $helper::TYPE_OVERDUE);
        $this->assertCount(3, $users);
        $this->assertArrayHasKey($user1->id, $users);
        $this->assertArrayHasKey($user2->id, $users);
        $this->assertArrayHasKey($user3->id, $users);

        // Create an overdue assignment with no submission plugins enabled.
        $duedate = $clock->time() - HOURSECS;
        $nosubassignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 0,
            'assignsubmission_file_enabled' => 0,
        ]);

        // There should be 0 users b/c this assignment has no deliverables.
        $users = $helper::get_users_within_assignment($nosubassignment->id, $helper::TYPE_OVERDUE);
        $this->assertCount(0, $users);

        // Create an assignment that requires a 'view' to be completed.
        $viewassignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'name' => 'View, Grade Assignment',
            'duedate' => $duedate,
            'assignsubmission_onlinetext_enabled' => true,
         ], [
            'completion' => \COMPLETION_TRACKING_AUTOMATIC,
            'completionview' => 1,
         ]);

        // Mark the assignment as viewed by User1, excluding them from the notification.
        $completion = new \completion_info($course);
        $assigncm = get_coursemodule_from_instance('assign', $viewassignment->id);
        $completion->set_module_viewed($assigncm, $user1->id);

        // Submit a grade for User2, excluding them from the notification, even though they don't have a submission.
        $context = \context_module::instance($assigncm->id);
        $assign = new \assign($context, $assigncm, $course);
        $this->setUser($user7);
        $gradedata = new \stdClass();
        $gradedata->grade = '80.0';
        $gradedata->attemptnumber = 1;
        $assign->save_grade($user2->id, $gradedata);
        $this->setUser();

        // There should be 4 users, because user1 met activity completion and user2 has a grade.
        $users = $helper::get_users_within_assignment($viewassignment->id, $helper::TYPE_OVERDUE);
        $this->assertCount(4, $users);
        $this->assertArrayHasKey($user3->id, $users);
        $this->assertArrayHasKey($user4->id, $users);
        $this->assertArrayHasKey($user5->id, $users);
        $this->assertArrayHasKey($user6->id, $users);
    }

    /**
     * Test sending the assignment overdue notification to a user.
     */
    public function test_send_overdue_notification_to_user(): void {
        global $DB;
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $clock = $this->mock_clock_with_frozen();
        $sink = $this->redirectMessages();

        // Create a course and enrol a user.
        $course = $generator->create_course();
        $user1 = $generator->create_and_enrol($course, 'student');

        // Suspended user, should not receive notification.
        $user2 = $generator->create_user(['suspended' => 1]);
        $generator->enrol_user($user2->id, $course->id, 'student');

        // Nologin user, should not receive notification.
        $user3 = $generator->create_user(['auth' => 'nologin']);
        $generator->enrol_user($user3->id, $course->id, 'student');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an assignment that is overdue.
        $duedate = $clock->time() - HOURSECS;
        $cutoffdate = $clock->time() + DAYSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'cutoffdate' => $cutoffdate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $clock->bump(5);

        // Run the tasks.
        $this->run_overdue_notification_helper_tasks();

        // Get the notifications that should have been created during the adhoc task.
        $this->assertCount(1, $sink->get_messages());

        // Check the subject matches.
        $messages = $sink->get_messages_by_component('mod_assign');
        $message = reset($messages);
        $expectedsubject = get_string('assignmentoverduesubject', 'mod_assign', ['assignmentname' => $assignment->name]);
        $this->assertEquals($expectedsubject, $message->subject);

        // Clear sink.
        $sink->clear();

        // Run the tasks again.
        $this->run_overdue_notification_helper_tasks();

        // There should be no notification because nothing has changed.
        $this->assertEmpty($sink->get_messages_by_component('mod_assign'));

        // Let's modify the 'duedate' for the assignment (it will still be overdue).
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->duedate = $duedate + MINSECS;
        $DB->update_record('assign', $updatedata);

        // Clear sink.
        $sink->clear();

        // Run the tasks again.
        $this->run_overdue_notification_helper_tasks();

        // There should be a new notification because the 'duedate' has been updated.
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);
        $message = reset($messages);
        $expectedsubject = get_string('assignmentoverduesubject', 'mod_assign', ['assignmentname' => $assignment->name]);
        $this->assertEquals($expectedsubject, $message->subject);

        // Let's modify the 'cut-off date'.
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->cutoffdate = $cutoffdate + MINSECS;
        $DB->update_record('assign', $updatedata);

        // Clear sink.
        $sink->clear();

        // Run the tasks again.
        $this->run_overdue_notification_helper_tasks();

        // There should be a new notification because the 'cut-off date' has been updated.
        $messages = $sink->get_messages_by_component('mod_assign');
        $message = reset($messages);
        $expectedsubject = get_string('assignmentoverduesubject', 'mod_assign', ['assignmentname' => $assignment->name]);
        $this->assertEquals($expectedsubject, $message->subject);

        // Let's update the assignment visibility.
        $cm = get_coursemodule_from_instance('assign', $assignment->id, $course->id);
        $DB->set_field('course_modules', 'visible', 0, ['id' => $cm->id]);

        // Update the duedate to force a new notification.
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->duedate = $duedate + (MINSECS * 3);
        $DB->update_record('assign', $updatedata);

        // Clear sink.
        $sink->clear();

        // Run the tasks again.
        $this->run_overdue_notification_helper_tasks();

        // There should not be a new notification because the assignment is not visible.
        $this->assertEmpty($sink->get_messages_by_component('mod_assign'));

        // Update the visibility back to visible before the next assert.
        $DB->set_field('course_modules', 'visible', 1, ['id' => $cm->id]);

        // Let's modify the 'duedate' one more time.
        $updatedata = new \stdClass();
        $updatedata->id = $assignment->id;
        $updatedata->duedate = $duedate + (MINSECS * 2);
        $DB->update_record('assign', $updatedata);

        // This time, the user will submit the assignment.
        $assignmentgenerator->create_submission([
            'userid' => $user1->id,
            'cmid' => $assignment->cmid,
            'status' => 'submitted',
            'timemodified' => $clock->time(),
            'onlinetext' => 'Some text',
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // Clear sink.
        $sink->clear();

        // Run the tasks again.
        $this->run_overdue_notification_helper_tasks();

        // No new notification should have been sent.
        $this->assertEmpty($sink->get_messages_by_component('mod_assign'));
    }

    /**
     * Test that we do not fail on deleted assignments with overdue notifications to a user.
     */
    public function test_not_to_fail_on_deleted_assigment_with_overdue_notifications_to_user(): void {
        global $DB;
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $clock = $this->mock_clock_with_frozen();

        // Create a course and enrol a user.
        $course = $generator->create_course();
        $user1 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an assignment that is overdue.
        $duedate = $clock->time() - HOURSECS;
        $cutoffdate = $clock->time() + DAYSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'cutoffdate' => $cutoffdate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $clock->bump(5);

        // Run the scheduled and ad-hoc task to queue the notifications.
        $task = \core\task\manager::get_scheduled_task(\mod_assign\task\queue_all_assignment_overdue_notification_tasks::class);
        $task->execute();

        $clock->bump(5);
        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        $this->assertInstanceOf(\mod_assign\task\queue_assignment_overdue_notification_tasks_for_users::class, $adhoctask);
        $adhoctask->execute();
        \core\task\manager::adhoc_task_complete($adhoctask);

        // Delete the assignment.
        $DB->delete_records('assign', ['id' => $assignment->id]);

        // Try to run the ad-hoc task to send the notifications.
        $clock->bump(5);
        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        $this->assertInstanceOf(\mod_assign\task\send_assignment_overdue_notification_to_user::class, $adhoctask);

        ob_start();
        $adhoctask->execute();
        $output = ob_get_clean();

        \core\task\manager::adhoc_task_complete($adhoctask);

        // The ad-hoc task should be deleted.
        $this->assertNull(\core\task\manager::get_next_adhoc_task($clock->time()));
        $this->assertStringContainsString(
            needle: "No notification send as the assignment $assignment->id can no longer be found in the database.",
            haystack: $output
        );
    }

    /**
     * Run all the tasks related to the due digest notifications.
     */
    protected function run_due_digest_notification_helper_tasks(): void {
        $task = \core\task\manager::get_scheduled_task(\mod_assign\task\queue_all_assignment_due_digest_notification_tasks::class);
        $task->execute();
        $clock = \core\di::get(\core\clock::class);

        $adhoctask = \core\task\manager::get_next_adhoc_task($clock->time());
        if ($adhoctask) {
            $this->assertInstanceOf(\mod_assign\task\send_assignment_due_digest_notification_to_user::class, $adhoctask);
            $adhoctask->execute();
            \core\task\manager::adhoc_task_complete($adhoctask);
        }
    }

    /**
     * Test getting users for the due digest.
     */
    public function test_get_users_for_due_digest(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $helper = \core\di::get(notification_helper::class);
        $clock = $this->mock_clock_with_frozen();

        // Create a course and enrol some users.
        $course = $generator->create_course();
        $user1 = $generator->create_user();
        $user2 = $generator->create_user();
        $user3 = $generator->create_user();
        $user4 = $generator->create_user();
        $user5 = $generator->create_user();
        $user6 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');
        $generator->enrol_user($user2->id, $course->id, 'student');
        $generator->enrol_user($user3->id, $course->id, 'student');
        $generator->enrol_user($user4->id, $course->id, 'student');
        $generator->enrol_user($user5->id, $course->id, 'student');
        $generator->enrol_user($user6->id, $course->id, 'teacher');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create an assignment with a due date 7 days from now (the due digest range).
        $duedate = $clock->time() + WEEKSECS;
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // User1 will have a user override, giving them an extra 1 day for 'duedate', excluding them from the results.
        $userduedate = $duedate + DAYSECS;
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'userid' => $user1->id,
            'duedate' => $userduedate,
        ]);

        // User2 and user3 will have a group override, giving them an extra 2 days for 'duedate', excluding them from the results.
        $groupduedate = $duedate + (DAYSECS * 2);
        $group = $generator->create_group(['courseid' => $course->id]);
        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user2->id]);
        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user3->id]);
        $assignmentgenerator->create_override([
            'assignid' => $assignment->id,
            'groupid' => $group->id,
            'duedate' => $groupduedate,
        ]);

        // User4 will submit the assignment, excluding them from the results.
        $assignmentgenerator->create_submission([
            'userid' => $user4->id,
            'cmid' => $assignment->cmid,
            'status' => 'submitted',
            'timemodified' => $clock->time(),
            'onlinetext' => 'Some text',
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // There should be 1 user with the teacher excluded.
        $users = $helper::get_users_within_assignment($assignment->id, $helper::TYPE_DUE_DIGEST);
        $this->assertCount(1, $users);
    }

    /**
     * Test sending the assignment due digest notification to a user.
     */
    public function test_send_due_digest_notification_to_user(): void {
        global $DB;
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $clock = $this->mock_clock_with_frozen();
        $sink = $this->redirectMessages();

        // Create a course and enrol a user.
        $course = $generator->create_course();
        $user1 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');

        // Suspended user, should not receive notification.
        $user2 = $generator->create_user(['suspended' => 1]);
        $generator->enrol_user($user2->id, $course->id, 'student');

        // Nologin user, should not receive notification.
        $user3 = $generator->create_user(['auth' => 'nologin']);
        $generator->enrol_user($user3->id, $course->id, 'student');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create a few assignments with different due dates.
        $duedate1 = $clock->time() + WEEKSECS;
        $assignment1 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate1,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $duedate2 = $clock->time() + WEEKSECS;
        $assignment2 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate2,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $duedate3 = $clock->time() + WEEKSECS + DAYSECS;
        $assignment3 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate3,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        // Create an assignment with a visibility restriction.
        $duedate4 = $clock->time() + WEEKSECS;
        $assignment4 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $duedate4,
            'visible' => 0,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $clock->bump(5);

        // Run the tasks.
        $this->run_due_digest_notification_helper_tasks();

        // Get the notifications that should have been created during the adhoc task.
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);

        // Check the message for the expected assignments.
        $message = reset($messages);
        $this->assertStringContainsString($assignment1->name, $message->fullmessagehtml);
        $this->assertStringContainsString($assignment2->name, $message->fullmessagehtml);
        $this->assertStringNotContainsString($assignment3->name, $message->fullmessagehtml);
        $this->assertStringNotContainsString($assignment4->name, $message->fullmessagehtml);

        // Check the message contains the formatted due date.
        $formatteddate = userdate($duedate1, get_string('strftimedaydate', 'langconfig'));
        $this->assertStringContainsString($formatteddate, $message->fullmessagehtml);

        // Check the subject matches.
        $expectedsubject = get_string('assignmentduedigestsubject', 'mod_assign', $message->subject);
        $this->assertEquals($expectedsubject, $message->subject);

        // Clear sink.
        $sink->clear();

        // Let's modify the due date for one of the assignment.
        $updatedata = new \stdClass();
        $updatedata->id = $assignment1->id;
        $updatedata->duedate = $duedate1 + DAYSECS;
        $DB->update_record('assign', $updatedata);

        // Run the tasks again.
        $this->run_due_digest_notification_helper_tasks();

        // Check the message for the expected assignments.
        $messages = $sink->get_messages_by_component('mod_assign');
        $message = reset($messages);
        $this->assertStringNotContainsString($assignment1->name, $message->fullmessagehtml);
        $this->assertStringContainsString($assignment2->name, $message->fullmessagehtml);
        $this->assertStringNotContainsString($assignment3->name, $message->fullmessagehtml);
        $this->assertStringNotContainsString($assignment4->name, $message->fullmessagehtml);

        // Clear sink.
        $sink->clear();

        // This time, the user will submit an assignment.
        $assignmentgenerator->create_submission([
            'userid' => $user1->id,
            'cmid' => $assignment2->cmid,
            'status' => 'submitted',
            'timemodified' => $clock->time(),
            'onlinetext' => 'Some text',
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $clock->bump(5);

        // Run the tasks again.
        $this->run_due_digest_notification_helper_tasks();

        // There are no assignments left to report, so no notification should have been sent.
        // There should only be one notifcation for submission.
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);
        $expectedsubject = get_string('submissionreceiptsmall', 'mod_assign', ['assignment' => $assignment2->name]);
        $this->assertEquals($expectedsubject, reset($messages)->subject);

        // Clear sink.
        $sink->clear();
    }

    /**
     * Test sending the assignment due digest notification to users in groups with restricted access.
     */
    public function test_send_due_digest_notification_to_users_in_groups(): void {
        global $DB;
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $clock = $this->mock_clock_with_incrementing();
        $sink = $this->redirectMessages();
        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create a course, users and enrol users.
        $course = $generator->create_course();
        $user1 = $generator->create_user();
        $user2 = $generator->create_user();
        $user3 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');
        $generator->enrol_user($user2->id, $course->id, 'student');
        $generator->enrol_user($user3->id, $course->id, 'student');

        // Create groups and add users to groups.
        $group1 = $generator->create_group(['courseid' => $course->id]);
        $group2 = $generator->create_group(['courseid' => $course->id]);
        $generator->create_group_member(['groupid' => $group1->id, 'userid' => $user1->id]);
        $generator->create_group_member(['groupid' => $group2->id, 'userid' => $user2->id]);

        // Create assignments.
        $assignment1 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $clock->time() + WEEKSECS,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $assignment2 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $clock->time() + WEEKSECS,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);
        $assignment3 = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'duedate' => $clock->time() + WEEKSECS,
            'submissiondrafts' => 0,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        // Set restricted access for assignment1 and assignment2 to groups.
        $availability = [
            'op' => '&',
            'showc' => [true],
            'c' => [
                [
                    'type' => 'group',
                    'id' => (int) $group1->id,
                ],
            ],
        ];
        $cm = get_coursemodule_from_instance('assign', $assignment1->id, $course->id);
        $DB->set_field('course_modules', 'availability', json_encode($availability), ['id' => $cm->id]);

        $availability = [
            'op' => '&',
            'showc' => [true],
            'c' => [
                [
                    'type' => 'group',
                    'id' => (int) $group2->id,
                ],
            ],
        ];
        $cm = get_coursemodule_from_instance('assign', $assignment2->id, $course->id);
        $DB->set_field('course_modules', 'availability', json_encode($availability), ['id' => $cm->id]);

        // Rebuild course cache to apply changes.
        rebuild_course_cache($course->id, true);

        // Run the tasks. We want to run all the adhoc tasks at the same time. So we will use the normal task runner.
        $this->execute_task('\mod_assign\task\queue_all_assignment_due_digest_notification_tasks');
        // Execute the remaining ad-hoc backup task.
        $this->start_output_buffering();
        $this->runAdhocTasks('\mod_assign\task\send_assignment_due_digest_notification_to_user');
        $this->stop_output_buffering();
        $messages = $sink->get_messages_by_component('mod_assign');

        // Process the messages.
        $processedmessages = [];
        foreach ($messages as $message) {
            $processedmessages[$message->useridto] = $message;
        }

        // Verify the messages.
        $this->assertCount(3, $processedmessages);
        // User1 should receive a message for assignment1 and assignment3.
        $this->assertArrayHasKey($user1->id, $processedmessages);
        $this->assertStringContainsString($assignment1->name, $processedmessages[$user1->id]->fullmessagehtml);
        $this->assertStringContainsString($assignment3->name, $processedmessages[$user1->id]->fullmessagehtml);
        $this->assertStringNotContainsString($assignment2->name, $processedmessages[$user1->id]->fullmessagehtml);
        // User2 should receive a message for assignment2 and assignment3.
        $this->assertArrayHasKey($user2->id, $processedmessages);
        $this->assertStringContainsString($assignment2->name, $processedmessages[$user2->id]->fullmessagehtml);
        $this->assertStringContainsString($assignment3->name, $processedmessages[$user2->id]->fullmessagehtml);
        $this->assertStringNotContainsString($assignment1->name, $processedmessages[$user2->id]->fullmessagehtml);
        // User3 should receive a message for assignment3 only.
        $this->assertArrayHasKey($user3->id, $processedmessages);
        $this->assertStringContainsString($assignment3->name, $processedmessages[$user3->id]->fullmessagehtml);
        $this->assertStringNotContainsString($assignment1->name, $processedmessages[$user3->id]->fullmessagehtml);
        $this->assertStringNotContainsString($assignment2->name, $processedmessages[$user3->id]->fullmessagehtml);
    }

    /**
     * Test sending the assignment notification to a user with a list of the submitted files.
     */
    public function test_send_notification_with_summary_to_user(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator();
        $sink = $this->redirectMessages();

        // Create a course and enrol a user.
        $course = $generator->create_course(['shortname' => 'A100']);
        $user1 = $generator->create_user();
        $generator->enrol_user($user1->id, $course->id, 'student');

        /** @var \mod_assign_generator $assignmentgenerator */
        $assignmentgenerator = $generator->get_plugin_generator('mod_assign');

        // Create activity.
        $assignment = $assignmentgenerator->create_instance([
            'course' => $course->id,
            'name' => 'Assignment 1',
            'submissiondrafts' => 0,
            'assignsubmission_file_enabled' => 1,
            'assignsubmission_file_maxfiles' => 12,
            'assignsubmission_file_maxsizebytes' => 1024 * 1024,
            'assignsubmission_onlinetext_enabled' => 1,
        ]);

        $filename1 = 'submissionsample01.txt';
        $filename2 = 'submissionsample02.txt';
        $files = [
            "mod/assign/tests/fixtures/" . $filename1,
            "mod/assign/tests/fixtures/" . $filename2,
        ];

        // Generate submissions.
        $assignmentgenerator->create_submission([
            'userid' => $user1->id,
            'cmid' => $assignment->cmid,
            'status' => 'submitted',
            'file' => implode(',', $files),
            'onlinetext' => 'Some text example',
        ]);

        // Get the notifications.
        $messages = $sink->get_messages_by_component('mod_assign');
        $this->assertCount(1, $messages);
        $message = reset($messages);

        // Check the subject line and short message.
        $this->assertEquals('Assignment submission confirmation - Assignment 1', $message->subject);
        $this->assertEquals('Assignment submission confirmation - Assignment 1', $message->smallmessage);

        // Check the plain text message.
        $this->assertEquals('A100 -> Assignment -> Assignment 1
---------------------------------------------------------------------
You have submitted an assignment submission for \'Assignment 1\'.

You can see the status of your assignment submission:

    https://www.example.com/moodle/mod/assign/view.php?id=' . $assignment->cmid . '

Your submission contains:

Online text
(3 words)

File submissions
* submissionsample01.txt (42 bytes)
* submissionsample02.txt (42 bytes)


---------------------------------------------------------------------
', $message->fullmessage);

        $expectedfragments = [
            '<p>Your assignment submission for \'Assignment 1\' has been submitted.</p>',
            '<p>You can view your submission and check its status on the <a href="' .
                'https://www.example.com/moodle/mod/assign/view.php?id=' .
                $assignment->cmid . '">assignment page</a>.</p>',
            '<h2>Your submission contains:</h2>',
            '<h3>Online text</h3>',
            '<p>(3 words)</p>',
            '<h3>File submissions</h3>',
            '<li>submissionsample01.txt (42 bytes)</li>',
            '<li>submissionsample02.txt (42 bytes)</li>',
        ];
        foreach ($expectedfragments as $html) {
            $this->assertStringContainsString($html, $message->fullmessagehtml);
        }

        // Clear sink.
        $sink->clear();
    }
}
