<?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\UserGroup;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowGuardTransition;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
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\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;

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

    public static function buildRegistry(?EventSubscriberInterface $eventSubscriber): Registry
    {
        $builder = new DefinitionBuilder();
        $builder
            ->setInitialPlaces(['initial'])
            ->addPlaces(['initial', 'intermediate', 'step1', 'step2', 'step3'])
            ->addTransition(new Transition('intermediate', 'initial', 'intermediate'))
            ->addTransition($transition1 = new Transition('transition1', 'intermediate', 'step1'))
            ->addTransition($transition2 = new Transition('transition2', 'intermediate', 'step2'))
            ->addTransition($transition3 = new Transition('transition3', 'intermediate', 'step3'))
        ;

        $transitionMetadata = new \SplObjectStorage();
        $transitionMetadata->attach($transition1, ['transitionGuard' => 'only-dest']);
        $transitionMetadata->attach($transition2, ['transitionGuard' => 'only-dest+system']);
        $transitionMetadata->attach($transition3, ['transitionGuard' => 'system']);

        $builder->setMetadataStore(new InMemoryMetadataStore(transitionsMetadata: $transitionMetadata));

        if (null !== $eventSubscriber) {
            $eventDispatcher = new EventDispatcher();
            $eventDispatcher->addSubscriber($eventSubscriber);
        }

        $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;
    }

    /**
     * @dataProvider provideBlockingTransition
     */
    public function testTransitionGuardBlocked(EntityWorkflow $entityWorkflow, string $transition, ?User $user, bool $isGrantedAllTransition, string $uuid): void
    {
        $userRender = $this->prophesize(UserRender::class);
        $userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string');
        $security = $this->prophesize(Security::class);
        $security->getUser()->willReturn($user);
        $security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())
            ->willReturn($isGrantedAllTransition);

        $transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal());
        $registry = self::buildRegistry($transitionGuard);

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

        $context = new WorkflowTransitionContextDTO($entityWorkflow);

        self::expectException(NotEnabledTransitionException::class);
        try {
            $workflow->apply($entityWorkflow, $transition, ['context' => $context, 'byUser' => $user, 'transition' => $transition, 'transitionAt' => new \DateTimeImmutable('now')]);
        } catch (NotEnabledTransitionException $e) {
            $list = $e->getTransitionBlockerList();

            self::assertEquals(1, $list->count());
            $list = iterator_to_array($list->getIterator());
            self::assertEquals($uuid, $list[0]->getCode());

            throw $e;
        }
    }

    public static function provideBlockingTransition(): iterable
    {
        yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), false, 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc'];
        yield [self::buildEntityWorkflow([]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7'];
        yield [self::buildEntityWorkflow([new User()]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7'];
        yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, false, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'];
        yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, true, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'];

        $userGroup = new UserGroup();
        $userGroup->addUser(new User());

        yield [self::buildEntityWorkflow([$userGroup]), 'transition1', new User(), false, 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc'];
    }

    /**
     * @dataProvider provideValidTransition
     */
    public function testTransitionGuardValid(EntityWorkflow $entityWorkflow, string $transition, ?User $user, bool $isGrantedAllTransition, string $newStep): void
    {
        $userRender = $this->prophesize(UserRender::class);
        $userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string');
        $security = $this->prophesize(Security::class);
        $security->getUser()->willReturn($user);
        $security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())
            ->willReturn($isGrantedAllTransition);

        $transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal());
        $registry = self::buildRegistry($transitionGuard);

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

        $workflow->apply($entityWorkflow, $transition, ['context' => $context, 'byUser' => $user, 'transition' => $transition, 'transitionAt' => new \DateTimeImmutable('now')]);

        self::assertEquals($newStep, $entityWorkflow->getStep());
    }

    public static function provideValidTransition(): iterable
    {
        yield [self::buildEntityWorkflow([$u = new User()]), 'transition1', $u, false, 'step1'];
        yield [self::buildEntityWorkflow([$u = new User()]), 'transition2', $u, false, 'step2'];
        yield [self::buildEntityWorkflow([new User()]), 'transition2', null, false, 'step2'];
        yield [self::buildEntityWorkflow([]), 'transition2', null, false, 'step2'];
        yield [self::buildEntityWorkflow([new User()]), 'transition3', null, false, 'step3'];
        yield [self::buildEntityWorkflow([]), 'transition3', null, false, 'step3'];

        // transition allowed thanks to permission "apply all transitions"
        yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), true, 'step1'];
        yield [self::buildEntityWorkflow([new User()]), 'transition2', new User(), true, 'step2'];

        $userGroup = new UserGroup();
        $userGroup->addUser($u = new User());
        yield [self::buildEntityWorkflow([$userGroup]), 'transition1', $u, false, 'step1'];
    }

    public static function buildEntityWorkflow(array $futureDestUsers): EntityWorkflow
    {
        $registry = self::buildRegistry(null);
        $baseContext = ['transition' => 'intermediate', 'transitionAt' => new \DateTimeImmutable()];

        // test a user not is destination is blocked
        $entityWorkflow = new EntityWorkflow();
        $workflow = $registry->get($entityWorkflow, 'dummy');
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
        $dto->futureDestUsers = $futureDestUsers;
        $workflow->apply($entityWorkflow, 'intermediate', ['context' => $dto, ...$baseContext]);

        return $entityWorkflow;
    }
}
