<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Workflow\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Event\EnteredEvent;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Event\TransitionEvent;
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
use Symfony\Component\Workflow\Exception\UndefinedTransitionException;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\TransitionBlocker;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class WorkflowTest extends TestCase
{
    use WorkflowBuilderTrait;

    public function testGetMarkingWithEmptyDefinition()
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('The Marking is empty and there is no initial place for workflow "unnamed".');
        $subject = new Subject();
        $workflow = new Workflow(new Definition([], []), new MethodMarkingStore());

        $workflow->getMarking($subject);
    }

    public function testGetMarkingWithImpossiblePlace()
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('Place "nope" is not valid for workflow "unnamed".');
        $subject = new Subject();
        $subject->setMarking(['nope' => 1]);
        $workflow = new Workflow(new Definition([], []), new MethodMarkingStore());

        $workflow->getMarking($subject);
    }

    public function testGetMarkingWithEmptyInitialMarking()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $marking = $workflow->getMarking($subject);

        $this->assertInstanceOf(Marking::class, $marking);
        $this->assertTrue($marking->has('a'));
        $this->assertSame(['a' => 1], $subject->getMarking());
    }

    public function testGetMarkingWithExistingMarking()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $subject->setMarking(['b' => 1, 'c' => 1]);
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $marking = $workflow->getMarking($subject);

        $this->assertInstanceOf(Marking::class, $marking);
        $this->assertTrue($marking->has('b'));
        $this->assertTrue($marking->has('c'));
    }

    public function testCanWithUnexistingTransition()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $this->assertFalse($workflow->can($subject, 'foobar'));
    }

    public function testCan()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $this->assertTrue($workflow->can($subject, 't1'));
        $this->assertFalse($workflow->can($subject, 't2'));

        $subject->setMarking(['b' => 1]);

        $this->assertFalse($workflow->can($subject, 't1'));
        // In a workflow net, all "from" places should contain a token to enable
        // the transition.
        $this->assertFalse($workflow->can($subject, 't2'));

        $subject->setMarking(['b' => 1, 'c' => 1]);

        $this->assertFalse($workflow->can($subject, 't1'));
        $this->assertTrue($workflow->can($subject, 't2'));

        $subject->setMarking(['f' => 1]);

        $this->assertFalse($workflow->can($subject, 't5'));
        $this->assertTrue($workflow->can($subject, 't6'));
    }

    public function testCanWithGuard()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $eventDispatcher = new EventDispatcher();
        $eventDispatcher->addListener('workflow.workflow_name.guard.t1', static function (GuardEvent $event) {
            $event->setBlocked(true);
        });
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');

        $this->assertFalse($workflow->can($subject, 't1'));
    }

    public function testCanDoesNotTriggerGuardEventsForNotEnabledTransitions()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();

        $dispatchedEvents = [];
        $eventDispatcher = new EventDispatcher();

        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');
        $workflow->apply($subject, 't1');
        $workflow->apply($subject, 't2');

        $eventDispatcher->addListener('workflow.workflow_name.guard.t3', static function () use (&$dispatchedEvents) {
            $dispatchedEvents[] = 'workflow_name.guard.t3';
        });
        $eventDispatcher->addListener('workflow.workflow_name.guard.t4', static function () use (&$dispatchedEvents) {
            $dispatchedEvents[] = 'workflow_name.guard.t4';
        });

        $workflow->can($subject, 't3');

        $this->assertSame(['workflow_name.guard.t3'], $dispatchedEvents);
    }

    public function testCanWithSameNameTransition()
    {
        $definition = $this->createWorkflowWithSameNameTransition();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $subject = new Subject();
        $this->assertTrue($workflow->can($subject, 'a_to_bc'));
        $this->assertFalse($workflow->can($subject, 'b_to_c'));
        $this->assertFalse($workflow->can($subject, 'to_a'));

        $subject->setMarking(['b' => 1]);
        $this->assertFalse($workflow->can($subject, 'a_to_bc'));
        $this->assertTrue($workflow->can($subject, 'b_to_c'));
        $this->assertTrue($workflow->can($subject, 'to_a'));
    }

    public function testBuildTransitionBlockerListReturnsUndefinedTransition()
    {
        $this->expectException(UndefinedTransitionException::class);
        $this->expectExceptionMessage('Transition "404 Not Found" is not defined for workflow "unnamed".');
        $definition = $this->createSimpleWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $workflow->buildTransitionBlockerList($subject, '404 Not Found');
    }

    public function testBuildTransitionBlockerList()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $this->assertTrue($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty());
        $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty());

        $subject->setMarking(['b' => 1]);

        $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty());
        $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty());

        $subject->setMarking(['b' => 1, 'c' => 1]);

        $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty());
        $this->assertTrue($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty());

        $subject->setMarking(['f' => 1]);

        $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't5')->isEmpty());
        $this->assertTrue($workflow->buildTransitionBlockerList($subject, 't6')->isEmpty());
    }

    public function testBuildTransitionBlockerListReturnsReasonsProvidedByMarking()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't2');
        $this->assertCount(1, $transitionBlockerList);
        $blockers = iterator_to_array($transitionBlockerList);
        $this->assertSame('The marking does not enable the transition.', $blockers[0]->getMessage());
        $this->assertSame('19beefc8-6b1e-4716-9d07-a39bd6d16e34', $blockers[0]->getCode());
    }

    public function testBuildTransitionBlockerListReturnsReasonsProvidedInGuards()
    {
        $definition = $this->createSimpleWorkflowDefinition();
        $subject = new Subject();
        $dispatcher = new EventDispatcher();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher);

        $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) {
            $event->addTransitionBlocker(new TransitionBlocker('Transition blocker 1', 'blocker_1'));
            $event->addTransitionBlocker(new TransitionBlocker('Transition blocker 2', 'blocker_2'));
        });
        $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) {
            $event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_3'));
        });
        $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) {
            $event->setBlocked(true);
        });
        $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) {
            $event->setBlocked(true, 'You should not pass !!');
        });

        $transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't1');
        $this->assertCount(5, $transitionBlockerList);
        $blockers = iterator_to_array($transitionBlockerList);
        $this->assertSame('Transition blocker 1', $blockers[0]->getMessage());
        $this->assertSame('blocker_1', $blockers[0]->getCode());
        $this->assertSame('Transition blocker 2', $blockers[1]->getMessage());
        $this->assertSame('blocker_2', $blockers[1]->getCode());
        $this->assertSame('Transition blocker 3', $blockers[2]->getMessage());
        $this->assertSame('blocker_3', $blockers[2]->getCode());
        $this->assertSame('The transition has been blocked by a guard (Symfony\Component\Workflow\Tests\WorkflowTest).', $blockers[3]->getMessage());
        $this->assertSame('e8b5bbb9-5913-4b98-bfa6-65dbd228a82a', $blockers[3]->getCode());
        $this->assertSame('You should not pass !!', $blockers[4]->getMessage());
        $this->assertSame('e8b5bbb9-5913-4b98-bfa6-65dbd228a82a', $blockers[4]->getCode());
    }

    public function testApplyWithNotExisingTransition()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());
        $context = [
            'lorem' => 'ipsum',
        ];

        try {
            $workflow->apply($subject, '404 Not Found', $context);

            $this->fail('Should throw an exception');
        } catch (UndefinedTransitionException $e) {
            $this->assertSame('Transition "404 Not Found" is not defined for workflow "unnamed".', $e->getMessage());
            $this->assertSame($e->getContext(), $context);
        }
    }

    public function testApplyWithNotEnabledTransition()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());
        $context = [
            'lorem' => 'ipsum',
        ];

        try {
            $workflow->apply($subject, 't2', $context);

            $this->fail('Should throw an exception');
        } catch (NotEnabledTransitionException $e) {
            $this->assertSame('Cannot apply transition "t2" on workflow "unnamed".', $e->getMessage());
            $this->assertCount(1, $e->getTransitionBlockerList());
            $list = iterator_to_array($e->getTransitionBlockerList());
            $this->assertSame('The marking does not enable the transition.', $list[0]->getMessage());
            $this->assertSame($e->getWorkflow(), $workflow);
            $this->assertSame($e->getSubject(), $subject);
            $this->assertSame($e->getTransitionName(), 't2');
            $this->assertSame($e->getContext(), $context);
        }
    }

    public function testApply()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $marking = $workflow->apply($subject, 't1');

        $this->assertInstanceOf(Marking::class, $marking);
        $this->assertFalse($marking->has('a'));
        $this->assertTrue($marking->has('b'));
        $this->assertTrue($marking->has('c'));
    }

    public function testApplyWithSameNameTransition()
    {
        $subject = new Subject();
        $definition = $this->createWorkflowWithSameNameTransition();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $marking = $workflow->apply($subject, 'a_to_bc');

        $this->assertPlaces([
            'b' => 1,
            'c' => 1,
        ], $marking);

        $marking = $workflow->apply($subject, 'to_a');

        // Two tokens in "a"
        $this->assertPlaces([
            'a' => 2,
        ], $marking);

        $workflow->apply($subject, 'a_to_bc');
        $marking = $workflow->apply($subject, 'b_to_c');

        $this->assertPlaces([
            'a' => 1,
            'c' => 2,
        ], $marking);

        $marking = $workflow->apply($subject, 'to_a');

        $this->assertPlaces([
            'a' => 2,
            'c' => 1,
        ], $marking);
    }

    public function testApplyWithSameNameTransition2()
    {
        $subject = new Subject();
        $subject->setMarking(['a' => 1, 'b' => 1]);

        $places = range('a', 'd');
        $transitions = [];
        $transitions[] = new Transition('t', 'a', 'c');
        $transitions[] = new Transition('t', 'b', 'd');
        $definition = new Definition($places, $transitions);
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $marking = $workflow->apply($subject, 't');

        $this->assertFalse($marking->has('a'));
        $this->assertFalse($marking->has('b'));
        $this->assertTrue($marking->has('c'));
        $this->assertTrue($marking->has('d'));
    }

    public function testApplyWithSameNameTransition3()
    {
        $subject = new Subject();
        $subject->setMarking(['a' => 1]);

        $places = range('a', 'd');
        $transitions = [];
        $transitions[] = new Transition('t', 'a', 'b');
        $transitions[] = new Transition('t', 'b', 'c');
        $transitions[] = new Transition('t', 'c', 'd');
        $definition = new Definition($places, $transitions);
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $marking = $workflow->apply($subject, 't');
        // We want to make sure we do not end up in "d"
        $this->assertTrue($marking->has('b'));
        $this->assertFalse($marking->has('d'));
    }

    public function testApplyWithEventDispatcher()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $eventDispatcher = new EventDispatcherMock();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');

        $eventNameExpected = [
            'workflow.entered',
            'workflow.workflow_name.entered',
            'workflow.workflow_name.entered.a',
            'workflow.guard',
            'workflow.workflow_name.guard',
            'workflow.workflow_name.guard.t1',
            'workflow.leave',
            'workflow.workflow_name.leave',
            'workflow.workflow_name.leave.a',
            'workflow.transition',
            'workflow.workflow_name.transition',
            'workflow.workflow_name.transition.t1',
            'workflow.enter',
            'workflow.workflow_name.enter',
            'workflow.workflow_name.enter.b',
            'workflow.workflow_name.enter.c',
            'workflow.entered',
            'workflow.workflow_name.entered',
            'workflow.workflow_name.entered.b',
            'workflow.workflow_name.entered.c',
            'workflow.completed',
            'workflow.workflow_name.completed',
            'workflow.workflow_name.completed.t1',
            // Following events are fired because of announce() method
            'workflow.announce',
            'workflow.workflow_name.announce',
            'workflow.guard',
            'workflow.workflow_name.guard',
            'workflow.workflow_name.guard.t2',
            'workflow.workflow_name.announce.t2',
        ];

        $workflow->apply($subject, 't1');

        $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents);
    }

    public static function provideApplyWithEventDispatcherForAnnounceTests(): \Generator
    {
        yield [false, [Workflow::DISABLE_ANNOUNCE_EVENT => true]];
        yield [true, [Workflow::DISABLE_ANNOUNCE_EVENT => false]];
        yield [true, []];
    }

    #[DataProvider('provideApplyWithEventDispatcherForAnnounceTests')]
    public function testApplyWithEventDispatcherForAnnounce(bool $fired, array $context)
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $eventDispatcher = new EventDispatcherMock();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');

        $workflow->apply($subject, 't1', $context);

        if ($fired) {
            $this->assertContains('workflow.workflow_name.announce', $eventDispatcher->dispatchedEvents);
        } else {
            $this->assertNotContains('workflow.workflow_name.announce', $eventDispatcher->dispatchedEvents);
        }
    }

    public function testApplyDispatchesWithDisableEventInContext()
    {
        $transitions[] = new Transition('a-b', 'a', 'b');
        $transitions[] = new Transition('a-c', 'a', 'c');
        $definition = new Definition(['a', 'b', 'c'], $transitions);

        $subject = new Subject();
        $eventDispatcher = new EventDispatcherMock();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');

        $eventNameExpected = [
            'workflow.guard',
            'workflow.workflow_name.guard',
            'workflow.workflow_name.guard.a-b',
            'workflow.transition',
            'workflow.workflow_name.transition',
            'workflow.workflow_name.transition.a-b',
        ];

        $workflow->apply($subject, 'a-b', [
            Workflow::DISABLE_LEAVE_EVENT => true,
            Workflow::DISABLE_ENTER_EVENT => true,
            Workflow::DISABLE_ENTERED_EVENT => true,
            Workflow::DISABLE_COMPLETED_EVENT => true,
            Workflow::DISABLE_ANNOUNCE_EVENT => true,
        ]);

        $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents);
    }

    public function testApplyDispatchesNoEventsWhenSpecifiedByDefinition()
    {
        $transitions[] = new Transition('a-b', 'a', 'b');
        $transitions[] = new Transition('a-c', 'a', 'c');
        $definition = new Definition(['a', 'b', 'c'], $transitions);

        $subject = new Subject();
        $eventDispatcher = new EventDispatcherMock();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name', []);

        $eventNameExpected = [
            'workflow.guard',
            'workflow.workflow_name.guard',
            'workflow.workflow_name.guard.a-b',
        ];

        $workflow->apply($subject, 'a-b');

        $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents);
    }

    public function testApplyOnlyDispatchesEventsThatHaveBeenSpecifiedByDefinition()
    {
        $transitions[] = new Transition('a-b', 'a', 'b');
        $transitions[] = new Transition('a-c', 'a', 'c');
        $definition = new Definition(['a', 'b', 'c'], $transitions);

        $subject = new Subject();
        $eventDispatcher = new EventDispatcherMock();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name', [WorkflowEvents::COMPLETED]);

        $eventNameExpected = [
            'workflow.guard',
            'workflow.workflow_name.guard',
            'workflow.workflow_name.guard.a-b',
            'workflow.completed',
            'workflow.workflow_name.completed',
            'workflow.workflow_name.completed.a-b',
        ];

        $workflow->apply($subject, 'a-b');

        $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents);
    }

    public function testApplyDoesNotTriggerExtraGuardWithEventDispatcher()
    {
        $transitions[] = new Transition('a-b', 'a', 'b');
        $transitions[] = new Transition('a-c', 'a', 'c');
        $definition = new Definition(['a', 'b', 'c'], $transitions);

        $subject = new Subject();
        $eventDispatcher = new EventDispatcherMock();
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');

        $eventNameExpected = [
            'workflow.entered',
            'workflow.workflow_name.entered',
            'workflow.workflow_name.entered.a',
            'workflow.guard',
            'workflow.workflow_name.guard',
            'workflow.workflow_name.guard.a-b',
            'workflow.leave',
            'workflow.workflow_name.leave',
            'workflow.workflow_name.leave.a',
            'workflow.transition',
            'workflow.workflow_name.transition',
            'workflow.workflow_name.transition.a-b',
            'workflow.enter',
            'workflow.workflow_name.enter',
            'workflow.workflow_name.enter.b',
            'workflow.entered',
            'workflow.workflow_name.entered',
            'workflow.workflow_name.entered.b',
            'workflow.completed',
            'workflow.workflow_name.completed',
            'workflow.workflow_name.completed.a-b',
            'workflow.announce',
            'workflow.workflow_name.announce',
        ];

        $workflow->apply($subject, 'a-b');

        $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents);
    }

    public function testApplyWithContext()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $eventDispatcher = new EventDispatcher();
        $eventDispatcher->addListener('workflow.transition', static function (TransitionEvent $event) {
            $event->setContext(array_merge($event->getContext(), ['user' => 'admin']));
        });
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher);

        $workflow->apply($subject, 't1', ['foo' => 'bar']);

        $this->assertSame(['foo' => 'bar', 'user' => 'admin'], $subject->getContext());
    }

    public function testEventName()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $dispatcher = new EventDispatcher();
        $name = 'workflow_name';
        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name);

        $assertWorkflowName = function (Event $event) use ($name) {
            $this->assertEquals($name, $event->getWorkflowName());
        };

        $eventNames = [
            'workflow.guard',
            'workflow.leave',
            'workflow.transition',
            'workflow.enter',
            'workflow.entered',
            'workflow.announce',
        ];

        foreach ($eventNames as $eventName) {
            $dispatcher->addListener($eventName, $assertWorkflowName);
        }

        $workflow->apply($subject, 't1');
    }

    public function testEventContext()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $dispatcher = new EventDispatcher();
        $name = 'workflow_name';
        $context = ['context'];
        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name);

        $assertWorkflowContext = function (Event $event) use ($context) {
            $this->assertEquals($context, $event->getContext());
        };

        $eventNames = [
            'workflow.leave',
            'workflow.transition',
            'workflow.enter',
            'workflow.entered',
            'workflow.announce',
        ];

        foreach ($eventNames as $eventName) {
            $dispatcher->addListener($eventName, $assertWorkflowContext);
        }

        $marking = $workflow->apply($subject, 't1', $context);

        $this->assertInstanceOf(Marking::class, $marking);
        $this->assertSame($context, $marking->getContext());
    }

    public function testEventContextUpdated()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $dispatcher = new EventDispatcher();

        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher);

        $dispatcher->addListener('workflow.transition', static function (TransitionEvent $event) {
            $event->setContext(['foo' => 'bar']);
        });

        $marking = $workflow->apply($subject, 't1', ['initial']);

        $this->assertInstanceOf(Marking::class, $marking);
        $this->assertSame(['foo' => 'bar'], $marking->getContext());
    }

    public function testEventDefaultInitialContext()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $dispatcher = new EventDispatcher();
        $name = 'workflow_name';
        $context = Workflow::DEFAULT_INITIAL_CONTEXT;
        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name);

        $assertWorkflowContext = function (Event $event) use ($context) {
            $this->assertEquals($context, $event->getContext());
        };

        $eventNames = [
            'workflow.workflow_name.entered.a',
        ];

        foreach ($eventNames as $eventName) {
            $dispatcher->addListener($eventName, $assertWorkflowContext);
        }

        $workflow->apply($subject, 't1');
    }

    public function testEventWhenAlreadyInThisPlace()
    {
        // ┌──────┐     ┌──────────────────────┐     ┌───┐     ┌─────────────┐     ┌───┐
        // │ init │ ──▶ │ from_init_to_a_and_b │ ──▶ │ B │ ──▶ │ from_b_to_c │ ──▶ │ C │
        // └──────┘     └──────────────────────┘     └───┘     └─────────────┘     └───┘
        //                         │
        //                         │
        //                         ▼
        //                     ┌───────────────────────────────┐
        //                     │               A               │
        //                     └───────────────────────────────┘
        $definition = new Definition(
            ['init', 'A', 'B', 'C'],
            [
                new Transition('from_init_to_a_and_b', 'init', ['A', 'B']),
                new Transition('from_b_to_c', 'B', 'C'),
            ],
        );

        $subject = new Subject();
        $dispatcher = new EventDispatcher();
        $name = 'workflow_name';
        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name);

        $calls = [];
        $listener = static function (Event $event) use (&$calls) {
            $calls[] = $event;
        };
        $dispatcher->addListener("workflow.$name.entered.A", $listener);

        $workflow->apply($subject, 'from_init_to_a_and_b');
        $workflow->apply($subject, 'from_b_to_c');

        $this->assertCount(1, $calls);
        $this->assertInstanceOf(EnteredEvent::class, $calls[0]);
        $this->assertSame('from_init_to_a_and_b', $calls[0]->getTransition()->getName());
    }

    public function testMarkingStateOnApplyWithEventDispatcher()
    {
        $definition = new Definition(range('a', 'f'), [new Transition('t', range('a', 'c'), range('d', 'f'))]);

        $subject = new Subject();
        $subject->setMarking(['a' => 1, 'b' => 1, 'c' => 1]);

        $dispatcher = new EventDispatcher();

        $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, 'test');

        $assertInitialState = function (Event $event) {
            $this->assertEquals(new Marking(['a' => 1, 'b' => 1, 'c' => 1]), $event->getMarking());
        };
        $assertTransitionState = function (Event $event) {
            $this->assertEquals(new Marking([]), $event->getMarking());
        };

        $dispatcher->addListener('workflow.leave', $assertInitialState);
        $dispatcher->addListener('workflow.test.leave', $assertInitialState);
        $dispatcher->addListener('workflow.test.leave.a', $assertInitialState);
        $dispatcher->addListener('workflow.test.leave.b', $assertInitialState);
        $dispatcher->addListener('workflow.test.leave.c', $assertInitialState);
        $dispatcher->addListener('workflow.transition', $assertTransitionState);
        $dispatcher->addListener('workflow.test.transition', $assertTransitionState);
        $dispatcher->addListener('workflow.test.transition.t', $assertTransitionState);
        $dispatcher->addListener('workflow.enter', $assertTransitionState);
        $dispatcher->addListener('workflow.test.enter', $assertTransitionState);
        $dispatcher->addListener('workflow.test.enter.d', $assertTransitionState);
        $dispatcher->addListener('workflow.test.enter.e', $assertTransitionState);
        $dispatcher->addListener('workflow.test.enter.f', $assertTransitionState);

        $workflow->apply($subject, 't');
    }

    public function testGetEnabledTransitions()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $eventDispatcher = new EventDispatcher();
        $eventDispatcher->addListener('workflow.workflow_name.guard.t1', static function (GuardEvent $event) {
            $event->setBlocked(true);
        });
        $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name');

        $this->assertSame([], $workflow->getEnabledTransitions($subject));

        $subject->setMarking(['d' => 1]);
        $transitions = $workflow->getEnabledTransitions($subject);
        $this->assertCount(2, $transitions);
        $this->assertSame('t3', $transitions[0]->getName());
        $this->assertSame('t4', $transitions[1]->getName());

        $subject->setMarking(['c' => 1, 'e' => 1]);
        $transitions = $workflow->getEnabledTransitions($subject);
        $this->assertCount(1, $transitions);
        $this->assertSame('t5', $transitions[0]->getName());
    }

    public function testGetEnabledTransition()
    {
        $definition = $this->createComplexWorkflowDefinition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $subject->setMarking(['d' => 1]);
        $transition = $workflow->getEnabledTransition($subject, 't3');
        $this->assertInstanceOf(Transition::class, $transition);
        $this->assertSame('t3', $transition->getName());

        $transition = $workflow->getEnabledTransition($subject, 'does_not_exist');
        $this->assertNull($transition);
    }

    public function testGetEnabledTransitionsWithSameNameTransition()
    {
        $definition = $this->createWorkflowWithSameNameTransition();
        $subject = new Subject();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $transitions = $workflow->getEnabledTransitions($subject);
        $this->assertCount(1, $transitions);
        $this->assertSame('a_to_bc', $transitions[0]->getName());

        $subject->setMarking(['b' => 1, 'c' => 1]);
        $transitions = $workflow->getEnabledTransitions($subject);
        $this->assertCount(3, $transitions);
        $this->assertSame('b_to_c', $transitions[0]->getName());
        $this->assertSame('to_a', $transitions[1]->getName());
        $this->assertSame('to_a', $transitions[2]->getName());
    }

    #[TestWith(['back1'])]
    #[TestWith(['back2'])]
    public function testApplyWithSameNameBackTransition(string $transition)
    {
        $definition = $this->createWorkflowWithSameNameBackTransition();
        $workflow = new Workflow($definition, new MethodMarkingStore());

        $subject = new Subject();

        $marking = $workflow->apply($subject, 'a_to_bc');
        $this->assertPlaces([
            'b' => 1,
            'c' => 1,
        ], $marking);

        $marking = $workflow->apply($subject, $transition);
        $this->assertPlaces([
            'a' => 1,
            'b' => 1,
        ], $marking);

        $marking = $workflow->apply($subject, $transition);
        $this->assertPlaces([
            'a' => 2,
        ], $marking);

        $marking = $workflow->apply($subject, 'a_to_bc');
        $this->assertPlaces([
            'a' => 1,
            'b' => 1,
            'c' => 1,
        ], $marking);

        $marking = $workflow->apply($subject, 'c_to_cb');
        $this->assertPlaces([
            'a' => 1,
            'b' => 2,
            'c' => 1,
        ], $marking);

        $marking = $workflow->apply($subject, 'c_to_cb');
        $this->assertPlaces([
            'a' => 1,
            'b' => 3,
            'c' => 1,
        ], $marking);
    }

    public function testWithArcAndWeight()
    {
        //              ┌───────────────────┐     ┌─────────────┐     ┌─────────────┐  4
        //              │    prepare_leg    │ ──▶ │  build_leg  │ ──▶ │ leg_created │ ───────────────────────────┐
        //              └───────────────────┘     └─────────────┘     └─────────────┘                            │
        //                ▲                                                                                      │
        //                │ 4                                                                                    │
        //                │                                                                                      ▼
        // ┌──────┐     ┌───────────────────┐     ┌─────────────┐     ┌─────────────┐      ┌─────────────┐     ┌──────┐     ┌──────────┐
        // │ init │ ──▶ │       start       │ ──▶ │ prepare_top │ ──▶ │  build_top  │ ───▶ │ top_created │ ──▶ │ join │ ──▶ │ finished │
        // └──────┘     └───────────────────┘     └─────────────┘     └─────────────┘      └─────────────┘     └──────┘     └──────────┘
        //                │                                                                                      ▲
        //                │                                                                                      │
        //                ▼                                                                                      │
        //              ┌───────────────────┐                                                                    │
        //              │ stopwatch_running │ ───────────────────────────────────────────────────────────────────┘
        //              └───────────────────┘
        //
        // make_table:
        //     transitions:
        //         start:
        //             from: init
        //             to:
        //                 -   place: prepare_leg
        //                     weight: 4
        //                 -   place: prepare_top
        //                     weight: 1
        //                 -   place: stopwatch_running
        //                     weight: 1
        //         build_leg:
        //             from: prepare_leg
        //             to: leg_created
        //         build_top:
        //             from: prepare_top
        //             to: top_created
        //         join:
        //             from:
        //                 - place: leg_created
        //                   weight: 4
        //                 - top_created
        //                 - stopwatch_running
        //             to: finished

        $definition = new Definition(
            [],
            [
                new Transition('start', 'init', [new Arc('prepare_leg', 4), 'prepare_top', 'stopwatch_running']),
                new Transition('build_leg', 'prepare_leg', 'leg_created'),
                new Transition('build_top', 'prepare_top', 'top_created'),
                new Transition('join', [new Arc('leg_created', 4), 'top_created', 'stopwatch_running'], 'finished'),
            ]
        );

        $subject = new Subject();
        $workflow = new Workflow($definition);

        $this->assertTrue($workflow->can($subject, 'start'));
        $this->assertFalse($workflow->can($subject, 'build_leg'));
        $this->assertFalse($workflow->can($subject, 'build_top'));
        $this->assertFalse($workflow->can($subject, 'join'));

        $workflow->apply($subject, 'start');

        $this->assertSame([
            'prepare_leg' => 4,
            'prepare_top' => 1,
            'stopwatch_running' => 1,
        ], $subject->getMarking());
        $this->assertTrue($workflow->can($subject, 'build_leg'));
        $this->assertTrue($workflow->can($subject, 'build_top'));
        $this->assertFalse($workflow->can($subject, 'join'));

        $workflow->apply($subject, 'build_leg');

        $this->assertSame([
            'prepare_leg' => 3,
            'prepare_top' => 1,
            'stopwatch_running' => 1,
            'leg_created' => 1,
        ], $subject->getMarking());
        $this->assertTrue($workflow->can($subject, 'build_leg'));
        $this->assertTrue($workflow->can($subject, 'build_top'));
        $this->assertFalse($workflow->can($subject, 'join'));

        $workflow->apply($subject, 'build_top');

        $this->assertSame([
            'prepare_leg' => 3,
            'stopwatch_running' => 1,
            'leg_created' => 1,
            'top_created' => 1,
        ], $subject->getMarking());
        $this->assertTrue($workflow->can($subject, 'build_leg'));
        $this->assertFalse($workflow->can($subject, 'build_top'));
        $this->assertFalse($workflow->can($subject, 'join'));

        $workflow->apply($subject, 'build_leg');

        $this->assertSame([
            'prepare_leg' => 2,
            'stopwatch_running' => 1,
            'leg_created' => 2,
            'top_created' => 1,
        ], $subject->getMarking());
        $this->assertTrue($workflow->can($subject, 'build_leg'));
        $this->assertFalse($workflow->can($subject, 'build_top'));
        $this->assertFalse($workflow->can($subject, 'join'));

        $workflow->apply($subject, 'build_leg');
        $workflow->apply($subject, 'build_leg');

        $this->assertSame([
            'stopwatch_running' => 1,
            'leg_created' => 4,
            'top_created' => 1,
        ], $subject->getMarking());
        $this->assertFalse($workflow->can($subject, 'build_leg'));
        $this->assertFalse($workflow->can($subject, 'build_top'));
        $this->assertTrue($workflow->can($subject, 'join'));

        $workflow->apply($subject, 'join');

        $this->assertSame([
            'finished' => 1,
        ], $subject->getMarking());
    }

    private function assertPlaces(array $expected, Marking $marking)
    {
        $places = $marking->getPlaces();
        ksort($places);
        $this->assertSame($expected, $places);
    }
}

class EventDispatcherMock implements EventDispatcherInterface
{
    public array $dispatchedEvents = [];

    public function dispatch($event, ?string $eventName = null): object
    {
        $this->dispatchedEvents[] = $eventName;

        return $event;
    }
}
