<?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 theme_snap;
use theme_snap\services\course;
use theme_snap\renderables\course_card;
use theme_snap\local;

/**
 * Test course card service.
 * @package   theme_snap
 * @author    gthomas2
 * @copyright Copyright (c) 2016 Open LMS (https://www.openlms.net)
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class services_course_test extends \advanced_testcase {

    /**
     * @var stdClass
     */
    protected $user1;

    /**
     * @var array
     */
    protected $courses = [];

    /**
     * @var course
     */
    protected $courseservice;

    /**
     * Pre-requisites for tests.
     * @throws \coding_exception
     */
    public function setUp(): void {
        global $CFG, $DB;

        require_once($CFG->dirroot . '/mod/forum/lib.php');

        $this->resetAfterTest();

        $CFG->theme = 'snap';

        // Create 10 courses.
        for ($c = 0; $c < 10; $c++) {
            $this->courses[] = $this->getDataGenerator()->create_course();
        }

        // Create 5 courses in the past.
        for ($c = 0; $c < 5; $c++) {
            $enddate = time() - DAYSECS * ($c + 1) * 10;
            $startdate = $enddate - YEARSECS;
            $record = (object) [
                'startdate' => $startdate,
                'enddate' => $enddate,
            ];
            $this->courses[] = $this->getDataGenerator()->create_course($record);
        }

        $this->user1 = $this->getDataGenerator()->create_user();

        // Enrol user to all courses.
        $sturole = $DB->get_record('role', array('shortname' => 'student'));

        foreach ($this->courses as $course) {
            $this->getDataGenerator()->enrol_user($this->user1->id,
                $course->id,
                $sturole->id);
        }

        $this->courseservice = course::service();
    }

    public function test_service() {
        $testservice = course::service();
        $this->assertEquals($this->courseservice, $testservice);
        $this->assertTrue($testservice instanceof course);
    }

    public function test_favorited() {
        $service = $this->courseservice;
        $favorited = $service->favorited($this->courses[0], $this->user1->id);
        $this->assertFalse($favorited);

        $service->setfavorite($this->courses[0]->shortname, true, $this->user1->id);

        // Make sure marked as favorite.
        $favorited = $service->favorited($this->courses[0]->id, $this->user1->id);
        $this->assertTrue($favorited);
    }

    public function test_favorites() {
        $service = $this->courseservice;
        $service->setfavorite($this->courses[0]->shortname, true, $this->user1->id);
        $service->setfavorite($this->courses[1]->shortname, true, $this->user1->id);

        $favorites = $service->favorites($this->user1->id, false);
        $this->assertTrue(isset($favorites[$this->courses[0]->id]));
        $this->assertTrue(isset($favorites[$this->courses[1]->id]));
        $this->assertFalse(isset($favorites[$this->courses[2]->id]));
    }

    public function test_my_courses_split_by_past_courses_favorites() {
        $service = $this->courseservice;
        $service->setfavorite($this->courses[0]->shortname, true, $this->user1->id);
        $service->setfavorite($this->courses[1]->shortname, true, $this->user1->id);

        $this->setUser($this->user1);
        list ($pastcourses, $favorites, $notfavorites) = $service->my_courses_split_by_favorites();
        $notfavorites = array_keys($notfavorites);
        sort($notfavorites);

        $expectedpastcourses = [
            $this->courses[10]->id,
            $this->courses[11]->id,
            $this->courses[12]->id,
            $this->courses[13]->id,
            $this->courses[14]->id,
        ];

        // Collapse pastcourses (currently hashed by year).
        $collapsed = [];
        foreach ($pastcourses as $year => $courses) {
            $collapsed = array_merge($collapsed, array_keys($courses));
        }
        $pastcourses = $collapsed;
        foreach ($expectedpastcourses as $expectedpastcourse) {
            $this->assertTrue(in_array($expectedpastcourse, $pastcourses));
        }
        $expectedfavorites = [
            $this->courses[0]->id,
            $this->courses[1]->id,
        ];
        $this->assertEquals($expectedfavorites, array_keys($favorites));

        $notfavoritecourses = array_slice($this->courses, 2, 8);
        $expectednotfavorites = array_keys($notfavoritecourses);
        sort($expectednotfavorites);
        $expectednotfavorites = [];
        foreach ($notfavoritecourses as $course) {
            $expectednotfavorites[] = $course->id;
        }
        $this->assertEquals($expectednotfavorites, $notfavorites);
    }

    public function test_setfavorite() {
        $returned = $this->courseservice->setfavorite($this->courses[0]->shortname, true, $this->user1->id);
        $this->assertTrue($returned);
    }

    public function test_coursebyshortname() {
        $expected = get_course($this->courses[0]->id);
        $actual = $this->courseservice->coursebyshortname($this->courses[0]->shortname);

        $this->assertEquals($expected, $actual);
    }

    public function test_cardbyshortname() {
        $card = $this->courseservice->cardbyshortname($this->courses[0]->shortname);
        $this->assertTrue($card instanceof course_card);
        $this->assertEquals($card->courseid, $this->courses[0]->id);
    }

    public function test_course_completion() {
        global $DB;
        $this->resetAfterTest();

        // Enable avaibility.
        // If not enabled all conditional fields will be ignored.
        set_config('enableavailability', 1);

        // Enable course completion.
        // If not enabled all completion settings will be ignored.
        set_config('enablecompletion', COMPLETION_ENABLED);

        $generator = $this->getDataGenerator();

        // Create course with completion tracking enabled.
        $course = $generator->create_course([
            'enablecompletion' => 1,
            'numsections' => 3,
        ], ['createsections' => true]);

        // Enrol user to completion tracking course.
        $sturole = $DB->get_record('role', array('shortname' => 'student'));
        $generator->enrol_user($this->user1->id,
            $course->id,
            $sturole->id);

        // Create page with completion marked on view.
        $page1 = $generator->create_module('page', array('course' => $course->id, 'name' => 'page1 complete on view'),
            array('completion' => 2, 'completionview' => 1));
        $modinfo = get_fast_modinfo($course);
        $page1cm = $modinfo->get_cm($page1->cmid);

        // Create page restricted to only show when first page is viewed.
        $moduleinfo = (object)[];
        $moduleinfo->course = $course->id;
        $moduleinfo->name = 'page2 available after page1 viewed';
        $moduleinfo->availability = json_encode(\core_availability\tree::get_root_json(
            [\availability_completion\condition::get_json($page1->cmid, COMPLETION_COMPLETE)], '&'));
        $page2 = $generator->create_module('page', $moduleinfo);

        // Make section 2 restricted to only show when first page is viewed.
        $section = $modinfo->get_section_info(2);
        $sectionupdate = [
            'id' => $section->id,
            'availability' => json_encode(\core_availability\tree::get_root_json(
                [\availability_completion\condition::get_json($page1->cmid, COMPLETION_COMPLETE)], '&')),
        ];
        $DB->update_record('course_sections', $sectionupdate);

        // Check user1 has expected unavailable section and mod.
        $this->setUser($this->user1);

        // Dump cache and reget modinfo.
        get_fast_modinfo($course, 0, true);
        $modinfo = get_fast_modinfo($course);

        $page2cm = $modinfo->get_cm($page2->cmid);
        list ($previouslyunavailablesections, $previouslyunavailablemods) = local::conditionally_unavailable_elements($course);
        $this->assertContains(2, $previouslyunavailablesections);
        $this->assertContains($page2cm->id, $previouslyunavailablemods);

        // View page1 to trigger completion.
        $context = \context_module::instance($page1->cmid);
        page_view($page1, $course, $page1cm, $context);
        $completion = new \completion_info($course);
        $completiondata = $completion->get_data($page1cm);
        $this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate);

        get_fast_modinfo($course, 0, true); // Reset modinfo.

        // Make sure that unavailable sections and mods no longer contain the ones requiring availabililty criteria
        // satisfying.
        list ($unavailablesections, $unavailablemods) = local::conditionally_unavailable_elements($course);
        $this->assertNotContains($page2cm->id, $unavailablemods);
        $this->assertNotContains(2, $unavailablesections);

        $result = $this->courseservice->course_completion($course->shortname,
            $previouslyunavailablesections,
            $previouslyunavailablemods);

        // Make sure that the second page module (which is now newly available) appears in the list of changed
        // module html.
        $this->assertTrue(isset($result['changedmodhtml'][$page2->cmid]));

        // Make sure that the second section (which is now wnely available) appears in the list of changed
        // section html.
        $this->assertTrue(isset($result['changedsectionhtml'][2]));

    }

    public function test_course_toc() {
        $generator = $this->getDataGenerator();

        // Create topics course with 10 sections and 1 module.
        $course = $generator->create_course([
            'shortname' => 'testlistlarge',
            'format' => 'topics',
            'numsections' => 10,
        ], ['createsections' => true]);
        $page = $generator->create_module('page', array('course' => $course->id, 'name' => 'test page'));

        $toc = $this->courseservice->course_toc('testlistlarge');
        $this->assertTrue($toc->modules[0] instanceof \theme_snap\renderables\course_toc_module);
        $this->assertTrue($toc->modules[0]->url === '#section-0&module-'.$page->cmid);
        $this->assertTrue($toc instanceof \theme_snap\renderables\course_toc);
        $this->assertEquals(true, $toc->formatsupportstoc);
        $this->assertEquals('list-large', $toc->chapters->listlarge);
        $this->assertCount(11, $toc->chapters->chapters);

        // Create topics course with 9 sections.
        $generator->create_course([
            'shortname' => 'testlistsmall',
            'format' => 'topics',
            'numsections' => 9,
        ], ['createsections' => true]);
        $toc = $this->courseservice->course_toc('testlistsmall');
        $this->assertNotEquals('list-large', $toc->chapters->listlarge);

        // Create social format course.
        $generator->create_course([
            'shortname' => 'socialcourse',
            'format' => 'social',
            'numsections' => 2,
        ], ['createsections' => true]);
        $toc = $this->courseservice->course_toc('socialcourse');
        $this->assertFalse($toc->formatsupportstoc);
    }

    public function test_course_toc_chapters() {
        $generator = $this->getDataGenerator();

        // Create topics course.
        $generator->create_course([
            'shortname' => 'testcourse',
            'format' => 'topics',
            'numsections' => 2,
        ], ['createsections' => true]);
        $chapters = $this->courseservice->course_toc_chapters('testcourse');

        $this->assertCount(3, $chapters->chapters);
        $this->assertTrue($chapters->chapters[0] instanceof \theme_snap\renderables\course_toc_chapter);
    }

    public function test_course_toc_chapters_escaped_chars() {
        global $OUTPUT, $DB;
        $titles = [ "This & that", "This < that", "This > that", "This & & that"];
        $generator = $this->getDataGenerator();

        $course = $generator->create_course([
            'shortname' => 'testcourse',
            'format' => 'topics',
            'numsections' => count($titles) - 1,
        ], ['createsections' => true]);

        // Get section names for course.
        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));

        // Modify section names.
        $t = 0;
        foreach ($coursesections as $section) {
            $section->name = $titles[$t];
            $t++;
            $DB->update_record('course_sections', $section);
        }

        $chapters = $this->courseservice->course_toc_chapters('testcourse');

        $tochtml = $OUTPUT->render_from_template('theme_snap/course_toc_chapters',
            (object) ['chapters' => $chapters->chapters, 'listlarge' => (count($chapters->chapters) > 9)]);
        $pattern = '/>(.*)<\/a>/';
        preg_match_all($pattern, $tochtml, $matches);
        for ($x = 0; $x < count($titles); $x++) {
            $this->assertEquals(htmlspecialchars($titles[$x], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401), $matches[1][$x]);
        }
    }

    public function test_highlight_section() {
        $generator = $this->getDataGenerator();

        // Create topics course.
        $generator->create_course([
            'shortname' => 'testcourse',
            'format' => 'topics',
            'numsections' => 5,
        ], ['createsections' => true]);

        $this->setAdminUser();

        // Highlight the section.
        $highlight = $this->courseservice->highlight_section('testcourse', 3, true);
        $this->assertTrue(isset($highlight['actionmodel']));
        $this->assertTrue(isset($highlight['toc']));
        $actionmodel = $highlight['actionmodel'];
        $toc = $highlight['toc'];
        $this->assertTrue($actionmodel instanceof \theme_snap\renderables\course_action_section_highlight);
        $this->assertTrue($toc instanceof \theme_snap\renderables\course_toc);

        // Check that action model has toggled after highlight.
        $this->assertEquals('aria-pressed="true"', $actionmodel->ariapressed);
        $this->assertStringContainsString('marker=0', $actionmodel->url);

        // Unhiglight the section.
        $highlight = $this->courseservice->highlight_section('testcourse', 3, false);
        $actionmodel = $highlight['actionmodel'];
        $this->assertTrue($actionmodel instanceof \theme_snap\renderables\course_action_section_highlight);

        // Check that action model now corresponds to unhighlighted state.
        $this->assertEquals('aria-pressed="false"', $actionmodel->ariapressed);
        $this->assertStringContainsString('marker=3', $actionmodel->url);
    }

    public function test_set_section_visibility() {
        $generator = $this->getDataGenerator();

        // Create topics course.
        $generator->create_course([
            'shortname' => 'testcourse',
            'format' => 'topics',
            'numsections' => 5,
        ], ['createsections' => true]);

        $this->setAdminUser();

        // Hide the section.
        $visibility = $this->courseservice->set_section_visibility('testcourse', 3, false);
        $this->assertTrue(isset($visibility['actionmodel']));
        $this->assertTrue(isset($visibility['toc']));
        $actionmodel = $visibility['actionmodel'];
        $toc = $visibility['toc'];
        $this->assertTrue($actionmodel instanceof \theme_snap\renderables\course_action_section_visibility);
        $this->assertTrue($toc instanceof \theme_snap\renderables\course_toc);

        // Check that action model has toggled after section hidden.
        $this->assertEquals('snap-visibility snap-show', $actionmodel->class);
        $this->assertEquals('Show topic', $actionmodel->title);
        $this->assertStringContainsString('show=3', $actionmodel->url);

        // Unhide the section.
        $visibility = $this->courseservice->set_section_visibility('testcourse', 3, true);
        $actionmodel = $visibility['actionmodel'];
        $this->assertTrue($actionmodel instanceof \theme_snap\renderables\course_action_section_visibility);

        // Check that action model now corresponds to unhighlighted state.
        $this->assertEquals('snap-visibility snap-hide', $actionmodel->class);
        $this->assertEquals('Hide topic', $actionmodel->title);
        $this->assertStringContainsString('hide=3', $actionmodel->url);
    }

    // Records for favorite courses should not exist when the user is deleted.
    public function test_user_deletion() {
        global $DB;
        $service = $this->courseservice;
        $service->setfavorite($this->courses[0]->shortname, true, $this->user1->id);
        $service->setfavorite($this->courses[1]->shortname, true, $this->user1->id);
        $params = array('userid' => $this->user1->id, 'component' => 'core_course');
        $favorites = $DB->get_records('favourite', $params);
        $this->assertNotEmpty($favorites);
        delete_user($this->user1);
        $favorites = $DB->get_records('favourite', $params);
        $this->assertEmpty($favorites);
    }

    // Records for favorite courses should not exist when the course is deleted.
    public function test_course_deletion() {
        global $DB;
        $service = $this->courseservice;
        $service->setfavorite($this->courses[0]->shortname, true, $this->user1->id);
        $service->setfavorite($this->courses[1]->shortname, true, $this->user1->id);
        $params = array('userid' => $this->user1->id, 'component' => 'core_course');
        $favorites = $DB->count_records('favourite', $params);
        $this->assertEquals(2, $favorites);
        $this->assertNotEmpty($favorites);
        delete_course($this->courses[0], false);
        $favorites = $DB->count_records('favourite', $params);
        $this->assertEquals(1, $favorites);
        delete_course($this->courses[1], false);
        $favorites = $DB->get_records('favourite', $params);
        $this->assertEmpty($favorites);
    }

    private function count_course_sections($courseid) {
        global $DB;
        $count = $DB->count_records('course_sections', ['course' => $courseid]);
        return $count;
    }

    // Test course section deletion.
    public function test_section_deletion() {
        $service = $this->courseservice;
        $generator = $this->getDataGenerator();

        // Create topics course.
        $course = $generator->create_course([
            'shortname' => 'testcourse',
            'format' => 'topics',
            'numsections' => 5,
        ], ['createsections' => true]);

        $this->assertEquals(6, $this->count_course_sections($course->id));

        $this->setAdminUser();

        $service->delete_section($course->shortname, 1);

        $this->assertEquals(5, $this->count_course_sections($course->id));
    }

    public function test_module_toggle_completion() {
        global $DB;

        $service = $this->courseservice;
        $this->resetAfterTest();

        // Enable avaibility.
        // If not enabled all conditional fields will be ignored.
        set_config('enableavailability', 1);

        // Enable course completion.
        // If not enabled all completion settings will be ignored.
        set_config('enablecompletion', COMPLETION_ENABLED);

        $generator = $this->getDataGenerator();

        // Create course with completion tracking enabled.
        $course = $generator->create_course([
            'enablecompletion' => 1,
            'numsections' => 3,
        ], ['createsections' => true]);

        // Enrol user to completion tracking course.
        $sturole = $DB->get_record('role', array('shortname' => 'student'));
        $generator->enrol_user($this->user1->id,
            $course->id,
            $sturole->id);

        $this->setUser($this->user1);

        // Create page with completion marked manually.
        $page1 = $generator->create_module('page', array('course' => $course->id, 'name' => 'page1 complete manually'),
            array('completion' => 1, 'completionview' => 0));
        $modinfo = get_fast_modinfo($course);
        $page1cm = $modinfo->get_cm($page1->cmid);
        $completion = new \completion_info($course);
        $completiondata = $completion->get_data($page1cm);
        $this->assertEquals(COMPLETION_INCOMPLETE, $completiondata->completionstate);

        // Manually mark page complete.
        $service->module_toggle_completion($page1cm->id, COMPLETION_COMPLETE);

        // Dump cache and reget modinfo.
        get_fast_modinfo($course, 0, true);
        $modinfo = get_fast_modinfo($course);
        $page1cm = $modinfo->get_cm($page1->cmid);
        $completion = new \completion_info($course);
        $completiondata = $completion->get_data($page1cm);
        // Assert complete.
        $this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate);

        // Manually mark page incomplete.
        $service->module_toggle_completion($page1cm->id, COMPLETION_INCOMPLETE);

        // Dump cache and reget modinfo.
        get_fast_modinfo($course, 0, true);
        $modinfo = get_fast_modinfo($course);
        $page1cm = $modinfo->get_cm($page1->cmid);
        $completion = new \completion_info($course);
        $completiondata = $completion->get_data($page1cm);
        // Assert incomplete.
        $this->assertEquals(COMPLETION_INCOMPLETE, $completiondata->completionstate);

    }

    public function test_section_fragment() {
        global $CFG, $DB;
        require_once($CFG->dirroot .'/theme/snap/lib.php');
        $topics = $this->getDataGenerator()->create_course(
            array('numsections' => 5, 'format' => 'topics'),
            array('createsections' => true));
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
        $teacherole = $DB->get_record('role', array('shortname' => 'editingteacher'));

        $student = $this->getDataGenerator()->create_user();
        $teacher = $this->getDataGenerator()->create_user();
        $this->getDataGenerator()->enrol_user($student->id,
            $topics->id,
            'student');
        $this->getDataGenerator()->enrol_user($teacher->id,
            $topics->id,
            'editingteacher');
        $this->getDataGenerator()->create_module('assign', ['course' => $topics->id, 'section' => 1,
            'name' => 'Section Assign', ]);
        $params = ['courseid' => $topics->id, 'section' => 1];
        $this->setUser($student);
        $section = theme_snap_output_fragment_section($params);
        $this->assertStringContainsString('aria-label="Topic 1"', $section);
        // Section doesn't have the modchooser div.
        $this->assertStringNotContainsString('snap-modchooser', $section);
        $this->assertStringContainsString('Section Assign', $section);
        $this->getDataGenerator()->create_module('forum', ['course' => $topics->id, 'section' => 2,
            'name' => 'Fragment forum', ]);
        $params['section'] = 2;
        $section = theme_snap_output_fragment_section($params);
        $this->assertStringContainsString('Fragment forum', $section);
        $this->setUser($teacher);
        // Missing param will result on empty text.
        $params['section'] = '';
        $section = theme_snap_output_fragment_section($params);
        $this->assertEmpty($section);
        $params['section'] = 2;
        $section = theme_snap_output_fragment_section($params);
        $this->assertStringContainsString('Fragment forum', $section);
        $this->assertStringContainsString('snap-modchooser', $section);
    }
}
