<?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 /lib/filestorage/file_storage.php
 *
 * @package   core
 * @category  test
 * @copyright 2012 David Mudrak <david@moodle.com>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace core;

use file_exception;
use file_reference_exception;
use file_storage;
use file_system;
use repository;
use stored_file;
use stored_file_creation_exception;

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

global $CFG;
require_once($CFG->libdir . '/filelib.php');
require_once($CFG->dirroot . '/repository/lib.php');
require_once($CFG->libdir . '/filestorage/stored_file.php');

/**
 * Unit tests for /lib/filestorage/file_storage.php
 *
 * @package   core
 * @category  test
 * @copyright 2012 David Mudrak <david@moodle.com>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @coversDefaultClass \file_storage
 */
final class file_storage_test extends \advanced_testcase {

    /**
     * Files can be created from strings.
     *
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string(): void {
        global $DB;

        $this->resetAfterTest(true);

        // Number of files installed in the database on a fresh Moodle site.
        $installedfiles = $DB->count_records('files', array());

        $content = 'abcd';
        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/images/',
            'filename'  => 'testfile.txt',
        );
        $pathhash = sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].$filerecord['filepath'].$filerecord['filename']);

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

        $this->assertInstanceOf('stored_file', $file);
        $this->assertTrue($file->compare_to_string($content));
        $this->assertSame($pathhash, $file->get_pathnamehash());

        $this->assertTrue($DB->record_exists('files', array('pathnamehash'=>$pathhash)));

        $filesystem = $fs->get_file_system();
        $location = $filesystem->get_local_path_from_storedfile($file, true);

        $this->assertFileExists($location);

        // Verify the dir placeholder files are created.
        $this->assertEquals($installedfiles + 3, $DB->count_records('files', array()));
        $this->assertTrue($DB->record_exists('files', array('pathnamehash'=>sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].'/.'))));
        $this->assertTrue($DB->record_exists('files', array('pathnamehash'=>sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].$filerecord['filepath'].'.'))));

        // Tests that missing content file is recreated.

        unlink($location);
        $this->assertFileDoesNotExist($location);

        $filerecord['filename'] = 'testfile2.txt';
        $file2 = $fs->create_file_from_string($filerecord, $content);
        $this->assertInstanceOf('stored_file', $file2);
        $this->assertSame($file->get_contenthash(), $file2->get_contenthash());
        $this->assertFileExists($location);

        $this->assertEquals($installedfiles + 4, $DB->count_records('files', array()));

        // Test that borked content file is recreated.

        $this->assertSame(2, file_put_contents($location, 'xx'));

        $filerecord['filename'] = 'testfile3.txt';
        $file3 = $fs->create_file_from_string($filerecord, $content);
        $this->assertInstanceOf('stored_file', $file3);
        $this->assertSame($file->get_contenthash(), $file3->get_contenthash());
        $this->assertFileExists($location);

        $this->assertSame($content, file_get_contents($location));
        $this->assertDebuggingCalled();

        $this->assertEquals($installedfiles + 5, $DB->count_records('files', array()));
    }

    /**
     * Local files can be added to the filepool
     *
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname(): void {
        global $CFG, $DB;

        $this->resetAfterTest(true);

        // Number of files installed in the database on a fresh Moodle site.
        $installedfiles = $DB->count_records('files', array());

        $filepath = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/images/',
            'filename'  => 'testimage.jpg',
        );
        $pathhash = sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].$filerecord['filepath'].$filerecord['filename']);

        $fs = get_file_storage();
        $file = $fs->create_file_from_pathname($filerecord, $filepath);

        $this->assertInstanceOf('stored_file', $file);
        $this->assertTrue($file->compare_to_path($filepath));

        $this->assertTrue($DB->record_exists('files', array('pathnamehash'=>$pathhash)));

        $filesystem = $fs->get_file_system();
        $location = $filesystem->get_local_path_from_storedfile($file, true);

        $this->assertFileExists($location);

        // Verify the dir placeholder files are created.
        $this->assertEquals($installedfiles + 3, $DB->count_records('files', array()));
        $this->assertTrue($DB->record_exists('files', array('pathnamehash'=>sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].'/.'))));
        $this->assertTrue($DB->record_exists('files', array('pathnamehash'=>sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].$filerecord['filepath'].'.'))));

        // Tests that missing content file is recreated.

        unlink($location);
        $this->assertFileDoesNotExist($location);

        $filerecord['filename'] = 'testfile2.jpg';
        $file2 = $fs->create_file_from_pathname($filerecord, $filepath);
        $this->assertInstanceOf('stored_file', $file2);
        $this->assertSame($file->get_contenthash(), $file2->get_contenthash());
        $this->assertFileExists($location);

        $this->assertEquals($installedfiles + 4, $DB->count_records('files', array()));

        // Test that borked content file is recreated.

        $this->assertSame(2, file_put_contents($location, 'xx'));

        $filerecord['filename'] = 'testfile3.jpg';
        $file3 = $fs->create_file_from_pathname($filerecord, $filepath);
        $this->assertInstanceOf('stored_file', $file3);
        $this->assertSame($file->get_contenthash(), $file3->get_contenthash());
        $this->assertFileExists($location);

        $this->assertSame(file_get_contents($filepath), file_get_contents($location));
        $this->assertDebuggingCalled();

        $this->assertEquals($installedfiles + 5, $DB->count_records('files', array()));

        // Test invalid file creation.

        $filerecord['filename'] = 'testfile4.jpg';
        try {
            $fs->create_file_from_pathname($filerecord, $filepath.'nonexistent');
            $this->fail('Exception expected when trying to add non-existent stored file.');
        } catch (\Exception $e) {
            $this->assertInstanceOf('file_exception', $e);
        }
    }

    /**
     * Tests get get file.
     *
     * @covers ::get_file
     */
    public function test_get_file(): stored_file {
        global $CFG;

        $this->resetAfterTest(false);

        $filepath = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/images/',
            'filename'  => 'testimage.jpg',
        );
        $pathhash = sha1('/'.$filerecord['contextid'].'/'.$filerecord['component'].'/'.$filerecord['filearea'].'/'.$filerecord['itemid'].$filerecord['filepath'].$filerecord['filename']);

        $fs = get_file_storage();
        $file = $fs->create_file_from_pathname($filerecord, $filepath);

        $this->assertInstanceOf('stored_file', $file);
        $this->assertEquals($syscontext->id, $file->get_contextid());
        $this->assertEquals('core', $file->get_component());
        $this->assertEquals('unittest', $file->get_filearea());
        $this->assertEquals(0, $file->get_itemid());
        $this->assertEquals('/images/', $file->get_filepath());
        $this->assertEquals('testimage.jpg', $file->get_filename());
        $this->assertEquals(filesize($filepath), $file->get_filesize());
        $this->assertEquals($pathhash, $file->get_pathnamehash());

        return $file;
    }

    /**
     * Local images can be added to the filepool and their preview can be obtained
     *
     * @param stored_file $file
     * @depends test_get_file
     * @covers ::get_file_preview
     */
    public function test_get_file_preview(stored_file $file): void {
        global $CFG;

        $this->resetAfterTest();
        $fs = get_file_storage();

        $previewtinyicon = $fs->get_file_preview($file, 'tinyicon');
        $this->assertInstanceOf('stored_file', $previewtinyicon);
        $this->assertEquals('6b9864ae1536a8eeef54e097319175a8be12f07c', $previewtinyicon->get_filename());

        $previewtinyicon = $fs->get_file_preview($file, 'thumb');
        $this->assertInstanceOf('stored_file', $previewtinyicon);
        $this->assertEquals('6b9864ae1536a8eeef54e097319175a8be12f07c', $previewtinyicon->get_filename());

        $this->expectException('file_exception');
        $fs->get_file_preview($file, 'amodewhichdoesntexist');
    }

    /**
     * Tests for get_file_preview without an image.
     *
     * @covers ::get_file_preview
     */
    public function test_get_file_preview_nonimage(): void {
        $this->resetAfterTest(true);
        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/textfiles/',
            'filename'  => 'testtext.txt',
        );

        $fs = get_file_storage();
        $fs->create_file_from_string($filerecord, 'text contents');
        $textfile = $fs->get_file($syscontext->id, $filerecord['component'], $filerecord['filearea'],
            $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);

        $preview = $fs->get_file_preview($textfile, 'thumb');
        $this->assertFalse($preview);
    }

    /**
     * Make sure renaming is working
     *
     * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
     * @covers \stored_file::rename
     */
    public function test_file_renaming(): void {
        global $CFG;

        $this->resetAfterTest();
        $fs = get_file_storage();
        $syscontext = \context_system::instance();
        $component = 'core';
        $filearea  = 'unittest';
        $itemid    = 0;
        $filepath  = '/';
        $filename  = 'test.txt';

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

        $originalfile = $fs->create_file_from_string($filerecord, 'Test content');
        $this->assertInstanceOf('stored_file', $originalfile);
        $contenthash = $originalfile->get_contenthash();
        $newpath = '/test/';
        $newname = 'newtest.txt';

        // This should work.
        $originalfile->rename($newpath, $newname);
        $file = $fs->get_file($syscontext->id, $component, $filearea, $itemid, $newpath, $newname);
        $this->assertInstanceOf('stored_file', $file);
        $this->assertEquals($contenthash, $file->get_contenthash());

        // Try break it.
        $this->expectException('file_exception');
        $this->expectExceptionMessage('Cannot create file 1/core/unittest/0/test/newtest.txt (file exists, cannot rename)');
        // This shall throw exception.
        $originalfile->rename($newpath, $newname);
    }

    /**
     * Create file from reference tests
     *
     * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
     * @covers ::create_file_from_reference
     */
    public function test_create_file_from_reference(): void {
        global $CFG, $DB;

        $this->resetAfterTest();
        // Create user.
        $generator = $this->getDataGenerator();
        $user = $generator->create_user();
        $this->setUser($user);
        $usercontext = \context_user::instance($user->id);
        $syscontext = \context_system::instance();

        $fs = get_file_storage();

        $repositorypluginname = 'user';
        // Override repository permission.
        $capability = 'repository/' . $repositorypluginname . ':view';
        $guestroleid = $DB->get_field('role', 'id', array('shortname' => 'guest'));
        assign_capability($capability, CAP_ALLOW, $guestroleid, $syscontext->id, true);

        $args = array();
        $args['type'] = $repositorypluginname;
        $repos = repository::get_instances($args);
        $userrepository = reset($repos);
        $this->assertInstanceOf('repository', $userrepository);

        $component = 'user';
        $filearea  = 'private';
        $itemid    = 0;
        $filepath  = '/';
        $filename  = 'userfile.txt';

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

        $content = 'Test content';
        $originalfile = $fs->create_file_from_string($filerecord, $content);
        $this->assertInstanceOf('stored_file', $originalfile);

        $newfilerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'phpunit',
            'itemid'    => 0,
            'filepath'  => $filepath,
            'filename'  => $filename,
        );
        $ref = $fs->pack_reference($filerecord);
        $newstoredfile = $fs->create_file_from_reference($newfilerecord, $userrepository->id, $ref);
        $this->assertInstanceOf('stored_file', $newstoredfile);
        $this->assertEquals($userrepository->id, $newstoredfile->get_repository_id());
        $this->assertEquals($originalfile->get_contenthash(), $newstoredfile->get_contenthash());
        $this->assertEquals($originalfile->get_filesize(), $newstoredfile->get_filesize());
        $this->assertMatchesRegularExpression('#' . $filename. '$#', $newstoredfile->get_reference_details());

        // Test looking for references.
        $count = $fs->get_references_count_by_storedfile($originalfile);
        $this->assertEquals(1, $count);
        $files = $fs->get_references_by_storedfile($originalfile);
        $file = reset($files);
        $this->assertEquals($file, $newstoredfile);

        // Look for references by repository ID.
        $files = $fs->get_external_files($userrepository->id);
        $file = reset($files);
        $this->assertEquals($file, $newstoredfile);

        // Try convert reference to local file.
        $importedfile = $fs->import_external_file($newstoredfile);
        $this->assertFalse($importedfile->is_external_file());
        $this->assertInstanceOf('stored_file', $importedfile);
        // Still readable?
        $this->assertEquals($content, $importedfile->get_content());
    }

    /**
     * Create file from reference tests
     *
     * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
     * @covers ::create_file_from_reference
     */
    public function test_create_file_from_reference_with_content_hash(): void {
        global $CFG, $DB;

        $this->resetAfterTest();
        // Create user.
        $generator = $this->getDataGenerator();
        $user = $generator->create_user();
        $this->setUser($user);
        $usercontext = \context_user::instance($user->id);
        $syscontext = \context_system::instance();

        $fs = get_file_storage();

        $repositorypluginname = 'user';
        // Override repository permission.
        $capability = 'repository/' . $repositorypluginname . ':view';
        $guestroleid = $DB->get_field('role', 'id', array('shortname' => 'guest'));
        assign_capability($capability, CAP_ALLOW, $guestroleid, $syscontext->id, true);

        $args = array();
        $args['type'] = $repositorypluginname;
        $repos = repository::get_instances($args);
        $userrepository = reset($repos);
        $this->assertInstanceOf('repository', $userrepository);

        $component = 'user';
        $filearea = 'private';
        $itemid = 0;
        $filepath = '/';
        $filename = 'userfile.txt';

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

        $content = 'Test content';
        $originalfile = $fs->create_file_from_string($filerecord, $content);
        $this->assertInstanceOf('stored_file', $originalfile);

        $otherfilerecord = $filerecord;
        $otherfilerecord['filename'] = 'other-filename.txt';
        $otherfilewithsamecontents = $fs->create_file_from_string($otherfilerecord, $content);
        $this->assertInstanceOf('stored_file', $otherfilewithsamecontents);

        $newfilerecord = array(
                'contextid' => $syscontext->id,
                'component' => 'core',
                'filearea' => 'phpunit',
                'itemid' => 0,
                'filepath' => $filepath,
                'filename' => $filename,
                'contenthash' => $originalfile->get_contenthash(),
        );
        $ref = $fs->pack_reference($filerecord);
        $newstoredfile = $fs->create_file_from_reference($newfilerecord, $userrepository->id, $ref);
        $this->assertInstanceOf('stored_file', $newstoredfile);
        $this->assertEquals($userrepository->id, $newstoredfile->get_repository_id());
        $this->assertEquals($originalfile->get_contenthash(), $newstoredfile->get_contenthash());
        $this->assertEquals($originalfile->get_filesize(), $newstoredfile->get_filesize());
        $this->assertMatchesRegularExpression('#' . $filename . '$#', $newstoredfile->get_reference_details());
    }

    private function setup_three_private_files() {

        $this->resetAfterTest();

        $generator = $this->getDataGenerator();
        $user = $generator->create_user();
        $this->setUser($user->id);
        $usercontext = \context_user::instance($user->id);
        // Create a user private file.
        $file1 = new \stdClass;
        $file1->contextid = $usercontext->id;
        $file1->component = 'user';
        $file1->filearea  = 'private';
        $file1->itemid    = 0;
        $file1->filepath  = '/';
        $file1->filename  = '1.txt';
        $file1->source    = 'test';

        $fs = get_file_storage();
        $userfile1 = $fs->create_file_from_string($file1, 'file1 content');
        $this->assertInstanceOf('stored_file', $userfile1);

        $file2 = clone($file1);
        $file2->filename = '2.txt';
        $userfile2 = $fs->create_file_from_string($file2, 'file2 content longer');
        $this->assertInstanceOf('stored_file', $userfile2);

        $file3 = clone($file1);
        $file3->filename = '3.txt';
        $userfile3 = $fs->create_file_from_storedfile($file3, $userfile2);
        $this->assertInstanceOf('stored_file', $userfile3);

        $user->ctxid = $usercontext->id;

        return $user;
    }

    /**
     * Tests for get_area_files
     *
     * @covers ::get_area_files
     */
    public function test_get_area_files(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        // Get area files with default options.
        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');

        // Should be the two files we added plus the folder.
        $this->assertEquals(4, count($areafiles));

        // Verify structure.
        foreach ($areafiles as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }

        // Get area files without a folder.
        $folderlessfiles = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'sortorder', false);
        // Should be the two files without folder.
        $this->assertEquals(3, count($folderlessfiles));

        // Verify structure.
        foreach ($folderlessfiles as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }

        // Get area files ordered by id.
        $filesbyid  = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'id', false);
        // Should be the two files without folder.
        $this->assertEquals(3, count($filesbyid));

        // Verify structure.
        foreach ($filesbyid as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }

        // Test the limit feature to retrieve each individual file.
        $limited = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'filename', false,
                0, 0, 1);
        $mapfunc = function($f) {
            return $f->get_filename();
        };
        $this->assertEquals(array('1.txt'), array_values(array_map($mapfunc, $limited)));
        $limited = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'filename', false,
                0, 1, 50);
        $this->assertEquals(array('2.txt', '3.txt'), array_values(array_map($mapfunc, $limited)));

        // Test with an itemid with no files.
        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private', 666, 'sortorder', false);
        // Should be none.
        $this->assertEmpty($areafiles);
    }

    /**
     * Tests for get_area_tree
     *
     * @covers ::get_area_tree
     */
    public function test_get_area_tree(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        // Get area files with default options.
        $areatree = $fs->get_area_tree($user->ctxid, 'user', 'private', 0);
        $this->assertEmpty($areatree['subdirs']);
        $this->assertNotEmpty($areatree['files']);
        $this->assertCount(3, $areatree['files']);

        // Ensure an empty try with a fake itemid.
        $emptytree = $fs->get_area_tree($user->ctxid, 'user', 'private', 666);
        $this->assertEmpty($emptytree['subdirs']);
        $this->assertEmpty($emptytree['files']);

        // Create a subdir.
        $dir = $fs->create_directory($user->ctxid, 'user', 'private', 0, '/testsubdir/');
        $this->assertInstanceOf('stored_file', $dir);

        // Add a file to the subdir.
        $filerecord = array(
            'contextid' => $user->ctxid,
            'component' => 'user',
            'filearea'  => 'private',
            'itemid'    => 0,
            'filepath'  => '/testsubdir/',
            'filename'  => 'test-get-area-tree.txt',
        );

        $directoryfile = $fs->create_file_from_string($filerecord, 'Test content');
        $this->assertInstanceOf('stored_file', $directoryfile);

        $areatree = $fs->get_area_tree($user->ctxid, 'user', 'private', 0);

        // At the top level there should still be 3 files.
        $this->assertCount(3, $areatree['files']);

        // There should now be a subdirectory.
        $this->assertCount(1, $areatree['subdirs']);

        // The test subdir is named testsubdir.
        $subdir = $areatree['subdirs']['testsubdir'];
        $this->assertNotEmpty($subdir);
        // It should have one file we added.
        $this->assertCount(1, $subdir['files']);
        // And no subdirs itself.
        $this->assertCount(0, $subdir['subdirs']);

        // Verify the file is the one we added.
        $subdirfile = reset($subdir['files']);
        $this->assertInstanceOf('stored_file', $subdirfile);
        $this->assertEquals($filerecord['filename'], $subdirfile->get_filename());
    }

    /**
     * Tests for get_file_by_id
     *
     * @covers ::get_file_by_id
     */
    public function test_get_file_by_id(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');

        // Test get_file_by_id.
        $filebyid = reset($areafiles);
        $shouldbesame = $fs->get_file_by_id($filebyid->get_id());
        $this->assertEquals($filebyid->get_contenthash(), $shouldbesame->get_contenthash());

        // Test an id which doens't exist.
        $doesntexist = $fs->get_file_by_id(99999);
        $this->assertFalse($doesntexist);
    }

    /**
     * Tests for get_file_by_hash
     *
     * @covers ::get_file_by_hash
     */
    public function test_get_file_by_hash(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        // Test get_file_by_hash.
        $filebyhash = reset($areafiles);
        $shouldbesame = $fs->get_file_by_hash($filebyhash->get_pathnamehash());
        $this->assertEquals($filebyhash->get_id(), $shouldbesame->get_id());

        // Test an hash which doens't exist.
        $doesntexist = $fs->get_file_by_hash('DOESNTEXIST');
        $this->assertFalse($doesntexist);
    }

    /**
     * Tests for get_external_files
     *
     * @covers ::get_external_files
     */
    public function test_get_external_files(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        $repos = repository::get_instances(array('type'=>'user'));
        $userrepository = reset($repos);
        $this->assertInstanceOf('repository', $userrepository);

        // No aliases yet.
        $exfiles = $fs->get_external_files($userrepository->id, 'id');
        $this->assertEquals(array(), $exfiles);

        // Create three aliases linking the same original: $aliasfile1 and $aliasfile2 are
        // created via create_file_from_reference(), $aliasfile3 created from $aliasfile2.
        /** @var \stored_file $originalfile */
        $originalfile = null;
        foreach ($fs->get_area_files($user->ctxid, 'user', 'private') as $areafile) {
            if (!$areafile->is_directory()) {
                $originalfile = $areafile;
                break;
            }
        }
        $this->assertInstanceOf('stored_file', $originalfile);
        $originalrecord = array(
            'contextid' => $originalfile->get_contextid(),
            'component' => $originalfile->get_component(),
            'filearea'  => $originalfile->get_filearea(),
            'itemid'    => $originalfile->get_itemid(),
            'filepath'  => $originalfile->get_filepath(),
            'filename'  => $originalfile->get_filename(),
        );

        $aliasrecord = $this->generate_file_record();
        $aliasrecord->filepath = '/foo/';
        $aliasrecord->filename = 'one.txt';

        $ref = $fs->pack_reference($originalrecord);
        $aliasfile1 = $fs->create_file_from_reference($aliasrecord, $userrepository->id, $ref);

        $aliasrecord->filepath = '/bar/';
        $aliasrecord->filename = 'uno.txt';
        // Change the order of the items in the array to make sure that it does not matter.
        ksort($originalrecord);
        $ref = $fs->pack_reference($originalrecord);
        $aliasfile2 = $fs->create_file_from_reference($aliasrecord, $userrepository->id, $ref);

        $aliasrecord->filepath = '/bar/';
        $aliasrecord->filename = 'jedna.txt';
        $aliasfile3 = $fs->create_file_from_storedfile($aliasrecord, $aliasfile2);

        // Make sure we get three aliases now.
        $exfiles = $fs->get_external_files($userrepository->id, 'id');
        $this->assertEquals(3, count($exfiles));
        foreach ($exfiles as $exfile) {
            $this->assertTrue($exfile->is_external_file());
        }
        // Make sure they all link the same original (thence that all are linked with the same
        // record in {files_reference}).
        $this->assertEquals($aliasfile1->get_referencefileid(), $aliasfile2->get_referencefileid());
        $this->assertEquals($aliasfile3->get_referencefileid(), $aliasfile2->get_referencefileid());
    }

    /**
     * Tests for create_directory with a negative contextid.
     *
     * @covers ::create_directory
     */
    public function test_create_directory_contextid_negative(): void {
        $fs = get_file_storage();

        $this->expectException('file_exception');
        $fs->create_directory(-1, 'core', 'unittest', 0, '/');
    }

    /**
     * Tests for create_directory with an invalid contextid.
     *
     * @covers ::create_directory
     */
    public function test_create_directory_contextid_invalid(): void {
        $fs = get_file_storage();

        $this->expectException('file_exception');
        $fs->create_directory('not an int', 'core', 'unittest', 0, '/');
    }

    /**
     * Tests for create_directory with an invalid component.
     *
     * @covers ::create_directory
     */
    public function test_create_directory_component_invalid(): void {
        $fs = get_file_storage();
        $syscontext = \context_system::instance();

        $this->expectException('file_exception');
        $fs->create_directory($syscontext->id, 'bad/component', 'unittest', 0, '/');
    }

    /**
     * Tests for create_directory with an invalid filearea.
     *
     * @covers ::create_directory
     */
    public function test_create_directory_filearea_invalid(): void {
        $fs = get_file_storage();
        $syscontext = \context_system::instance();

        $this->expectException('file_exception');
        $fs->create_directory($syscontext->id, 'core', 'bad-filearea', 0, '/');
    }

    /**
     * Tests for create_directory with a negative itemid
     *
     * @covers ::create_directory
     */
    public function test_create_directory_itemid_negative(): void {
        $fs = get_file_storage();
        $syscontext = \context_system::instance();

        $this->expectException('file_exception');
        $fs->create_directory($syscontext->id, 'core', 'unittest', -1, '/');
    }

    /**
     * Tests for create_directory with an invalid itemid
     *
     * @covers ::create_directory
     */
    public function test_create_directory_itemid_invalid(): void {
        $fs = get_file_storage();
        $syscontext = \context_system::instance();

        $this->expectException('file_exception');
        $fs->create_directory($syscontext->id, 'core', 'unittest', 'notanint', '/');
    }

    /**
     * Tests for create_directory with an invalid filepath
     *
     * @covers ::create_directory
     */
    public function test_create_directory_filepath_invalid(): void {
        $fs = get_file_storage();
        $syscontext = \context_system::instance();

        $this->expectException('file_exception');
        $fs->create_directory($syscontext->id, 'core', 'unittest', 0, '/not-with-trailing/or-leading-slash');
    }

    /**
     * Tests for get_directory_files.
     *
     * @covers ::get_directory_files
     */
    public function test_get_directory_files(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        $dir = $fs->create_directory($user->ctxid, 'user', 'private', 0, '/testsubdir/');
        $this->assertInstanceOf('stored_file', $dir);

        // Add a file to the subdir.
        $filerecord = array(
            'contextid' => $user->ctxid,
            'component' => 'user',
            'filearea'  => 'private',
            'itemid'    => 0,
            'filepath'  => '/testsubdir/',
            'filename'  => 'test-get-area-tree.txt',
        );

        $directoryfile = $fs->create_file_from_string($filerecord, 'Test content');
        $this->assertInstanceOf('stored_file', $directoryfile);

        // Don't recurse without dirs.
        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', false, false, 'id');
        // 3 files only.
        $this->assertCount(3, $files);
        foreach ($files as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }

        // Don't recurse with dirs.
        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', false, true, 'id');
        // 3 files + 1 directory.
        $this->assertCount(4, $files);
        foreach ($files as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }

        // Recurse with dirs.
        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', true, true, 'id');
        // 3 files + 1 directory +  1 subdir file.
        $this->assertCount(5, $files);
        foreach ($files as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }

        // Recurse without dirs.
        $files = $fs->get_directory_files($user->ctxid, 'user', 'private', 0, '/', true, false, 'id');
        // 3 files +  1 subdir file.
        $this->assertCount(4, $files);
        foreach ($files as $key => $file) {
            $this->assertInstanceOf('stored_file', $file);
            $this->assertEquals($key, $file->get_pathnamehash());
        }
    }

    /**
     * Tests for search_references.
     *
     * @covers ::search_references
     */
    public function test_search_references(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();
        $repos = repository::get_instances(array('type'=>'user'));
        $repo = reset($repos);

        $alias1 = array(
            'contextid' => $user->ctxid,
            'component' => 'user',
            'filearea'  => 'private',
            'itemid'    => 0,
            'filepath'  => '/aliases/',
            'filename'  => 'alias-to-1.txt'
        );

        $alias2 = array(
            'contextid' => $user->ctxid,
            'component' => 'user',
            'filearea'  => 'private',
            'itemid'    => 0,
            'filepath'  => '/aliases/',
            'filename'  => 'another-alias-to-1.txt'
        );

        $reference = \file_storage::pack_reference(array(
            'contextid' => $user->ctxid,
            'component' => 'user',
            'filearea'  => 'private',
            'itemid'    => 0,
            'filepath'  => '/',
            'filename'  => '1.txt'
        ));

        // There are no aliases now.
        $result = $fs->search_references($reference);
        $this->assertEquals(array(), $result);

        $result = $fs->search_references_count($reference);
        $this->assertSame($result, 0);

        // Create two aliases and make sure they are returned.
        $fs->create_file_from_reference($alias1, $repo->id, $reference);
        $fs->create_file_from_reference($alias2, $repo->id, $reference);

        $result = $fs->search_references($reference);
        $this->assertTrue(is_array($result));
        $this->assertEquals(count($result), 2);
        foreach ($result as $alias) {
            $this->assertTrue($alias instanceof stored_file);
        }

        $result = $fs->search_references_count($reference);
        $this->assertSame($result, 2);

        // The method can't be used for references to files outside the filepool.
        $exceptionthrown = false;
        try {
            $fs->search_references('http://dl.dropbox.com/download/1234567/naked-dougiamas.jpg');
        } catch (file_reference_exception $e) {
            $exceptionthrown = true;
        }
        $this->assertTrue($exceptionthrown);

        $exceptionthrown = false;
        try {
            $fs->search_references_count('http://dl.dropbox.com/download/1234567/naked-dougiamas.jpg');
        } catch (file_reference_exception $e) {
            $exceptionthrown = true;
        }
        $this->assertTrue($exceptionthrown);
    }

    /**
     * Tests for delete_area_files.
     *
     * @covers ::delete_area_files
     */
    public function test_delete_area_files(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        // Get area files with default options.
        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        // Should be the two files we added plus the folder.
        $this->assertEquals(4, count($areafiles));
        $fs->delete_area_files($user->ctxid, 'user', 'private');

        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        // Should be the two files we added plus the folder.
        $this->assertEquals(0, count($areafiles));
    }

    /**
     * Tests for delete_area_files using an itemid.
     *
     * @covers ::delete_area_files
     */
    public function test_delete_area_files_itemid(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        // Get area files with default options.
        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        // Should be the two files we added plus the folder.
        $this->assertEquals(4, count($areafiles));
        $fs->delete_area_files($user->ctxid, 'user', 'private', 9999);

        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        $this->assertEquals(4, count($areafiles));
    }

    /**
     * Tests for delete_area_files_select.
     *
     * @covers ::delete_area_files_select
     */
    public function test_delete_area_files_select(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        // Get area files with default options.
        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        // Should be the two files we added plus the folder.
        $this->assertEquals(4, count($areafiles));
        $fs->delete_area_files_select($user->ctxid, 'user', 'private', '!= :notitemid', array('notitemid'=>9999));

        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        // Should be the two files we added plus the folder.
        $this->assertEquals(0, count($areafiles));
    }

    /**
     * Tests for delete_component_files.
     *
     * @covers ::delete_component_files
     */
    public function test_delete_component_files(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        $this->assertEquals(4, count($areafiles));
        $fs->delete_component_files('user');
        $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
        $this->assertEquals(0, count($areafiles));
    }

    /**
     * Tests for create_file_from_url.
     *
     * @covers ::create_file_from_url
     */
    public function test_create_file_from_url(): void {
        $this->resetAfterTest(true);

        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/downloadtest/',
        );
        $url = $this->getExternalTestFileUrl('/test.html');

        $fs = get_file_storage();

        // Test creating file without filename.
        $file1 = $fs->create_file_from_url($filerecord, $url);
        $this->assertInstanceOf('stored_file', $file1);

        // Set filename.
        $filerecord['filename'] = 'unit-test-filename.html';
        $file2 = $fs->create_file_from_url($filerecord, $url);
        $this->assertInstanceOf('stored_file', $file2);

        // Use temporary file.
        $filerecord['filename'] = 'unit-test-with-temp-file.html';
        $file3 = $fs->create_file_from_url($filerecord, $url, null, true);
        $file3 = $this->assertInstanceOf('stored_file', $file3);
    }

    /**
     * Tests for cron.
     *
     * @covers ::cron
     */
    public function test_cron(): void {
        $this->resetAfterTest(true);

        // Note: this is only testing DB compatibility atm, rather than
        // that work is done.
        $fs = get_file_storage();

        $this->expectOutputRegex('/Cleaning up/');
        $fs->cron();
    }

    /**
     * Tests for is_area_empty.
     *
     * @covers ::is_area_empty
     */
    public function test_is_area_empty(): void {
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();

        $this->assertFalse($fs->is_area_empty($user->ctxid, 'user', 'private'));

        // File area with madeup itemid should be empty.
        $this->assertTrue($fs->is_area_empty($user->ctxid, 'user', 'private', 9999));
        // Still empty with dirs included.
        $this->assertTrue($fs->is_area_empty($user->ctxid, 'user', 'private', 9999, false));
    }

    /**
     * Tests for move_area_files_to_new_context.
     *
     * @covers ::move_area_files_to_new_context
     */
    public function test_move_area_files_to_new_context(): void {
        $this->resetAfterTest(true);

        // Create a course with a page resource.
        $course = $this->getDataGenerator()->create_course();
        $page1 = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
        $page1context = \context_module::instance($page1->cmid);

        // Add a file to the page.
        $fs = get_file_storage();
        $filerecord = array(
            'contextid' => $page1context->id,
            'component' => 'mod_page',
            'filearea'  => 'content',
            'itemid'    => 0,
            'filepath'  => '/',
            'filename'  => 'unit-test-file.txt',
        );

        $originalfile = $fs->create_file_from_string($filerecord, 'Test content');
        $this->assertInstanceOf('stored_file', $originalfile);

        $pagefiles = $fs->get_area_files($page1context->id, 'mod_page', 'content', 0, 'sortorder', false);
        // Should be one file in filearea.
        $this->assertFalse($fs->is_area_empty($page1context->id, 'mod_page', 'content'));

        // Create a new page.
        $page2 = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
        $page2context = \context_module::instance($page2->cmid);

        // Newly created page area is empty.
        $this->assertTrue($fs->is_area_empty($page2context->id, 'mod_page', 'content'));

        // Move the files.
        $fs->move_area_files_to_new_context($page1context->id, $page2context->id, 'mod_page', 'content');

        // Page2 filearea should no longer be empty.
        $this->assertFalse($fs->is_area_empty($page2context->id, 'mod_page', 'content'));

        // Page1 filearea should now be empty.
        $this->assertTrue($fs->is_area_empty($page1context->id, 'mod_page', 'content'));

        $page2files = $fs->get_area_files($page2context->id, 'mod_page', 'content', 0, 'sortorder', false);
        $movedfile = reset($page2files);

        // The two files should have the same content hash.
        $this->assertEquals($movedfile->get_contenthash(), $originalfile->get_contenthash());
    }

    /**
     * Tests for convert_image.
     *
     * @covers ::convert_image
     */
    public function test_convert_image(): void {
        global $CFG;

        $this->resetAfterTest(false);

        $filepath = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';
        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/images/',
            'filename'  => 'testimage.jpg',
        );

        $fs = get_file_storage();
        $original = $fs->create_file_from_pathname($filerecord, $filepath);

        $filerecord['filename'] = 'testimage-converted-10x10.jpg';
        $converted = $fs->convert_image($filerecord, $original, 10, 10, true, 100);
        $this->assertInstanceOf('stored_file', $converted);

        $filerecord['filename'] = 'testimage-convereted-nosize.jpg';
        $converted = $fs->convert_image($filerecord, $original);
        $this->assertInstanceOf('stored_file', $converted);
    }

    /**
     * Tests for convert_image with a PNG.
     *
     * @covers ::convert_image
     */
    public function test_convert_image_png(): void {
        global $CFG;

        $this->resetAfterTest(false);

        $filepath = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.png';
        $syscontext = \context_system::instance();
        $filerecord = array(
            'contextid' => $syscontext->id,
            'component' => 'core',
            'filearea'  => 'unittest',
            'itemid'    => 0,
            'filepath'  => '/images/',
            'filename'  => 'testimage.png',
        );

        $fs = get_file_storage();
        $original = $fs->create_file_from_pathname($filerecord, $filepath);

        // Vanilla test.
        $filerecord['filename'] = 'testimage-converted-nosize.png';
        $vanilla = $fs->convert_image($filerecord, $original);
        $this->assertInstanceOf('stored_file', $vanilla);
        // Assert that byte 25 has the ascii value 6 for PNG-24.
        $this->assertTrue(ord(substr($vanilla->get_content(), 25, 1)) == 6);

        // 10x10 resize test; also testing for a ridiculous quality setting, which
        // we should if necessary scale to the 0 - 9 range.
        $filerecord['filename'] = 'testimage-converted-10x10.png';
        $converted = $fs->convert_image($filerecord, $original, 10, 10, true, 100);
        $this->assertInstanceOf('stored_file', $converted);
        // Assert that byte 25 has the ascii value 6 for PNG-24.
        $this->assertTrue(ord(substr($converted->get_content(), 25, 1)) == 6);

        // Transparency test.
        $filerecord['filename'] = 'testimage-converted-102x31.png';
        $converted = $fs->convert_image($filerecord, $original, 102, 31, true, 9);
        $this->assertInstanceOf('stored_file', $converted);
        // Assert that byte 25 has the ascii value 6 for PNG-24.
        $this->assertTrue(ord(substr($converted->get_content(), 25, 1)) == 6);

        $originalfile = imagecreatefromstring($original->get_content());
        $convertedfile = imagecreatefromstring($converted->get_content());
        $vanillafile = imagecreatefromstring($vanilla->get_content());

        $originalcolors = imagecolorsforindex($originalfile, imagecolorat($originalfile, 0, 0));
        $convertedcolors = imagecolorsforindex($convertedfile, imagecolorat($convertedfile, 0, 0));
        $vanillacolors = imagecolorsforindex($vanillafile, imagecolorat($vanillafile, 0, 0));
        $this->assertEquals(count($originalcolors), 4);
        $this->assertEquals(count($convertedcolors), 4);
        $this->assertEquals(count($vanillacolors), 4);
        $this->assertEquals($originalcolors['red'], $convertedcolors['red']);
        $this->assertEquals($originalcolors['green'], $convertedcolors['green']);
        $this->assertEquals($originalcolors['blue'], $convertedcolors['blue']);
        $this->assertEquals($originalcolors['alpha'], $convertedcolors['alpha']);
        $this->assertEquals($originalcolors['red'], $vanillacolors['red']);
        $this->assertEquals($originalcolors['green'], $vanillacolors['green']);
        $this->assertEquals($originalcolors['blue'], $vanillacolors['blue']);
        $this->assertEquals($originalcolors['alpha'], $vanillacolors['alpha']);
        $this->assertEquals($originalcolors['alpha'], 127);

    }

    private function generate_file_record() {
        $syscontext = \context_system::instance();
        $filerecord = new \stdClass();
        $filerecord->contextid = $syscontext->id;
        $filerecord->component = 'core';
        $filerecord->filearea = 'phpunit';
        $filerecord->filepath = '/';
        $filerecord->filename = 'testfile.txt';
        $filerecord->itemid = 0;

        return $filerecord;
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_file_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();

        // Create a file from a file id which doesn't exist.
        $this->expectException(file_exception::class);
        $fs->create_file_from_storedfile($filerecord,  9999);
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_contextid_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->contextid = 'invalid';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid contextid');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_component_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->component = 'bad/component';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid component');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_filearea_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->filearea = 'bad-filearea';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid filearea');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_itemid_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->itemid = 'bad-itemid';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid itemid');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_filepath_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->filepath = 'a-/bad/-filepath';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file path');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_filename_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = '';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file name');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_timecreated_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->timecreated = 'today';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file timecreated');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_timemodified_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'invalid.txt';
        $filerecord->timemodified  = 'today';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file timemodified');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile_duplicate(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();

        $fs = get_file_storage();
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        // Creating a file validating unique constraint.
        $this->expectException(stored_file_creation_exception::class);
        $this->expectExceptionMessage('Cannot create file 1/core/phpunit/0/testfile.txt');
        $fs->create_file_from_storedfile($filerecord, $file1->get_id());
    }

    /**
     * Tests for create_file_from_storedfile.
     *
     * @covers ::create_file_from_storedfile
     */
    public function test_create_file_from_storedfile(): void {
        $this->resetAfterTest(true);

        $syscontext = \context_system::instance();

        $filerecord = new \stdClass();
        $filerecord->contextid = $syscontext->id;
        $filerecord->component = 'core';
        $filerecord->filearea = 'phpunit';
        $filerecord->filepath = '/';
        $filerecord->filename = 'testfile.txt';
        $filerecord->itemid = 0;

        $fs = get_file_storage();

        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
        $this->assertInstanceOf('stored_file', $file1);

        $filerecord->filename = 'test-create-file-from-storedfile.txt';
        $file2 = $fs->create_file_from_storedfile($filerecord, $file1->get_id());
        $this->assertInstanceOf('stored_file', $file2);

        // These will be normalised to current time..
        $filerecord->timecreated = -100;
        $filerecord->timemodified= -100;
        $filerecord->filename = 'test-create-file-from-storedfile-bad-dates.txt';

        $file3 = $fs->create_file_from_storedfile($filerecord, $file1->get_id());
        $this->assertInstanceOf('stored_file', $file3);

        $this->assertNotEquals($file3->get_timemodified(), $filerecord->timemodified);
        $this->assertNotEquals($file3->get_timecreated(), $filerecord->timecreated);
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_contextid_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->contextid = 'invalid';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid contextid');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_component_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->component = 'bad/component';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid component');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_filearea_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->filearea = 'bad-filearea';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid filearea');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_itemid_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->itemid = 'bad-itemid';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid itemid');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_filepath_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->filepath = 'a-/bad/-filepath';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file path');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_filename_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->filename = '';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file name');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_timecreated_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->timecreated = 'today';

        $this->expectException('file_exception');
        $this->expectExceptionMessage('Invalid file timecreated');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_timemodified_invalid(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->timemodified  = 'today';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file timemodified');
        $file1 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * Tests for create_file_from_string with a duplicate string.
     * @covers ::create_file_from_string
     */
    public function test_create_file_from_string_duplicate(): void {
        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $file1 = $fs->create_file_from_string($filerecord, 'text contents');

        // Creating a file validating unique constraint.
        $this->expectException('stored_file_creation_exception');
        $file2 = $fs->create_file_from_string($filerecord, 'text contents');
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_contextid_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->contextid = 'invalid';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid contextid');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_component_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->component = 'bad/component';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid component');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_filearea_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->filearea = 'bad-filearea';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid filearea');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_itemid_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->itemid = 'bad-itemid';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid itemid');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_filepath_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->filepath = 'a-/bad/-filepath';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file path');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_filename_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->filename = '';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file name');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_timecreated_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->timecreated = 'today';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file timecreated');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_timemodified_invalid(): void {
        global $CFG;
        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $this->resetAfterTest(true);

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $filerecord->timemodified  = 'today';

        $this->expectException(file_exception::class);
        $this->expectExceptionMessage('Invalid file timemodified');
        $file1 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * @covers ::create_file_from_pathname
     */
    public function test_create_file_from_pathname_duplicate_file(): void {
        global $CFG;
        $this->resetAfterTest(true);

        $path = $CFG->dirroot.'/lib/filestorage/tests/fixtures/testimage.jpg';

        $filerecord = $this->generate_file_record();
        $fs = get_file_storage();

        $file1 = $fs->create_file_from_pathname($filerecord, $path);
        $this->assertInstanceOf('stored_file', $file1);

        // Creating a file validating unique constraint.
        $this->expectException(stored_file_creation_exception::class);
        $this->expectExceptionMessage('Cannot create file 1/core/phpunit/0/testfile.txt');
        $file2 = $fs->create_file_from_pathname($filerecord, $path);
    }

    /**
     * Calling \stored_file::delete_reference() on a non-reference file throws coding_exception
     *
     * @covers \stored_file::delete_reference
     */
    public function test_delete_reference_on_nonreference(): void {

        $this->resetAfterTest(true);
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();
        $repos = repository::get_instances(array('type'=>'user'));
        $repo = reset($repos);

        /** @var \stored_file $file */
        $file = null;
        foreach ($fs->get_area_files($user->ctxid, 'user', 'private') as $areafile) {
            if (!$areafile->is_directory()) {
                $file = $areafile;
                break;
            }
        }
        $this->assertInstanceOf('stored_file', $file);
        $this->assertFalse($file->is_external_file());

        $this->expectException('coding_exception');
        $file->delete_reference();
    }

    /**
     * Calling \stored_file::delete_reference() on a reference file does not affect other
     * symlinks to the same original
     *
     * @covers \stored_file::delete_reference
     */
    public function test_delete_reference_one_symlink_does_not_rule_them_all(): void {

        $this->resetAfterTest(true);
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();
        $repos = repository::get_instances(array('type'=>'user'));
        $repo = reset($repos);

        // Create two aliases linking the same original.

        /** @var \stored_file $originalfile */
        $originalfile = null;
        foreach ($fs->get_area_files($user->ctxid, 'user', 'private') as $areafile) {
            if (!$areafile->is_directory()) {
                $originalfile = $areafile;
                break;
            }
        }
        $this->assertInstanceOf('stored_file', $originalfile);

        // Calling delete_reference() on a non-reference file.

        $originalrecord = array(
            'contextid' => $originalfile->get_contextid(),
            'component' => $originalfile->get_component(),
            'filearea'  => $originalfile->get_filearea(),
            'itemid'    => $originalfile->get_itemid(),
            'filepath'  => $originalfile->get_filepath(),
            'filename'  => $originalfile->get_filename(),
        );

        $aliasrecord = $this->generate_file_record();
        $aliasrecord->filepath = '/A/';
        $aliasrecord->filename = 'symlink.txt';

        $ref = $fs->pack_reference($originalrecord);
        $aliasfile1 = $fs->create_file_from_reference($aliasrecord, $repo->id, $ref);

        $aliasrecord->filepath = '/B/';
        $aliasrecord->filename = 'symlink.txt';
        $ref = $fs->pack_reference($originalrecord);
        $aliasfile2 = $fs->create_file_from_reference($aliasrecord, $repo->id, $ref);

        // Refetch A/symlink.txt file.
        $symlink1 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
            $aliasrecord->filearea, $aliasrecord->itemid, '/A/', 'symlink.txt');
        $this->assertTrue($symlink1->is_external_file());

        // Unlink the A/symlink.txt file.
        $symlink1->delete_reference();
        $this->assertFalse($symlink1->is_external_file());

        // Make sure that B/symlink.txt has not been affected.
        $symlink2 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
            $aliasrecord->filearea, $aliasrecord->itemid, '/B/', 'symlink.txt');
        $this->assertTrue($symlink2->is_external_file());
    }

    /**
     * Make sure that when internal file is updated all references to it are
     * updated immediately. When it is deleted, the references are converted
     * to true copies.
     */
    public function test_update_reference_internal(): void {
        purge_all_caches();
        $this->resetAfterTest(true);
        $user = $this->setup_three_private_files();
        $fs = get_file_storage();
        $repos = repository::get_instances(array('type' => 'user'));
        $repo = reset($repos);

        // Create two aliases linking the same original.

        $areafiles = array_values($fs->get_area_files($user->ctxid, 'user', 'private', false, 'filename', false));

        $originalfile = $areafiles[0];
        $this->assertInstanceOf('stored_file', $originalfile);
        $contenthash = $originalfile->get_contenthash();
        $filesize = $originalfile->get_filesize();

        $substitutefile = $areafiles[1];
        $this->assertInstanceOf('stored_file', $substitutefile);
        $newcontenthash = $substitutefile->get_contenthash();
        $newfilesize = $substitutefile->get_filesize();

        $originalrecord = array(
            'contextid' => $originalfile->get_contextid(),
            'component' => $originalfile->get_component(),
            'filearea'  => $originalfile->get_filearea(),
            'itemid'    => $originalfile->get_itemid(),
            'filepath'  => $originalfile->get_filepath(),
            'filename'  => $originalfile->get_filename(),
        );

        $aliasrecord = $this->generate_file_record();
        $aliasrecord->filepath = '/A/';
        $aliasrecord->filename = 'symlink.txt';

        $ref = $fs->pack_reference($originalrecord);
        $symlink1 = $fs->create_file_from_reference($aliasrecord, $repo->id, $ref);
        // Make sure created alias is a reference and has the same size and contenthash as source.
        $this->assertEquals($contenthash, $symlink1->get_contenthash());
        $this->assertEquals($filesize, $symlink1->get_filesize());
        $this->assertEquals($repo->id, $symlink1->get_repository_id());
        $this->assertNotEmpty($symlink1->get_referencefileid());
        $referenceid = $symlink1->get_referencefileid();

        $aliasrecord->filepath = '/B/';
        $aliasrecord->filename = 'symlink.txt';
        $ref = $fs->pack_reference($originalrecord);
        $symlink2 = $fs->create_file_from_reference($aliasrecord, $repo->id, $ref);
        // Make sure created alias is a reference and has the same size and contenthash as source.
        $this->assertEquals($contenthash, $symlink2->get_contenthash());
        $this->assertEquals($filesize, $symlink2->get_filesize());
        $this->assertEquals($repo->id, $symlink2->get_repository_id());
        // Make sure both aliases have the same reference id.
        $this->assertEquals($referenceid, $symlink2->get_referencefileid());

        // Overwrite ofiginal file.
        $originalfile->replace_file_with($substitutefile);
        $this->assertEquals($newcontenthash, $originalfile->get_contenthash());
        $this->assertEquals($newfilesize, $originalfile->get_filesize());

        // References to the internal files must be synchronised immediately.
        // Refetch A/symlink.txt file.
        $symlink1 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
            $aliasrecord->filearea, $aliasrecord->itemid, '/A/', 'symlink.txt');
        $this->assertTrue($symlink1->is_external_file());
        $this->assertEquals($newcontenthash, $symlink1->get_contenthash());
        $this->assertEquals($newfilesize, $symlink1->get_filesize());
        $this->assertEquals($repo->id, $symlink1->get_repository_id());
        $this->assertEquals($referenceid, $symlink1->get_referencefileid());

        // Refetch B/symlink.txt file.
        $symlink2 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
            $aliasrecord->filearea, $aliasrecord->itemid, '/B/', 'symlink.txt');
        $this->assertTrue($symlink2->is_external_file());
        $this->assertEquals($newcontenthash, $symlink2->get_contenthash());
        $this->assertEquals($newfilesize, $symlink2->get_filesize());
        $this->assertEquals($repo->id, $symlink2->get_repository_id());
        $this->assertEquals($referenceid, $symlink2->get_referencefileid());

        // Remove original file.
        $originalfile->delete();

        // References must be converted to independend files.
        // Refetch A/symlink.txt file.
        $symlink1 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
            $aliasrecord->filearea, $aliasrecord->itemid, '/A/', 'symlink.txt');
        $this->assertFalse($symlink1->is_external_file());
        $this->assertEquals($newcontenthash, $symlink1->get_contenthash());
        $this->assertEquals($newfilesize, $symlink1->get_filesize());
        $this->assertNull($symlink1->get_repository_id());
        $this->assertNull($symlink1->get_referencefileid());

        // Refetch B/symlink.txt file.
        $symlink2 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
            $aliasrecord->filearea, $aliasrecord->itemid, '/B/', 'symlink.txt');
        $this->assertFalse($symlink2->is_external_file());
        $this->assertEquals($newcontenthash, $symlink2->get_contenthash());
        $this->assertEquals($newfilesize, $symlink2->get_filesize());
        $this->assertNull($symlink2->get_repository_id());
        $this->assertNull($symlink2->get_referencefileid());
    }

    /**
     * Tests for get_unused_filename.
     *
     * @covers ::get_unused_filename
     */
    public function test_get_unused_filename(): void {
        global $USER;
        $this->resetAfterTest(true);

        $fs = get_file_storage();
        $this->setAdminUser();
        $contextid = \context_user::instance($USER->id)->id;
        $component = 'user';
        $filearea = 'private';
        $itemid = 0;
        $filepath = '/';

        // Create some private files.
        $file = new \stdClass;
        $file->contextid = $contextid;
        $file->component = 'user';
        $file->filearea  = 'private';
        $file->itemid    = 0;
        $file->filepath  = '/';
        $file->source    = 'test';
        $filenames = array('foo.txt', 'foo (1).txt', 'foo (20).txt', 'foo (999)', 'bar.jpg', 'What (a cool file).jpg',
                'Hurray! (1).php', 'Hurray! (2).php', 'Hurray! (9a).php', 'Hurray! (abc).php');
        foreach ($filenames as $key => $filename) {
            $file->filename = $filename;
            $userfile = $fs->create_file_from_string($file, "file $key $filename content");
            $this->assertInstanceOf('stored_file', $userfile);
        }

        // Asserting new generated names.
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'unused.txt');
        $this->assertEquals('unused.txt', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo.txt');
        $this->assertEquals('foo (21).txt', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (1).txt');
        $this->assertEquals('foo (21).txt', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (2).txt');
        $this->assertEquals('foo (2).txt', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (20).txt');
        $this->assertEquals('foo (21).txt', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo');
        $this->assertEquals('foo', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (123)');
        $this->assertEquals('foo (123)', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (999)');
        $this->assertEquals('foo (1000)', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar.png');
        $this->assertEquals('bar.png', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar (12).png');
        $this->assertEquals('bar (12).png', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar.jpg');
        $this->assertEquals('bar (1).jpg', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar (1).jpg');
        $this->assertEquals('bar (1).jpg', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'What (a cool file).jpg');
        $this->assertEquals('What (a cool file) (1).jpg', $newfilename);
        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'Hurray! (1).php');
        $this->assertEquals('Hurray! (3).php', $newfilename);

        $this->expectException('coding_exception');
        $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, '');
    }

    /**
     * Test that mimetype_from_file returns appropriate output when the
     * file could not be found.
     *
     * @covers ::mimetype
     */
    public function test_mimetype_not_found(): void {
        $mimetype = \file_storage::mimetype('/path/to/nonexistent/file');
        $this->assertEquals('document/unknown', $mimetype);
    }

    /**
     * Data provider to return fixture files and their expected mimetype
     *
     * @return array[]
     */
    public static function filepath_mimetype_provider(): array {
        return [
            [__DIR__ . '/fixtures/testimage.jpg', 'image/jpeg'],
            [__DIR__ . '/fixtures/testimage.svg', 'image/svg+xml'],
            [__DIR__ . '/fixtures/testimage_basic.svg', 'image/svg+xml'],
        ];
    }

    /**
     * Test that mimetype returns appropriate output for a known file.
     *
     * Note: this is not intended to check that functions outside of this
     * file works. It is intended to validate the codepath contains no
     * errors and behaves as expected.
     *
     * @covers ::mimetype
     *
     * @param string $filepath
     * @param string $expectedmimetype
     *
     * @dataProvider filepath_mimetype_provider
     */
    public function test_mimetype_known(string $filepath, string $expectedmimetype): void {
        $mimetype = \file_storage::mimetype($filepath);
        $this->assertEquals($expectedmimetype, $mimetype);
    }

    /**
     * Test that mimetype_from_file returns appropriate output when the
     * file could not be found.
     *
     * @covers ::mimetype_from_file
     */
    public function test_mimetype_from_file_not_found(): void {
        $mimetype = \file_storage::mimetype_from_file('/path/to/nonexistent/file');
        $this->assertEquals('document/unknown', $mimetype);
    }

    /**
     * Test that mimetype_from_file returns appropriate output for a known
     * file.
     *
     * Note: this is not intended to check that functions outside of this
     * file works. It is intended to validate the codepath contains no
     * errors and behaves as expected.
     *
     * @covers ::mimetype_from_file
     *
     * @param string $filepath
     * @param string $expectedmimetype
     *
     * @dataProvider filepath_mimetype_provider
     */
    public function test_mimetype_from_file_known(string $filepath, string $expectedmimetype): void {
        $mimetype = \file_storage::mimetype_from_file($filepath);
        $this->assertEquals($expectedmimetype, $mimetype);
    }

    /**
     * Test that get_pathname_hash returns the same file hash for pathnames
     * with and without trailing / leading slash.
     *
     * @covers ::get_pathname_hash
     *
     */
    public function test_get_pathname_hash(): void {
        $contextid = 2;
        $component = 'mod_test';
        $filearea = 'data';
        $itemid = 0;
        $filepath1 = '/path';
        $filepath2 = '/path/';
        $filepath3 = 'path/';
        $filename = 'example.jpg';
        $hash1 = \file_storage::get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath1, $filename);
        $hash2 = \file_storage::get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath2, $filename);
        $hash3 = \file_storage::get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath3, $filename);
        $this->assertEquals($hash1, $hash2);
        $this->assertEquals($hash2, $hash3);
    }

    /**
     * Test that the before_file_created hook has no impact if not called.
     *
     * @covers \core_files\hook\before_file_created
     */
    public function test_before_file_created_hook_executed_nochange(): void {
        global $TESTCALLBACK; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase

        $this->resetAfterTest(true);
        $testdata = self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php');

        \core\di::set(
            \core\hook\manager::class,
            \core\hook\manager::phpunit_get_instance([]),
        );

        // Create a file.
        $fs = get_file_storage();
        $file = $fs->create_file_from_pathname(
            (object) [
                'contextid' => 1,
                'component' => 'core',
                'filearea' => 'phpunit',
                'itemid' => 0,
                'filepath' => '/',
                'filename' => 'testfile.csv',
            ],
            $testdata,
        );

        // The content should have been updated.
        $this->assertEquals(
            file_get_contents($testdata),
            $file->get_content(),
        );

        // The content hash should match the new content.
        $this->assertEquals(
            file_storage::hash_from_path($testdata),
            $file->get_contenthash(),
        );
    }

    /**
     * Test that the before_file_created hook is called before a file is created.
     *
     * @covers \core_files\hook\before_file_created
     */
    public function test_before_file_created_hook_executed_filepath(): void {
        global $TESTCALLBACK; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase

        $this->resetAfterTest(true);
        $testdata = self::get_fixture_path('core', 'tabfile.csv');

        // The before_file_created test hook calls a callback function at TESTCALLBACK.
        $TESTCALLBACK = function( // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
            \core_files\hook\before_file_created $hook,
        ) use ($testdata) {
            if ($hook->get_filecontent() === '') {
                return;
            }
            $hook->update_filepath($testdata);
        };

        \core\di::set(
            \core\hook\manager::class,
            \core\hook\manager::phpunit_get_instance([
                'example' => self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'),
            ]),
        );

        // Create a file.
        $fs = get_file_storage();
        $file = $fs->create_file_from_pathname(
            (object) [
                'contextid' => 1,
                'component' => 'core',
                'filearea' => 'phpunit',
                'itemid' => 0,
                'filepath' => '/',
                'filename' => 'testfile.csv',
            ],
            self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'),
        );

        // The content should have been updated.
        $this->assertEquals(
            file_get_contents($testdata),
            $file->get_content(),
        );

        // The content hash should match the new content.
        $this->assertEquals(
            file_storage::hash_from_path($testdata),
            $file->get_contenthash(),
        );
    }

    /**
     * Test that the before_file_created hook is called before a file is created with content.
     *
     * @covers \core_files\hook\before_file_created
     */
    public function test_before_file_created_hook_executed_filecontent(): void {
        global $TESTCALLBACK; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase

        $this->resetAfterTest(true);
        $testdata = 'New content';

        // The before_file_created test hook calls a callback function at TESTCALLBACK.
        $TESTCALLBACK = function( // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
            \core_files\hook\before_file_created $hook,
        ) use ($testdata) {
            if ($hook->get_filecontent() === '') {
                return;
            }
            $hook->update_filecontent($testdata);
        };

        \core\di::set(
            \core\hook\manager::class,
            \core\hook\manager::phpunit_get_instance([
                'example' => self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'),
            ]),
        );

        // Create a file.
        $fs = get_file_storage();
        $file = $fs->create_file_from_string(
            (object) [
                'contextid' => 1,
                'component' => 'core',
                'filearea' => 'phpunit',
                'itemid' => 0,
                'filepath' => '/',
                'filename' => 'testfile.csv',
            ],
            'Original content',
        );

        // The content should have been updated.
        $this->assertEquals(
            $testdata,
            $file->get_content(),
        );

        // The content hash should match the new content.
        $this->assertEquals(
            file_storage::hash_from_string($testdata),
            $file->get_contenthash(),
        );
    }
}

class test_stored_file_inspection extends stored_file {
    public static function get_pretected_pathname(stored_file $file) {
        return $file->get_pathname_by_contenthash();
    }
}
