<?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 qtype_ordering;

use core_question_generator;
use phpunit_util;
use qtype_ordering;
use qtype_ordering_test_helper;
use qtype_ordering_edit_form;
use qtype_ordering_question;
use test_question_maker;
use question_bank;
use question_possible_response;
use qformat_xml;
use qformat_gift;
use question_check_specified_fields_expectation;

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

global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/type/edit_question_form.php');
require_once($CFG->dirroot . '/question/type/ordering/questiontype.php');
require_once($CFG->dirroot . '/question/type/ordering/edit_ordering_form.php');

require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/format/gift/format.php');
require_once($CFG->dirroot . '/question/format/xml/format.php');

/**
 * Unit tests for the ordering question type class.
 *
 * @package   qtype_ordering
 * @copyright 2018 The Open University
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @covers    \qtype_ordering
 * @covers    \qtype_ordering_question
 */
final class questiontype_test extends \question_testcase {

    /**
     * Define the import object to compare against.
     *
     * @return object The expected import object.
     */
    private static function expectedimport(): object {
        return (object) [
            'qtype' => 'ordering',
            'idnumber' => 'myid',
            'name' => 'Moodle',
            'length' => 1,
            'penalty' => 0.3333333,
            'questiontext' => 'Put these words in order.',
            'questiontextformat' => 1,
            'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
            'generalfeedbackformat' => 1,
            'defaultmark' => 1,
        ];
    }

    public function test_name(): void {
        $ordering = new qtype_ordering();
        $this->assertEquals('ordering', $ordering->name());
    }

    public function test_can_analyse_responses(): void {
        $ordering = new qtype_ordering();
        $this->assertTrue($ordering->can_analyse_responses());
    }

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

        $questiondata = test_question_maker::get_question_data('ordering');
        $formdata = test_question_maker::get_question_form_data('ordering');

        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
        $cat = $generator->create_question_category([]);

        $formdata->category = "{$cat->id},{$cat->contextid}";

        qtype_ordering_edit_form::mock_submit((array) $formdata);

        $form = qtype_ordering_test_helper::get_question_editing_form($cat, $questiondata);
        $this->assertTrue($form->is_validated());

        $fromform = $form->get_data();
        $ordering = new qtype_ordering();

        $returnedfromsave = $ordering->save_question($questiondata, $fromform);
        $actualquestiondata = question_bank::load_question_data($returnedfromsave->id);

        foreach ($questiondata as $property => $value) {
            if (!in_array($property, ['id', 'version', 'timemodified', 'timecreated', 'options', 'stamp'])) {
                $this->assertContainsEquals($value, (array)$actualquestiondata);
                $this->assertContainsEquals($property, array_keys((array)$actualquestiondata));
            }
        }

        foreach ($questiondata->options as $optionname => $value) {
            if ($optionname != 'answers') {
                $this->assertContainsEquals($value, (array)$actualquestiondata->options);
                $this->assertContainsEquals($optionname, array_keys((array)$actualquestiondata->options));
            }
        }

        foreach ($questiondata->options->answers as $answer) {
            $actualanswer = array_shift($actualquestiondata->options->answers);
            foreach ($answer as $ansproperty => $ansvalue) {
                if ($ansproperty === 'question') {
                    $this->assertContainsEquals($returnedfromsave->id, (array)$actualanswer);
                    $this->assertContainsEquals($ansproperty, array_keys((array)$actualanswer));
                } else if ($ansproperty !== 'id') {
                    $this->assertContainsEquals($ansvalue, (array)$actualanswer);
                    $this->assertContainsEquals($ansproperty, array_keys((array)$actualanswer));
                }
            }
        }
    }

    public function test_get_possible_responses(): void {
        $questiondata = test_question_maker::get_question_data('ordering');
        $ordering = new qtype_ordering();
        $possibleresponses = $ordering->get_possible_responses($questiondata);
        $expectedresponseclasses = [
            'Modular' => [
                    1 => new question_possible_response('Position 1', 0.1666667),
                    2 => new question_possible_response('Position 2', 0),
                    3 => new question_possible_response('Position 3', 0),
                    4 => new question_possible_response('Position 4', 0),
                    5 => new question_possible_response('Position 5', 0),
                    6 => new question_possible_response('Position 6', 0),
            ],
            'Object' => [
                    1 => new question_possible_response('Position 1', 0),
                    2 => new question_possible_response('Position 2', 0.1666667),
                    3 => new question_possible_response('Position 3', 0),
                    4 => new question_possible_response('Position 4', 0),
                    5 => new question_possible_response('Position 5', 0),
                    6 => new question_possible_response('Position 6', 0),
            ],
            'Oriented' => [
                    1 => new question_possible_response('Position 1', 0),
                    2 => new question_possible_response('Position 2', 0),
                    3 => new question_possible_response('Position 3', 0.1666667),
                    4 => new question_possible_response('Position 4', 0),
                    5 => new question_possible_response('Position 5', 0),
                    6 => new question_possible_response('Position 6', 0),
            ],
            'Dynamic' => [
                    1 => new question_possible_response('Position 1', 0),
                    2 => new question_possible_response('Position 2', 0),
                    3 => new question_possible_response('Position 3', 0),
                    4 => new question_possible_response('Position 4', 0.1666667),
                    5 => new question_possible_response('Position 5', 0),
                    6 => new question_possible_response('Position 6', 0),
            ],
            'Learning' => [
                    1 => new question_possible_response('Position 1', 0),
                    2 => new question_possible_response('Position 2', 0),
                    3 => new question_possible_response('Position 3', 0),
                    4 => new question_possible_response('Position 4', 0),
                    5 => new question_possible_response('Position 5', 0.1666667),
                    6 => new question_possible_response('Position 6', 0),
            ],
            'Environment' => [
                    1 => new question_possible_response('Position 1', 0),
                    2 => new question_possible_response('Position 2', 0),
                    3 => new question_possible_response('Position 3', 0),
                    4 => new question_possible_response('Position 4', 0),
                    5 => new question_possible_response('Position 5', 0),
                    6 => new question_possible_response('Position 6', 0.1666667),
            ],
        ];
        $this->assertEqualsWithDelta($expectedresponseclasses, $possibleresponses, 0.0000005);
    }

    public function test_get_possible_responses_very_long(): void {
        $questiondata = test_question_maker::get_question_data('ordering');
        $ordering = new qtype_ordering();
        $onehundredchars = str_repeat('1234567890', 9) . '123456789碁';
        // Set one of the answers to over 100 chars, with a multi-byte UTF-8 character at position 100.
        $questiondata->options->answers[13]->answer = $onehundredchars . 'and some more';
        $possibleresponses = $ordering->get_possible_responses($questiondata);
        $this->assertArrayHasKey($onehundredchars, $possibleresponses);
    }

    public function test_get_numberingstyle(): void {
        $questiondata = test_question_maker::get_question_data('ordering');
        $ordering = new qtype_ordering();
        $expected = qtype_ordering_question::NUMBERING_STYLE_DEFAULT;
        $actual = $ordering->get_numberingstyle($questiondata);
        $this->assertEquals($expected, $actual);

        $questiondata->options->numberingstyle = 'abc';
        $expected = 'abc';
        $actual = $ordering->get_numberingstyle($questiondata);
        $this->assertEquals($expected, $actual);

        $questiondata->options->numberingstyle = 'ABCD';
        $expected = 'ABCD';
        $actual = $ordering->get_numberingstyle($questiondata);
        $this->assertEquals($expected, $actual);

        $questiondata->options->numberingstyle = '123';
        $expected = '123';
        $actual = $ordering->get_numberingstyle($questiondata);
        $this->assertEquals($expected, $actual);

        $questiondata->options->numberingstyle = 'iii';
        $expected = 'iii';
        $actual = $ordering->get_numberingstyle($questiondata);
        $this->assertEquals($expected, $actual);

        $questiondata->options->numberingstyle = 'III';
        $expected = 'III';
        $actual = $ordering->get_numberingstyle($questiondata);
        $this->assertEquals($expected, $actual);
    }

    public function test_xml_import(): void {
        $this->resetAfterTest();
        // Import a question from XML.
        $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'testimport.moodle.xml'));
        $xmldata = (new \core\xml_parser())->parse($xml);
        $format = new qformat_xml();
        $imported = $format->try_importing_using_qtypes(
            $xmldata['question'], null, null, 'ordering');

        $this->assert(new question_check_specified_fields_expectation(self::expectedimport()), $imported);
    }

    public function test_xml_import_long(): void {
        $this->resetAfterTest();
        // Import a question from XML.
        $xml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'testimportlong.moodle.xml'));
        $xmldata = (new \core\xml_parser())->parse($xml);
        $format = new qformat_xml();
        $imported = $format->try_importing_using_qtypes(
            $xmldata['question'], null, null, 'ordering');

        $expected = self::expectedimport();
        $expected->name = 'Moodle Moodle Moodle Moodle Moodle Moodle Moodle';

        $this->assert(new question_check_specified_fields_expectation($expected), $imported);
    }

    public function test_xml_export(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
        $category = $generator->create_question_category();
        $question = $generator->create_question('ordering', 'moodle',
            ['category' => $category->id, 'idnumber' => 'myid']);

        // Export it.
        $questiondata = question_bank::load_question_data($question->id);
        // Force the question id to be 123, to ensure it comes through the export.
        $questiondata->id = 123;
        $questiondata->options->numberingstyle = null;
        // Add some feedback to ensure it comes through the export.
        foreach ($questiondata->options->answers as $answer) {
            $answer->feedback = $answer->answer . ' is correct.';
            $answer->feedbackformat = FORMAT_HTML;
            $answer->feedbackfiles = 0;
        }

        $exporter = new qformat_xml();
        $xml = $exporter->writequestion($questiondata);

        $expectedxml = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'testexport.moodle.xml'));

        $this->assert_same_xml($expectedxml, $xml);
    }

    public function test_gift_import(): void {
        $this->resetAfterTest();
        // Import a question from GIFT.
        $gift = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'testimport.gift.txt'));
        $format = new qformat_gift();
        $lines = preg_split('/[\\n\\r]/', phpunit_util::normalise_line_endings($gift));
        $imported = $format->readquestion($lines);

        $this->assert(new question_check_specified_fields_expectation(self::expectedimport()), $imported);
    }

    public function test_gift_export(): void {
        $this->resetAfterTest();
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
        $category = $generator->create_question_category();
        $question = $generator->create_question('ordering', 'moodle',
            ['category' => $category->id, 'idnumber' => 'myid']);

        // Export it.
        $questiondata = question_bank::load_question_data($question->id);
        // Force the question id to be 123, to ensure it comes through the export.
        $questiondata->id = 123;

        $exporter = new qformat_gift();
        $gift = $exporter->writequestion($questiondata);

        $expectedgift = file_get_contents(self::get_fixture_path(__NAMESPACE__, 'testexport.gift.txt'));

        $this->assertEquals(
            phpunit_util::normalise_line_endings($expectedgift),
            phpunit_util::normalise_line_endings($gift)
        );
    }

    /**
     * Test that move_files moves the files for answer, combined feedback and hints
     */
    public function test_move_files(): void {
        global $DB;

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

        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
        $category = $generator->create_question_category();
        $question = $generator->create_question('ordering', 'moodle', ['category' => $category->id]);

        $oldcontextid = $category->contextid;

        $fs = get_file_storage();

        // Put files into various fileareas in the old context.
        // Answer areas (both 'answer' and 'answerfeedback').
        $answers = $DB->get_records('question_answers', ['question' => $question->id]);
        foreach ($answers as $ans) {
            $fs->create_file_from_string(
                [
                    'contextid' => $oldcontextid,
                    'component' => 'question',
                    'filearea' => 'answer',
                    'itemid' => $ans->id,
                    'filepath' => '/',
                    'filename' => 'ans.txt',
                ],
                'answer area content'
            );

            $fs->create_file_from_string(
                [
                    'contextid' => $oldcontextid,
                    'component' => 'question',
                    'filearea' => 'answerfeedback',
                    'itemid' => $ans->id,
                    'filepath' => '/',
                    'filename' => 'ansfb.txt',
                ],
                'answer feedback content'
            );
        }

        // Combined feedback areas.
        $fs->create_file_from_string(
            [
                'contextid' => $oldcontextid,
                'component' => 'question',
                'filearea' => 'correctfeedback',
                'itemid' => $question->id,
                'filepath' => '/',
                'filename' => 'correct.txt',
            ],
            'correct'
        );
        $fs->create_file_from_string(
            [
                'contextid' => $oldcontextid,
                'component' => 'question',
                'filearea' => 'partiallycorrectfeedback',
                'itemid' => $question->id,
                'filepath' => '/',
                'filename' => 'partial.txt',
            ],
            'partial'
        );
        $fs->create_file_from_string(
            [
                'contextid' => $oldcontextid,
                'component' => 'question',
                'filearea' => 'incorrectfeedback',
                'itemid' => $question->id,
                'filepath' => '/',
                'filename' => 'incorrect.txt',
            ],
            'incorrect'
        );

        // Create a hint and add a file for it.
        $hint = new \stdClass();
        $hint->questionid = $question->id;
        $hint->hint = 'a hint';
        $hint->hintformat = FORMAT_MOODLE;
        $hint->shownumcorrect = 0;
        $hintid = $DB->insert_record('question_hints', $hint);
        $fs->create_file_from_string(
            [
            'contextid' => $oldcontextid,
            'component' => 'question',
            'filearea' => 'hint',
            'itemid' => $hintid,
            'filepath' => '/',
            'filename' => 'hint.txt',
            ],
            'hinttext'
        );

        // Destination context (different category).
        $newcategory = $generator->create_question_category();
        $newcontextid = $newcategory->contextid;

        $qtype = new qtype_ordering();
        $qtype->move_files($question->id, $oldcontextid, $newcontextid);

        foreach ($answers as $ans) {
            // Answer 'answer' files should been moved.
            $oldfiles = $fs->get_area_files($oldcontextid, 'question', 'answer', $ans->id, 'id', false);
            $this->assertEmpty($oldfiles);
            $newfiles = $fs->get_area_files($newcontextid, 'question', 'answer', $ans->id, 'id', false);
            $this->assertNotEmpty($newfiles);

            // Answer feedback should have been moved.
            $oldaf = $fs->get_area_files($oldcontextid, 'question', 'answerfeedback', $ans->id, 'id', false);
            $this->assertEmpty($oldaf);
            $newaf = $fs->get_area_files($newcontextid, 'question', 'answerfeedback', $ans->id, 'id', false);
            $this->assertNotEmpty($newaf);
        }

        // Combined feedback and hint files should have been moved.
        $this->assertEmpty($fs->get_area_files($oldcontextid, 'question', 'correctfeedback', $question->id, 'id', false));
        $this->assertNotEmpty($fs->get_area_files($newcontextid, 'question', 'correctfeedback', $question->id, 'id', false));

        $this->assertEmpty($fs->get_area_files($oldcontextid, 'question', 'partiallycorrectfeedback', $question->id, 'id', false));
        $this->assertNotEmpty(
            $fs->get_area_files(
                $newcontextid,
                'question',
                'partiallycorrectfeedback',
                $question->id,
                'id',
                false
            )
        );

        $this->assertEmpty(
            $fs->get_area_files(
                $oldcontextid,
                'question',
                'incorrectfeedback',
                $question->id,
                'id',
                false
            )
        );
        $this->assertNotEmpty(
            $fs->get_area_files(
                $newcontextid,
                'question',
                'incorrectfeedback',
                $question->id,
                'id',
                false
            )
        );

        $this->assertEmpty($fs->get_area_files($oldcontextid, 'question', 'hint', $hintid, 'id', false));
        $this->assertNotEmpty($fs->get_area_files($newcontextid, 'question', 'hint', $hintid, 'id', false));
    }

    /**
     * Test that delete_files deletes question's files in questiontext,
     * generalfeedback, answers, combined feedback and hints.
     */
    public function test_delete_files(): void {
        global $DB;

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

        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
        $category = $generator->create_question_category();
        $question = $generator->create_question('ordering', 'moodle', ['category' => $category->id]);

        $contextid = $category->contextid;
        $fs = get_file_storage();

        // Add files to questiontext and generalfeedback.
        $fs->create_file_from_string([
            'contextid' => $contextid,
            'component' => 'question',
            'filearea' => 'questiontext',
            'itemid' => $question->id,
            'filepath' => '/',
            'filename' => 'qtext.txt',
        ], 'qtext');
        $fs->create_file_from_string([
            'contextid' => $contextid,
            'component' => 'question',
            'filearea' => 'generalfeedback',
            'itemid' => $question->id,
            'filepath' => '/',
            'filename' => 'gfb.txt',
        ], 'gfb');

        // Answer files.
        $answers = $DB->get_records('question_answers', ['question' => $question->id]);
        foreach ($answers as $ans) {
            $fs->create_file_from_string([
                'contextid' => $contextid,
                'component' => 'question',
                'filearea' => 'answer',
                'itemid' => $ans->id,
                'filepath' => '/',
                'filename' => 'ans.txt',
            ], 'answer area content');
            $fs->create_file_from_string([
                'contextid' => $contextid,
                'component' => 'question',
                'filearea' => 'answerfeedback',
                'itemid' => $ans->id,
                'filepath' => '/',
                'filename' => 'ansfb.txt',
            ], 'answer feedback content');
        }

        // Combined feedback.
        $fs->create_file_from_string(
            [
                'contextid' => $contextid,
                'component' => 'question',
                'filearea' => 'correctfeedback',
                'itemid' => $question->id,
                'filepath' => '/',
                'filename' => 'correct.txt',
            ],
            'correct'
        );

        // Hint.
        $hint = new \stdClass();
        $hint->questionid = $question->id;
        $hint->hint = 'a hint';
        $hint->hintformat = FORMAT_MOODLE;
        $hint->shownumcorrect = 0;
        $hintid = $DB->insert_record('question_hints', $hint);
        $fs->create_file_from_string(
            [
                'contextid' => $contextid,
                'component' => 'question',
                'filearea' => 'hint',
                'itemid' => $hintid,
                'filepath' => '/',
                'filename' => 'hint.txt',
            ],
            'hinttext'
        );

        $qtype = new qtype_ordering();
        $qtype->delete_files($question->id, $contextid);

        // Questiontext and generalfeedback removed.
        $this->assertEmpty($fs->get_area_files($contextid, 'question', 'questiontext', $question->id, 'id', false));
        $this->assertEmpty($fs->get_area_files($contextid, 'question', 'generalfeedback', $question->id, 'id', false));

        // Answer and answerfeedback removed.
        foreach ($answers as $ans) {
            $this->assertEmpty($fs->get_area_files($contextid, 'question', 'answer', $ans->id, 'id', false));
            $this->assertEmpty($fs->get_area_files($contextid, 'question', 'answerfeedback', $ans->id, 'id', false));
        }

        // Combined feedback and hint files removed.
        $this->assertEmpty($fs->get_area_files($contextid, 'question', 'correctfeedback', $question->id, 'id', false));
        $this->assertEmpty($fs->get_area_files($contextid, 'question', 'hint', $hintid, 'id', false));
    }
}
