<?php

/*
 * This file is part of Chevere.
 *
 * (c) Rodolfo Berrios <rodolfo@chevere.org>
 *
 * 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\Tests\src\TestActionIntToString;
use Chevere\Tests\src\TestActionNoParams;
use Chevere\Tests\src\TestActionNoParamsArrayIntResponse;
use Chevere\Tests\src\TestActionNoParamsBoolResponses;
use Chevere\Tests\src\TestActionParamFooResponse1;
use Chevere\Tests\src\TestActionParamsFooBarResponse2;
use Chevere\Tests\src\TestActionThrows;
use Chevere\Tests\src\TestActionUnion;
use Chevere\Tests\src\TestActionVariadic;
use Chevere\Workflow\Exceptions\JobsException;
use Chevere\Workflow\Interfaces\JobInterface;
use Chevere\Workflow\Interfaces\RunInterface;
use Chevere\Workflow\Run;
use Chevere\Workflow\Runner;
use Chevere\Workflow\Traits\ExpectWorkflowExceptionTrait;
use Exception;
use OutOfBoundsException;
use OverflowException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use function Chevere\Workflow\async;
use function Chevere\Workflow\response;
use function Chevere\Workflow\run;
use function Chevere\Workflow\sync;
use function Chevere\Workflow\variable;
use function Chevere\Workflow\workflow;

final class RunnerTest extends TestCase
{
    use ExpectWorkflowExceptionTrait;

    public function testRunnerForArguments(): void
    {
        $jobsRunArguments = [
            'job1' => [
                'foo' => 'viva Chile!',
            ],
            'job2' => [
                'foo' => 'tenemos el litio botado',
                'bar' => 'y no lo sabemos aprovechar',
            ],
            'job3' => [
                'foo' => 'condimento bueno',
            ],
        ];
        $job1 = async(
            new TestActionParamFooResponse1(),
            ...$jobsRunArguments['job1']
        );
        $job2 = async(
            new TestActionParamsFooBarResponse2(),
            ...$jobsRunArguments['job2']
        );
        $job3 = sync(
            new TestActionParamFooResponse1(),
            ...$jobsRunArguments['job3']
        );
        $jobs = [
            'job1' => $job1,
            'job2' => $job2,
            'job3' => $job3,
        ];
        $workflow = workflow(...$jobs);
        $run = new Run($workflow);
        $runner = new Runner($run);
        foreach (array_keys($jobs) as $name) {
            $runner = $runner->withRunJob($name);
        }
        $this->assertExpectedRun($jobs, $jobsRunArguments, $runner->run());
        $run = run($workflow);
        $this->assertExpectedRun($jobs, $jobsRunArguments, $run);
    }

    public function testRunnerForVariables(): void
    {
        $variables = [
            'uno' => 'ha salido un nuevo estilo de baile',
            'dos' => 'y yo, no lo sabia',
            'tres' => 'en las discos todos lo practican',
        ];
        $jobsRunArguments = [
            'job1' => [
                'foo' => $variables['uno'],
            ],
            'job2' => [
                'foo' => $variables['dos'],
                'bar' => $variables['dos'],
            ],
            'job3' => [
                'foo' => $variables['tres'],
            ],
        ];
        $jobsVariables = [
            'job1' => [
                'foo' => variable('uno'),
            ],
            'job2' => [
                'foo' => variable('dos'),
                'bar' => variable('dos'),
            ],
            'job3' => [
                'foo' => variable('tres'),
            ],
        ];
        $job1 = async(
            new TestActionParamFooResponse1(),
            ...$jobsVariables['job1']
        );
        $job2 = async(
            new TestActionParamsFooBarResponse2(),
            ...$jobsVariables['job2']
        );
        $job3 = sync(
            new TestActionParamFooResponse1(),
            ...$jobsVariables['job3']
        );
        $jobs = [
            'job1' => $job1,
            'job2' => $job2,
            'job3' => $job3,
        ];
        $workflow = workflow(...$jobs);
        $run = new Run($workflow, ...$variables);
        $runner = new Runner($run);
        foreach (array_keys($jobs) as $name) {
            $runner = $runner->withRunJob($name);
        }
        $this->assertExpectedRun($jobs, $jobsRunArguments, $runner->run());
        $run = run($workflow, ...$variables);
        $this->assertExpectedRun($jobs, $jobsRunArguments, $run);
    }

    public function testRunnerForReferences(): void
    {
        $references = [
            'uno' => 'quisiera sacarte a caminar, en un largo tour',
            'dos' => 'por Pudahuel y La bandera',
            'tres' => 'y verías la vida... tal como es',
        ];
        $jobsRunArguments = [
            'job1' => [
                'foo' => $references['uno'],
            ],
            'job2' => [
                'foo' => $references['uno'],
                'bar' => $references['uno'],
            ],
            'job3' => [
                'foo' => $references['uno'] . '^' . $references['uno'],
            ],
        ];
        $job1 = async(
            new TestActionParamFooResponse1(),
            foo: $references['uno'],
        );
        $job2 = async(
            new TestActionParamsFooBarResponse2(),
            foo: response('job1', 'response1'),
            bar: response('job1', 'response1'),
        );
        $job3 = sync(
            new TestActionParamFooResponse1(),
            foo: response('job2', 'response2'),
        );
        $jobs = [
            'job1' => $job1,
            'job2' => $job2,
            'job3' => $job3,
        ];
        $workflow = workflow(...$jobs);
        $run = new Run($workflow);
        $runner = new Runner($run);
        foreach (array_keys($jobs) as $name) {
            $runner = $runner->withRunJob($name);
        }
        $this->assertExpectedRun($jobs, $jobsRunArguments, $runner->run());
        $run = run($workflow);
        $this->assertExpectedRun($jobs, $jobsRunArguments, $run);
    }

    public function testWithRunIfVariable(): void
    {
        $name = 'variable';
        $job = async(new TestActionNoParams())
            ->withRunIf(variable($name));
        $workflow = workflow(job1: $job);
        $arguments = [
            $name => true,
        ];
        $run = new Run($workflow, ...$arguments);
        $runner = new Runner($run);
        $runner = $runner->withRunJob('job1');
        $action = $job->action();
        $this->assertSame(
            $action->__invoke(),
            $runner->run()->response('job1')->array()
        );
        $arguments = [
            $name => false,
        ];
        $run = new Run($workflow, ...$arguments);
        $runner = new Runner($run);
        $runner = $runner->withRunJob('job1');
        $this->assertSame($workflow->jobs()->keys(), $runner->run()->skip()->toArray());
        $run = run($workflow, ...$arguments);
        $this->assertSame($workflow->jobs()->keys(), $runner->run()->skip()->toArray());
        $this->expectException(OutOfBoundsException::class);
        $runner->run()->response('job1');
    }

    public function testRunIfReference(): void
    {
        $job1 = async(new TestActionNoParamsBoolResponses());
        $job2 = async(new TestActionNoParamsBoolResponses());
        $job3 = async(new TestActionNoParamsArrayIntResponse());
        $job4 = async(new TestActionNoParamsArrayIntResponse());
        $workflow = workflow(
            job1: $job1,
            job2: $job2->withRunIf(response('job1', 'true')),
            job3: $job3->withRunIf(response('job1', 'true')),
            job4: $job4->withDepends('job3')
        );
        $run = new Run($workflow);
        $runner = new Runner($run);
        foreach ($workflow->jobs()->keys() as $name) {
            $runner = $runner->withRunJob($name);
            $runner->run()->response($name)->array();
        }
        $workflow = workflow(
            job1: $job1,
            job2: $job2->withRunIf(response('job1', 'true'), response('job1', 'false')),
            job3: $job3->withRunIf(response('job1', 'true'), response('job1', 'false')),
            job4: $job1->withDepends('job3')
        );
        $run = new Run($workflow);
        $runner = new Runner($run);
        foreach ($workflow->jobs()->keys() as $name) {
            $runner = $runner->withRunJob($name);
        }
        $jobsKeysSkip = ['job2', 'job3', 'job4'];
        $this->assertSame($jobsKeysSkip, $runner->run()->skip()->toArray());
        $run = run($workflow);
        $this->assertSame($jobsKeysSkip, $runner->run()->skip()->toArray());
    }

    #[DataProvider('dataProviderRunIfCallable')]
    public function testRunIfCallable(bool $runIf): void
    {
        $callable = fn () => $runIf;
        $job = async(new TestActionNoParams())
            ->withRunIf($callable);
        $workflow = workflow(job1: $job);
        $run = run($workflow);
        $this->assertSame(
            ! $runIf,
            $run->skip()->contains('job1'),
        );
    }

    public function testRunIfCallableOverflow(): void
    {
        $this->expectException(OverflowException::class);
        $callable = fn () => true;
        async(new TestActionNoParams())
            ->withRunIf($callable, $callable);
    }

    public static function dataProviderRunIfCallable(): array
    {
        return [
            [true],
            [false],
        ];
    }

    public function testActionThrows(): void
    {
        $closure = fn () => run(
            workflow(
                job1: sync(
                    new TestActionThrows()
                ),
            )
        );
        $this->expectWorkflowException(
            closure: $closure,
            instance: Exception::class,
            job: 'job1',
            message: 'Test exception',
            code: 666
        );
    }

    public function testActionUnion(): void
    {
        $run = run(
            workflow(
                job1: sync(
                    new TestActionNoParamsArrayIntResponse(),
                ),
                job2: sync(
                    new TestActionUnion(),
                    foo: response('job1', 'id')
                ),
            ),
        );
        $this->assertSame(
            [
                'foo' => $run->response('job1', 'id')->int(),
            ],
            $run->response('job2')->array()
        );
    }

    public function testActionUnionConflict(): void
    {
        // previous: TypeError
        $this->expectException(JobsException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [job2]: Response **job1** is of type `string`, parameter **foo** expects `union`
            PLAIN
        );
        run(
            workflow(
                job1: sync(
                    new TestActionIntToString(),
                    int: 123,
                ),
                job2: sync(
                    new TestActionUnion(),
                    foo: response('job1')
                ),
            ),
        );
    }

    public function testActionVariadic(): void
    {
        $run = run(
            workflow(
                job1: sync(
                    new TestActionIntToString(),
                    int: variable('intVariable'),
                ),
                job2: sync(
                    new TestActionNoParamsArrayIntResponse(),
                ),
                job3: sync(
                    new TestActionVariadic(),
                    bar1: variable('intVariable'),
                    bar2: response('job2', 'id'),
                ),
            ),
            intVariable: 321
        );
        $this->assertSame(
            '321',
            $run->response('job1')->string()
        );
        $this->assertSame(
            [
                'id' => 123,
            ],
            $run->response('job2')->array()
        );
        $this->assertSame(
            [
                'foo' => 'baz',
                'bar' => [
                    'bar1' => 321,
                    'bar2' => 123,
                ],
            ],
            $run->response('job3')->array()
        );
    }

    /**
     * @param array<string, JobInterface> $jobs
     */
    private function assertExpectedRun(array $jobs, array $runArguments, RunInterface $run): void
    {
        foreach ($jobs as $name => $job) {
            $action = $job->action();
            $this->assertSame(
                $action->__invoke(...$runArguments[$name]),
                $run->response($name)->array()
            );
        }
    }
}
