<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Tests\Security\Authorization;

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Test\PrepareCenterTrait;
use Chill\MainBundle\Test\PrepareScopeTrait;
use Chill\MainBundle\Test\PrepareUserTrait;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

/**
 * @internal
 *
 * @coversNothing
 */
final class AuthorizationHelperTest extends KernelTestCase
{
    use PrepareCenterTrait;

    use PrepareScopeTrait;

    use PrepareUserTrait;

    use ProphecyTrait;

    protected function setUp(): void
    {
        self::bootKernel();
    }

    /**
     * @group legacy
     */
    public function testGetParentRoles()
    {
        $parentRoles = $this->getAuthorizationHelper()
            ->getParentRoles('CHILL_INHERITED_ROLE_1');

        $this->assertContains(
            'CHILL_MASTER_ROLE',
            $parentRoles,
            'Assert that `CHILL_MASTER_ROLE` is a parent of `CHILL_INHERITED_ROLE_1`'
        );
    }

    /**
     * @dataProvider dataProvider_getReachableCenters
     */
    public function testGetReachableCenters(mixed $test, mixed $result, mixed $msg)
    {
        $this->assertEquals($test, $result, $msg);
    }

    public static function dataProvider_getReachableCenters()
    {
        self::bootKernel();

        $centerA = self::prepareCenter(1, 'center A');
        $centerB = self::prepareCenter(2, 'center B');
        $scopeA = self::prepareScope(1, 'scope default');
        $scopeB = self::prepareScope(2, 'scope B');
        $scopeC = self::prepareScope(3, 'scope C');

        $userA = self::prepareUser([
            [
                'center' => $centerA,
                'permissionsGroup' => [
                    ['scope' => $scopeB, 'role' => 'CHILL_ROLE_1'],
                    ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'],
                ],
            ],
            [
                'center' => $centerB,
                'permissionsGroup' => [
                    ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'],
                    ['scope' => $scopeC, 'role' => 'CHILL_ROLE_2'],
                ],
            ],
        ]);

        $ah = self::getAuthorizationHelper();

        return [
            // without scopes
            [
                true,
                \in_array($centerA, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_1',
                    null
                ), true),
                'center A should be available for userA, with role 1 ',
            ],
            [
                true,
                \in_array($centerA, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_2',
                    null
                ), true),
                'center A should be available for userA, with role 2 ',
            ],
            [
                true,
                \in_array($centerB, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_2',
                    null
                ), true),
                'center A should be available for userA, with role 2 ',
            ],
            [
                false,
                \in_array($centerB, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_1',
                    null
                ), true),
                'center B should NOT be available for userA, with role 1 ',
            ],
            // with scope
            [
                true,
                \in_array($centerA, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_1',
                    $scopeB
                ), true),
                'center A should be available for userA, with role 1, scopeC ',
            ],
            [
                false,
                \in_array($centerA, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_2',
                    $scopeC
                ), true),
                'center A should NOT be available for userA, with role 2, scopeA ',
            ],
            [
                true,
                \in_array($centerB, $ah->getReachableCenters(
                    $userA,
                    'CHILL_ROLE_2',
                    $scopeA
                ), true),
                'center B should be available for userA, with role 2, scopeA ',
            ],
        ];
    }

    /**
     * @dataProvider dataProvider_getReachableScopes
     *
     * @param bool   $expectedResult
     * @param string $message
     */
    public function testGetReachableScopes(
        $expectedResult,
        Scope $testedScope,
        User $user,
        string $role,
        Center $center,
        $message,
    ) {
        $reachableScopes = $this->getAuthorizationHelper()
            ->getReachableScopes($user, $role, $center);

        $this->assertEquals(
            $expectedResult,
            \in_array($testedScope, $reachableScopes, true),
            $message
        );
    }

    public static function dataProvider_getReachableScopes()
    {
        $centerA = self::prepareCenter(1, 'center A');
        $centerB = self::prepareCenter(2, 'center B');
        $scopeA = self::prepareScope(1, 'scope default');
        $scopeB = self::prepareScope(2, 'scope B');
        $scopeC = self::prepareScope(3, 'scope C');

        $userA = self::prepareUser([
            [
                'center' => $centerA,
                'permissionsGroup' => [
                    ['scope' => $scopeB, 'role' => 'CHILL_ROLE_1'],
                    ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'],
                ],
            ],
            [
                'center' => $centerB,
                'permissionsGroup' => [
                    ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'],
                    ['scope' => $scopeC, 'role' => 'CHILL_ROLE_2'],
                    ['scope' => $scopeB, 'role' => 'CHILL_ROLE_2'],
                ],
            ],
        ]);

        return [
            [
                true,
                $scopeA,
                $userA,
                'CHILL_ROLE_2',
                $centerA,
                'Assert that a scope is found within accessible scopes',
            ],
            [
                false,
                $scopeB,
                $userA,
                'CHILL_ROLE_2',
                $centerA,
                'Assert that a scope not reachable is NOT found within accessible scopes',
            ],
            [
                false,
                $scopeB,
                $userA,
                'CHILL_ROLE_1',
                $centerB,
                'Assert that a scope not reachable is not found within accessible scopes.'
                .' Trying on filter centering',
            ],
        ];
    }

    public function testtestUserHasAccessUserShouldHaveAccessEntityWithScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->willImplement('\\'.HasScopeInterface::class);
        $entity->getCenter()->willReturn($center);
        $entity->getScope()->willReturn($scope);

        $this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    /**
     * Test function userCanReach of helper.
     *
     * A user can not reachcenter =>W the function should return false
     */
    public function testUserCanReachCenterUserShouldNotReach()
    {
        $centerA = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(2, 'centerB');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $centerA, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'ANY_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();

        $this->assertFalse($helper->userCanReachCenter($user, $centerB));
    }

    /**
     * Test function userCanReach of helper.
     *
     * A user can reach center => the function should return true.
     */
    public function testUserCanReachCenterUserShouldReach()
    {
        $center = $this->prepareCenter(1, 'center');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'ANY_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();

        $this->assertTrue($helper->userCanReachCenter($user, $center));
    }

    public function testUserHasAccessEntityMultiScope()
    {
        $centerA = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(2, 'centerB');
        $scopeA = $this->prepareScope(3, 'other'); // the user will be granted this scope
        $scopeB = $this->prepareScope(4, 'other'); // the user will be granted this scope
        $user = $this->prepareUser([
            [
                'center' => $centerA, 'permissionsGroup' => [
                    ['scope' => $scopeA, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement(HasCentersInterface::class);
        $entity->willImplement(HasScopesInterface::class);
        $entity->getCenters()->willReturn([$centerA, $centerB]);
        $entity->getScopes()->willReturn([$scopeA, $scopeB]);

        $this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testUserHasAccessEntityIsCenter()
    {
        $centerA = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(2, 'centerB');
        $user = $this->prepareUser([
            [
                'center' => $centerA, 'permissionsGroup' => [
                    ['scope' => null, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);

        $helper = $this->getAuthorizationHelper();

        $this->assertFalse($helper->userHasAccess($user, $centerB, 'CHILL_ROLE'));
        $this->assertTrue($helper->userHasAccess($user, $centerA, 'CHILL_ROLE'));
    }

    public function testUserHasAccessMultiCenterEntityWithoutScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(1, 'centerB');
        $scopeB = $this->prepareScope(2, 'other'); // the user will be granted this scope
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scopeB, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement(HasCentersInterface::class);
        $entity->getCenters()->willReturn([$center, $centerB]);

        $this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testUserHasAccessShouldHaveAccessEntityWithoutScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->getCenter()->willReturn($center);

        $this->assertTrue($helper->userHasAccess(
            $user,
            $entity->reveal(),
            'CHILL_ROLE'
        ));
    }

    public function testUserHasAccessShouldHaveAccessWithInheritanceEntityWithoutScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'CHILL_MASTER_ROLE'],
                ],
            ],
        ]);

        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->getCenter()->willReturn($center);

        $this->assertTrue($helper->userHasAccess(
            $user,
            $entity->reveal(),
            'CHILL_INHERITED_ROLE_1'
        ));
    }

    public function testUserHasAccessUserHasNoCenterEntityWithScope()
    {
        $centerA = $this->prepareCenter(1, 'center'); // the user will have this center
        $centerB = $this->prepareCenter(2, 'centerB'); // the entity will have another center
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $centerA, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->willImplement('\\'.HasScopeInterface::class);
        $entity->getCenter()->willReturn($centerB);
        $entity->getScope()->willReturn($scope);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testuserHasAccessUserHasNoRoleEntityWithoutScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'ANY_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->getCenter()->willReturn($center);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testUserHasAccessUserHasNoRoleEntityWithScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->willImplement('\\'.HasScopeInterface::class);
        $entity->getCenter()->willReturn($center);
        $entity->getScope()->willReturn($scope);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'ANOTHER_ROLE'));
    }

    /**
     * test that a user has no access on a entity, but is granted on the same role
     * on another center.
     */
    public function testUserHasAccessUserHasNoRoleUserHasRoleOnAnotherCenterEntityWithoutScope()
    {
        $centerA = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(2, 'centerB');
        $scope = $this->prepareScope(1, 'default');
        $user = $this->prepareUser([
            [
                'center' => $centerA, 'permissionsGroup' => [
                    ['scope' => $scope, 'role' => 'ANY_ROLE'],
                ],
                [
                    'centerB' => $centerB, 'permissionsGroup' => [
                        ['scope' => $scope, 'role' => 'ANY_ROLE'],
                        ['scope' => $scope, 'role' => 'CHILL_ROLE'],
                    ],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement('\\'.HasCenterInterface::class);
        $entity->getCenter()->willReturn($centerA);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testUserHasAccessUserHasNoScopeEntityWithScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $scopeA = $this->prepareScope(1, 'default'); // the entity will have this scope
        $scopeB = $this->prepareScope(2, 'other'); // the user will be granted this scope
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scopeB, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement(HasCenterInterface::class);
        $entity->willImplement(HasScopeInterface::class);
        $entity->getCenter()->willReturn($center);
        $entity->getScope()->willReturn($scopeA);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testUserHasNoAccessEntityMultiScope()
    {
        $centerA = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(2, 'centerB');
        $scopeA = $this->prepareScope(3, 'other'); // the user will be granted this scope
        $scopeB = $this->prepareScope(4, 'other'); // the user will be granted this scope
        $scopeC = $this->prepareScope(5, 'other'); // the user will be granted this scope
        $user = $this->prepareUser([
            [
                'center' => $centerA, 'permissionsGroup' => [
                    ['scope' => $scopeA, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement(HasCentersInterface::class);
        $entity->willImplement(HasScopesInterface::class);
        $entity->getCenters()->willReturn([$centerA, $centerB]);
        $entity->getScopes()->willReturn([$scopeB, $scopeC]);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    public function testUserHasNoAccessMultiCenterEntityWithoutScope()
    {
        $center = $this->prepareCenter(1, 'center');
        $centerB = $this->prepareCenter(2, 'centerB');
        $centerC = $this->prepareCenter(3, 'centerC');
        $scopeB = $this->prepareScope(4, 'other'); // the user will be granted this scope
        $user = $this->prepareUser([
            [
                'center' => $center, 'permissionsGroup' => [
                    ['scope' => $scopeB, 'role' => 'CHILL_ROLE'],
                ],
            ],
        ]);
        $helper = $this->getAuthorizationHelper();
        $entity = $this->prophesize();
        $entity->willImplement(HasCentersInterface::class);
        $entity->getCenters()->willReturn([$centerB, $centerC]);

        $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
    }

    /**
     * @return AuthorizationHelper
     */
    private static function getAuthorizationHelper()
    {
        return self::getContainer()
            ->get(AuthorizationHelper::class);
    }
}
