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

/**
 * Meeting test.
 *
 * @package   mod_bigbluebuttonbn
 * @copyright 2018 - present, Blindside Networks Inc
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
 */

namespace mod_bigbluebuttonbn;

use mod_bigbluebuttonbn\test\testcase_helper_trait;

/**
 * Meeting tests class.
 *
 * @package   mod_bigbluebuttonbn
 * @copyright 2018 - present, Blindside Networks Inc
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
 * @covers \mod_bigbluebuttonbn\meeting
 * @coversDefaultClass \mod_bigbluebuttonbn\meeting
 */
final class meeting_test extends \advanced_testcase {
    use testcase_helper_trait;

    /**
     * Setup Test
     */
    public function setUp(): void {
        parent::setUp();
        $this->initialise_mock_server();
        // We do not force the group mode so we can change the activity group mode during test.
        $this->course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]);
        $this->getDataGenerator()->create_group(['name' => 'G1', 'courseid' => $this->course->id]);
        $this->getDataGenerator()->create_group(['name' => 'G2', 'courseid' => $this->course->id]);
    }

    /**
     * Get a list of possible test (dataprovider)
     *
     * @return array[]
     */
    public static function get_instance_types_meeting_info(): array {
        return [
            'Instance Type ALL - No Group' => [
                'type' => instance::TYPE_ALL,
                'groupname' => null,
                'groupmode' => NOGROUPS,
                'canjoin' => ['useringroup' => true, 'usernotingroup' => true],
            ],
            'Instance Type ALL - Group 1 - Visible groups' => [
                'type' => instance::TYPE_ALL,
                'groupname' => 'G1',
                'groupmode' => VISIBLEGROUPS,
                'canjoin' => ['useringroup' => true, 'usernotingroup' => true],
            ],
            'Instance Type ALL - Group 1 - Separate groups' => [
                'type' => instance::TYPE_ALL,
                'groupname' => 'G1',
                'groupmode' => SEPARATEGROUPS,
                'canjoin' => ['useringroup' => true, 'usernotingroup' => false],
            ],
            'Instance Type ROOM Only - No Group' => [
                'type' => instance::TYPE_ROOM_ONLY,
                'groupname' => null,
                'groupmode' => NOGROUPS,
                'canjoin' => ['useringroup' => true, 'usernotingroup' => true],
            ],
            'Instance Type ROOM Only - Group 1 - Visible groups' => [
                'type' => instance::TYPE_ROOM_ONLY,
                'groupname' => 'G1',
                'groupmode' => VISIBLEGROUPS,
                'canjoin' => ['useringroup' => true, 'usernotingroup' => true],
            ],
            'Instance Type ROOM Only - Group 1 - Separate groups' => [
                'type' => instance::TYPE_ROOM_ONLY,
                'groupname' => 'G1',
                'groupmode' => SEPARATEGROUPS,
                'canjoin' => ['useringroup' => true, 'usernotingroup' => false],
            ],
            'Instance Type Recording Only - No Group' => [
                'type' => instance::TYPE_RECORDING_ONLY,
                'groupname' => null,
                'groupmode' => NOGROUPS,
                'canjoin' => ['useringroup' => false, 'usernotingroup' => false]
            ],
            'Instance Type Recording Only - Group 1' => [
                'type' => instance::TYPE_RECORDING_ONLY,
                'groupname' => 'G1',
                'groupmode' => VISIBLEGROUPS,
                'canjoin' => ['useringroup' => false, 'usernotingroup' => false]
            ]
        ];
    }

    /**
     * Test that create meeing is working for all types.
     *
     * @dataProvider get_instance_types_meeting_info
     * @param int $type
     * @param string|null $groupname
     * @covers ::create_meeting
     * @covers ::create_meeting_data
     * @covers ::create_meeting_metadata
     */
    public function test_create_meeting(int $type, ?string $groupname, $groupmode, $canjoin): void {
        $this->resetAfterTest();
        [$meeting, $useringroup, $usernotingroup, $groupid, $activity] =
            $this->prepare_meeting($type, $groupname, SEPARATEGROUPS, false);
        $meeting->create_meeting();
        $meetinginfo = $meeting->get_meeting_info();
        $this->assertNotNull($meetinginfo);
        $this->assertEquals($activity->id, $meetinginfo->bigbluebuttonbnid);
        $this->assertFalse($meetinginfo->statusrunning);
        $this->assertStringContainsString("is ready", $meetinginfo->statusmessage);
        $this->assertEquals($groupid, $meetinginfo->groupid);
    }

    /**
     * Test for get meeting info for all types
     *
     * @param int $type
     * @param string|null $groupname
     * @dataProvider get_instance_types_meeting_info
     * @covers ::get_meeting_info
     * @covers ::do_get_meeting_info
     */
    public function test_get_meeting_info(int $type, ?string $groupname, $groupmode, $canjoin): void {
        $this->resetAfterTest();
        [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname);
        $meetinginfo = $meeting->get_meeting_info();
        $this->assertNotNull($meetinginfo);
        $this->assertEquals($activity->id, $meetinginfo->bigbluebuttonbnid);
        $this->assertTrue($meetinginfo->statusrunning);
        $this->assertStringContainsString("in progress", $meetinginfo->statusmessage);
        $this->assertEquals($groupid, $meetinginfo->groupid);
        $meeting->end_meeting();
        $meeting->update_cache();
        $meetinginfo = $meeting->get_meeting_info();
        $this->assertFalse($meetinginfo->statusrunning);

        if ($type == instance::TYPE_ALL) {
            $this->assertTrue($meetinginfo->features['showroom']);
            $this->assertTrue($meetinginfo->features['showrecordings']);
        } else if ($type == instance::TYPE_ROOM_ONLY) {
            $this->assertTrue($meetinginfo->features['showroom']);
            $this->assertFalse($meetinginfo->features['showrecordings']);
        } else if ($type == instance::TYPE_RECORDING_ONLY) {
            $this->assertFalse($meetinginfo->features['showroom']);
            $this->assertTrue($meetinginfo->features['showrecordings']);
        }
    }

    /**
     * Test can join is working for all types
     *
     * @param int $type
     * @param string|null $groupname
     * @param int $groupmode
     * @param array $canjoin
     * @dataProvider get_instance_types_meeting_info
     * @covers ::can_join
     */
    public function test_can_join(int $type, ?string $groupname, int $groupmode, array $canjoin): void {
        $this->resetAfterTest();
        [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname, $groupmode);
        $this->setUser($useringroup);
        $meeting->update_cache();
        $this->assertEquals($canjoin['useringroup'], $meeting->can_join());
        if ($meeting->can_join()) {
            $meetinginfo = $meeting->get_meeting_info();
            $this->assertStringContainsString("The session is in progress.", $meetinginfo->statusmessage);
        }
        if ($groupname) {
            $this->setUser($usernotingroup);
            $meeting->update_cache();
            $this->assertEquals($canjoin['usernotingroup'], $meeting->can_join());
        }
    }

    /**
     * Test can join is working if opening/closing time are set
     *
     * @param int $type
     * @param string|null $groupname
     * @param int $groupmode
     * @param array $canjoin
     * @param array $dates
     * @dataProvider get_data_can_join_with_dates
     * @covers ::can_join
     */
    public function test_can_join_with_dates(int $type, ?string $groupname, int $groupmode, array $canjoin, array $dates): void {
        // Apply the data provider relative values to now.
        array_walk($dates, function(&$val) {
            $val = time() + $val;
        });
        $this->resetAfterTest();
        [$meeting, $useringroup, $usernotingroup, $groupid, $activity] =
            $this->prepare_meeting($type, $groupname, $groupmode, true, $dates);
        $this->setUser($useringroup);
        $meeting->update_cache();
        $this->assertEquals($canjoin['useringroup'], $meeting->can_join());
        // We check that admin can not join outside opening/closing times either.
        $this->setAdminUser();
        $this->assertEquals(false, $meeting->can_join());
        if ($groupname) {
            $this->setUser($usernotingroup);
            $meeting->update_cache();
            $this->assertEquals($canjoin['usernotingroup'], $meeting->can_join());
            $this->setAdminUser();
            $this->assertEquals(false, $meeting->can_join());
        }
    }

    /**
     * Test can join is working if the "Wait for moderator to join" setting is set and a moderator has not yet joined.
     *
     * @covers ::join
     * @covers ::join_meeting
     */
    public function test_join_wait_for_moderator_not_joined(): void {
        $this->resetAfterTest();

        $this->setAdminUser();
        $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
        $student = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $meetinginfo = [
            'course' => $this->get_course()->id,
            'type' => instance::TYPE_ALL,
            'wait' => 1,
        ];
        $activity = $bbbgenerator->create_instance($meetinginfo, [
            'wait' => 1,
        ]);
        $instance = instance::get_from_instanceid($activity->id);
        $meeting = new meeting($instance);

        // The moderator has not joined.
        $this->setUser($student);
        $meeting->update_cache();
        $this->expectException(\mod_bigbluebuttonbn\local\exceptions\meeting_join_exception::class);
        meeting::join_meeting($instance);
    }

    /**
     * Test can join is working if the "Wait for moderator to join" setting is set and a moderator has already joined.
     *
     * @covers ::join
     * @covers ::join_meeting
     */
    public function test_join_wait_for_moderator_is_joined(): void {
        $this->resetAfterTest();

        $this->setAdminUser();
        $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
        $moderator = $this->getDataGenerator()->create_and_enrol($this->get_course(), 'editingteacher');
        $student = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $meetinginfo = [
            'course' => $this->get_course()->id,
            'type' => instance::TYPE_ALL,
            'wait' => 1,
            'moderators' => 'role:editingteacher',
        ];
        $activity = $bbbgenerator->create_instance($meetinginfo, [
            'wait' => 1,
        ]);
        $instance = instance::get_from_instanceid($activity->id);
        $meeting = new meeting($instance);
        $bbbgenerator->create_meeting([
            'instanceid' => $instance->get_instance_id(),
        ]);

        $this->setUser($moderator);
        $meeting->update_cache();
        $joinurl = $meeting->join(logger::ORIGIN_BASE);
        $this->assertIsString($joinurl);
        $this->join_meeting($joinurl);
        $meeting->update_cache();
        $this->assertCount(1, $meeting->get_attendees());

        // The student can now join the meeting as a moderator is present.
        $this->setUser($student);
        $joinurl = $meeting->join(logger::ORIGIN_BASE);
        $this->assertIsString($joinurl);
    }

    /**
     * Test can join is working if the "user limit" setting is set and reached.
     *
     * @covers ::join
     * @covers ::join_meeting
     */
    public function test_join_user_limit_reached(): void {
        $this->resetAfterTest();
        set_config('bigbluebuttonbn_userlimit_editable', true);
        $this->setAdminUser();
        $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
        $moderator = $this->getDataGenerator()->create_and_enrol($this->get_course(), 'editingteacher');
        $student1 = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $student2 = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $meetinginfo = [
            'course' => $this->get_course()->id,
            'type' => instance::TYPE_ALL,
            'userlimit' => 2,
        ];
        $activity = $bbbgenerator->create_instance($meetinginfo, [
            'userlimit' => 2,
        ]);
        $instance = instance::get_from_instanceid($activity->id);
        $meeting = new meeting($instance);
        $bbbgenerator->create_meeting([
            'instanceid' => $instance->get_instance_id(),
        ]);
        // Moderator joins the meeting.
        $this->setUser($moderator);
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $this->assertEquals(1, $meeting->get_participant_count());

        // Student1 joins the meeting.
        $this->setUser($student1);
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $this->assertEquals(2, $meeting->get_participant_count());
        $this->assertTrue($instance->has_user_limit_been_reached($meeting->get_participant_count()));

        // Student2 tries to join but the limit has been reached.
        $this->setUser($student2);
        $meeting->update_cache();
        $this->assertFalse($meeting->can_join());
        $this->expectException(\mod_bigbluebuttonbn\local\exceptions\meeting_join_exception::class);
        meeting::join_meeting($instance);
    }

    /**
     * Test that attendees returns the right list of attendees
     *
     * @covers ::get_attendees
     */
    public function test_get_attendees(): void {
        $this->resetAfterTest();
        [$meeting, $useringroup, $usernotingroup, $groupid, $activity] =
            $this->prepare_meeting(instance::TYPE_ALL, null, NOGROUPS, true);
        $this->setUser($useringroup);
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $this->assertCount(1, $meeting->get_attendees());
        $otheruser = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $this->setUser($otheruser);
        $meeting->update_cache();
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $this->assertCount(2, $meeting->get_attendees());
    }

    /**
     * Test that attendees returns the right list of attendees
     *
     * @covers ::get_attendees
     */
    public function test_participant_count(): void {
        $this->resetAfterTest();
        [$meeting, $useringroup, $usernotingroup, $groupid, $activity] =
            $this->prepare_meeting(instance::TYPE_ALL, null, NOGROUPS, true);
        $this->setUser($useringroup);
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $meetinginfo = $meeting->get_meeting_info();
        $this->assertEquals(1, $meetinginfo->participantcount);
        $this->assertEquals(1, $meetinginfo->totalusercount);
        $this->assertEquals(0, $meetinginfo->moderatorcount);
        $this->setUser($usernotingroup);
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $meetinginfo = $meeting->get_meeting_info();
        $this->assertEquals(2, $meetinginfo->participantcount);
        $this->assertEquals(2, $meetinginfo->totalusercount);
        $this->assertEquals(0, $meetinginfo->moderatorcount);
        $this->setAdminUser();
        $this->join_meeting($meeting->join(logger::ORIGIN_BASE));
        $meeting->update_cache();
        $meetinginfo = $meeting->get_meeting_info();
        $this->assertEquals(2, $meetinginfo->participantcount);
        $this->assertEquals(3, $meetinginfo->totalusercount);
        $this->assertEquals(1, $meetinginfo->moderatorcount);
    }
    /**
     * Send a join meeting API CALL
     *
     * @param string $url
     */
    protected function join_meeting(string $url) {
        $curl = new \curl();
        $url = new \moodle_url($url);
        $curl->get($url->out_omit_querystring(), $url->params());
    }

    /**
     * Get a list of possible test (dataprovider)
     *
     * @return array[]
     */
    public static function get_data_can_join_with_dates(): array {
        return [
            'Instance Type ALL - No Group - Closed in past' => [
                'type' => instance::TYPE_ALL,
                'groupname' => null,
                'groupmode' => NOGROUPS,
                'canjoin' => ['useringroup' => false, 'usernotingroup' => false],
                'dates' => ['openingtime' => -7200, 'closingtime' => -3600]
            ],
            'Instance Type ALL - No Group - Open in future' => [
                'type' => instance::TYPE_ALL,
                'groupname' => null,
                'groupmode' => NOGROUPS,
                'canjoin' => ['useringroup' => false, 'usernotingroup' => false],
                'dates' => ['openingtime' => 3600, 'closingtime' => 7200]
            ],
        ];
    }

    /**
     * Helper to prepare for a meeting
     *
     * @param int $type
     * @param string|null $groupname
     * @param int $groupmode
     * @param bool $createmeeting
     * @param array $dates
     * @return array
     */
    protected function prepare_meeting(int $type, ?string $groupname, int $groupmode = SEPARATEGROUPS, bool $createmeeting = true,
        array $dates = []) {
        $this->setAdminUser();
        $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
        $groupid = 0;
        $useringroup = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $usernotingroup = $this->getDataGenerator()->create_and_enrol($this->get_course());
        if (!empty($groupname)) {
            $groupid = groups_get_group_by_name($this->get_course()->id, $groupname);
            $this->getDataGenerator()->create_group_member(['groupid' => $groupid, 'userid' => $useringroup->id]);
        }
        $meetinginfo = [
            'course' => $this->get_course()->id,
            'type' => $type
        ];
        if ($dates) {
            $meetinginfo = array_merge($meetinginfo, $dates);
        };
        $activity = $bbbgenerator->create_instance($meetinginfo, ['groupmode' => $groupmode]);
        $instance = instance::get_from_instanceid($activity->id);
        if ($groupid) {
            $instance->set_group_id($groupid);
        }
        if ($createmeeting) {
            // Create the meetings on the mock server, so we can join it as a simple user.
            $bbbgenerator->create_meeting([
                'instanceid' => $instance->get_instance_id(),
                'groupid' => $instance->get_group_id()
            ]);
        }
        $meeting = new meeting($instance);
        return [$meeting, $useringroup, $usernotingroup, $groupid, $activity];
    }

    /**
     * Test process_meeting_events.
     *
     * @covers ::meeting_events
     */
    public function test_process_meeting_events(): void {
        global $DB;
        $this->resetAfterTest();

        [$bbactivitycontext, $bbactivitycm, $bbactivity] = $this->create_instance(
            $this->get_course(),
            ['type' => instance::TYPE_ALL]
        );
        $generator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
        $user = $this->getDataGenerator()->create_and_enrol($this->get_course());
        $instance = instance::get_from_instanceid($bbactivity->id);

        // Prepare meeting event data with guest user and Moodle user.
        $data = json_decode(json_encode([
            'meeting_id' => $instance->get_meeting_id(),
            'internal_meeting_id' => sha1($instance->get_meeting_id()),
            'data' => [
                'attendees' => [
                    [
                        'ext_user_id' => 'w_abcdefg',
                        'engagement' => [
                            'chats' => 1,
                            'talks' => 1,
                        ],
                    ],
                    [
                        'ext_user_id' => (string) $user->id,
                        'engagement' => [
                            'chats' => 2,
                            'talks' => 3,
                        ],
                    ],
                ],
            ],
        ]));

        // Process the meeting events.
        $result = meeting::meeting_events($instance, $data);

        $logs = $DB->get_records('bigbluebuttonbn_logs', [
            'bigbluebuttonbnid' => $bbactivity->id,
            'log' => 'Summary',
        ]);
        $this->assertCount(1, $logs);

        $log = reset($logs);
        $this->assertEquals($user->id, $log->userid);
        $meta = json_decode($log->meta);
        $this->assertEquals(2, $meta->data->engagement->chats);
        $this->assertEquals(3, $meta->data->engagement->talks);
    }
}
