<?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\Workflow\EventSubscriber;

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowGuardUnsignedTransition;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\TransitionBlocker;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;

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

    /**
     * @dataProvider guardWaitingForSignatureWithoutPermissionToApplyAllTransitionsProvider
     */
    public function testGuardWaitingForSignatureWithoutPermissionToApplyAllTransitions(EntityWorkflow $entityWorkflow, string $transition, array $expectedErrors, array $expectedParameters, string $message)
    {
        $chillEntityRender = $this->prophesize(ChillEntityRenderManagerInterface::class);
        $chillEntityRender->renderString(Argument::type('object'), Argument::type('array'))->will(fn ($args) => spl_object_hash($args[0]));

        $security = $this->prophesize(Security::class);
        if ([] !== $expectedErrors) {
            $security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow)
                ->shouldBeCalled()
                ->willReturn(false);
        }

        $registry = self::buildRegistry($chillEntityRender->reveal(), $security->reveal());

        $workflow = $registry->get($entityWorkflow, 'dummy');

        $actual = $workflow->buildTransitionBlockerList($entityWorkflow, $transition);

        self::assertCount(count($expectedErrors), $actual, $message);
        $blockers = iterator_to_array($actual->getIterator());

        if ([] !== $expectedErrors) {
            foreach ($expectedErrors as $k => $expectedError) {
                self::assertcontains($expectedError, array_map(fn (TransitionBlocker $blocker) => $blocker->getCode(), $blockers));
                self::assertEquals($expectedParameters[$k], $blockers[$k]->getParameters());
            }
        }
    }

    public static function guardWaitingForSignatureWithoutPermissionToApplyAllTransitionsProvider(): iterable
    {
        $registry = self::buildRegistry();
        $entityWorkflow = new EntityWorkflow();
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
        $dto->futureDestUsers = [$user = new User()];
        $dto->futureUserSignature = $user;

        $workflow = $registry->get($entityWorkflow, 'dummy');
        $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User(), 'transition' => 'to_signature']);

        yield [$entityWorkflow, 'to_post-signature', ['2eabe9e6-79c2-11ef-986c-2ba376180859'], [['signer' => spl_object_hash($user)]], 'A transition forward is blocked if a signature is pending'];
        yield [$entityWorkflow, 'to_cancel', [], [], 'A transition backward is allowed, even if a signature is pending'];

        $registry = self::buildRegistry();
        $entityWorkflow = new EntityWorkflow();
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
        $dto->futureDestUsers = [$user = new User()];
        $dto->futureUserSignature = $user;

        $workflow = $registry->get($entityWorkflow, 'dummy');
        $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User(), 'transition' => 'to_signature']);
        $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);

        yield [$entityWorkflow, 'to_post-signature', [], [], 'A transition forward is allowed when the signature is applyied'];

        $registry = self::buildRegistry();
        $entityWorkflow = new EntityWorkflow();
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
        $dto->futureDestUsers = [new User()];
        $dto->futurePersonSignatures = [new Person(), $p2 = new Person()];

        $workflow = $registry->get($entityWorkflow, 'dummy');
        $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User(), 'transition' => 'to_signature']);
        $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);

        yield [$entityWorkflow, 'to_post-signature', ['2eabe9e6-79c2-11ef-986c-2ba376180859'], [['signer' => spl_object_hash($p2)]], 'A transition forward is not allowed as a signature is still pending'];
        yield [$entityWorkflow, 'to_cancel', [], [], 'A transition backward is allowed, even if a signature is pending'];
    }

    /**
     * @dataProvider guardWaitingForSignatureWithPermissionToApplyAllTransitionsProvider
     */
    public function testGuardWaitingForSignatureWithPermissionToApplyAllTransitions(EntityWorkflow $entityWorkflow, string $transition, bool $expectIsGranted, string $message)
    {
        $chillEntityRender = $this->prophesize(ChillEntityRenderManagerInterface::class);
        $chillEntityRender->renderString(Argument::type('object'), Argument::type('array'))->will(fn ($args) => spl_object_hash($args[0]));

        $security = $this->prophesize(Security::class);
        $isGranted = $security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, Argument::type(EntityWorkflow::class));
        if ($expectIsGranted) {
            $isGranted->shouldBeCalled();
        }
        $isGranted->willReturn(true);

        $registry = self::buildRegistry($chillEntityRender->reveal(), $security->reveal());

        $workflow = $registry->get($entityWorkflow, 'dummy');

        $actual = $workflow->buildTransitionBlockerList($entityWorkflow, $transition);

        self::assertCount(0, $actual, $message);
    }

    public static function guardWaitingForSignatureWithPermissionToApplyAllTransitionsProvider(): iterable
    {
        $registry = self::buildRegistry();
        $entityWorkflow = new EntityWorkflow();
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
        $dto->futureDestUsers = [$user = new User()];
        $dto->futureUserSignature = $user;

        $workflow = $registry->get($entityWorkflow, 'dummy');
        $workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User(), 'transition' => 'to_signature']);

        yield [$entityWorkflow, 'to_post-signature', true, 'A transition forward is allowed, even if a signature is pending, because the user has permission to apply all transition'];
        yield [$entityWorkflow, 'to_cancel', false, 'A transition backward is allowed, even if a signature is pending'];
    }

    private static function buildRegistry(?ChillEntityRenderManagerInterface $chillEntityRender = null, ?Security $security = null): Registry
    {
        $builder = new DefinitionBuilder();
        $builder
            ->setInitialPlaces('initial')
            ->addPlaces(['initial', 'signature', 'post-signature', 'cancel'])
            ->addTransition(new Transition('to_signature', 'initial', 'signature'))
            ->addTransition($postSignature = new Transition('to_post-signature', 'signature', 'post-signature'))
            ->addTransition($cancel = new Transition('to_cancel', 'signature', 'cancel'))
        ;

        $transitionsMetadata = new \SplObjectStorage();
        $transitionsMetadata->attach($postSignature, ['isForward' => true]);
        $transitionsMetadata->attach($cancel, ['isForward' => false]);

        $metadata = new InMemoryMetadataStore(
            placesMetadata: ['signature' => ['isSignature' => ['person', 'user']]],
            transitionsMetadata: $transitionsMetadata,
        );

        $builder->setMetadataStore($metadata);

        if (null !== $chillEntityRender) {
            $eventDispatcher = new EventDispatcher();
            $eventDispatcher->addSubscriber(new EntityWorkflowGuardUnsignedTransition($chillEntityRender, $security));
        }

        $workflow = new Workflow(
            $builder->build(),
            new EntityWorkflowMarkingStore(),
            $eventDispatcher ?? null,
            'dummy',
        );

        $registry = new Registry();
        $registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
            public function supports(WorkflowInterface $workflow, object $subject): bool
            {
                return true;
            }
        });

        return $registry;
    }
}
