<?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 the core_notes implementation of the privacy API.
 *
 * @package    core_notes
 * @category   test
 * @copyright  2018 Zig Tan <zig@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
namespace core_notes\privacy;

defined('MOODLE_INTERNAL') || die();
global $CFG;

require_once($CFG->dirroot . "/notes/lib.php");

use core_notes\privacy\provider;
use core_privacy\local\request\writer;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;

/**
 * Unit tests for the core_notes implementation of the privacy API.
 *
 * @copyright  2018 Zig Tan <zig@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @covers \core_notes\privacy\provider
 */
final class provider_test extends \core_privacy\tests\provider_testcase {

    /**
     * Test for provider::get_contexts_for_userid().
     */
    public function test_get_contexts_for_userid(): void {
        global $DB;

        // Test setup.
        $this->resetAfterTest(true);
        $this->setAdminUser();
        set_config('enablenotes', true);

        $teacher1 = $this->getDataGenerator()->create_user();
        $this->setUser($teacher1);
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));

        $student = $this->getDataGenerator()->create_user();
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));

        // Create Courses, then enrol a teacher and a student.
        $nocourses = 5;
        $courses = [];
        $coursecontextids = [];
        for ($c = 1; $c <= $nocourses; $c++) {
            $course = $this->getDataGenerator()->create_course();
            $coursecontext = \context_course::instance($course->id);

            role_assign($teacherrole->id, $teacher1->id, $coursecontext->id);
            role_assign($studentrole->id, $student->id, $coursecontext->id);

            // Only create private user notes (i.e. NOTES_STATE_DRAFT) for student in Course 1, 2, 3 written by the teacher.
            if ($c <= 3) {
                $this->help_create_user_note(
                    $student->id,
                    NOTES_STATE_DRAFT,
                    $course->id,
                    "Test private user note about the student in Course $c by the teacher"
                );
            }

            $courses[$c] = $course;
            $coursecontextids[] = $coursecontext->id;
        }

        // Test Teacher 1's contexts equals 3 because only 3 user notes were added for Course 1, 2, and 3.
        // Course 4 and 5 does not have any notes associated with it, so the contexts should not be returned.
        $contexts = provider::get_contexts_for_userid($teacher1->id);
        $this->assertCount(3, $contexts->get_contextids());

        // Test the Student's contexts is 0 because the notes written by the teacher are private.
        $contexts = provider::get_contexts_for_userid($student->id);
        $this->assertCount(0, $contexts->get_contextids());

        // Add a public user note (i.e. NOTES_STATE_PUBLIC) written by the Teacher about the Student in Course 4.
        $course = $courses[4];
        $this->help_create_user_note(
            $student->id,
            NOTES_STATE_PUBLIC,
            $course->id,
            "Test public user note about the student in Course 4 by the teacher"
        );

        // Test Teacher 1's contexts equals 4 after adding a public note about a student in Course 4.
        $contexts = provider::get_contexts_for_userid($teacher1->id);
        $this->assertCount(4, $contexts->get_contextids());

        // Test the Student's contexts is 1 for Course 4 because there is a public note written by the teacher.
        $contexts = provider::get_contexts_for_userid($student->id);
        $this->assertCount(1, $contexts->get_contextids());

        // Add a site-wide user note (i.e. NOTES_STATE_SITE) written by the Teacher 1 about the Student in Course 3.
        $course = $courses[3];
        $this->help_create_user_note(
            $student->id,
            NOTES_STATE_SITE,
            $course->id,
            "Test site-wide user note about the student in Course 3 by the teacher"
        );

        // Test the Student's contexts is 2 for Courses 3, 4 because there is a public and site-wide note written by the Teacher.
        $contexts = provider::get_contexts_for_userid($student->id);
        $this->assertCount(2, $contexts->get_contextids());

        // Add a site-wide user note for the Teacher 1 by another Teacher 2 in Course 5.
        $teacher2 = $this->getDataGenerator()->create_user();
        $this->setUser($teacher2);

        $course = $courses[5];
        $this->help_create_user_note(
            $teacher1->id,
            NOTES_STATE_SITE,
            $course->id,
            "Test site-wide user note about the teacher in Course 5 by another teacher"
        );

        // Test Teacher 1's contexts equals 5 after adding the note from another teacher.
        $contextlist = provider::get_contexts_for_userid($teacher1->id);
        $this->assertCount(5, $contextlist->get_contextids());

        // Test Teacher 1's contexts match the contexts of the Courses associated with notes created.
        $this->assertEmpty(array_diff($coursecontextids, $contextlist->get_contextids()));
    }

    /**
     * Test for provider::export_user_data().
     */
    public function test_export_user_data(): void {
        global $DB;

        // Test setup.
        $this->resetAfterTest(true);
        $this->setAdminUser();
        set_config('enablenotes', true);

        $teacher1 = $this->getDataGenerator()->create_user();
        $this->setUser($teacher1);
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));

        $nocourses = 5;
        $nostudents = 2;
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));

        $courses = [];
        $coursecontextids = [];
        for ($c = 1; $c <= $nocourses; $c++) {
            // Create a Course, then enrol a teacher and enrol 2 students.
            $course = $this->getDataGenerator()->create_course();
            $coursecontext = \context_course::instance($course->id);

            role_assign($teacherrole->id, $teacher1->id, $coursecontext->id);

            // Only create public user notes (i.e. NOTES_STATE_PUBLIC) for students in Course 1, 2, 3 written by the teacher.
            if ($c <= 3) {
                for ($s = 0; $s < $nostudents; $s++) {
                    $student = $this->getDataGenerator()->create_user();
                    role_assign($studentrole->id, $student->id, $coursecontext->id);

                    // Create test public user note data written for students by the teacher.
                    $this->help_create_user_note(
                        $student->id,
                        NOTES_STATE_PUBLIC,
                        $course->id,
                        "Test public user note for student $s in Course $c by the teacher"
                    );
                }
                // Store the Course context for those which have test notes added for verification.
                $coursecontextids[] = $coursecontext->id;
            }

            $courses[$c] = $course;
        }

        // Add a site-wide user note for Teacher 1 by another Teacher 2 in Course 4.
        $teacher2 = $this->getDataGenerator()->create_user();
        $this->setUser($teacher2);

        $course = $courses[4];
        $this->help_create_user_note(
            $teacher1->id,
            NOTES_STATE_SITE,
            $course->id,
            "Test site-wide user note about the teacher in Course 4 by another teacher"
        );
        // Store the Course context for those which have test notes added for verification.
        $coursecontextids[] = \context_course::instance($course->id)->id;

        // Add a private user note for Teacher 1 by another Teacher 2 in Course 5.
        $course = $courses[5];
        $this->help_create_user_note(
            $teacher1->id,
            NOTES_STATE_DRAFT,
            $course->id,
            "Test private user note about the teacher in Course 5 by another teacher"
        );

        // Test the number of contexts returned matches the Course contexts created with notes.
        $contextlist = provider::get_contexts_for_userid($teacher1->id);
        $this->assertEmpty(array_diff($coursecontextids, $contextlist->get_contextids()));

        $approvedcontextlist = new approved_contextlist($teacher1, 'core_notes', $contextlist->get_contextids());

        // Retrieve User notes created by the teacher.
        provider::export_user_data($approvedcontextlist);

        // Test the core_notes data is exported at the Course context level and has content.
        foreach ($contextlist as $context) {
            $this->assertEquals(CONTEXT_COURSE, $context->contextlevel);

            /** @var \core_privacy\tests\request\content_writer $writer */
            $writer = writer::with_context($context);
            $this->assertTrue($writer->has_any_data());
        }
    }

    /**
     * Test for provider::delete_data_for_all_users_in_context().
     */
    public function test_delete_data_for_all_users_in_context(): void {
        global $DB;

        // Test setup.
        $this->resetAfterTest(true);
        $this->setAdminUser();
        set_config('enablenotes', true);

        $teacher = $this->getDataGenerator()->create_user();
        $this->setUser($teacher);
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));

        $nocourses = 2;
        $nostudents = 5;
        $nonotes = 7;
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));

        $n = 0;
        for ($c = 0; $c < $nocourses; $c++) {
            // Create a Course, then enrol a teacher and enrol 2 students.
            $course = $this->getDataGenerator()->create_course();
            $coursecontext = \context_course::instance($course->id);

            role_assign($teacherrole->id, $teacher->id, $coursecontext->id);

            for ($s = 0; $s < $nostudents; $s++) {
                if ($n < $nonotes) {
                    $student = $this->getDataGenerator()->create_user();
                    role_assign($studentrole->id, $student->id, $coursecontext->id);

                    // Create test note data.
                    $this->help_create_user_note(
                        $student->id,
                        NOTES_STATE_PUBLIC,
                        $course->id,
                        "Test user note for student $s in Course $c"
                    );
                }
                $n++;
            }
        }

        // Test the number of contexts returned equals the number of Courses created with user notes for its students.
        $contextlist = provider::get_contexts_for_userid($teacher->id);
        $this->assertCount($nocourses, $contextlist->get_contextids());

        // Test the created user note records in mdl_post table matches the test number of user notes specified.
        $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]);
        $this->assertCount($nonotes, $notes);

        // Delete all user note records in mdl_post table by the specified Course context.
        foreach ($contextlist->get_contexts() as $context) {
            provider::delete_data_for_all_users_in_context($context);
        }

        // Test the core_note records in mdl_post table is equals zero.
        $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]);
        $this->assertCount(0, $notes);
    }

    /**
     * Test for provider::delete_data_for_user().
     */
    public function test_delete_data_for_user(): void {
        global $DB;

        // Test setup.
        $this->resetAfterTest(true);
        $this->setAdminUser();
        set_config('enablenotes', true);

        $teacher = $this->getDataGenerator()->create_user();
        $this->setUser($teacher);
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));

        $nocourses = 2;
        $nostudents = 5;
        $nonotes = 7;
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));

        $n = 0;
        for ($c = 0; $c < $nocourses; $c++) {
            // Create a Course, then enrol a teacher and enrol 2 students.
            $course = $this->getDataGenerator()->create_course();
            $coursecontext = \context_course::instance($course->id);

            role_assign($teacherrole->id, $teacher->id, $coursecontext->id);

            for ($s = 0; $s < $nostudents; $s++) {
                if ($n < $nonotes) {
                    $student = $this->getDataGenerator()->create_user();
                    role_assign($studentrole->id, $student->id, $coursecontext->id);

                    // Create test note data.
                    $this->help_create_user_note(
                        $student->id,
                        NOTES_STATE_PUBLIC,
                        $course->id,
                        "Test user note for student $s in Course $c"
                    );
                }
                $n++;
            }
        }

        // Test the number of contexts returned equals the number of Courses created with user notes for its students.
        $contextlist = provider::get_contexts_for_userid($teacher->id);
        $this->assertCount($nocourses, $contextlist->get_contextids());

        // Test the created user note records in mdl_post table matches the test number of user notes specified.
        $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]);
        $this->assertCount($nonotes, $notes);

        // Delete all user note records in mdl_post table created by the specified teacher.
        $approvedcontextlist = new approved_contextlist($teacher, 'core_notes', $contextlist->get_contextids());
        provider::delete_data_for_user($approvedcontextlist);

        // Test the core_note records in mdl_post table is equals zero.
        $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]);
        $this->assertCount(0, $notes);
    }

    /**
     * Test that only users within a course context are fetched.
     */
    public function test_get_users_in_context(): void {
        global $DB;

        $this->resetAfterTest(true);

        $component = 'core_notes';
        // Test setup.
        $this->setAdminUser();
        set_config('enablenotes', true);
        // Create a teacher.
        $teacher1 = $this->getDataGenerator()->create_user();
        $this->setUser($teacher1);
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
        // Create a student.
        $student = $this->getDataGenerator()->create_user();
        // Create student2.
        $student2 = $this->getDataGenerator()->create_user();
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));

        // Create courses, then enrol a teacher and a student.
        $nocourses = 3;
        for ($c = 1; $c <= $nocourses; $c++) {
            ${'course' . $c} = $this->getDataGenerator()->create_course();
            ${'coursecontext' . $c} = \context_course::instance(${'course' . $c}->id);

            role_assign($teacherrole->id, $teacher1->id, ${'coursecontext' . $c}->id);
            role_assign($studentrole->id, $student->id, ${'coursecontext' . $c}->id);
            role_assign($studentrole->id, $student2->id, ${'coursecontext' . $c}->id);
        }
        // The list of users in coursecontext1 should be empty (related data still have not been created).
        $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
        provider::get_users_in_context($userlist1);
        $this->assertCount(0, $userlist1);
        // The list of users in coursecontext2 should be empty (related data still have not been created).
        $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
        provider::get_users_in_context($userlist2);
        $this->assertCount(0, $userlist2);
        // The list of users in coursecontext3 should be empty (related data still have not been created).
        $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component);
        provider::get_users_in_context($userlist3);
        $this->assertCount(0, $userlist3);

        // Create private user notes (i.e. NOTES_STATE_DRAFT) for student in course1 and course2 written by the teacher.
        $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course1->id,
            "Test private user note about the student in Course 1 by the teacher");
        $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course2->id,
            "Test private user note about the student in Course 2 by the teacher");

        // The list of users in coursecontext1 should return one user (teacher1).
        provider::get_users_in_context($userlist1);
        $this->assertCount(1, $userlist1);
        $this->assertTrue(in_array($teacher1->id, $userlist1->get_userids()));
        // The list of users in coursecontext2 should return one user (teacher1).
        provider::get_users_in_context($userlist2);
        $this->assertCount(1, $userlist2);
        $this->assertTrue(in_array($teacher1->id, $userlist2->get_userids()));
        // The list of users in coursecontext3 should not return any users.
        provider::get_users_in_context($userlist3);
        $this->assertCount(0, $userlist3);

        // Create public user note (i.e. NOTES_STATE_PUBLIC) for student in course3 written by the teacher.
        $this->help_create_user_note($student->id, NOTES_STATE_PUBLIC, $course3->id,
            "Test public user note about the student in Course 3 by the teacher");

        // The list of users in coursecontext3 should return 2 users (teacher and student).
        provider::get_users_in_context($userlist3);
        $this->assertCount(2, $userlist3);
        $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids()));
        $this->assertTrue(in_array($student->id, $userlist3->get_userids()));

        // Create site user note (i.e. NOTES_STATE_SITE) for student2 in course3 written by the teacher.
        $this->help_create_user_note($student2->id, NOTES_STATE_SITE, $course3->id,
            "Test site-wide user note about student2 in Course 3 by the teacher"
        );

        // The list of users in coursecontext3 should return 3 users (teacher, student and student2).
        provider::get_users_in_context($userlist3);
        $this->assertCount(3, $userlist3);
        $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids()));
        $this->assertTrue(in_array($student->id, $userlist3->get_userids()));
        $this->assertTrue(in_array($student2->id, $userlist3->get_userids()));

        // The list of users should not return any users in a different context than course context.
        $contextsystem = \context_system::instance();
        $userlist4 = new \core_privacy\local\request\userlist($contextsystem, $component);
        provider::get_users_in_context($userlist4);
        $this->assertCount(0, $userlist4);
    }

    /**
     * Test that data for users in approved userlist is deleted.
     */
    public function test_delete_data_for_users(): void {
        global $DB;

        $this->resetAfterTest(true);

        $component = 'core_notes';
        // Test setup.
        $this->setAdminUser();
        set_config('enablenotes', true);
        // Create a teacher.
        $teacher1 = $this->getDataGenerator()->create_user();
        $this->setUser($teacher1);
        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
        // Create a student.
        $student = $this->getDataGenerator()->create_user();
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));

        // Create Courses, then enrol a teacher and a student.
        $nocourses = 3;
        for ($c = 1; $c <= $nocourses; $c++) {
            ${'course' . $c} = $this->getDataGenerator()->create_course();
            ${'coursecontext' . $c} = \context_course::instance(${'course' . $c}->id);

            role_assign($teacherrole->id, $teacher1->id, ${'coursecontext' . $c}->id);
            role_assign($studentrole->id, $student->id, ${'coursecontext' . $c}->id);
        }

        // Create private notes for student in the course1 and course2 written by the teacher.
        $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course1->id,
            "Test private user note about the student in Course 1 by the teacher");
        $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course2->id,
            "Test private user note about the student in Course 2 by the teacher");
        // Create public notes for student in the course3 written by the teacher.
        $this->help_create_user_note($student->id, NOTES_STATE_PUBLIC, $course3->id,
            "Test public user note about the student in Course 3 by the teacher");

        // The list of users in coursecontext1 should return one user (teacher1).
        $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
        provider::get_users_in_context($userlist1);
        $this->assertCount(1, $userlist1);
        $this->assertTrue(in_array($teacher1->id, $userlist1->get_userids()));
        // The list of users in coursecontext2 should return one user (teacher1).
        $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
        provider::get_users_in_context($userlist2);
        $this->assertCount(1, $userlist2);
        $this->assertTrue(in_array($teacher1->id, $userlist2->get_userids()));
        // The list of users in coursecontext3 should return two users (teacher1 and student).
        $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component);
        provider::get_users_in_context($userlist3);
        $this->assertCount(2, $userlist3);
        $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids()));
        $this->assertTrue(in_array($student->id, $userlist3->get_userids()));

        $approvedlist = new approved_userlist($coursecontext3, $component, [$student->id]);
        // Delete using delete_data_for_user.
        provider::delete_data_for_users($approvedlist);
        // Re-fetch users in the coursecontext3.
        $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component);
        // The user data in coursecontext3 should not be removed.
        provider::get_users_in_context($userlist3);
        $this->assertCount(2, $userlist3);
        $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids()));
        $this->assertTrue(in_array($student->id, $userlist3->get_userids()));

        $approvedlist = new approved_userlist($coursecontext3, $component, [$teacher1->id]);
        // Delete using delete_data_for_user.
        provider::delete_data_for_users($approvedlist);
        // Re-fetch users in the coursecontext3.
        $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component);
        // The user data in coursecontext3 should be removed.
        provider::get_users_in_context($userlist3);
        $this->assertCount(0, $userlist3);

        // Re-fetch users in the coursecontext1.
        $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
        provider::get_users_in_context($userlist1);
        $this->assertCount(1, $userlist1);

        $approvedlist = new approved_userlist($coursecontext1, $component, [$student->id]);
        // Delete using delete_data_for_user.
        provider::delete_data_for_users($approvedlist);
        // Re-fetch users in the coursecontext1.
        $userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component);
        // The user data in coursecontext1 should not be removed.
        provider::get_users_in_context($userlist3);
        $this->assertCount(1, $userlist3);
        $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids()));

        $approvedlist = new approved_userlist($coursecontext1, $component, [$teacher1->id]);
        // Delete using delete_data_for_user.
        provider::delete_data_for_users($approvedlist);
        // Re-fetch users in the coursecontext1.
        $userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component);
        // The user data in coursecontext1 should be removed.
        provider::get_users_in_context($userlist3);
        $this->assertCount(0, $userlist3);

        // Re-fetch users in the coursecontext2.
        $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
        provider::get_users_in_context($userlist2);
        $this->assertCount(1, $userlist2);

        // The list of users should not return any users for contexts different than course context.
        $systemcontext = \context_system::instance();
        $userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component);
        provider::get_users_in_context($userlist4);
        $this->assertCount(0, $userlist4);
    }

    /**
     * Helper function to create user notes for testing.
     *
     * @param int       $userid   The ID of the User associated with the note.
     * @param string    $state    The publish status
     * @param int       $courseid The ID of the Course associated with the note.
     * @param string    $content  The note content.
     */
    protected function help_create_user_note($userid, $state, $courseid, $content) {
        $note = (object) [
            'userid' => $userid,
            'publishstate' => $state,
            'courseid' => $courseid,
            'content' => $content,
        ];
        note_save($note);
    }
}
