<?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\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Security;

/**
 * @internal
 *
 * @coversNothing
 */
class AbstractStoredObjectVoterTest extends TestCase
{
    use ProphecyTrait;

    private function buildStoredObjectVoter(
        bool $canBeAssociatedWithWorkflow,
        AssociatedEntityToStoredObjectInterface $repository,
        Security $security,
        ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
    ): AbstractStoredObjectVoter {
        // Anonymous class extending the abstract class
        return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
            public function __construct(
                private readonly bool $canBeAssociatedWithWorkflow,
                private readonly AssociatedEntityToStoredObjectInterface $repository,
                Security $security,
                ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
            ) {
                parent::__construct($security, $workflowDocumentService);
            }

            protected function attributeToRole($attribute): string
            {
                return 'SOME_ROLE';
            }

            protected function getRepository(): AssociatedEntityToStoredObjectInterface
            {
                return $this->repository;
            }

            protected function getClass(): string
            {
                return \stdClass::class;
            }

            protected function canBeAssociatedWithWorkflow(): bool
            {
                return $this->canBeAssociatedWithWorkflow;
            }
        };
    }

    public function testSupportsOnAttribute(): void
    {
        $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);

        self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));

        $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null);

        self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));

        $voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null);

        self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
    }

    /**
     * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
     */
    public function testVoteOnAttributeWithStoredObjectPermission(
        StoredObjectRoleEnum $attribute,
        bool $expected,
        bool $isGrantedRegularPermission,
        string $isGrantedWorkflowPermission,
        string $isGrantedStoredObjectAttachment,
    ): void {
        $storedObject = new StoredObject();
        $repository = new DummyRepository($related = new \stdClass());
        $token = new UsernamePasswordToken(new User(), 'dummy');

        $security = $this->prophesize(Security::class);
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);

        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);

        $security = $this->prophesize(Security::class);
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);

        if (StoredObjectRoleEnum::SEE === $attribute) {
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
                ->shouldBeCalled()
                ->willReturn($isGrantedStoredObjectAttachment);
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
                ->willReturn($isGrantedWorkflowPermission);
        } elseif (StoredObjectRoleEnum::EDIT === $attribute) {
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
                ->shouldBeCalled()
                ->willReturn($isGrantedStoredObjectAttachment);
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
                ->willReturn($isGrantedWorkflowPermission);
        } else {
            throw new \LogicException('Invalid attribute for StoredObjectVoter');
        }

        $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
            public function __construct(private $repository, $helper, $security)
            {
                parent::__construct($security, $helper);
            }

            protected function getRepository(): AssociatedEntityToStoredObjectInterface
            {
                return $this->repository;
            }

            protected function getClass(): string
            {
                return \stdClass::class;
            }

            protected function attributeToRole(StoredObjectRoleEnum $attribute): string
            {
                return 'SOME_ROLE';
            }

            protected function canBeAssociatedWithWorkflow(): bool
            {
                return true;
            }
        };

        $actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token);

        self::assertEquals($expected, $actual);
    }

    public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
    {
        foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
            yield 'Not related to any workflow nor attachment ('.$action.')' => [
                $attribute,
                true,
                true,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
            ];

            yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
                $attribute,
                false,
                false,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
            ];

            yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
                $attribute,
                false,
                true,
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
            ];

            yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
                $attribute,
                false,
                true,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
            ];

            yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
                $attribute,
                false,
                true,
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
            ];

            yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
                $attribute,
                false,
                true,
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
            ];

            yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
                $attribute,
                false,
                false,
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
            ];

            yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
                $attribute,
                false,
                false,
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
            ];

            yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
                $attribute,
                true,
                false,
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
            ];

            yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
                $attribute,
                true,
                false,
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
            ];

        }
    }

    /**
     * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
     */
    public function testVoteOnAttributeWithoutStoredObjectPermission(
        StoredObjectRoleEnum $attribute,
        bool $expected,
        bool $canBeAssociatedWithWorkflow,
        bool $isGrantedRegularPermission,
        ?string $isGrantedWorkflowPermissionRead,
        ?string $isGrantedWorkflowPermissionWrite,
        string $message,
    ): void {
        $storedObject = new StoredObject();
        $dummyRepository = new DummyRepository($related = new \stdClass());
        $token = new UsernamePasswordToken(new User(), 'dummy');

        $security = $this->prophesize(Security::class);
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);

        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);

        $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
        $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);

        if (null !== $isGrantedWorkflowPermissionRead) {
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
                ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
        } else {
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)->shouldNotBeCalled();
        }

        if (null !== $isGrantedWorkflowPermissionWrite) {
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
                ->willReturn($isGrantedWorkflowPermissionWrite)->shouldBeCalled();
        } else {
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)->shouldNotBeCalled();
        }

        $voter = $this->buildStoredObjectVoter($canBeAssociatedWithWorkflow, $dummyRepository, $security->reveal(), $workflowRelatedEntityPermissionHelper->reveal());
        self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
    }

    public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
    {
        // not associated on a workflow
        yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
        yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];

        // associated on a workflow, read operation
        yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
        yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
        yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
        yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
        yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
        yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];

        // association on a workflow, write operation
        yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
        yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
        yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
        yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
        yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
        yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
    }
}

class DummyRepository implements AssociatedEntityToStoredObjectInterface
{
    public function __construct(private readonly ?object $relatedEntity) {}

    public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
    {
        return $this->relatedEntity;
    }
}
