<?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\DocStoreBundle\Tests\Security\Authorization;

use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Security;

/**
 * @internal
 *
 * @coversNothing
 */
class StoredObjectVoterTest extends TestCase
{
    /**
     * @dataProvider provideDataVote
     */
    public function testVote(array $storedObjectVotersDefinition, object $subject, string $attribute, bool $fallbackSecurityExpected, bool $securityIsGrantedResult, mixed $expected): void
    {
        $storedObjectVoters = array_map(fn (array $definition) => $this->buildStoredObjectVoter($definition[0], $definition[1], $definition[2]), $storedObjectVotersDefinition);
        $token = new UsernamePasswordToken(new User(), 'chill_main', ['ROLE_USER']);

        $security = $this->createMock(Security::class);
        $security->expects($fallbackSecurityExpected ? $this->atLeastOnce() : $this->never())
            ->method('isGranted')
            ->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
            ->willReturn($securityIsGrantedResult);

        $voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger(), $this->createMock(EntityWorkflowAttachmentRepository::class));

        self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
    }

    public static function provideDataVote(): iterable
    {
        yield [
            // we try with something else than a SToredObject, the voter should abstain
            [[false, false, false]],
            new \stdClass(),
            'SOMETHING',
            false,
            false,
            VoterInterface::ACCESS_ABSTAIN,
        ];
        yield [
            // we try with an unsupported attribute, the voter must abstain
            [[false, false, false]],
            new StoredObject(),
            'SOMETHING',
            false,
            false,
            VoterInterface::ACCESS_ABSTAIN,
        ];
        yield [
            // happy scenario: there is a role voter
            [[true, true, true]],
            new StoredObject(),
            StoredObjectRoleEnum::SEE->value,
            false,
            false,
            VoterInterface::ACCESS_GRANTED,
        ];
        yield [
            // there is a role voter, but not allowed to see the stored object
            [[true, true, false]],
            new StoredObject(),
            StoredObjectRoleEnum::SEE->value,
            false,
            false,
            VoterInterface::ACCESS_DENIED,
        ];
        yield [
            // there is no role voter, fallback to security, which does not grant access
            [[true, false, false]],
            new StoredObject(),
            StoredObjectRoleEnum::SEE->value,
            true,
            false,
            VoterInterface::ACCESS_DENIED,
        ];
        yield [
            // there is no role voter, fallback to security, which does grant access
            [[true, false, false]],
            new StoredObject(),
            StoredObjectRoleEnum::SEE->value,
            true,
            true,
            VoterInterface::ACCESS_GRANTED,
        ];
    }

    private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
    {
        $storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
        $storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
            ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
            ->willReturn($supports);
        $storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
            ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
            ->willReturn($voteOnAttribute);

        return $storedObjectVoter;
    }
}
