<?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 Moodle Content Writer.
 *
 * @package     core_privacy
 * @category    test
 * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

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

global $CFG;

use \core_privacy\local\request\writer;
use \core_privacy\local\request\moodle_content_writer;

/**
 * Tests for the \core_privacy API's moodle_content_writer functionality.
 *
 * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @coversDefaultClass \core_privacy\local\request\moodle_content_writer
 */
final class moodle_content_writer_test extends advanced_testcase {

    /**
     * Test that exported data is saved correctly within the system context.
     *
     * @dataProvider export_data_provider
     * @param   \stdClass  $data Data
     * @covers ::export_data
     */
    public function test_export_data($data): void {
        $context = \context_system::instance();
        $subcontext = [];

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_data($subcontext, $data);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertEquals($data, $expanded);
    }

    /**
     * Test that exported data is saved correctly for context/subcontext.
     *
     * @dataProvider export_data_provider
     * @param   \stdClass  $data Data
     * @covers ::export_data
     */
    public function test_export_data_different_context($data): void {
        $context = \context_user::instance(\core_user::get_user_by_username('admin')->id);
        $subcontext = ['sub', 'context'];

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_data($subcontext, $data);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertEquals($data, $expanded);
    }

    /**
     * Test that exported is saved within the correct directory locations.
     *
     * @covers ::export_data
     */
    public function test_export_data_writes_to_multiple_context(): void {
        $subcontext = ['sub', 'context'];

        $systemcontext = \context_system::instance();
        $systemdata = (object) [
            'belongsto' => 'system',
        ];
        $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id);
        $userdata = (object) [
            'belongsto' => 'user',
        ];

        $writer = $this->get_writer_instance();

        $writer
            ->set_context($systemcontext)
            ->export_data($subcontext, $systemdata);

        $writer
            ->set_context($usercontext)
            ->export_data($subcontext, $userdata);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertEquals($systemdata, $expanded);

        $contextpath = $this->get_context_path($usercontext, $subcontext, 'data.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertEquals($userdata, $expanded);
    }

    /**
     * Test that multiple writes to the same location cause the latest version to be written.
     *
     * @covers ::export_data
     */
    public function test_export_data_multiple_writes_same_context(): void {
        $subcontext = ['sub', 'context'];

        $systemcontext = \context_system::instance();
        $originaldata = (object) [
            'belongsto' => 'system',
        ];

        $newdata = (object) [
            'abc' => 'def',
        ];

        $writer = $this->get_writer_instance();

        $writer
            ->set_context($systemcontext)
            ->export_data($subcontext, $originaldata);

        $writer
            ->set_context($systemcontext)
            ->export_data($subcontext, $newdata);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertEquals($newdata, $expanded);
    }

    /**
     * Data provider for exporting user data.
     */
    public static function export_data_provider(): array {
        return [
            'basic' => [
                (object) [
                    'example' => (object) [
                        'key' => 'value',
                    ],
                ],
            ],
        ];
    }

    /**
     * Test that metadata can be set.
     *
     * @dataProvider export_metadata_provider
     * @param   string  $key Key
     * @param   string  $value Value
     * @param   string  $description Description
     * @covers ::export_metadata
     */
    public function test_export_metadata($key, $value, $description): void {
        $context = \context_system::instance();
        $subcontext = ['a', 'b', 'c'];

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_metadata($subcontext, $key, $value, $description);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $this->assertEquals($value, $expanded->$key->value);
        $this->assertEquals($description, $expanded->$key->description);
    }

    /**
     * Test that metadata can be set additively.
     *
     * @covers ::export_metadata
     */
    public function test_export_metadata_additive(): void {
        $context = \context_system::instance();
        $subcontext = [];

        $writer = $this->get_writer_instance();

        $writer
            ->set_context($context)
            ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription');

        $writer
            ->set_context($context)
            ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription');

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);

        $this->assertTrue(isset($expanded->firstkey));
        $this->assertEquals('firstvalue', $expanded->firstkey->value);
        $this->assertEquals('firstdescription', $expanded->firstkey->description);

        $this->assertTrue(isset($expanded->secondkey));
        $this->assertEquals('secondvalue', $expanded->secondkey->value);
        $this->assertEquals('seconddescription', $expanded->secondkey->description);
    }

    /**
     * Test that metadata can be set additively.
     *
     * @covers ::export_metadata
     */
    public function test_export_metadata_to_multiple_contexts(): void {
        $systemcontext = \context_system::instance();
        $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id);
        $subcontext = [];

        $writer = $this->get_writer_instance();

        $writer
            ->set_context($systemcontext)
            ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription')
            ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription');

        $writer
            ->set_context($usercontext)
            ->export_metadata($subcontext, 'firstkey', 'alternativevalue', 'alternativedescription')
            ->export_metadata($subcontext, 'thirdkey', 'thirdvalue', 'thirddescription');

        $fileroot = $this->fetch_exported_content($writer);

        $systemcontextpath = $this->get_context_path($systemcontext, $subcontext, 'metadata.json');
        $this->assertTrue($fileroot->hasChild($systemcontextpath));

        $json = $fileroot->getChild($systemcontextpath)->getContent();
        $expanded = json_decode($json);

        $this->assertTrue(isset($expanded->firstkey));
        $this->assertEquals('firstvalue', $expanded->firstkey->value);
        $this->assertEquals('firstdescription', $expanded->firstkey->description);
        $this->assertTrue(isset($expanded->secondkey));
        $this->assertEquals('secondvalue', $expanded->secondkey->value);
        $this->assertEquals('seconddescription', $expanded->secondkey->description);
        $this->assertFalse(isset($expanded->thirdkey));

        $usercontextpath = $this->get_context_path($usercontext, $subcontext, 'metadata.json');
        $this->assertTrue($fileroot->hasChild($usercontextpath));

        $json = $fileroot->getChild($usercontextpath)->getContent();
        $expanded = json_decode($json);

        $this->assertTrue(isset($expanded->firstkey));
        $this->assertEquals('alternativevalue', $expanded->firstkey->value);
        $this->assertEquals('alternativedescription', $expanded->firstkey->description);
        $this->assertFalse(isset($expanded->secondkey));
        $this->assertTrue(isset($expanded->thirdkey));
        $this->assertEquals('thirdvalue', $expanded->thirdkey->value);
        $this->assertEquals('thirddescription', $expanded->thirdkey->description);
    }

    /**
     * Data provider for exporting user metadata.
     *
     * return   array
     */
    public static function export_metadata_provider(): array {
        return [
            'basic' => [
                'key',
                'value',
                'This is a description',
            ],
            'valuewithspaces' => [
                'key',
                'value has mixed',
                'This is a description',
            ],
            'encodedvalue' => [
                'key',
                base64_encode('value has mixed'),
                'This is a description',
            ],
        ];
    }

    /**
     * Exporting a single stored_file should cause that file to be output in the files directory.
     *
     * @covers ::export_area_files
     */
    public function test_export_area_files(): void {
        $this->resetAfterTest();
        $context = \context_system::instance();
        $fs = get_file_storage();

        // Add two files to core_privacy::tests::0.
        $files = [];
        $file = (object) [
            'component' => 'core_privacy',
            'filearea' => 'tests',
            'itemid' => 0,
            'path' => '/',
            'name' => 'a.txt',
            'content' => 'Test file 0',
        ];
        $files[] = $file;

        $file = (object) [
            'component' => 'core_privacy',
            'filearea' => 'tests',
            'itemid' => 0,
            'path' => '/sub/',
            'name' => 'b.txt',
            'content' => 'Test file 1',
        ];
        $files[] = $file;

        // One with a different itemid.
        $file = (object) [
            'component' => 'core_privacy',
            'filearea' => 'tests',
            'itemid' => 1,
            'path' => '/',
            'name' => 'c.txt',
            'content' => 'Other',
        ];
        $files[] = $file;

        // One with a different filearea.
        $file = (object) [
            'component' => 'core_privacy',
            'filearea' => 'alternative',
            'itemid' => 0,
            'path' => '/',
            'name' => 'd.txt',
            'content' => 'Alternative',
        ];
        $files[] = $file;

        // One with a different component.
        $file = (object) [
            'component' => 'core',
            'filearea' => 'tests',
            'itemid' => 0,
            'path' => '/',
            'name' => 'e.txt',
            'content' => 'Other tests',
        ];
        $files[] = $file;

        foreach ($files as $file) {
            $record = [
                'contextid' => $context->id,
                'component' => $file->component,
                'filearea'  => $file->filearea,
                'itemid'    => $file->itemid,
                'filepath'  => $file->path,
                'filename'  => $file->name,
            ];

            $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
            $file->storedfile = $fs->create_file_from_string($record, $file->content);
        }

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_area_files([], 'core_privacy', 'tests', 0);

        $fileroot = $this->fetch_exported_content($writer);

        $firstfiles = array_slice($files, 0, 2);
        foreach ($firstfiles as $file) {
            $contextpath = $this->get_context_path($context, ['_files'], $file->namepath);
            $this->assertTrue($fileroot->hasChild($contextpath));
            $this->assertEquals($file->content, $fileroot->getChild($contextpath)->getContent());
        }

        $otherfiles = array_slice($files, 2);
        foreach ($otherfiles as $file) {
            $contextpath = $this->get_context_path($context, ['_files'], $file->namepath);
            $this->assertFalse($fileroot->hasChild($contextpath));
        }
    }

    /**
     * Exporting a single stored_file should cause that file to be output in the files directory.
     *
     * @dataProvider    export_file_provider
     * @param   string  $filearea File area
     * @param   int     $itemid Item ID
     * @param   string  $filepath File path
     * @param   string  $filename File name
     * @param   string  $content Content
     *
     * @covers ::export_file
     */
    public function test_export_file($filearea, $itemid, $filepath, $filename, $content): void {
        $this->resetAfterTest();
        $context = \context_system::instance();
        $filenamepath = '/' . $filearea . '/' . ($itemid ? '_' . $itemid : '') . $filepath . $filename;

        $filerecord = array(
            'contextid' => $context->id,
            'component' => 'core_privacy',
            'filearea'  => $filearea,
            'itemid'    => $itemid,
            'filepath'  => $filepath,
            'filename'  => $filename,
        );

        $fs = get_file_storage();
        $file = $fs->create_file_from_string($filerecord, $content);

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_file([], $file);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, ['_files'], $filenamepath);
        $this->assertTrue($fileroot->hasChild($contextpath));
        $this->assertEquals($content, $fileroot->getChild($contextpath)->getContent());
    }

    /**
     * Data provider for the test_export_file function.
     *
     * @return  array
     */
    public static function export_file_provider(): array {
        return [
            'basic' => [
                'intro',
                0,
                '/',
                'testfile.txt',
                'An example file content',
            ],
            'longpath' => [
                'attachments',
                '12',
                '/path/within/a/path/within/a/path/',
                'testfile.txt',
                'An example file content',
            ],
            'pathwithspaces' => [
                'intro',
                0,
                '/path with/some spaces/',
                'testfile.txt',
                'An example file content',
            ],
            'filewithspaces' => [
                'submission_attachments',
                1,
                '/path with/some spaces/',
                'test file.txt',
                'An example file content',
            ],
            'image' => [
                'intro',
                0,
                '/',
                'logo.png',
                file_get_contents(self::get_fixture_path('core_privacy', 'logo.png')),
            ],
            'UTF8' => [
                'submission_content',
                2,
                '/Žluťoučký/',
                'koníček.txt',
                'koníček',
            ],
            'EUC-JP' => [
                'intro',
                0,
                '/言語設定/',
                '言語設定.txt',
                '言語設定',
            ],
        ];
    }

    /**
     * User preferences can be exported against a user.
     *
     * @dataProvider    export_user_preference_provider
     * @param   string      $component  Component
     * @param   string      $key Key
     * @param   string      $value Value
     * @param   string      $desc Description
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_user($component, $key, $value, $desc): void {
        $admin = \core_user::get_user_by_username('admin');

        $writer = $this->get_writer_instance();

        $context = \context_user::instance($admin->id);
        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_user_preference($component, $key, $value, $desc);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals($value, $data->value);
        $this->assertEquals($desc, $data->description);
    }

    /**
     * User preferences can be exported against a course category.
     *
     * @dataProvider    export_user_preference_provider
     * @param   string      $component  Component
     * @param   string      $key Key
     * @param   string      $value Value
     * @param   string      $desc Description
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_coursecat($component, $key, $value, $desc): void {
        global $DB;

        $categories = $DB->get_records('course_categories');
        $firstcategory = reset($categories);

        $context = \context_coursecat::instance($firstcategory->id);
        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_user_preference($component, $key, $value, $desc);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals($value, $data->value);
        $this->assertEquals($desc, $data->description);
    }

    /**
     * User preferences can be exported against a course.
     *
     * @dataProvider    export_user_preference_provider
     * @param   string      $component  Component
     * @param   string      $key Key
     * @param   string      $value Value
     * @param   string      $desc Description
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_course($component, $key, $value, $desc): void {
        global $DB;

        $this->resetAfterTest();

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

        $context = \context_course::instance($course->id);
        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_user_preference($component, $key, $value, $desc);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals($value, $data->value);
        $this->assertEquals($desc, $data->description);
    }

    /**
     * User preferences can be exported against a module context.
     *
     * @dataProvider    export_user_preference_provider
     * @param   string      $component  Component
     * @param   string      $key Key
     * @param   string      $value Value
     * @param   string      $desc Description
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_module($component, $key, $value, $desc): void {
        global $DB;

        $this->resetAfterTest();

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

        $context = \context_module::instance($forum->cmid);
        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_user_preference($component, $key, $value, $desc);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals($value, $data->value);
        $this->assertEquals($desc, $data->description);
    }

    /**
     * User preferences can not be exported against a block context.
     *
     * @dataProvider    export_user_preference_provider
     * @param   string      $component  Component
     * @param   string      $key Key
     * @param   string      $value Value
     * @param   string      $desc Description
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_block($component, $key, $value, $desc): void {
        global $DB;

        $blocks = $DB->get_records('block_instances');
        $block = reset($blocks);

        $context = \context_block::instance($block->id);
        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_user_preference($component, $key, $value, $desc);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals($value, $data->value);
        $this->assertEquals($desc, $data->description);
    }

    /**
     * Writing user preferences for two different blocks with the same name and
     * same parent context should generate two different context paths and export
     * files.
     *
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_block_multiple_instances(): void {
        $this->resetAfterTest();

        $generator = $this->getDataGenerator();
        $course = $generator->create_course();
        $coursecontext = context_course::instance($course->id);
        $block1 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
        $block2 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
        $block1context = context_block::instance($block1->id);
        $block2context = context_block::instance($block2->id);
        $component = 'block';
        $desc = 'test preference';
        $block1key = 'block1key';
        $block1value = 'block1value';
        $block2key = 'block2key';
        $block2value = 'block2value';
        $writer = $this->get_writer_instance();

        // Confirm that we have two different block contexts with the same name
        // and the same parent context id.
        $this->assertNotEquals($block1context->id, $block2context->id);
        $this->assertEquals($block1context->get_context_name(), $block2context->get_context_name());
        $this->assertEquals($block1context->get_parent_context()->id, $block2context->get_parent_context()->id);

        $retrieveexport = function($context) use ($writer, $component) {
            $fileroot = $this->fetch_exported_content($writer);

            $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
            $this->assertTrue($fileroot->hasChild($contextpath));

            $json = $fileroot->getChild($contextpath)->getContent();
            return json_decode($json);
        };

        $writer->set_context($block1context)
            ->export_user_preference($component, $block1key, $block1value, $desc);
        $writer->set_context($block2context)
            ->export_user_preference($component, $block2key, $block2value, $desc);

        $block1export = $retrieveexport($block1context);
        $block2export = $retrieveexport($block2context);

        // Confirm that the exports didn't write to the same file.
        $this->assertTrue(isset($block1export->$block1key));
        $this->assertTrue(isset($block2export->$block2key));
        $this->assertFalse(isset($block1export->$block2key));
        $this->assertFalse(isset($block2export->$block1key));
        $this->assertEquals($block1value, $block1export->$block1key->value);
        $this->assertEquals($block2value, $block2export->$block2key->value);
    }

    /**
     * User preferences can be exported against the system.
     *
     * @dataProvider    export_user_preference_provider
     * @param   string      $component  Component
     * @param   string      $key Key
     * @param   string      $value Value
     * @param   string      $desc Description
     *
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_context_system($component, $key, $value, $desc): void {
        $context = \context_system::instance();
        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_user_preference($component, $key, $value, $desc);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals($value, $data->value);
        $this->assertEquals($desc, $data->description);
    }

    /**
     * User preferences can be exported against the system.
     *
     * @covers ::export_user_preference
     */
    public function test_export_multiple_user_preference_context_system(): void {
        $context = \context_system::instance();
        $writer = $this->get_writer_instance();
        $component = 'core_privacy';

        $writer
            ->set_context($context)
            ->export_user_preference($component, 'key1', 'val1', 'desc1')
            ->export_user_preference($component, 'key2', 'val2', 'desc2');

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);

        $this->assertTrue(isset($expanded->key1));
        $data = $expanded->key1;
        $this->assertEquals('val1', $data->value);
        $this->assertEquals('desc1', $data->description);

        $this->assertTrue(isset($expanded->key2));
        $data = $expanded->key2;
        $this->assertEquals('val2', $data->value);
        $this->assertEquals('desc2', $data->description);
    }

    /**
     * User preferences can be exported against the system.
     *
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_replace(): void {
        $context = \context_system::instance();
        $writer = $this->get_writer_instance();
        $component = 'core_privacy';
        $key = 'key';

        $writer
            ->set_context($context)
            ->export_user_preference($component, $key, 'val1', 'desc1');

        $writer
            ->set_context($context)
            ->export_user_preference($component, $key, 'val2', 'desc2');

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertTrue($fileroot->hasChild($contextpath));

        $json = $fileroot->getChild($contextpath)->getContent();
        $expanded = json_decode($json);

        $this->assertTrue(isset($expanded->$key));
        $data = $expanded->$key;
        $this->assertEquals('val2', $data->value);
        $this->assertEquals('desc2', $data->description);
    }

    /**
     * Provider for various user preferences.
     *
     * @return  array
     */
    public static function export_user_preference_provider(): array {
        return [
            'basic' => [
                'core_privacy',
                'onekey',
                'value',
                'description',
            ],
            'encodedvalue' => [
                'core_privacy',
                'donkey',
                base64_encode('value'),
                'description',
            ],
            'long description' => [
                'core_privacy',
                'twokey',
                'value',
                'This is a much longer description which actually states what this is used for. Blah blah blah.',
            ],
        ];
    }

    /**
     * Test that exported data is human readable.
     *
     * @dataProvider unescaped_unicode_export_provider
     * @param string $text
     * @covers ::export_data
     */
    public function test_export_data_unescaped_unicode($text): void {
        $context = \context_system::instance();
        $subcontext = [];
        $data = (object) ['key' => $text];

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_data($subcontext, $data);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'data.json');

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text/", $json);

        $expanded = json_decode($json);
        $this->assertEquals($data, $expanded);
    }

    /**
     * Test that exported metadata is human readable.
     *
     * @dataProvider unescaped_unicode_export_provider
     * @param string $text
     * @covers ::export_metadata
     */
    public function test_export_metadata_unescaped_unicode($text): void {
        $context = \context_system::instance();
        $subcontext = ['a', 'b', 'c'];

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_metadata($subcontext, $text, $text, $text);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);

        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$text));
        $this->assertEquals($text, $expanded->$text->value);
        $this->assertEquals($text, $expanded->$text->description);
    }

    /**
     * Test that exported related data is human readable.
     *
     * @dataProvider unescaped_unicode_export_provider
     * @param string $text
     * @covers ::export_related_data
     */
    public function test_export_related_data_unescaped_unicode($text): void {
        $context = \context_system::instance();
        $subcontext = [];
        $data = (object) ['key' => $text];

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_related_data($subcontext, 'name', $data);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'name.json');

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text/", $json);

        $expanded = json_decode($json);
        $this->assertEquals($data, $expanded);
    }

    /**
     * Test that exported related data name is properly cleaned
     *
     * @covers ::export_related_data
     */
    public function test_export_related_data_clean_name(): void {
        $context = \context_system::instance();
        $subcontext = [];
        $data = (object) ['foo' => 'bar'];

        $name = 'Bad/chars:>';

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_related_data($subcontext, $name, $data);

        $nameclean = clean_param($name, PARAM_FILE);

        $contextpath = $this->get_context_path($context, $subcontext, "{$nameclean}.json");
        $expectedpath = "System _.{$context->id}/Badchars.json";
        $this->assertEquals($expectedpath, $contextpath);

        $fileroot = $this->fetch_exported_content($writer);
        $json = $fileroot->getChild($contextpath)->getContent();

        $this->assertEquals($data, json_decode($json));
    }

    /**
     * Test that exported user preference is human readable.
     *
     * @dataProvider unescaped_unicode_export_provider
     * @param string $text
     * @covers ::export_user_preference
     */
    public function test_export_user_preference_unescaped_unicode($text): void {
        $context = \context_system::instance();
        $component = 'core_privacy';

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_user_preference($component, $text, $text, $text);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);

        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$text));
        $this->assertEquals($text, $expanded->$text->value);
        $this->assertEquals($text, $expanded->$text->description);
    }

    /**
     * Provider for various user preferences.
     *
     * @return array
     */
    public static function unescaped_unicode_export_provider(): array {
        return [
            'Unicode' => ['ةكءيٓ‌پچژکگیٹڈڑہھےâîûğŞAaÇÖáǽ你好!'],
        ];
    }

    /**
     * Test that exported data subcontext is properly cleaned
     *
     * @covers ::export_data
     */
    public function test_export_data_clean_subcontext(): void {
        $context = \context_system::instance();
        $subcontext = ['Something/weird', 'More/bad:>', 'Bad&chars:>'];
        $data = (object) ['foo' => 'bar'];

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_data($subcontext, $data);

        $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
        $expectedpath = "System _.{$context->id}/Something/weird/More/bad/Badchars/data.json";
        $this->assertEquals($expectedpath, $contextpath);

        $fileroot = $this->fetch_exported_content($writer);
        $json = $fileroot->getChild($contextpath)->getContent();

        $this->assertEquals($data, json_decode($json));
    }

    /**
     * Test that exported data is shortened when exceeds the limit.
     *
     * @dataProvider long_filename_provider
     * @param string $longtext
     * @param string $expected
     * @param string $text
     *
     * @covers ::export_data
     */
    public function test_export_data_long_filename($longtext, $expected, $text): void {
        $context = \context_system::instance();
        $subcontext = [$longtext];
        $data = (object) ['key' => $text];

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_data($subcontext, $data);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
        $expectedpath = "System _.{$context->id}/{$expected}/data.json";
        $this->assertEquals($expectedpath, $contextpath);

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text/", $json);

        $expanded = json_decode($json);
        $this->assertEquals($data, $expanded);
    }

    /**
     * Test that exported related data is shortened when exceeds the limit.
     *
     * @dataProvider long_filename_provider
     * @param string $longtext
     * @param string $expected
     * @param string $text
     *
     * @covers ::export_related_data
     */
    public function test_export_related_data_long_filename($longtext, $expected, $text): void {
        $context = \context_system::instance();
        $subcontext = [$longtext];
        $data = (object) ['key' => $text];

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_related_data($subcontext, 'name', $data);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'name.json');
        $expectedpath = "System _.{$context->id}/{$expected}/name.json";
        $this->assertEquals($expectedpath, $contextpath);

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text/", $json);

        $expanded = json_decode($json);
        $this->assertEquals($data, $expanded);
    }

    /**
     * Test that exported metadata is shortened when exceeds the limit.
     *
     * @dataProvider long_filename_provider
     * @param string $longtext
     * @param string $expected
     * @param string $text
     */
    public function test_export_metadata_long_filename($longtext, $expected, $text): void {
        $context = \context_system::instance();
        $subcontext = [$longtext];
        $data = (object) ['key' => $text];

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_metadata($subcontext, $text, $text, $text);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
        $expectedpath = "System _.{$context->id}/{$expected}/metadata.json";
        $this->assertEquals($expectedpath, $contextpath);

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);

        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$text));
        $this->assertEquals($text, $expanded->$text->value);
        $this->assertEquals($text, $expanded->$text->description);
    }

    /**
     * Test that exported user preference is shortened when exceeds the limit.
     *
     * @dataProvider long_filename_provider
     * @param string $longtext
     * @param string $expected
     * @param string $text
     */
    public function test_export_user_preference_long_filename($longtext, $expected, $text): void {
        $this->resetAfterTest();

        if (!array_key_exists('json', core_filetypes::get_types())) {
            // Add json as mime type to avoid lose the extension when shortening filenames.
            core_filetypes::add_type('json', 'application/json', 'archive', [], '', 'JSON file archive');
        }
        $context = \context_system::instance();
        $expectedpath = "System _.{$context->id}/User preferences/{$expected}.json";

        $component = $longtext;

        $writer = $this->get_writer_instance()
                ->set_context($context)
                ->export_user_preference($component, $text, $text, $text);

        $fileroot = $this->fetch_exported_content($writer);

        $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
        $this->assertEquals($expectedpath, $contextpath);

        $json = $fileroot->getChild($contextpath)->getContent();
        $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);

        $expanded = json_decode($json);
        $this->assertTrue(isset($expanded->$text));
        $this->assertEquals($text, $expanded->$text->value);
        $this->assertEquals($text, $expanded->$text->description);
    }

    /**
     * Provider for long filenames.
     *
     * @return array
     */
    public static function long_filename_provider(): array {
        return [
            'More than 100 characters' => [
                'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean efficitur suscipit nibh nec suscipit',
                'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean effici - 22f7a5030d',
                'value',
            ],
        ];
    }

    /**
     * Get a fresh content writer.
     *
     * @return  moodle_content_writer
     */
    public function get_writer_instance() {
        $factory = $this->createMock(writer::class);
        return new moodle_content_writer($factory);
    }

    /**
     * Fetch the exported content for inspection.
     *
     * @param   moodle_content_writer   $writer
     * @return  \org\bovigo\vfs\vfsStreamDirectory
     */
    protected function fetch_exported_content(moodle_content_writer $writer) {
        $export = $writer
            ->set_context(\context_system::instance())
            ->finalise_content();

        $fileroot = \org\bovigo\vfs\vfsStream::setup('root');

        $target = \org\bovigo\vfs\vfsStream::url('root');
        $fp = get_file_packer();
        $fp->extract_to_pathname($export, $target);

        return $fileroot;
    }

    /**
     * Determine the path for the current context.
     *
     * Note: This is a wrapper around the real function.
     *
     * @param   \context        $context    The context being written
     * @param   array           $subcontext The subcontext path
     * @param   string          $name       THe name of the file target
     * @return  array                       The context path.
     */
    protected function get_context_path($context, $subcontext = null, $name = '') {
        $rc = new ReflectionClass(moodle_content_writer::class);
        $writer = $this->get_writer_instance();
        $writer->set_context($context);

        if (null === $subcontext) {
            $rcm = $rc->getMethod('get_context_path');
            $path = $rcm->invoke($writer);
        } else {
            $rcm = $rc->getMethod('get_path');
            $path = $rcm->invoke($writer, $subcontext, $name);
        }

        // PHPUnit uses mikey179/vfsStream which is a stream wrapper for a virtual file system that uses '/'
        // as the directory separator.
        $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);

        return $path;
    }

    /**
     * Test correct rewriting of @@PLUGINFILE@@ in the exported contents.
     *
     * @dataProvider rewrite_pluginfile_urls_provider
     * @param string $filearea The filearea within that component.
     * @param int $itemid Which item those files belong to.
     * @param string $input Raw text as stored in the database.
     * @param string $expectedoutput Expected output of URL rewriting.
     * @covers ::rewrite_pluginfile_urls
     */
    public function test_rewrite_pluginfile_urls($filearea, $itemid, $input, $expectedoutput): void {

        $writer = $this->get_writer_instance();
        $writer->set_context(\context_system::instance());

        $realoutput = $writer->rewrite_pluginfile_urls([], 'core_test', $filearea, $itemid, $input);

        $this->assertEquals($expectedoutput, $realoutput);
    }

    /**
     * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}.
     *
     * @return array
     */
    public static function rewrite_pluginfile_urls_provider(): array {
        return [
            'nullcontent' => [
                'intro',
                0,
                null,
                '',
            ],
            'emptycontent' => [
                'intro',
                0,
                '',
                '',
            ],
            'zeroitemid' => [
                'intro',
                0,
                '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
                '<p><img src="System _.1/_files/intro/hello.gif" /></p>',
            ],
            'nonzeroitemid' => [
                'submission_content',
                34,
                '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
                '<p><img src="System _.1/_files/submission_content/_34/first.png" alt="First" /></p>',
            ],
            'withfilepath' => [
                'post_content',
                9889,
                '<a href="@@PLUGINFILE@@/embedded/docs/muhehe.exe">Click here!</a>',
                '<a href="System _.1/_files/post_content/_9889/embedded/docs/muhehe.exe">Click here!</a>',
            ],
        ];
    }

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

        $data = (object) ['key' => 'value'];

        $context = \context_system::instance();
        $subcontext = [];

        $writer = $this->get_writer_instance()
            ->set_context($context)
            ->export_data($subcontext, (object) $data);

        $writer->set_context($context)->export_data(['paper'], $data);

        $coursecategory = $this->getDataGenerator()->create_category();
        $categorycontext = \context_coursecat::instance($coursecategory->id);
        $course = $this->getDataGenerator()->create_course();
        $misccoursecxt = \context_coursecat::instance($course->category);
        $coursecontext = \context_course::instance($course->id);
        $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]);
        $modulecontext = \context_module::instance($cm->cmid);

        $writer->set_context($modulecontext)->export_data([], $data);
        $writer->set_context($coursecontext)->export_data(['grades'], $data);
        $writer->set_context($categorycontext)->export_data([], $data);
        $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data);

        // Add a file.
        $fs = get_file_storage();
        $file = (object) [
            'component' => 'core_privacy',
            'filearea' => 'tests',
            'itemid' => 0,
            'path' => '/',
            'name' => 'a.txt',
            'content' => 'Test file 0',
        ];
        $record = [
            'contextid' => $context->id,
            'component' => $file->component,
            'filearea'  => $file->filearea,
            'itemid'    => $file->itemid,
            'filepath'  => $file->path,
            'filename'  => $file->name,
        ];

        $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
        $file->storedfile = $fs->create_file_from_string($record, $file->content);
        $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0);

        list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [],
                '\core_privacy\local\request\moodle_content_writer');

        $expectedtreeoutput = [
            'System _.1' => [
                'data.json',
                'paper' => 'data.json',
                'Category Category 1 _.' . $misccoursecxt->id => [
                    'Course Test course 1 _.' . $coursecontext->id => [
                        'Chat Chat 1 _.' . $modulecontext->id => 'data.json',
                        'grades' => 'data.json'
                    ]
                ],
                'Category Course category 1 _.' . $categorycontext->id => 'data.json',
                '_files' => [
                    'tests' => 'a.txt'
                ],
                'Logs' => [
                    'Standard log' => 'data.json'
                ]
            ]
        ];
        $this->assertEquals($expectedtreeoutput, $tree);

        $expectedlistoutput = [
            'System _.1/data.json' => 'data_file_1',
            'System _.1/paper/data.json' => 'data_file_2',
            'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
                    $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.json'   => 'data_file_3',
            'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
                    $coursecontext->id . '/grades/data.json'   => 'data_file_4',
            'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5',
            'System _.1/_files/tests/a.txt' => 'No var',
            'System _.1/Logs/Standard log/data.json' => 'data_file_6'
        ];
        $this->assertEquals($expectedlistoutput, $treelist);

        $expectedindex = [
            'data_file_1' => 'System _.1/data.js',
            'data_file_2' => 'System _.1/paper/data.js',
            'data_file_3' => 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
                    $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.js',
            'data_file_4' => 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
                    $coursecontext->id . '/grades/data.js',
            'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js',
            'data_file_6' => 'System _.1/Logs/Standard log/data.js'
        ];
        $this->assertEquals($expectedindex, $indexdata);

        $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist],
                '\core_privacy\local\request\moodle_content_writer');

        // This is a big one.
        $expectedrichtree = [
            'System _.1' => (object) [
                'itemtype' => 'treeitem',
                'name' => 'System ',
                'context' => \context_system::instance(),
                'children' => [
                    (object) [
                        'name' => 'data.json',
                        'itemtype' => 'item',
                        'datavar' => 'data_file_1'
                    ],
                    'paper' => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'paper',
                        'children' => [
                            'data.json' => (object) [
                                'name' => 'data.json',
                                'itemtype' => 'item',
                                'datavar' => 'data_file_2'
                            ]
                        ]
                    ],
                    'Category Category 1 _.' . $misccoursecxt->id => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'Category Category 1 ',
                        'context' => $misccoursecxt,
                        'children' => [
                            'Course Test course 1 _.' . $coursecontext->id => (object) [
                                'itemtype' => 'treeitem',
                                'name' => 'Course Test course 1 ',
                                'context' => $coursecontext,
                                'children' => [
                                    'Chat Chat 1 _.' . $modulecontext->id => (object) [
                                        'itemtype' => 'treeitem',
                                        'name' => 'Chat Chat 1 ',
                                        'context' => $modulecontext,
                                        'children' => [
                                            'data.json' => (object) [
                                                'name' => 'data.json',
                                                'itemtype' => 'item',
                                                'datavar' => 'data_file_3'
                                            ]
                                        ]
                                    ],
                                    'grades' => (object) [
                                        'itemtype' => 'treeitem',
                                        'name' => 'grades',
                                        'children' => [
                                            'data.json' => (object) [
                                                'name' => 'data.json',
                                                'itemtype' => 'item',
                                                'datavar' => 'data_file_4'
                                            ]
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ],
                    'Category Course category 1 _.' . $categorycontext->id => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'Category Course category 1 ',
                        'context' => $categorycontext,
                        'children' => [
                            'data.json' => (object) [
                                'name' => 'data.json',
                                'itemtype' => 'item',
                                'datavar' => 'data_file_5'
                            ]
                        ]
                    ],
                    '_files' => (object) [
                        'itemtype' => 'treeitem',
                        'name' => '_files',
                        'children' => [
                            'tests' => (object) [
                                'itemtype' => 'treeitem',
                                'name' => 'tests',
                                'children' => [
                                    'a.txt' => (object) [
                                        'name' => 'a.txt',
                                        'itemtype' => 'item',
                                        'url' => new \moodle_url('System _.1/_files/tests/a.txt')
                                    ]
                                ]
                            ]
                        ]
                    ],
                    'Logs' => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'Logs',
                        'children' => [
                            'Standard log' => (object) [
                                'itemtype' => 'treeitem',
                                'name' => 'Standard log',
                                'children' => [
                                    'data.json' => (object) [
                                        'name' => 'data.json',
                                        'itemtype' => 'item',
                                        'datavar' => 'data_file_6'
                                    ]
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ];
        $this->assertEquals($expectedrichtree, $richtree);

        // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code
        // instead to do the same thing, but with references working obviously.
        $funfunction = function($object, $data) {
            return $object->sort_my_list($data);
        };

        $funfunction = Closure::bind($funfunction, null, $writer);
        $funfunction($writer, $richtree);

        // This is a big one.
        $expectedsortedtree = [
            'System _.1' => (object) [
                'itemtype' => 'treeitem',
                'name' => 'System ',
                'context' => \context_system::instance(),
                'children' => [
                    'Category Category 1 _.' . $misccoursecxt->id => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'Category Category 1 ',
                        'context' => $misccoursecxt,
                        'children' => [
                            'Course Test course 1 _.' . $coursecontext->id => (object) [
                                'itemtype' => 'treeitem',
                                'name' => 'Course Test course 1 ',
                                'context' => $coursecontext,
                                'children' => [
                                    'Chat Chat 1 _.' . $modulecontext->id => (object) [
                                        'itemtype' => 'treeitem',
                                        'name' => 'Chat Chat 1 ',
                                        'context' => $modulecontext,
                                        'children' => [
                                            'data.json' => (object) [
                                                'name' => 'data.json',
                                                'itemtype' => 'item',
                                                'datavar' => 'data_file_3'
                                            ]
                                        ]
                                    ],
                                    'grades' => (object) [
                                        'itemtype' => 'treeitem',
                                        'name' => 'grades',
                                        'children' => [
                                            'data.json' => (object) [
                                                'name' => 'data.json',
                                                'itemtype' => 'item',
                                                'datavar' => 'data_file_4'
                                            ]
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ],
                    'Category Course category 1 _.' . $categorycontext->id => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'Category Course category 1 ',
                        'context' => $categorycontext,
                        'children' => [
                            'data.json' => (object) [
                                'name' => 'data.json',
                                'itemtype' => 'item',
                                'datavar' => 'data_file_5'
                            ]
                        ]
                    ],
                    '_files' => (object) [
                        'itemtype' => 'treeitem',
                        'name' => '_files',
                        'children' => [
                            'tests' => (object) [
                                'itemtype' => 'treeitem',
                                'name' => 'tests',
                                'children' => [
                                    'a.txt' => (object) [
                                        'name' => 'a.txt',
                                        'itemtype' => 'item',
                                        'url' => new \moodle_url('System _.1/_files/tests/a.txt')
                                    ]
                                ]
                            ]
                        ]
                    ],
                    'Logs' => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'Logs',
                        'children' => [
                            'Standard log' => (object) [
                                'itemtype' => 'treeitem',
                                'name' => 'Standard log',
                                'children' => [
                                    'data.json' => (object) [
                                        'name' => 'data.json',
                                        'itemtype' => 'item',
                                        'datavar' => 'data_file_6'
                                    ]
                                ]
                            ]
                        ]
                    ],
                    'paper' => (object) [
                        'itemtype' => 'treeitem',
                        'name' => 'paper',
                        'children' => [
                            'data.json' => (object) [
                                'name' => 'data.json',
                                'itemtype' => 'item',
                                'datavar' => 'data_file_2'
                            ]
                        ]
                    ],
                    (object) [
                        'name' => 'data.json',
                        'itemtype' => 'item',
                        'datavar' => 'data_file_1'
                    ]
                ]
            ]
        ];
        $this->assertEquals($expectedsortedtree, $richtree);
    }
}
