<?php
/*
 * This file is part of Chevere.
 *
 * (c) Rodolfo Berrios < [email protected]>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
declare(strict_types=1);
namespace Chevere\Tests;
use Chevere\Parameter\Interfaces\BoolParameterInterface;
use Chevere\Tests\src\TestActionNoParams;
use Chevere\Tests\src\TestActionNoParamsArrayIntResponse;
use Chevere\Tests\src\TestActionNoParamsBoolResponses;
use Chevere\Tests\src\TestActionParamFooResponse1;
use Chevere\Tests\src\TestActionParamFooResponseBar;
use Chevere\Tests\src\TestActionParams;
use Chevere\Workflow\Exceptions\JobsException;
use Chevere\Workflow\Jobs;
use OutOfBoundsException;
use OverflowException;
use PHPUnit\Framework\TestCase;
use TypeError;
use function Chevere\Workflow\async;
use function Chevere\Workflow\response;
use function Chevere\Workflow\sync;
use function Chevere\Workflow\variable;
final class JobsTest extends TestCase
{
    public function testConstruct(): void
    {
        $jobs = new Jobs();
        $this->assertSame([], $jobs->keys());
        $this->assertSame([], iterator_to_array($jobs->getIterator()));
        $this->expectException(OutOfBoundsException::class);
        $jobs->get('j1');
    }
    public function testConstructWithJob(): void
    {
        $j1 = async(new TestActionNoParams());
        $jobs = new Jobs(
            j1: $j1
        );
        $this->assertSame(['j1'], $jobs->keys());
        $this->assertSame([
            'j1' => $j1,
        ], iterator_to_array($jobs->getIterator()));
    }
    public function testWithAdded(): void
    {
        $j1 = async(new TestActionNoParams());
        $jobs = new Jobs();
        $this->assertFalse($jobs->has('j1'));
        $withAdded = $jobs->withAdded(
            j1: $j1,
        );
        $this->assertNotSame($jobs, $withAdded);
        $this->assertTrue($withAdded->has('j1'));
        $this->assertSame(['j1'], $withAdded->keys());
        $this->assertSame([
            'j1' => $j1,
        ], iterator_to_array($withAdded->getIterator()));
        $this->expectException(OverflowException::class);
        $withAdded->withAdded(j1: $j1);
    }
    public function testAsync(): void
    {
        $jobs = new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams()),
        );
        $this->assertSame(
            [
                ['j1', 'j2'],
            ],
            $jobs->graph()->toArray()
        );
    }
    public function testSync(): void
    {
        $jobs = new Jobs(
            j1: sync(new TestActionNoParams()),
            j2: sync(new TestActionNoParams()),
        );
        $this->assertSame(
            [
                ['j1'],
                ['j2'],
            ],
            $jobs->graph()->toArray()
        );
    }
    public function testWithDependsOnJob(): void
    {
        $jobs = new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams())->withDepends('j1'),
        );
        $this->assertSame(
            [
                ['j1'],
                ['j2'],
            ],
            $jobs->graph()->toArray()
        );
    }
    public function testWithDependsMissing(): void
    {
        $this->expectException(OutOfBoundsException::class);
        $this->expectExceptionMessageMatches('/undeclared dependencies\: `j0`$/');
        new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams())
                ->withDepends('j0', 'j1'),
        );
    }
    public function testWithDependsOnPreviousMultiple(): void
    {
        $jobs = new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams()),
            j3: async(new TestActionNoParams())
                ->withDepends('j2', 'j1'),
        );
        $this->assertSame(
            [
                ['j1', 'j2'],
                ['j3'],
            ],
            $jobs->graph()->toArray()
        );
    }
    public function testWithDependsOnPreviousSingle(): void
    {
        $jobs = new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams())
                ->withDepends('j1'),
            j3: async(new TestActionNoParams())
                ->withDepends('j2'),
        );
        $this->assertSame(
            [
                ['j1'],
                ['j2'],
                ['j3'],
            ],
            $jobs->graph()->toArray()
        );
    }
    public function testWithDependsMix(): void
    {
        $jobs = new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams()),
            j3: async(new TestActionNoParams())
                ->withDepends('j1', 'j2'),
            j4: async(new TestActionNoParams()),
            j5: async(new TestActionNoParams())
                ->withDepends('j4'),
            j6: async(new TestActionNoParams())
                ->withDepends('j5'),
        );
        $this->assertSame(
            [
                ['j1', 'j2', 'j4'],
                ['j3', 'j5'],
                ['j6'],
            ],
            $jobs->graph()->toArray()
        );
    }
    public function testWithReferenceShouldFail(): void
    {
        // previous: InvalidArgumentException
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<STRING
            [two]: Reference **one:bar** conflict for parameter **foo** (Expected regex `/^bar$/`, provided `/^.*$/s`)
            STRING
        );
        new Jobs(
            one: async(
                new TestActionParamFooResponseBar(),
                foo: 'bar'
            ),
            two: async(
                new TestActionParamFooResponse1(),
                foo: response('one', 'bar')
            )
        );
    }
    public function testMissingReference(): void
    {
        // previous: OutOfBoundsException
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [two]: Reference **zero:key** not found
            PLAIN
        );
        new Jobs(
            one: async(
                new TestActionNoParams()
            ),
            two: async(
                new TestActionParamFooResponseBar(),
                foo: response('zero', 'key')
            )
        );
    }
    public function testWrongReferenceType(): void
    {
        // previous: TypeError
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [two]: Reference **one:id** is of type `int`, parameter **foo** expects `string`
            PLAIN
        );
        new Jobs(
            one: async(
                new TestActionNoParamsArrayIntResponse(),
            ),
            two: async(
                new TestActionParams(),
                foo: response('one', 'id'),
                bar: response('one', 'id')
            )
        );
    }
    public function testWithRunIfUndeclaredJob(): void
    {
        $this->expectException(OutOfBoundsException::class);
        new Jobs(
            j1: async(new TestActionNoParams())
                ->withRunIf(
                    response('job', 'parameter')
                ),
        );
    }
    public function testWithRunIfUndeclaredJobResponseKey(): void
    {
        $this->expectException(OutOfBoundsException::class);
        new Jobs(
            j1: async(new TestActionNoParams()),
            j2: async(new TestActionNoParams())
                ->withRunIf(
                    response('j1', '404')
                ),
        );
    }
    public function testWithRunIfInvalidJobKeyType(): void
    {
        $this->expectException(TypeError::class);
        $this->expectExceptionMessage('Reference **j1:id** must be of type `bool`');
        new Jobs(
            j1: async(new TestActionNoParamsArrayIntResponse()),
            j2: async(new TestActionNoParams())
                ->withRunIf(
                    response('j1', 'id')
                ),
        );
    }
    public function testWithRunIfInvalidVariableType(): void
    {
        $this->expectException(TypeError::class);
        $this->expectExceptionMessage('Variable **theFoo** (previously declared as `string`) is not of type `bool` at Job **j2**');
        new Jobs(
            j1: async(
                new TestActionParams(),
                foo: variable('theFoo'),
                bar: 'bar'
            )
                ->withRunIf(
                    variable('true')
                ),
            j2: async(
                new TestActionNoParams()
            )
                ->withRunIf(
                    variable('true'),
                    variable('theFoo')
                ),
        );
    }
    public function testWithRunIfVariable(): void
    {
        $name = 'the_variable';
        $jobs = new Jobs(
            j1: async(
                new TestActionNoParams(),
            )
                ->withRunIf(
                    variable($name)
                ),
        );
        $this->assertTrue($jobs->variables()->has($name));
        $this->assertInstanceOf(BoolParameterInterface::class, $jobs->variables()->get($name));
    }
    public function testWithRunIfReference(): void
    {
        $true = response('j1', 'true');
        $false = response('j1', 'false');
        $jobs = new Jobs(
            j1: async(
                new TestActionNoParamsBoolResponses(),
            ),
            j2: async(
                new TestActionNoParamsBoolResponses(),
            )->withRunIf($true, $false),
            j3: async(
                new TestActionNoParams(),
            )->withRunIf($false, $true),
        );
        $this->assertSame(
            [
                ['j1'],
                ['j2', 'j3'],
            ],
            $jobs->graph()->toArray()
        );
        $this->assertTrue(
            $jobs->references()->has($true->__toString(), $false->__toString())
        );
        $j4 = async(new TestActionNoParams())
            ->withRunIf(response('j5', 'missing'));
        $this->expectException(OutOfBoundsException::class);
        $jobs->withAdded(j4: $j4);
    }
    public function testWithMissingReference(): void
    {
        // previous: OutOfBoundsException
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [job2]: Reference **job1:missing** not found
            PLAIN
        );
        new Jobs(
            job1: async(
                new TestActionParamFooResponseBar(),
                foo: 'bar'
            ),
            job2: async(
                new TestActionParamFooResponseBar(),
                foo: response('job1', 'missing'),
            )
        );
    }
    public function testWithInvalidReference(): void
    {
        // previous: LogicException
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [job2]: Invalid reference **job1:missing** as **job1** doesn't return an object implementing Chevere\Parameter\Interfaces\ParametersAccessInterface interface
            PLAIN
        );
        new Jobs(
            job1: async(
                new TestActionNoParams(),
            ),
            job2: async(
                new TestActionParamFooResponseBar(),
                foo: response('job1', 'missing'),
            )
        );
    }
    public function testWithInvalidTypeReference(): void
    {
        // previous: TypeError
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [job2]: Reference **job1:baz** is of type `float`, parameter **foo** expects `string`
            PLAIN
        );
        new Jobs(
            job1: async(
                new TestActionParamFooResponseBar(),
                foo: 'bar'
            ),
            job2: async(
                new TestActionParamFooResponseBar(),
                foo: response('job1', 'baz'),
            )
        );
    }
}
 
  |