<?php

declare(strict_types=1);

/**
 * This program 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; under version 2
 * of the License (non-upgradable).
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2020 (original work) Open Assessment Technologies SA;
 *
 */

namespace oat\taoDacSimple\test\unit\model;

use core_kernel_classes_Class;
use oat\generis\test\MockObject;
use oat\generis\test\TestCase;
use oat\oatbox\event\EventManager;
use oat\tao\model\event\DataAccessControlChangedEvent;
use oat\taoDacSimple\model\DataBaseAccess;
use oat\taoDacSimple\model\event\DacAffectedUsersEvent;
use oat\taoDacSimple\model\event\DacRootAddedEvent;
use oat\taoDacSimple\model\PermissionsService;
use oat\taoDacSimple\model\PermissionsServiceException;
use oat\taoDacSimple\model\PermissionsStrategyInterface;

class PermissionsServiceTest extends TestCase
{
    /**
     * @var MockObject|DataBaseAccess
     */
    private $databaseAccess;
    /**
     * @var PermissionsService
     */
    private $service;
    /**
     * @var MockObject|PermissionsStrategyInterface
     */
    private $strategy;
    /**
     * @var MockObject|EventManager
     */
    private $eventManager;

    protected function setUp(): void
    {
        $this->eventManager = $this->createMock(EventManager::class);

        $this->databaseAccess = $this->createMock(DataBaseAccess::class);
        $this->databaseAccess->method('getResourcePermissions')->willReturn([]);

        $this->strategy = $this->createMock(PermissionsStrategyInterface::class);

        $this->service = new PermissionsService(
            $this->databaseAccess,
            $this->strategy,
            $this->eventManager
        );
    }

    public function testSaveAddPermissions(): void
    {
        $this->databaseAccess->expects($this->atLeast(1))->method('addMultiplePermissions');
        $this->databaseAccess->expects($this->never())->method('removeMultiplePermissions');

        $this->databaseAccess->method('getResourcesPermissions')->willReturn(
            [
                'res1' => []
            ]
        );

        $this->strategy->method('normalizeRequest')->willReturn(
            [
                'add' => [
                    'uid1' => ['GRANT', 'READ', 'WRITE']
                ]
            ]
        );

        $this->strategy->method('getPermissionsToAdd')->willReturn(
            [
                'uid1' => ['GRANT', 'READ', 'WRITE']
            ]
        );

        $resource = $this->createMock(core_kernel_classes_Class::class);
        $resource->method('getUri')->willReturn('res1');

        $this->mockTriggeredEvents('res1', 'uid1', false);

        $this->service->savePermissions(
            false,
            $resource,
            [
                'uid1' => ['GRANT', 'READ', 'WRITE']
            ]
        );
    }

    public function testSavePermissionsAddRecursively(): void
    {
        $this->databaseAccess->method('getResourcesPermissions')->willReturn(
            [
                'uid2uri' => []
            ]
        );

        $this->databaseAccess->expects($this->once())->method('addMultiplePermissions');
        $this->databaseAccess->expects($this->never())->method('removeMultiplePermissions');

        $this->strategy->method('normalizeRequest')->willReturn(
            [
                'add' => [
                    'uid1' => ['GRANT', 'READ', 'WRITE']
                ]
            ]
        );

        $this->strategy->method('getPermissionsToAdd')->willReturn(
            [
                'uid1' => ['GRANT', 'READ', 'WRITE']
            ]
        );

        /** @var MockObject|core_kernel_classes_Class $childResource */
        $childResource = $this->createMock(core_kernel_classes_Class::class);
        $childResource->method('getUri')->willReturn('uid2uri');

        /** @var MockObject|core_kernel_classes_Class $resource */
        $resource = $this->createMock(core_kernel_classes_Class::class);
        $resource->method('getSubClasses')->willReturn(
            [
                $childResource
            ]
        );

        $resource->method('getInstances')->willReturn([]);
        $resource->method('getUri')->willReturn('uid2uri');

        $this->mockTriggeredEvents('uid2uri', 'uid1', true);

        $this->service->savePermissions(
            true,
            $resource,
            [
                'uid1' => ['GRANT', 'READ', 'WRITE']
            ]
        );
    }

    public function testCantRemoveResourceWithNoGrantLeft(): void
    {
        $this->expectException(PermissionsServiceException::class);

        $this->databaseAccess->method('getResourcesPermissions')->willReturn(
            [
                'uid2uri' => [
                    'uid1' => ['GRANT', 'READ', 'WRITE']
                ]
            ]
        );

        $this->strategy->method('normalizeRequest')->willReturn(
            [
                'remove' => [
                    'uid1' => ['GRANT', 'READ', 'WRITE']
                ]
            ]
        );

        $this->strategy->method('getPermissionsToRemove')->willReturn(
            [
                'uid1' => ['GRANT', 'READ', 'WRITE']
            ]
        );

        $resource = $this->createMock(core_kernel_classes_Class::class);
        $resource->method('getUri')->willReturn('uid2uri');

        $this->service->savePermissions(
            false,
            $resource,
            [
                'uid1' => ['GRANT', 'READ', 'WRITE']
            ]
        );
    }

    public function testSaveRemovePermissions(): void
    {
        $this->databaseAccess->method('getResourcesPermissions')->willReturn(
            [
                'uid2uri' => [
                    'uid1' => ['GRANT', 'READ', 'WRITE']
                ]
            ]
        );

        $this->databaseAccess->expects($this->never())->method('addMultiplePermissions');
        $this->databaseAccess->expects($this->once())->method('removeMultiplePermissions');

        $this->strategy->method('normalizeRequest')->willReturn(
            [
                'remove' => [
                    'uid1' => ['GRANT', 'READ', 'WRITE']
                ]
            ]
        );

        $this->strategy->method('getPermissionsToRemove')->willReturn(
            [
                'uid1' => ['READ', 'WRITE']
            ]
        );

        $resource = $this->createMock(core_kernel_classes_Class::class);
        $resource->method('getUri')->willReturn('uid2uri');

        $this->service->savePermissions(
            false,
            $resource,
            [
                'uid1' => ['READ', 'WRITE']
            ]
        );
    }

    public function testDoNotSaveAnythingWhenThereIsNothingToSave(): void
    {
        $this->databaseAccess->expects($this->never())->method('addPermissions');
        $this->databaseAccess->expects($this->never())->method('removePermissions');

        $this->databaseAccess->method('getResourcePermissions')->willReturn([]);

        $this->strategy->method('normalizeRequest')->willReturn([]);

        $resource = $this->createMock(core_kernel_classes_Class::class);

        $this->service->savePermissions(false, $resource, []);
    }

    private function mockTriggeredEvents(string $resourceId, string $userId, bool $isRecursive): void
    {
        $this->eventManager->expects($this->at(0))
            ->method('trigger')
            ->with(new DacRootAddedEvent($userId, $resourceId, ['GRANT', 'READ', 'WRITE']));

        $this->eventManager->expects($this->at(1))
            ->method('trigger')
            ->with(
                new DacAffectedUsersEvent(
                    [$userId],
                    []
                )
            );

        $this->eventManager->expects($this->at(2))
            ->method('trigger')
            ->with(
                new DataAccessControlChangedEvent(
                    $resourceId,
                    [
                        'add' => ['uid1' => ['GRANT', 'READ', 'WRITE']]
                    ],
                    $isRecursive
                )
            );
    }
}