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

namespace tool_mobile;

use externallib_advanced_testcase;
use core_external\external_api;

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

global $CFG;

require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->dirroot . '/admin/tool/mobile/tests/fixtures/output/mobile.php');
require_once($CFG->dirroot . '/webservice/lib.php');

/**
 * Moodle Mobile admin tool external functions tests.
 *
 * @package     tool_mobile
 * @copyright   2016 Juan Leyva
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since       Moodle 3.1
 */
final class externallib_test extends externallib_advanced_testcase {

    /**
     * Test get_plugins_supporting_mobile.
     * This is a very basic test because currently there aren't plugins supporting Mobile in core.
     */
    public function test_get_plugins_supporting_mobile(): void {
        $result = external::get_plugins_supporting_mobile();
        $result = external_api::clean_returnvalue(external::get_plugins_supporting_mobile_returns(), $result);
        $this->assertCount(0, $result['warnings']);
        $this->assertArrayHasKey('plugins', $result);
        $this->assertTrue(is_array($result['plugins']));
    }

    public function test_get_public_config(): void {
        global $CFG, $SITE, $OUTPUT;

        $this->resetAfterTest(true);
        $result = external::get_public_config();
        $result = external_api::clean_returnvalue(external::get_public_config_returns(), $result);

        // Test default values.
        $context = \context_system::instance();
        [$authinstructions] = \core_external\util::format_text(
            $CFG->auth_instructions,
            FORMAT_MOODLE,
            $context->id
        );
        [$maintenancemessage] = \core_external\util::format_text(
            $CFG->maintenance_message,
            FORMAT_MOODLE,
            $context->id
        );

        $expected = array(
            'wwwroot' => $CFG->wwwroot,
            'httpswwwroot' => $CFG->wwwroot,
            'sitename' => \core_external\util::format_string($SITE->fullname, $context->id, true),
            'guestlogin' => $CFG->guestloginbutton,
            'rememberusername' => $CFG->rememberusername,
            'authloginviaemail' => $CFG->authloginviaemail,
            'registerauth' => $CFG->registerauth,
            'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
            'authinstructions' => $authinstructions,
            'authnoneenabled' => (int) is_enabled_auth('none'),
            'enablewebservices' => $CFG->enablewebservices,
            'enablemobilewebservice' => $CFG->enablemobilewebservice,
            'maintenanceenabled' => $CFG->maintenance_enabled,
            'maintenancemessage' => $maintenancemessage,
            'typeoflogin' => api::LOGIN_VIA_APP,
            'mobilecssurl' => '',
            'tool_mobile_disabledfeatures' => '',
            'launchurl' => "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php",
            'country' => $CFG->country,
            'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
            'autolang' => $CFG->autolang,
            'lang' => $CFG->lang,
            'langmenu' => $CFG->langmenu,
            'langlist' => $CFG->langlist,
            'locale' => $CFG->locale,
            'tool_mobile_minimumversion' => '',
            'tool_mobile_iosappid' => get_config('tool_mobile', 'iosappid'),
            'tool_mobile_androidappid' => get_config('tool_mobile', 'androidappid'),
            'tool_mobile_setuplink' => get_config('tool_mobile', 'setuplink'),
            'tool_mobile_qrcodetype' => get_config('tool_mobile', 'qrcodetype'),
            'supportpage' => $CFG->supportpage,
            'supportavailability' => $CFG->supportavailability,
            'warnings' => [],
            'showloginform' => (int) get_config('core', 'showloginform'),
        );
        $this->assertEquals($expected, $result);

        $this->setAdminUser();
        // Change some values.
        set_config('registerauth', 'email');
        $authinstructions = 'Something with <b>html tags</b>';
        set_config('auth_instructions', $authinstructions);
        set_config('typeoflogin', api::LOGIN_VIA_BROWSER, 'tool_mobile');
        set_config('logo', 'mock.png', 'core_admin');
        set_config('logocompact', 'mock.png', 'core_admin');
        set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
        set_config('agedigitalconsentverification', 1);
        set_config('autolang', 1);
        set_config('lang', 'a_b');  // Set invalid lang.
        set_config('disabledfeatures', 'myoverview', 'tool_mobile');
        set_config('minimumversion', '3.8.0', 'tool_mobile');
        set_config('supportemail', 'test@test.com');
        set_config('supportavailability', CONTACT_SUPPORT_ANYONE);

        // Enable an issuer.
        $issuer = \core\oauth2\api::create_standard_issuer('google');
        $irecord = $issuer->to_record();
        $irecord->clientid = 'mock';
        $irecord->clientsecret = 'mock';
        \core\oauth2\api::update_issuer($irecord);

        set_config('auth', 'oauth2');

        list($authinstructions, $notusedformat) = \core_external\util::format_text($authinstructions, FORMAT_MOODLE, $context->id);
        $expected['registerauth'] = 'email';
        $expected['authinstructions'] = $authinstructions;
        $expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
        $expected['forgottenpasswordurl'] = ''; // Expect empty when it's not an URL.
        $expected['agedigitalconsentverification'] = true;
        $expected['supportname'] = $CFG->supportname;
        $expected['supportemail'] = $CFG->supportemail;
        $expected['supportavailability'] = $CFG->supportavailability;
        $expected['autolang'] = '1';
        $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
        $expected['tool_mobile_disabledfeatures'] = 'myoverview';
        $expected['tool_mobile_minimumversion'] = '3.8.0';

        if ($logourl = $OUTPUT->get_logo_url()) {
            $expected['logourl'] = $logourl->out(false);
        }
        if ($compactlogourl = $OUTPUT->get_compact_logo_url()) {
            $expected['compactlogourl'] = $compactlogourl->out(false);
        }

        $result = external::get_public_config();
        $result = external_api::clean_returnvalue(external::get_public_config_returns(), $result);

        // First check the provider.
        $identityprovider = $result['identityproviders'];
        unset($result['identityproviders']);

        $this->assertEquals('Google', $identityprovider[0]['name']);
        $this->assertEquals($irecord->image, $identityprovider[0]['iconurl']);
        $this->assertStringContainsString($CFG->wwwroot, $identityprovider[0]['url']);
        $this->assertEquals($expected, $result);
    }

    /**
     * Test get_config
     *
     * @covers \tool_mobile\external::get_config
     */
    public function test_get_config(): void {
        global $CFG, $SITE;
        require_once($CFG->dirroot . '/course/format/lib.php');

        $this->resetAfterTest(true);

        $mysitepolicy = 'http://mysite.is/policy/';
        set_config('sitepolicy', $mysitepolicy);
        set_config('supportemail', 'test@test.com');

        $result = external::get_config();
        $result = external_api::clean_returnvalue(external::get_config_returns(), $result);

        // SITE summary is null in phpunit which gets transformed to an empty string by format_text.
        [$sitesummary, $summaryformat] = \core_external\util::format_text(
            $SITE->summary,
            $SITE->summaryformat,
            \context_system::instance()->id
        );

        // Test default values.
        $context = \context_system::instance();
        $expected = array(
            array('name' => 'fullname', 'value' => $SITE->fullname),
            array('name' => 'shortname', 'value' => $SITE->shortname),
            array('name' => 'summary', 'value' => $sitesummary),
            array('name' => 'summaryformat', 'value' => $summaryformat),
            array('name' => 'frontpage', 'value' => $CFG->frontpage),
            array('name' => 'frontpageloggedin', 'value' => $CFG->frontpageloggedin),
            array('name' => 'maxcategorydepth', 'value' => $CFG->maxcategorydepth),
            array('name' => 'frontpagecourselimit', 'value' => $CFG->frontpagecourselimit),
            array('name' => 'numsections', 'value' => course_get_format($SITE)->get_last_section_number()),
            array('name' => 'newsitems', 'value' => $SITE->newsitems),
            array('name' => 'commentsperpage', 'value' => $CFG->commentsperpage),
            array('name' => 'sitepolicy', 'value' => $mysitepolicy),
            array('name' => 'sitepolicyhandler', 'value' => ''),
            array('name' => 'disableuserimages', 'value' => $CFG->disableuserimages),
            array('name' => 'mygradesurl', 'value' => user_mygrades_url()->out(false)),
            array('name' => 'tool_mobile_forcelogout', 'value' => 0),
            array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
            array('name' => 'tool_mobile_disabledfeatures', 'value' => ''),
            array('name' => 'tool_mobile_filetypeexclusionlist', 'value' => ''),
            array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
            array('name' => 'tool_mobile_apppolicy', 'value' => ''),
            array('name' => 'tool_mobile_autologinmintimebetweenreq', 'value' => 6 * MINSECS),
            array('name' => 'tool_mobile_autologout', 'value' => get_config('tool_mobile', 'autologout')),
            array('name' => 'tool_mobile_autologouttime', 'value' => get_config('tool_mobile', 'autologouttime')),
            array('name' => 'calendartype', 'value' => $CFG->calendartype),
            array('name' => 'calendar_site_timeformat', 'value' => $CFG->calendar_site_timeformat),
            array('name' => 'calendar_startwday', 'value' => $CFG->calendar_startwday),
            array('name' => 'calendar_adminseesall', 'value' => $CFG->calendar_adminseesall),
            array('name' => 'calendar_lookahead', 'value' => $CFG->calendar_lookahead),
            array('name' => 'calendar_maxevents', 'value' => $CFG->calendar_maxevents),
        );
        $colornumbers = range(1, 10);
        foreach ($colornumbers as $number) {
            $expected[] = [
                'name' => 'core_admin_coursecolor' . $number,
                'value' => get_config('core_admin', 'coursecolor' . $number)
            ];
        }
        $expected[] = ['name' => 'supportavailability', 'value' => $CFG->supportavailability];
        $expected[] = ['name' => 'supportname', 'value' => $CFG->supportname];
        $expected[] = ['name' => 'supportemail', 'value' => $CFG->supportemail];
        $expected[] = ['name' => 'supportpage', 'value' => $CFG->supportpage];

        $expected[] = ['name' => 'coursegraceperiodafter', 'value' => $CFG->coursegraceperiodafter];
        $expected[] = ['name' => 'coursegraceperiodbefore', 'value' => $CFG->coursegraceperiodbefore];

        $expected[] = ['name' => 'enabledashboard', 'value' => $CFG->enabledashboard];
        $expected[] = ['name' => 'customusermenuitems', 'value' => $CFG->customusermenuitems];
        $expected[] = ['name' => 'timezone', 'value' => $CFG->timezone];
        $expected[] = ['name' => 'forcetimezone', 'value' => $CFG->forcetimezone];

        $expected[] = ['name' => 'searchengine', 'value' => $CFG->searchengine];
        $expected[] = ['name' => 'searchenablecategories', 'value' => $CFG->searchenablecategories];
        $expected[] = ['name' => 'searchdefaultcategory', 'value' => $CFG->searchdefaultcategory];
        $expected[] = ['name' => 'searchhideallcategory', 'value' => $CFG->searchhideallcategory];
        $expected[] = ['name' => 'searchmaxtopresults', 'value' => $CFG->searchmaxtopresults];
        $expected[] = ['name' => 'searchbannerenable', 'value' => $CFG->searchbannerenable];
        $expected[] = ['name' => 'searchbanner', 'value' => $CFG->searchbanner];

        $expected[] = ['name' => 'tool_dataprivacy_contactdataprotectionofficer', 'value' => get_config('tool_dataprivacy', 'contactdataprotectionofficer')];
        $expected[] = ['name' => 'tool_dataprivacy_showdataretentionsummary', 'value' => get_config('tool_dataprivacy', 'showdataretentionsummary')];

        $expected[] = ['name' => 'useblogassociations', 'value' => $CFG->useblogassociations];
        $expected[] = ['name' => 'bloglevel', 'value' => $CFG->bloglevel];
        $expected[] = ['name' => 'blogusecomments', 'value' => $CFG->blogusecomments];

        $this->assertCount(0, $result['warnings']);
        $this->assertEquals($expected, $result['settings']);

        // H5P custom CSS.
        set_config('h5pcustomcss', '.debug { color: #fab; }', 'core_h5p');
        \core_h5p\local\library\autoloader::register();
        \core_h5p\file_storage::generate_custom_styles();
        $result = external::get_config();
        $result = external_api::clean_returnvalue(external::get_config_returns(), $result);

        $customcss = \core_h5p\file_storage::get_custom_styles();
        $expected[] = ['name' => 'h5pcustomcssurl', 'value' => $customcss['cssurl']->out() . '?ver=' . $customcss['cssversion']];

        $this->assertCount(0, $result['warnings']);
        $this->assertEquals($expected, $result['settings']);

        // Change a value and retrieve filtering by section.
        set_config('commentsperpage', 1);
        $expected[10]['value'] = 1;
        // Remove not expected elements.
        array_splice($expected, 11);

        $result = external::get_config('frontpagesettings');
        $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
        $this->assertCount(0, $result['warnings']);
        $this->assertEquals($expected, $result['settings']);
    }

    /*
     * Test get_autologin_key.
     */
    public function test_get_autologin_key(): void {
        global $DB, $CFG, $USER;

        $this->resetAfterTest(true);

        $user = $this->getDataGenerator()->create_user();
        $this->setUser($user);
        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));

        $token = \core_external\util::generate_token_for_current_user($service);

        // Check we got the private token.
        $this->assertTrue(isset($token->privatetoken));

        // Enable requeriments.
        $_GET['wstoken'] = $token->token;   // Mock parameters.

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        // Even if we force the password change for the current user we should be able to retrieve the key.
        set_user_preference('auth_forcepasswordchange', 1, $user->id);

        $this->setCurrentTimeStart();
        $result = external::get_autologin_key($token->privatetoken);
        $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
        // Validate the key.
        $this->assertEquals(32, \core_text::strlen($result['key']));
        $key = $DB->get_record('user_private_key', array('value' => $result['key']));
        $this->assertEquals($USER->id, $key->userid);
        $this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);

        // Now, try with an invalid private token.
        set_user_preference('tool_mobile_autologin_request_last', time() - HOURSECS, $USER);

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('invalidprivatetoken', 'tool_mobile'));
        $result = external::get_autologin_key(random_string('64'));
    }

    /**
     * Test get_autologin_key missing ws.
     */
    public function test_get_autologin_key_missing_ws(): void {
        global $CFG;
        $this->resetAfterTest(true);

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        // Need to disable webservices to verify that's checked.
        $CFG->enablewebservices = 0;
        $CFG->enablemobilewebservice = 0;

        $this->setAdminUser();
        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
        $result = external::get_autologin_key('');
    }

    /**
     * Test get_autologin_key missing https.
     */
    public function test_get_autologin_key_missing_https(): void {
        global $CFG;

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        // Need to simulate a non HTTPS site here.
        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);

        $this->resetAfterTest(true);
        $this->setAdminUser();

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
        $result = external::get_autologin_key('');
    }

    /**
     * Test get_autologin_key missing admin.
     */
    public function test_get_autologin_key_missing_admin(): void {
        global $CFG;

        $this->resetAfterTest(true);
        $this->setAdminUser();

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
        $result = external::get_autologin_key('');
    }

    /**
     * Test get_autologin_key locked.
     */
    public function test_get_autologin_key_missing_locked(): void {
        global $CFG, $DB, $USER;

        $this->resetAfterTest(true);
        $user = $this->getDataGenerator()->create_user();
        $this->setUser($user);

        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));

        $token = \core_external\util::generate_token_for_current_user($service);
        $_GET['wstoken'] = $token->token;   // Mock parameters.

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        $result = external::get_autologin_key($token->privatetoken);
        $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);

        // Mock last time request.
        $mocktime = time() - 7 * MINSECS;
        set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER);
        $result = external::get_autologin_key($token->privatetoken);
        $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);

        // Change min time between requests to 3 minutes.
        set_config('autologinmintimebetweenreq', 3 * MINSECS, 'tool_mobile');

        // Mock a previous request, 4 minutes ago.
        $mocktime = time() - (4 * MINSECS);
        set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER);
        $result = external::get_autologin_key($token->privatetoken);
        $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);

        // We just requested one token, we must wait.
        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile', 3));
        $result = external::get_autologin_key($token->privatetoken);
    }

    /**
     * Test get_autologin_key missing app_request.
     */
    public function test_get_autologin_key_missing_app_request(): void {
        global $CFG;

        $this->resetAfterTest(true);
        $this->setAdminUser();

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
        $result = external::get_autologin_key('');
    }

    /**
     * Test get_content.
     */
    public function test_get_content(): void {

        $paramval = 16;
        $result = external::get_content('tool_mobile', 'test_view', array(array('name' => 'param1', 'value' => $paramval)));
        $result = external_api::clean_returnvalue(external::get_content_returns(), $result);
        $this->assertCount(1, $result['templates']);
        $this->assertCount(1, $result['otherdata']);
        $this->assertCount(2, $result['restrict']['users']);
        $this->assertCount(2, $result['restrict']['courses']);
        $this->assertEquals('alert();', $result['javascript']);
        $this->assertEquals('main', $result['templates'][0]['id']);
        $this->assertEquals('The HTML code', $result['templates'][0]['html']);
        $this->assertEquals('otherdata1', $result['otherdata'][0]['name']);
        $this->assertEquals($paramval, $result['otherdata'][0]['value']);
        $this->assertEquals(array(1, 2), $result['restrict']['users']);
        $this->assertEquals(array(3, 4), $result['restrict']['courses']);
        $this->assertEmpty($result['files']);
        $this->assertFalse($result['disabled']);
    }

    /**
     * Test get_content disabled.
     */
    public function test_get_content_disabled(): void {

        $paramval = 16;
        $result = external::get_content('tool_mobile', 'test_view_disabled',
            array(array('name' => 'param1', 'value' => $paramval)));
        $result = external_api::clean_returnvalue(external::get_content_returns(), $result);
        $this->assertTrue($result['disabled']);
    }

    /**
     * Test get_content non existent function in valid component.
     */
    public function test_get_content_non_existent_function(): void {

        $this->expectException('coding_exception');
        $result = external::get_content('tool_mobile', 'test_blahblah');
    }

    /**
     * Test get_content incorrect component.
     */
    public function test_get_content_invalid_component(): void {

        $this->expectException('moodle_exception');
        $result = external::get_content('tool_mobile\hack', 'test_view');
    }

    /**
     * Test get_content non existent component.
     */
    public function test_get_content_non_existent_component(): void {

        $this->expectException('moodle_exception');
        $result = external::get_content('tool_blahblahblah', 'test_view');
    }

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

        $this->resetAfterTest(true);

        $category = self::getDataGenerator()->create_category(array('name' => 'Category 1'));
        $course = self::getDataGenerator()->create_course([
            'category' => $category->id,
            'shortname' => 'c1',
            'summary' => '<span lang="en" class="multilang">Course summary</span>'
                . '<span lang="eo" class="multilang">Kurso resumo</span>'
                . '@@PLUGINFILE@@/filename.txt'
                . '<!-- Comment stripped when formatting text -->',
            'summaryformat' => FORMAT_MOODLE
        ]);
        $user1 = self::getDataGenerator()->create_user(['username' => 'user1', 'lastaccess' => time()]);
        $user2 = self::getDataGenerator()->create_user(['username' => 'user2', 'lastaccess' => time()]);

        self::setUser($user1);

        // Setup WS token.
        $webservicemanager = new \webservice;
        $service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
        $token = \core_external\util::generate_token_for_current_user($service);
        $_POST['wstoken'] = $token->token;

        // Workaround for external_api::call_external_function requiring sesskey.
        $_POST['sesskey'] = sesskey();

        // Call some functions.
        $requests = [
            [
                'function' => 'core_course_get_courses_by_field',
                'arguments' => json_encode(['field' => 'id', 'value' => $course->id])
            ],
            [
                'function' => 'core_user_get_users_by_field',
                'arguments' => json_encode(['field' => 'id', 'values' => [$user1->id]])
            ],
            [
                'function' => 'core_user_get_user_preferences',
                'arguments' => json_encode(['name' => 'some_setting', 'userid' => $user2->id])
            ],
            [
                'function' => 'core_course_get_courses_by_field',
                'arguments' => json_encode(['field' => 'shortname', 'value' => $course->shortname])
            ],
        ];
        $result = external::call_external_functions($requests);

        // We need to execute the return values cleaning process to simulate the web service server.
        $result = external_api::clean_returnvalue(external::call_external_functions_returns(), $result);

        // Only 3 responses, the 4th request is not executed because the 3rd throws an exception.
        $this->assertCount(3, $result['responses']);

        $this->assertFalse($result['responses'][0]['error']);
        $coursedata = external_api::clean_returnvalue(
            \core_course_external::get_courses_by_field_returns(),
            \core_course_external::get_courses_by_field('id', $course->id));
         $this->assertEquals(json_encode($coursedata), $result['responses'][0]['data']);

        $this->assertFalse($result['responses'][1]['error']);
        $userdata = external_api::clean_returnvalue(
            \core_user_external::get_users_by_field_returns(),
            \core_user_external::get_users_by_field('id', [$user1->id]));
        $this->assertEquals(json_encode($userdata), $result['responses'][1]['data']);

        $this->assertTrue($result['responses'][2]['error']);
        $exception = json_decode($result['responses'][2]['exception'], true);
        $this->assertEquals('nopermissions', $exception['errorcode']);

        // Call a function not included in the external service.

        $_POST['wstoken'] = $token->token;
        $functions = $webservicemanager->get_not_associated_external_functions($service->id);
        $requests = [['function' => current($functions)->name]];
        $result = external::call_external_functions($requests);

        $this->assertTrue($result['responses'][0]['error']);
        $exception = json_decode($result['responses'][0]['exception'], true);
        $this->assertEquals('accessexception', $exception['errorcode']);
        $this->assertEquals('webservice', $exception['module']);

        // Call a function with different external settings.

        filter_set_global_state('multilang', TEXTFILTER_ON);
        $_POST['wstoken'] = $token->token;
        $SESSION->lang = 'eo'; // Change default language, so we can test changing it to "en".
        $requests = [
            [
                'function' => 'core_course_get_courses_by_field',
                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
            ],
            [
                'function' => 'core_course_get_courses_by_field',
                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
                'settingraw' => '1'
            ],
            [
                'function' => 'core_course_get_courses_by_field',
                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
                'settingraw' => '1',
                'settingfileurl' => '0'
            ],
            [
                'function' => 'core_course_get_courses_by_field',
                'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
                'settingfilter' => '1',
                'settinglang' => 'en'
            ],
        ];
        $result = external::call_external_functions($requests);

        $this->assertCount(4, $result['responses']);

        $context = \context_course::instance($course->id);
        $pluginfile = 'webservice/pluginfile.php';

        $this->assertFalse($result['responses'][0]['error']);
        $data = json_decode($result['responses'][0]['data']);
        $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
        $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => false]);
        $this->assertEquals($expected, $data->courses[0]->summary);

        $this->assertFalse($result['responses'][1]['error']);
        $data = json_decode($result['responses'][1]['data']);
        $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
        $this->assertEquals($expected, $data->courses[0]->summary);

        $this->assertFalse($result['responses'][2]['error']);
        $data = json_decode($result['responses'][2]['data']);
        $this->assertEquals($course->summary, $data->courses[0]->summary);

        $this->assertFalse($result['responses'][3]['error']);
        $data = json_decode($result['responses'][3]['data']);
        $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
        $SESSION->lang = 'en'; // We expect filtered text in english.
        $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
        $this->assertEquals($expected, $data->courses[0]->summary);
    }

    /*
     * Test get_tokens_for_qr_login.
     */
    public function test_get_tokens_for_qr_login(): void {
        global $DB, $CFG, $USER;

        $this->resetAfterTest(true);

        $user = $this->getDataGenerator()->create_user();
        $this->setUser($user);

        $mobilesettings = get_config('tool_mobile');
        $mobilesettings->qrsameipcheck = 1;
        $qrloginkey = api::get_qrlogin_key($mobilesettings);

        // Generate new tokens, the ones we expect to receive.
        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
        $token = \core_external\util::generate_token_for_current_user($service);

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
        $result = external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);

        $this->assertEmpty($result['warnings']);
        $this->assertEquals($token->token, $result['token']);
        $this->assertEquals($token->privatetoken, $result['privatetoken']);

        // Now, try with an invalid key.
        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('invalidkey', 'error'));
        $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
    }

    /*
     * Test get_tokens_for_qr_login ignore ip check.
     */
    public function test_get_tokens_for_qr_login_ignore_ip_check(): void {
        global $DB, $CFG, $USER;

        $this->resetAfterTest(true);

        $user = $this->getDataGenerator()->create_user();
        $this->setUser($user);

        $mobilesettings = get_config('tool_mobile');
        $mobilesettings->qrsameipcheck = 0;
        $qrloginkey = api::get_qrlogin_key($mobilesettings);

        $key = $DB->get_record('user_private_key', ['value' => $qrloginkey]);
        $this->assertNull($key->iprestriction);

        // Generate new tokens, the ones we expect to receive.
        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
        $token = \core_external\util::generate_token_for_current_user($service);

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
        $result = external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);

        $this->assertEmpty($result['warnings']);
        $this->assertEquals($token->token, $result['token']);
        $this->assertEquals($token->privatetoken, $result['privatetoken']);

        // Now, try with an invalid key.
        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('invalidkey', 'error'));
        $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
    }

    /*
     * Test get_tokens_for_qr_login ip check fails.
     */
    public function test_get_tokens_for_qr_login_ip_check_mismatch(): void {
        global $DB, $CFG, $USER;

        $this->resetAfterTest(true);

        $user = $this->getDataGenerator()->create_user();
        $this->setUser($user);

        $mobilesettings = get_config('tool_mobile');
        $mobilesettings->qrsameipcheck = 1;
        $qrloginkey = api::get_qrlogin_key($mobilesettings);

        // Alter expected ip.
        $DB->set_field('user_private_key', 'iprestriction', '6.6.6.6', ['value' => $qrloginkey]);

        // Generate new tokens, the ones we expect to receive.
        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
        $token = \core_external\util::generate_token_for_current_user($service);

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('ipmismatch', 'error'));
        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
    }

    /**
     * Test get_tokens_for_qr_login missing QR code enabled.
     */
    public function test_get_tokens_for_qr_login_missing_enableqr(): void {
        global $CFG, $USER;
        $this->resetAfterTest(true);
        $this->setAdminUser();

        set_config('qrcodetype', api::QR_CODE_DISABLED, 'tool_mobile');

        $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile'));
        $result = external::get_tokens_for_qr_login('', $USER->id);
    }

    /**
     * Test get_tokens_for_qr_login missing ws.
     */
    public function test_get_tokens_for_qr_login_missing_ws(): void {
        global $CFG;
        $this->resetAfterTest(true);

        $user = $this->getDataGenerator()->create_user();
        $this->setUser($user);

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        // Need to disable webservices to verify that's checked.
        $CFG->enablewebservices = 0;
        $CFG->enablemobilewebservice = 0;

        $this->setAdminUser();
        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
        $result = external::get_tokens_for_qr_login('', $user->id);
    }

    /**
     * Test get_tokens_for_qr_login missing https.
     */
    public function test_get_tokens_for_qr_login_missing_https(): void {
        global $CFG, $USER;

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        // Need to simulate a non HTTPS site here.
        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);

        $this->resetAfterTest(true);
        $this->setAdminUser();

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
        $result = external::get_tokens_for_qr_login('', $USER->id);
    }

    /**
     * Test get_tokens_for_qr_login missing admin.
     */
    public function test_get_tokens_for_qr_login_missing_admin(): void {
        global $CFG, $USER;

        $this->resetAfterTest(true);
        $this->setAdminUser();

        // Fake the app.
        \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
        $result = external::get_tokens_for_qr_login('', $USER->id);
    }

    /**
     * Test get_tokens_for_qr_login missing app_request.
     */
    public function test_get_tokens_for_qr_login_missing_app_request(): void {
        global $CFG, $USER;

        $this->resetAfterTest(true);
        $this->setAdminUser();

        $this->expectException('moodle_exception');
        $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
        $result = external::get_tokens_for_qr_login('', $USER->id);
    }

    /**
     * Test validate subscription key.
     */
    public function test_validate_subscription_key_valid(): void {
        $this->resetAfterTest(true);

        $sitesubscriptionkey = ['validuntil' => time() + MINSECS, 'key' => complex_random_string(32)];
        set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');

        $result = external::validate_subscription_key($sitesubscriptionkey['key']);
        $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
        $this->assertEmpty($result['warnings']);
        $this->assertTrue($result['validated']);
    }

    /**
     * Test validate subscription key invalid first and then a valid one.
     */
    public function test_validate_subscription_key_invalid_key_first(): void {
        $this->resetAfterTest(true);

        $sitesubscriptionkey = ['validuntil' => time() + MINSECS, 'key' => complex_random_string(32)];
        set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');

        $result = external::validate_subscription_key('fakekey');
        $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
        $this->assertEmpty($result['warnings']);
        $this->assertFalse($result['validated']);

        // The valid one has been invalidated because the previous attempt.
        $result = external::validate_subscription_key($sitesubscriptionkey['key']);
        $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
        $this->assertEmpty($result['warnings']);
        $this->assertFalse($result['validated']);
    }

    /**
     * Test validate subscription key invalid.
     */
    public function test_validate_subscription_key_invalid_key(): void {
        $this->resetAfterTest(true);

        $result = external::validate_subscription_key('fakekey');
        $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
        $this->assertEmpty($result['warnings']);
        $this->assertFalse($result['validated']);
    }

    /**
     * Test validate subscription key invalid.
     */
    public function test_validate_subscription_key_outdated(): void {
        $this->resetAfterTest(true);

        $sitesubscriptionkey = ['validuntil' => time() - MINSECS, 'key' => complex_random_string(32)];
        set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');

        $result = external::validate_subscription_key($sitesubscriptionkey['key']);
        $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
        $this->assertEmpty($result['warnings']);
        $this->assertFalse($result['validated']);
    }
}
