<?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\Entity\Workflow\EntityWorkflowAttachment;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
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;

    /**
     * @param array<int, EntityWorkflowAttachment> $attachments
     *
     * @return void
     */
    private function buildStoredObjectVoter(
        bool $canBeAssociatedWithWorkflow,
        AssociatedEntityToStoredObjectInterface $repository,
        Security $security,
        ?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null,
        array $attachments = [],
    ): AbstractStoredObjectVoter {
        $attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
        $attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);

        // Anonymous class extending the abstract class
        return new class ($canBeAssociatedWithWorkflow, $repository, $security, $attachmentsRepository->reveal(), $workflowDocumentService) extends AbstractStoredObjectVoter {
            public function __construct(
                private readonly bool $canBeAssociatedWithWorkflow,
                private readonly AssociatedEntityToStoredObjectInterface $repository,
                Security $security,
                EntityWorkflowAttachmentRepository $attachmentRepository,
                WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService,
            ) {
                parent::__construct($security, $attachmentRepository, $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
    {
        $entityWorkflowService = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);

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

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

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

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

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

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

    /**
     * @dataProvider dataProviderVoteOnAttributeWithWorkflow
     */
    public function testVoteOnAttributeWithWorkflow(
        StoredObjectRoleEnum $attribute,
        bool $expected,
        bool $isGrantedRegularPermission,
        string $isGrantedWorkflowPermission,
    ): 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(WorkflowRelatedEntityPermissionHelperInterface::class);

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

        $attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
        $attachementRepository->findByStoredObject($storedObject)->willReturn([]);

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

        $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter {
            public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository)
            {
                parent::__construct($security, $attachmentRepository, $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 dataProviderVoteOnAttributeWithWorkflow(): iterable
    {
        foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
            yield 'Not related to any workflow nor attachment ('.$action.')' => [
                $attribute,
                true,
                true,
                WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
            ];

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

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

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

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

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

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

    /**
     * @dataProvider dataProviderVoteOnAttribute
     */
    public function testVoteOnAttribute(
        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(WorkflowRelatedEntityPermissionHelperInterface::class);

        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 dataProviderVoteOnAttribute(): 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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
        yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
        yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
    }

    /**
     * @dataProvider dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments
     */
    public function testPrecedenceOfDirectAssociationOverWorkflowAttachments(
        StoredObjectRoleEnum $attribute,
        bool $expected,
        bool $regularPermission,
        string $directWorkflowPermission,
        string $attachmentWorkflowPermission,
        string $message,
    ): 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($regularPermission);

        $workflowHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class);

        // Direct association permission
        if (StoredObjectRoleEnum::SEE === $attribute) {
            $workflowHelper->isAllowedByWorkflowForReadOperation($related)
                ->willReturn($directWorkflowPermission);
        } else {
            $workflowHelper->isAllowedByWorkflowForWriteOperation($related)
                ->willReturn($directWorkflowPermission);
        }

        // Attachment permission
        $entityWorkflow = $this->prophesize(\Chill\MainBundle\Entity\Workflow\EntityWorkflow::class)->reveal();
        $attachment = $this->prophesize(EntityWorkflowAttachment::class);
        $attachment->getEntityWorkflow()->willReturn($entityWorkflow);

        if (StoredObjectRoleEnum::SEE === $attribute) {
            $workflowHelper->isAllowedByWorkflowForReadOperation($entityWorkflow)
                ->willReturn($attachmentWorkflowPermission);
        } else {
            $workflowHelper->isAllowedByWorkflowForWriteOperation($entityWorkflow)
                ->willReturn($attachmentWorkflowPermission);
        }

        $voter = $this->buildStoredObjectVoter(
            true,
            $repository,
            $security->reveal(),
            $workflowHelper->reveal(),
            [$attachment->reveal()]
        );

        self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
    }

    public static function dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments(): iterable
    {
        $cases = [
            [
                'expected' => true,
                'regular' => false,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
                'message' => 'Direct FORCE_GRANT should win over attachment FORCE_DENIED',
            ],
            [
                'expected' => false,
                'regular' => true,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
                'message' => 'Direct FORCE_DENIED should win over attachment FORCE_GRANT',
            ],
            [
                'expected' => true,
                'regular' => false,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'message' => 'Direct FORCE_GRANT should win over attachment ABSTAIN',
            ],
            [
                'expected' => false,
                'regular' => true,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'message' => 'Direct FORCE_DENIED should win over attachment ABSTAIN',
            ],
            [
                'expected' => true,
                'regular' => false,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT,
                'message' => 'Direct ABSTAIN should let attachment FORCE_GRANT win',
            ],
            [
                'expected' => false,
                'regular' => true,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED,
                'message' => 'Direct ABSTAIN should let attachment FORCE_DENIED win',
            ],
            [
                'expected' => true,
                'regular' => true,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'message' => 'Both ABSTAIN should let regular permission (true) win',
            ],
            [
                'expected' => false,
                'regular' => false,
                'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN,
                'message' => 'Both ABSTAIN should let regular permission (false) win',
            ],
        ];

        foreach ([StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT] as $attribute) {
            foreach ($cases as $case) {
                yield sprintf('%s - %s', $attribute->name, $case['message']) => [
                    $attribute,
                    $case['expected'],
                    $case['regular'],
                    $case['direct'],
                    $case['attachment'],
                    $case['message'],
                ];
            }
        }
    }
}

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

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