<?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/>.

/**
 * Quiz attempt walk through using data from csv file.
 *
 * The quiz stats below and the question stats found in qstats00.csv were calculated independently in a spreadsheet which is
 * available in open document or excel format here :
 * https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
 *
 * Similarly the question variant's stats in qstats00.csv are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
 * The calculations in the spreadsheets are the same as for the other question stats but applied just to the attempts where the
 * variants appeared.
 *
 * @package    quiz_statistics
 * @category   phpunit
 * @copyright  2013 The Open University
 * @author     Jamie Pratt <me@jamiep.org>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

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

global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/attempt_walkthrough_from_csv_test.php');
require_once($CFG->dirroot . '/mod/quiz/report/default.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');

/**
 * Quiz attempt walk through using data from csv file.
 *
 * @package    quiz_statistics
 * @category   phpunit
 * @copyright  2013 The Open University
 * @author     Jamie Pratt <me@jamiep.org>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkthrough_from_csv_testcase {

    /**
     * @var quiz_statistics_report object to do stats calculations.
     */
    protected $report;

    protected function get_full_path_of_csv_file($setname, $test) {
        // Overridden here so that __DIR__ points to the path of this file.
        return  __DIR__."/fixtures/{$setname}{$test}.csv";
    }

    protected $files = array('questions', 'steps', 'results', 'qstats', 'responsecounts');

    /**
     * Create a quiz add questions to it, walk through quiz attempts and then check results.
     *
     * @param PHPUnit\DbUnit\DataSet\ITable[] of data read from csv file "questionsXX.csv",
     *                                                                                  "stepsXX.csv" and "resultsXX.csv".
     * @dataProvider get_data_for_walkthrough
     */
    public function test_walkthrough_from_csv($quizsettings, $csvdata) {

        $this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);

        $whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
        $whichtries = question_attempt::ALL_TRIES;
        $groupstudentsjoins = new \core\dml\sql_join();
        list($questions, $quizstats, $questionstats, $qubaids) =
                    $this->check_stats_calculations_and_response_analysis($csvdata,
                            $whichattempts, $whichtries, $groupstudentsjoins);
        if ($quizsettings['testnumber'] === '00') {
            $this->check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids);
            $this->check_quiz_stats_for_quiz_00($quizstats);
        }
    }

    /**
     * Check actual question stats are the same as that found in csv file.
     *
     * @param $qstats         PHPUnit\DbUnit\DataSet\ITable data from csv file.
     * @param $questionstats  \core_question\statistics\questions\all_calculated_for_qubaid_condition Calculated stats.
     */
    protected function check_question_stats($qstats, $questionstats) {
        for ($rowno = 0; $rowno < $qstats->getRowCount(); $rowno++) {
            $slotqstats = $qstats->getRow($rowno);
            foreach ($slotqstats as $statname => $slotqstat) {
                if (!in_array($statname, array('slot', 'subqname'))  && $slotqstat !== '') {
                    $this->assert_stat_equals($slotqstat,
                                              $questionstats,
                                              $slotqstats['slot'],
                                              $slotqstats['subqname'],
                                              $slotqstats['variant'],
                                              $statname);
                }
            }
            // Check that sub-question boolean field is correctly set.
            $this->assert_stat_equals(!empty($slotqstats['subqname']),
                                      $questionstats,
                                      $slotqstats['slot'],
                                      $slotqstats['subqname'],
                                      $slotqstats['variant'],
                                      'subquestion');
        }
    }

    /**
     * Check that the stat is as expected within a reasonable tolerance.
     *
     * @param float|string|bool $expected expected value of stat.
     * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
     * @param int $slot
     * @param string $subqname if empty string then not an item stat.
     * @param int|string $variant if empty string then not a variantstat.
     * @param string $statname
     */
    protected function assert_stat_equals($expected, $questionstats, $slot, $subqname, $variant, $statname) {

        if ($variant === '' && $subqname === '') {
            $actual = $questionstats->for_slot($slot)->{$statname};
        } else if ($subqname !== '') {
            $actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
        } else {
            $actual = $questionstats->for_slot($slot, $variant)->{$statname};
        }
        $message = "$statname for slot $slot";
        if ($expected === '**NULL**') {
            $this->assertEquals(null, $actual, $message);
        } else if (is_bool($expected)) {
            $this->assertEquals($expected, $actual, $message);
        } else if (is_numeric($expected)) {
            switch ($statname) {
                case 'covariance' :
                case 'discriminationindex' :
                case 'discriminativeefficiency' :
                case 'effectiveweight' :
                    $precision = 1e-5;
                    break;
                default :
                    $precision = 1e-6;
            }
            $delta = abs($expected) * $precision;
            $this->assertEquals((float)$expected, $actual, $message, $delta);
        } else {
            $this->assertEquals($expected, $actual, $message);
        }
    }

    protected function assert_response_count_equals($question, $qubaids, $expected, $whichtries) {
        $responesstats = new \core_question\statistics\responses\analyser($question);
        $analysis = $responesstats->load_cached($qubaids, $whichtries);
        if (!isset($expected['subpart'])) {
            $subpart = 1;
        } else {
            $subpart = $expected['subpart'];
        }
        list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
                                                                                      $subpart,
                                                                                      $expected['modelresponse']);

        $subpartanalysis = $analysis->get_analysis_for_subpart($expected['variant'], $subpartid);
        $responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
        $actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');

        foreach ($actualresponsecounts as $actualresponsecount) {
            if ($actualresponsecount->response == $expected['actualresponse'] || count($actualresponsecounts) == 1) {
                $i = 1;
                $partofanalysis = " slot {$expected['slot']}, rand q '{$expected['randq']}', variant {$expected['variant']}, ".
                                    "for expected model response {$expected['modelresponse']}, ".
                                    "actual response {$expected['actualresponse']}";
                while (isset($expected['count'.$i])) {
                    if ($expected['count'.$i] != 0) {
                        $this->assertTrue(isset($actualresponsecount->trycount[$i]),
                            "There is no count at all for try $i on ".$partofanalysis);
                        $this->assertEquals($expected['count'.$i], $actualresponsecount->trycount[$i],
                                            "Count for try $i on ".$partofanalysis);
                    }
                    $i++;
                }
                if (isset($expected['totalcount'])) {
                    $this->assertEquals($expected['totalcount'], $actualresponsecount->totalcount,
                                        "Total count on ".$partofanalysis);
                }
                return;
            }
        }
        throw new coding_exception("Expected response '{$expected['actualresponse']}' not found.");
    }

    protected function get_response_subpart_and_class_id($question, $subpart, $modelresponse) {
        $qtypeobj = question_bank::get_qtype($question->qtype, false);
        $possibleresponses = $qtypeobj->get_possible_responses($question);
        $possibleresponsesubpartids = array_keys($possibleresponses);
        if (!isset($possibleresponsesubpartids[$subpart - 1])) {
            throw new coding_exception("Subpart '{$subpart}' not found.");
        }
        $subpartid = $possibleresponsesubpartids[$subpart - 1];

        if ($modelresponse == '[NO RESPONSE]') {
            return array($subpartid, null);

        } else if ($modelresponse == '[NO MATCH]') {
            return array($subpartid, 0);
        }

        $modelresponses = array();
        foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
            $modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
        }
        $this->assertContains($modelresponse, $modelresponses);
        $responseclassid = array_search($modelresponse, $modelresponses);
        return array($subpartid, $responseclassid);
    }

    /**
     * @param $responsecounts
     * @param $qubaids
     * @param $questions
     * @param $whichtries
     */
    protected function check_response_counts($responsecounts, $qubaids, $questions, $whichtries) {
        for ($rowno = 0; $rowno < $responsecounts->getRowCount(); $rowno++) {
            $expected = $responsecounts->getRow($rowno);
            $defaultsforexpected = array('randq' => '', 'variant' => '1', 'subpart' => '1');
            foreach ($defaultsforexpected as $key => $expecteddefault) {
                if (!isset($expected[$key])) {
                    $expected[$key] = $expecteddefault;
                }
            }
            if ($expected['randq'] == '') {
                $question = $questions[$expected['slot']];
            } else {
                $qid = $this->randqids[$expected['slot']][$expected['randq']];
                $question = question_finder::get_instance()->load_question_data($qid);
            }
            $this->assert_response_count_equals($question, $qubaids, $expected, $whichtries);
        }
    }

    /**
     * @param $questions
     * @param $questionstats
     * @param $whichtries
     * @param $qubaids
     */
    protected function check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids) {
        $expectedvariantcounts = array(2 => array(1  => 6,
                                                  4  => 4,
                                                  5  => 3,
                                                  6  => 4,
                                                  7  => 2,
                                                  8  => 5,
                                                  10 => 1));

        foreach ($questions as $slot => $question) {
            if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
                continue;
            }
            $responesstats = new \core_question\statistics\responses\analyser($question);
            $this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids, $whichtries));
            $analysis = $responesstats->load_cached($qubaids, $whichtries);
            $variantsnos = $analysis->get_variant_nos();
            if (isset($expectedvariantcounts[$slot])) {
                // Compare contents, ignore ordering of array, using canonicalize parameter of assertEquals.
                $this->assertEquals(array_keys($expectedvariantcounts[$slot]), $variantsnos, '', 0, 10, true);
            } else {
                $this->assertEquals(array(1), $variantsnos);
            }
            $totalspervariantno = array();
            foreach ($variantsnos as $variantno) {

                $subpartids = $analysis->get_subpart_ids($variantno);
                foreach ($subpartids as $subpartid) {
                    if (!isset($totalspervariantno[$subpartid])) {
                        $totalspervariantno[$subpartid] = array();
                    }
                    $totalspervariantno[$subpartid][$variantno] = 0;

                    $subpartanalysis = $analysis->get_analysis_for_subpart($variantno, $subpartid);
                    $classids = $subpartanalysis->get_response_class_ids();
                    foreach ($classids as $classid) {
                        $classanalysis = $subpartanalysis->get_response_class($classid);
                        $actualresponsecounts = $classanalysis->data_for_question_response_table('', '');
                        foreach ($actualresponsecounts as $actualresponsecount) {
                            $totalspervariantno[$subpartid][$variantno] += $actualresponsecount->totalcount;
                        }
                    }
                }
            }
            // Count all counted responses for each part of question and confirm that counted responses, for most question types
            // are the number of attempts at the question for each question part.
            if ($slot != 5) {
                // Slot 5 holds a multi-choice multiple question.
                // Multi-choice multiple is slightly strange. Actual answer counts given for each sub part do not add up to the
                // total attempt count.
                // This is because each option is counted as a sub part and each option can be off or on in each attempt. Off is
                // not counted in response analysis for this question type.
                foreach ($totalspervariantno as $totalpervariantno) {
                    if (isset($expectedvariantcounts[$slot])) {
                        // If we know how many attempts there are at each variant we can check
                        // that we have counted the correct amount of responses for each variant.
                        $this->assertEquals($expectedvariantcounts[$slot],
                                            $totalpervariantno,
                                            "Totals responses do not add up in response analysis for slot {$slot}.",
                                            0,
                                            10,
                                            true);
                    } else {
                        $this->assertEquals(25,
                                            array_sum($totalpervariantno),
                                            "Totals responses do not add up in response analysis for slot {$slot}.");
                    }
                }
            }
        }

        foreach ($expectedvariantcounts as $slot => $expectedvariantcount) {
            foreach ($expectedvariantcount as $variantno => $s) {
                $this->assertEquals($s, $questionstats->for_slot($slot, $variantno)->s);
            }
        }
    }

    /**
     * @param $quizstats
     */
    protected function check_quiz_stats_for_quiz_00($quizstats) {
        $quizstatsexpected = array(
            'median'             => 4.5,
            'firstattemptsavg'   => 4.617333332,
            'allattemptsavg'     => 4.617333332,
            'firstattemptscount' => 25,
            'allattemptscount'   => 25,
            'standarddeviation'  => 0.8117265554,
            'skewness'           => -0.092502502,
            'kurtosis'           => -0.7073968557,
            'cic'                => -87.2230935542,
            'errorratio'         => 136.8294900795,
            'standarderror'      => 1.1106813066
        );

        foreach ($quizstatsexpected as $statname => $statvalue) {
            $this->assertEquals($statvalue, $quizstats->$statname, $quizstats->$statname, abs($statvalue) * 1.5e-5);
        }
    }

    /**
     * Check the question stats and the response counts used in the statistics report. If the appropriate files exist in fixtures/.
     *
     * @param PHPUnit\DbUnit\DataSet\ITable[] $csvdata Data loaded from csv files for this test.
     * @param string $whichattempts
     * @param string $whichtries
     * @param \core\dml\sql_join $groupstudentsjoins
     * @return array with contents 0 => $questions, 1 => $quizstats, 2=> $questionstats, 3=> $qubaids Might be needed for further
     *               testing.
     */
    protected function check_stats_calculations_and_response_analysis($csvdata, $whichattempts, $whichtries,
            \core\dml\sql_join $groupstudentsjoins) {
        $this->report = new quiz_statistics_report();
        $questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
        list($quizstats, $questionstats) = $this->report->get_all_stats_and_analysis($this->quiz,
                                                                                     $whichattempts,
                                                                                     $whichtries,
                                                                                     $groupstudentsjoins,
                                                                                     $questions);

        $qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudentsjoins, $whichattempts);

        // We will create some quiz and question stat calculator instances and some response analyser instances, just in order
        // to check the last analysed time then returned.
        $quizcalc = new \quiz_statistics\calculator();
        // Should not be a delay of more than one second between the calculation of stats above and here.
        $this->assertTimeCurrent($quizcalc->get_last_calculated_time($qubaids));

        $qcalc = new \core_question\statistics\questions\calculator($questions);
        $this->assertTimeCurrent($qcalc->get_last_calculated_time($qubaids));

        if (isset($csvdata['responsecounts'])) {
            $this->check_response_counts($csvdata['responsecounts'], $qubaids, $questions, $whichtries);
        }
        if (isset($csvdata['qstats'])) {
            $this->check_question_stats($csvdata['qstats'], $questionstats);
            return array($questions, $quizstats, $questionstats, $qubaids);
        }
        return array($questions, $quizstats, $questionstats, $qubaids);
    }

}
