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

/**
 * Unit tests for the STACK question type class.
 *
 * @package   qtype_stack
 * @copyright 2012 The Open University.
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
 */

namespace qtype_stack;

use qtype_stack;
use qtype_stack_walkthrough_test_base;
use stack_potentialresponse_tree_state;
use stack_question_test;
use test_question_maker;
use question_possible_response;
use question_check_specified_fields_expectation;
use context_system;
use stdClass;
use function stack_utils\get_config;
use qformat_xml;


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

global $CFG;
require_once(__DIR__ . '/fixtures/test_base.php');
require_once($CFG->dirroot . '/question/format/xml/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once(__DIR__ . '/../questiontype.php');

/**
 * Unit tests for the STACK question type class.
 *
 * @group qtype_stack
 * @covers \qtype_stack
 */
final class questiontype_test extends qtype_stack_walkthrough_test_base {

    /** @var qtype_stack */
    private $qtype;

    public function setUp(): void {
        parent::setUp();
        $this->qtype = new qtype_stack();
    }

    public function tearDown(): void {
        $this->qtype = null;
        parent::tearDown();
    }

    // phpcs:ignore moodle.Commenting.MissingDocblock.MissingTestcaseMethodDescription
    public function assert_same_xml($expectedxml, $xml) {
        $this->assertEquals(str_replace("\r\n", "\n", $expectedxml),
                str_replace("\r\n", "\n", $xml));
    }

    public function test_name(): void {

        $this->assertEquals($this->qtype->name(), 'stack');
    }

    public function test_get_possible_responses_test0(): void {

        $qdata = test_question_maker::get_question_data('stack', 'test0');

        $expected = [
            'firsttree-0' => [
                'firsttree-1-F' => new question_possible_response('firsttree-1-F', 0),
                'firsttree-1-T' => new question_possible_response('firsttree-1-T', 1),
                null          => question_possible_response::no_response(),
            ],
        ];

        $this->assertEquals($expected, $this->qtype->get_possible_responses($qdata));
    }

    public function test_get_possible_responses_test3(): void {

        $qdata = test_question_maker::get_question_data('stack', 'test3');

        $expected = [
            'odd-0' => [
                'odd-0-0' => new question_possible_response('odd-0-0', 0),
                'odd-0-1' => new question_possible_response('odd-0-1', 0.25),
                null     => question_possible_response::no_response(),
            ],
            'even-0' => [
                'even-0-0' => new question_possible_response('even-0-0', 0),
                'even-0-1' => new question_possible_response('even-0-1', 0.25),
                null      => question_possible_response::no_response(),
            ],
            'oddeven-0' => [
                'oddeven-0-0' => new question_possible_response('oddeven-0-0', 0),
                'oddeven-0-1' => new question_possible_response('oddeven-0-1', 0.125),
                null         => question_possible_response::no_response(),
            ],
            'oddeven-1' => [
                'oddeven-1-0' => new question_possible_response('oddeven-1-0', 0),
                'oddeven-1-1' => new question_possible_response('oddeven-1-1', 0.125),
                null         => question_possible_response::no_response(),
            ],
            'unique-0' => [
                'unique-0-0' => new question_possible_response('unique-0-0', 0),
                'unique-0-1' => new question_possible_response('unique-0-1', 0.25),
                null        => question_possible_response::no_response(),
            ],
        ];

        $this->assertEquals($expected, $this->qtype->get_possible_responses($qdata));
    }

    public function test_get_possible_responses_test4(): void {

        $qdata = test_question_maker::get_question_data('stack', 'variable_grade');

        $expected = [
            'firsttree-0' => [
                'firsttree-1-F' => new question_possible_response('firsttree-1-F', 0),
                'firsttree-1-T' => new question_possible_response('firsttree-1-T', 0.0),
                null          => question_possible_response::no_response(),
            ],
        ];

        $this->assertEquals($expected, $this->qtype->get_possible_responses($qdata));
    }

    public function test_initialise_question_instance(): void {

        $qdata = test_question_maker::get_question_data('stack', 'test3');
        $q = $this->qtype->make_question($qdata);
        $expectedq = test_question_maker::make_question('stack', 'test3');
        $expectedq->stamp = $q->stamp;
        $expectedq->version = $q->version;
        $expectedq->timemodified = $q->timemodified;
        $expectedq->prts = null;
        $q->prts = null;
        $this->assertEquals($expectedq, $q);
    }

    public function test_question_tests_test3(): void {

        // This unit test runs a question test, really just to verify that
        // there are no errors.
        global $DB;
        $this->resetAfterTest();
        $this->setAdminUser();

        // Create a test question.
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
        $cat = $generator->create_question_category();
        $question = $generator->create_question('stack', 'test3', ['category' => $cat->id]);
        $questionid = $question->id;
        $seed = 1;

        $testcases = [];
        $qtest = new stack_question_test('', ['ans1' => 'x^3']);
        $qtest->add_expected_result('odd', new stack_potentialresponse_tree_state(
                1, true, 1, 0, '', ['odd-1-T']));
        $testcases[] = $qtest;

        $qtest = new stack_question_test('', ['ans1' => 'x^2']);
        $qtest->add_expected_result('odd', new stack_potentialresponse_tree_state(
                1, true, 0, 0.4, '', ['odd-1-F']));
        $testcases[] = $qtest;

        // This unit test runs a question test, with an input name as
        // the expected answer, which should work.
        $qtest = new stack_question_test('', ['ans2' => 'ans2']);
        $qtest->add_expected_result('even', new stack_potentialresponse_tree_state(
                1, true, 1, 0, '', ['even-1-T']));

        foreach ($testcases as $testcase) {
            $result = $testcase->test_question($questionid, $seed, context_system::instance());
            $this->assertTrue($result->passed());
        }
    }

    public function test_xml_export(): void {

        $qdata = test_question_maker::get_question_data('stack', 'test0');

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

        $expectedxml = '<!-- question: 0  -->
  <question type="stack">
    <name>
      <text>test-0</text>
    </name>
    <questiontext format="html">
      <text>What is $1+1$? [[input:ans1]]
                                [[validation:ans1]]</text>
    </questiontext>
    <generalfeedback format="html">
      <text></text>
    </generalfeedback>
    <defaultgrade>1</defaultgrade>
    <penalty>0.3333333</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <stackversion>
      <text>' . \get_config('qtype_stack', 'version') . '</text>
    </stackversion>
    <questionvariables>
      <text></text>
    </questionvariables>
    <specificfeedback format="html">
      <text>[[feedback:firsttree]]</text>
    </specificfeedback>
    <questionnote format="html">
      <text></text>
    </questionnote>
    <questiondescription format="html">
      <text>This is a rather wonderful question!</text>
    </questiondescription>
    <questionsimplify>1</questionsimplify>
    <assumepositive>0</assumepositive>
    <assumereal>0</assumereal>
    <prtcorrect format="html">
      <text><![CDATA[<p>Correct answer, well done.</p>]]></text>
    </prtcorrect>
    <prtpartiallycorrect format="html">
      <text><![CDATA[<p>Your answer is partially correct.</p>]]></text>
    </prtpartiallycorrect>
    <prtincorrect format="html">
      <text><![CDATA[<p>Incorrect answer.</p>]]></text>
    </prtincorrect>
    <decimals>.</decimals>
    <scientificnotation>*10</scientificnotation>
    <multiplicationsign>dot</multiplicationsign>
    <sqrtsign>1</sqrtsign>
    <complexno>i</complexno>
    <inversetrig>cos-1</inversetrig>
    <logicsymbol>lang</logicsymbol>
    <matrixparens>[</matrixparens>
    <isbroken>0</isbroken>
    <variantsselectionseed></variantsselectionseed>
    <input>
      <name>ans1</name>
      <type>algebraic</type>
      <tans>2</tans>
      <boxsize>5</boxsize>
      <strictsyntax>1</strictsyntax>
      <insertstars>0</insertstars>
      <syntaxhint></syntaxhint>
      <syntaxattribute>0</syntaxattribute>
      <forbidwords></forbidwords>
      <allowwords></allowwords>
      <forbidfloat>1</forbidfloat>
      <requirelowestterms>0</requirelowestterms>
      <checkanswertype>0</checkanswertype>
      <mustverify>1</mustverify>
      <showvalidation>1</showvalidation>
      <options></options>
    </input>
    <prt>
      <name>firsttree</name>
      <value>1</value>
      <autosimplify>1</autosimplify>
      <feedbackstyle>1</feedbackstyle>
      <feedbackvariables>
        <text></text>
      </feedbackvariables>
      <node>
        <name>0</name>
        <description></description>
        <answertest>EqualComAss</answertest>
        <sans>ans1</sans>
        <tans>2</tans>
        <testoptions></testoptions>
        <quiet>0</quiet>
        <truescoremode>=</truescoremode>
        <truescore>1</truescore>
        <truepenalty>0</truepenalty>
        <truenextnode>-1</truenextnode>
        <trueanswernote>firsttree-1-T</trueanswernote>
        <truefeedback format="html">
          <text></text>
        </truefeedback>
        <falsescoremode>=</falsescoremode>
        <falsescore>0</falsescore>
        <falsepenalty>0</falsepenalty>
        <falsenextnode>-1</falsenextnode>
        <falseanswernote>firsttree-1-F</falseanswernote>
        <falsefeedback format="html">
          <text></text>
        </falsefeedback>
      </node>
    </prt>
    <deployedseed>12345</deployedseed>
    <qtest>
      <testcase>1</testcase>
      <description>Basic test of question</description>
      <testinput>
        <name>ans1</name>
        <value>2</value>
      </testinput>
      <expected>
        <name>firsttree</name>
        <expectedscore>1</expectedscore>
        <expectedpenalty>0</expectedpenalty>
        <expectedanswernote>firsttree-1-T</expectedanswernote>
      </expected>
    </qtest>
  </question>
';

        // Hack so the test passes in both 3.5 and 3.6.
        if (strpos($xml, 'idnumber') === false) {
            $expectedxml = str_replace("    <idnumber></idnumber>\n", '', $expectedxml);
        }

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

    public function test_xml_import(): void {

        $xml = '<!-- question: 0  -->
  <question type="stack">
    <name>
      <text>test-0</text>
    </name>
    <questiontext format="html">
      <text>What is $1+1$? [[input:ans1]] [[validation:ans1]]</text>
    </questiontext>
    <generalfeedback format="html">
      <text></text>
    </generalfeedback>
    <defaultgrade>1</defaultgrade>
    <penalty>0.3333333</penalty>
    <hidden>0</hidden>
    <questionvariables>
      <text></text>
    </questionvariables>
    <specificfeedback format="html">
      <text>[[feedback:firsttree]]</text>
    </specificfeedback>
    <questionnote format="html">
      <text></text>
    </questionnote>
    <questionsimplify>1</questionsimplify>
    <assumepositive>0</assumepositive>
    <assumereal>0</assumereal>
    <prtcorrect format="html">
      <text><![CDATA[<p>Correct answer, well done.</p>]]></text>
    </prtcorrect>
    <prtpartiallycorrect format="html">
      <text><![CDATA[<p>Your answer is partially correct.</p>]]></text>
    </prtpartiallycorrect>
    <prtincorrect format="html">
      <text><![CDATA[<p>Incorrect answer.</p>]]></text>
    </prtincorrect>
    <decimals>.</decimals>
    <scientificnotation>*10</scientificnotation>
    <multiplicationsign>dot</multiplicationsign>
    <sqrtsign>1</sqrtsign>
    <complexno>i</complexno>
    <inversetrig>cos-1</inversetrig>
    <logicsymbol>lang</logicsymbol>
    <matrixparens>[</matrixparens>
    <variantsselectionseed></variantsselectionseed>
    <input>
      <name>ans1</name>
      <type>algebraic</type>
      <tans>2</tans>
      <boxsize>5</boxsize>
      <strictsyntax>1</strictsyntax>
      <insertstars>0</insertstars>
      <syntaxhint></syntaxhint>
      <syntaxattribute>0</syntaxattribute>
      <forbidwords></forbidwords>
      <allowwords></allowwords>
      <forbidfloat>1</forbidfloat>
      <requirelowestterms>0</requirelowestterms>
      <checkanswertype>0</checkanswertype>
      <mustverify>1</mustverify>
      <showvalidation>1</showvalidation>
      <options></options>
    </input>
    <prt>
      <name>firsttree</name>
      <value>1</value>
      <autosimplify>1</autosimplify>
      <feedbackvariables>
        <text></text>
      </feedbackvariables>
      <node>
        <name>0</name>
        <answertest>EqualComAss</answertest>
        <sans>ans1</sans>
        <tans>2</tans>
        <testoptions></testoptions>
        <quiet>0</quiet>
        <truescoremode>=</truescoremode>
        <truescore>1</truescore>
        <truepenalty>0</truepenalty>
        <truenextnode>-1</truenextnode>
        <trueanswernote>firsttree-1-T</trueanswernote>
        <truefeedback format="html">
          <text></text>
        </truefeedback>
        <falsescoremode>=</falsescoremode>
        <falsescore>0</falsescore>
        <falsepenalty>0</falsepenalty>
        <falsenextnode>-1</falsenextnode>
        <falseanswernote>firsttree-1-F</falseanswernote>
        <falsefeedback format="html">
          <text></text>
        </falsefeedback>
      </node>
    </prt>
    <deployedseed>12345</deployedseed>
    <qtest>
      <testcase>1</testcase>
      <testinput>
        <name>ans1</name>
        <value>2</value>
      </testinput>
      <expected>
        <name>firsttree</name>
        <expectedscore>1</expectedscore>
        <expectedpenalty>0</expectedpenalty>
        <expectedanswernote>firsttree-1-T</expectedanswernote>
      </expected>
    </qtest>
  </question>
';
        $xmldata = xmlize($xml);

        $importer = new qformat_xml();
        $q = $importer->try_importing_using_qtypes(
                $xmldata['question'], null, null, 'stack');

        $expectedq = new stdClass();
        $expectedq->qtype                 = 'stack';
        $expectedq->name                  = 'test-0';
        $expectedq->questiontext          = 'What is $1+1$? [[input:ans1]] [[validation:ans1]]';
        $expectedq->questiontextformat    = FORMAT_HTML;
        $expectedq->generalfeedback       = '';
        $expectedq->generalfeedbackformat = FORMAT_HTML;
        $expectedq->defaultmark           = 1;
        $expectedq->length                = 1;
        $expectedq->penalty               = 0.3333333;

        $expectedq->questionvariables     = '';
        $expectedq->specificfeedback      = ['text' => '[[feedback:firsttree]]', 'format' => FORMAT_HTML, 'files' => []];
        $expectedq->questionnote          = ['text' => '', 'format' => FORMAT_HTML, 'files' => []];
        $expectedq->questionsimplify      = 1;
        $expectedq->assumepositive        = 0;
        $expectedq->assumereal            = 0;
        $expectedq->prtcorrect            = [
            'text' => '<p>Correct answer, well done.</p>',
            'format' => FORMAT_HTML, 'files' => [],
        ];;
        $expectedq->prtpartiallycorrect   = [
            'text' => '<p>Your answer is partially correct.</p>',
            'format' => FORMAT_HTML, 'files' => [],
        ];;
        $expectedq->prtincorrect          = [
            'text' => '<p>Incorrect answer.</p>',
            'format' => FORMAT_HTML, 'files' => [],
        ];;
        $expectedq->decimals              = '.';
        $expectedq->scientificnotation    = '*10';
        $expectedq->multiplicationsign    = 'dot';
        $expectedq->sqrtsign              = 1;
        $expectedq->complexno             = 'i';
        $expectedq->inversetrig           = 'cos-1';
        $expectedq->logicsymbol           = 'lang';
        $expectedq->matrixparens          = '[';
        $expectedq->variantsselectionseed = '';

        $expectedq->ans1type               = 'algebraic';
        $expectedq->ans1modelans           = 2;
        $expectedq->ans1boxsize            = 5;
        $expectedq->ans1strictsyntax       = 1;
        $expectedq->ans1insertstars        = 0;
        $expectedq->ans1syntaxhint         = '';
        $expectedq->ans1syntaxattribute    = 0;
        $expectedq->ans1forbidwords        = '';
        $expectedq->ans1allowwords         = '';
        $expectedq->ans1forbidfloat        = 1;
        $expectedq->ans1requirelowestterms = 0;
        $expectedq->ans1checkanswertype    = 0;
        $expectedq->ans1mustverify         = 1;
        $expectedq->ans1showvalidation     = 1;
        $expectedq->ans1options            = '';

        $expectedq->firsttreevalue              = 1;
        $expectedq->firsttreeautosimplify       = 1;
        $expectedq->firsttreefeedbackstyle      = 1;
        $expectedq->firsttreefeedbackvariables  = '';
        $expectedq->firsttreeanswertest[0]      = 'EqualComAss';
        $expectedq->firsttreesans[0]            = 'ans1';
        $expectedq->firsttreetans[0]            = '2';
        $expectedq->firsttreetestoptions[0]     = '';
        $expectedq->firsttreequiet[0]           = 0;
        $expectedq->firsttreetruescoremode[0]   = '=';
        $expectedq->firsttreetruescore[0]       = 1;
        $expectedq->firsttreetruepenalty[0]     = 0;
        $expectedq->firsttreetruenextnode[0]    = -1;
        $expectedq->firsttreetrueanswernote[0]  = 'firsttree-1-T';
        $expectedq->firsttreetruefeedback[0]    = ['text' => '', 'format' => FORMAT_HTML, 'files' => []];
        $expectedq->firsttreefalsescoremode[0]  = '=';
        $expectedq->firsttreefalsescore[0]      = 0;
        $expectedq->firsttreefalsepenalty[0]    = 0;
        $expectedq->firsttreefalsenextnode[0]   = -1;
        $expectedq->firsttreefalseanswernote[0] = 'firsttree-1-F';
        $expectedq->firsttreefalsefeedback[0]   = ['text' => '', 'format' => FORMAT_HTML, 'files' => []];

        $expectedq->deployedseeds = ['12345'];

        $qtest = new stack_question_test('', ['ans1' => '2'], 1);
        $qtest->add_expected_result('firsttree', new stack_potentialresponse_tree_state(
                        1, true, 1, 0, '', ['firsttree-1-T']));
        $expectedq->testcases[1] = $qtest;

        $this->assertEquals($expectedq->deployedseeds, $q->deployedseeds); // Redundant, but gives better fail messages.
        $this->assertEquals($expectedq->testcases, $q->testcases); // Redundant, but gives better fail messages.
        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
    }

    public function test_get_input_names_from_question_text_input_only(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['ans123' => [1, 0]],
                $qtype->get_input_names_from_question_text('[[input:ans123]]'));
    }

    public function test_get_input_names_from_question_text_validation_only(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['ans123' => [0, 1]],
                $qtype->get_input_names_from_question_text('[Blah] [[validation:ans123]] [Blah]'));
    }

    public function test_get_input_names_from_question_text_invalid(): void {

        $qtype = new qtype_stack();

        $this->assertEquals([], $qtype->get_input_names_from_question_text('[[input:123]]'));
    }

    public function test_get_input_names_from_question_text_sloppy(): void {

        $qtype = new qtype_stack();
        $text = 'What is \(1+1\)?  [[input: ans1]]';

        $this->assertEquals(['[[input: ans1]]'], $qtype->validation_get_sloppy_tags($text));
    }

    public function test_get_prt_names_from_question_text(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['prt123' => 1],
                $qtype->get_prt_names_from_question('[[feedback:prt123]]', ''));
    }

    public function test_get_prt_names_from_question_feedback(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['prt123' => 1], $qtype->get_prt_names_from_question(
                'What is $1 + 1$? [[input:ans1]]', '[[feedback:prt123]]'));
    }

    public function test_get_prt_names_from_question_both(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['prt1' => 1, 'prt2' => 1], $qtype->get_prt_names_from_question(
                '[Blah] [[feedback:prt1]] [Blah]', '[Blah] [[feedback:prt2]] [Blah]'));
    }

    public function test_get_prt_names_from_question_invalid(): void {

        $qtype = new qtype_stack();

        $this->assertEquals([], $qtype->get_prt_names_from_question('[[feedback:123]]', ''));
    }

    public function test_get_prt_names_from_question_duplicate(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['prt1' => 2],
                $qtype->get_prt_names_from_question('[[feedback:prt1]] [[feedback:prt1]]', ''));
    }

    public function test_get_prt_names_from_question_duplicate_split(): void {

        $qtype = new qtype_stack();

        $this->assertEquals(['prt1' => 2], $qtype->get_prt_names_from_question('[[feedback:prt1]]',
                '[[feedback:prt1]]'));
    }
}
