<?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 BadMethodCallException;
use Chevere\Parameter\ArrayParameter;
use Chevere\Parameter\BoolParameter;
use Chevere\Parameter\FloatParameter;
use Chevere\Parameter\Interfaces\ParameterInterface;
use Chevere\Parameter\IntParameter;
use Chevere\Parameter\IterableParameter;
use Chevere\Parameter\NullParameter;
use Chevere\Parameter\ObjectParameter;
use Chevere\Parameter\Parameters;
use Chevere\Parameter\StringParameter;
use Chevere\Parameter\UnionParameter;
use Chevere\Tests\src\VariadicParameters;
use InvalidArgumentException;
use OutOfBoundsException;
use OverflowException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use function Chevere\Parameter\int;
use function Chevere\Parameter\reflectionToParameters;
use function Chevere\Parameter\string;
final class ParametersTest extends TestCase
{
    public function testConstructEmpty(): void
    {
        $name = 'name';
        $parameters = new Parameters();
        $this->assertCount(0, $parameters);
        $this->assertCount(0, $parameters->optionalKeys());
        $this->assertCount(0, $parameters->requiredKeys());
        $this->assertFalse($parameters->has($name));
        $this->assertFalse($parameters->isVariadic());
        $this->expectException(OutOfBoundsException::class);
        $parameters->get($name);
    }
    public function testAssertEmpty(): void
    {
        $name = 'name';
        $parameters = new Parameters();
        $this->expectException(OutOfBoundsException::class);
        $parameters->assertHas($name);
    }
    public function testConstruct(): void
    {
        $name = 'name';
        $parameter = new StringParameter();
        $parameters = new Parameters(...[
            $name => $parameter,
        ]);
        $this->assertCount(1, $parameters);
        $this->assertCount(0, $parameters->optionalKeys());
        $this->assertCount(1, $parameters->requiredKeys());
        $parameters->assertHas($name);
        $this->assertTrue($parameters->has($name));
        $this->assertTrue($parameters->requiredKeys()->contains($name));
        $this->assertSame($parameter, $parameters->get($name));
        $this->expectException(OverflowException::class);
        $parameters->withRequired(
            $name,
            $parameter,
        );
    }
    public function testConstructPositional(): void
    {
        $foo = string();
        $bar = int();
        $parameters = new Parameters($foo, $bar);
        $this->assertCount(2, $parameters);
        $this->assertTrue($parameters->has('0'));
        $this->assertTrue($parameters->has('1'));
        $this->assertSame($foo, $parameters->get('0'));
        $this->assertSame($bar, $parameters->get('1'));
    }
    public function testRequiredMissing(): void
    {
        $parameters = new Parameters();
        $this->assertFalse($parameters->has('foo'));
        $this->expectException(OutOfBoundsException::class);
        $this->expectExceptionMessage('Key `foo` not found');
        $parameters->required('foo');
    }
    public function testOptionalMissing(): void
    {
        $parameters = new Parameters();
        $this->assertFalse($parameters->has('foo'));
        $this->expectException(OutOfBoundsException::class);
        $this->expectExceptionMessage('Key `foo` not found');
        $parameters->optional('foo');
    }
    public function testRequiredCasting(): void
    {
        $parameter = string();
        $parameters = new Parameters(foo: $parameter);
        $this->assertTrue($parameters->has('foo'));
        $this->assertSame($parameter, $parameters->required('foo')->string());
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Parameter `foo` is required');
        $parameters->optional('foo');
    }
    public function testRequiredCastingPositional(): void
    {
        $parameter = string();
        $parameters = new Parameters($parameter);
        $this->assertTrue($parameters->has('0'));
        $this->assertSame($parameter, $parameters->required('0')->string());
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Parameter `0` is required');
        $parameters->optional('0');
    }
    public function testOptionalCasting(): void
    {
        $parameter = string();
        $parameters = (new Parameters())
            ->withOptional('foo', $parameter);
        $this->assertSame($parameter, $parameters->optional('foo')->string());
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Parameter `foo` is optional');
        $parameters->required('foo');
    }
    public function testWithRequiredOverflow(): void
    {
        $name = 'name';
        $parameter = new StringParameter();
        $parameters = new Parameters(
            ...[
                $name => $parameter,
            ]
        );
        $this->assertCount(1, $parameters);
        $this->assertCount(0, $parameters->optionalKeys());
        $this->assertCount(1, $parameters->requiredKeys());
        $parameters->assertHas($name);
        $this->assertTrue($parameters->has($name));
        $this->assertTrue($parameters->requiredKeys()->contains($name));
        $this->assertSame($parameter, $parameters->get($name));
        $parametersWith = $parameters->withRequired('test', $parameter);
        $this->assertNotSame($parameters, $parametersWith);
        $this->expectException(OverflowException::class);
        $parameters->withRequired(
            $name,
            $parameter,
        );
    }
    public function testWithout(): void
    {
        $parameters = (new Parameters())
            ->withRequired('a', string())
            ->withRequired('b', string())
            ->withRequired('c', string())
            ->withOptional('x', string())
            ->withOptional('y', string())
            ->withOptional('z', string());
        $parametersWith = $parameters->without('a', 'y');
        $this->assertNotSame($parameters, $parametersWith);
        $this->assertCount(4, $parametersWith);
        $this->assertSame(['b', 'c'], $parametersWith->requiredKeys()->toArray());
        $this->assertSame(['x', 'z'], $parametersWith->optionalKeys()->toArray());
    }
    public function testWithRequiredOptional(): void
    {
        $name = 'name';
        $parameter = new StringParameter();
        $parameters = new Parameters();
        $this->assertSame(false, $parameters->isVariadic());
        $parametersWith = $parameters->withOptional($name, $parameter);
        $this->assertNotSame($parameters, $parametersWith);
        $this->assertCount(1, $parametersWith);
        $this->assertCount(1, $parametersWith->optionalKeys());
        $this->assertCount(0, $parametersWith->requiredKeys());
        $this->assertTrue($parametersWith->has($name));
        $this->assertTrue($parametersWith->optionalKeys()->contains($name));
        $this->assertFalse($parametersWith->requiredKeys()->contains($name));
        $this->assertSame($parameter, $parametersWith->get($name));
        $this->expectException(OverflowException::class);
        $parametersWith->withOptional($name, $parameter);
    }
    public function testWithVariadicParameters(): void
    {
        $reflector = new ReflectionMethod(VariadicParameters::class, 'main');
        $parameters = reflectionToParameters($reflector);
        $this->assertTrue($parameters->isVariadic());
        $return = $parameters(
            _task: 'test',
            _priority: 2,
            _maxRetries: 3,
            foo: 'bar',
            bar: 'baz',
        );
        $this->assertSame(
            [
                '_task' => 'test',
                '_priority' => 2,
                '_maxRetries' => 3,
                'foo' => 'bar',
                'bar' => 'baz',
            ],
            $return->toArray()
        );
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage(
            <<<PLAIN
            [foo...argument]: Argument must be of type Stringable|string, true given
            PLAIN
        );
        $this->expectExceptionMessage(
            <<<PLAIN
            [bar...argument]: Argument must be of type Stringable|string, int given
            PLAIN
        );
        $parameters(
            _task: 'test',
            _priority: 0,
            _maxRetries: 0,
            foo: true,
            bar: 123,
        );
    }
    public static function dataProviderGetCast(): array
    {
        return [
            [new StringParameter(), 'string'],
            [new IntParameter(), 'int'],
            [new FloatParameter(), 'float'],
            [new BoolParameter(), 'bool'],
            [new ArrayParameter(), 'array'],
            [new ObjectParameter(), 'object'],
            [new NullParameter(), 'null', 'int'],
        ];
    }
    #[DataProvider('dataProviderGetCast')]
    public function testGetCast(
        ParameterInterface $parameter,
        string $type,
        string $error = 'null'
    ): void {
        $name = 'test';
        $parameters = new Parameters(...[
            $name => $parameter,
        ]);
        $this->assertSame(
            $parameter,
            $parameters->required($name)->{$type}()
        );
        $this->expectException(\TypeError::class);
        $parameters->required($name)->{$error}();
    }
    public function testGetUnion(): void
    {
        $name = 'test';
        $type1 = new StringParameter();
        $type2 = new IntParameter();
        $parameters = new Parameters($type1, $type2);
        $parameter = new UnionParameter($parameters);
        $parameters = new Parameters(...[
            $name => $parameter,
        ]);
        $this->assertSame(
            $parameter,
            $parameters->required($name)->union()
        );
        $this->expectException(\TypeError::class);
        $parameters->required($name)->null();
    }
    public function testGetIterable(): void
    {
        $name = 'test';
        $parameter = new IterableParameter(
            value: string(),
            key: int(),
        );
        $parameters = new Parameters(...[
            $name => $parameter,
        ]);
        $this->assertSame(
            $parameter,
            $parameters->required($name)->iterable()
        );
        $this->expectException(\TypeError::class);
        $parameters->required($name)->null();
    }
    public function testWithOptionalMinimum(): void
    {
        $parameters = (new Parameters())->withOptional('a', string());
        $parametersWith = $parameters->withOptionalMinimum(1);
        $this->assertNotSame($parameters, $parametersWith);
        $this->assertSame(1, $parametersWith->optionalMinimum());
    }
    public function testWithOptionalMinimumBadMethodCall(): void
    {
        $parameters = new Parameters();
        $this->expectException(BadMethodCallException::class);
        $parameters->withOptionalMinimum(1);
    }
    public function testWithOptionalMinimumInvalidArgument(): void
    {
        $parameters = (new Parameters())->withOptional('foo', string());
        $this->expectException(InvalidArgumentException::class);
        $parameters->withOptionalMinimum(2);
    }
    public function testWithOptionalMinimumInvalidArgumentNumber(): void
    {
        $parameters = (new Parameters())->withOptional('foo', string());
        $this->expectException(InvalidArgumentException::class);
        $parameters->withOptionalMinimum(-1);
    }
    public function testWithOptionalMinimumWithout(): void
    {
        $parameters = (new Parameters())
            ->withOptional('foo', string())
            ->withOptional('bar', string());
        $parametersWith = $parameters->withOptionalMinimum(1);
        $parametersWith = $parametersWith->without('foo');
        $parametersWith = $parametersWith->withOptionalMinimum(0);
        $this->expectNotToPerformAssertions();
        $parametersWith->without('bar');
    }
    public function testWithOptionalMinimumWithoutInvalidArgument(): void
    {
        $parameters = (new Parameters())->withOptional('foo', string());
        $parametersWith = $parameters->withOptionalMinimum(1);
        $this->expectException(InvalidArgumentException::class);
        $parametersWith->without('foo');
    }
    public function testWithMakeOptional(): void
    {
        $parameters = new Parameters(
            foo: string(),
            bar: int()
        );
        $parametersWith = $parameters->withMakeOptional('foo');
        $this->assertNotSame($parameters, $parametersWith);
        $this->assertCount(2, $parametersWith);
        $this->assertCount(1, $parametersWith->optionalKeys());
        $this->assertCount(1, $parametersWith->requiredKeys());
        $this->assertTrue($parametersWith->optionalKeys()->contains('foo'));
        $this->assertTrue($parametersWith->requiredKeys()->contains('bar'));
        $this->expectException(InvalidArgumentException::class);
        $parametersWith->withMakeOptional('foo');
    }
    public function testWithMakeRequired(): void
    {
        $parameters = (new Parameters())
            ->withOptional('foo', string())
            ->withOptional('bar', int());
        $parametersWith = $parameters->withMakeRequired('bar');
        $this->assertNotSame($parameters, $parametersWith);
        $this->assertCount(2, $parametersWith);
        $this->assertCount(1, $parametersWith->optionalKeys());
        $this->assertCount(1, $parametersWith->requiredKeys());
        $this->assertTrue($parametersWith->optionalKeys()->contains('foo'));
        $this->assertTrue($parametersWith->requiredKeys()->contains('bar'));
        $this->expectException(InvalidArgumentException::class);
        $parametersWith->withMakeRequired('bar');
    }
    public function testWithMerge(): void
    {
        $foo = string();
        $bar = int();
        $parametersFoo = new Parameters(foo: $foo);
        $parametersBar = (new Parameters())->withOptional('bar', $bar);
        $parameters = (new Parameters(foo: $foo))->withOptional('bar', $bar);
        $fooWithMerge = $parametersFoo->withMerge($parametersBar);
        $this->assertNotSame($parametersFoo, $fooWithMerge);
        $this->assertEquals($parameters, $fooWithMerge);
        $this->assertSame(['foo'], $fooWithMerge->requiredKeys()->toArray());
        $this->assertSame(['bar'], $fooWithMerge->optionalKeys()->toArray());
        $barWithMerge = $parametersBar->withMerge($parametersFoo);
        $this->assertSame(['foo'], $barWithMerge->requiredKeys()->toArray());
        $this->assertSame(['bar'], $barWithMerge->optionalKeys()->toArray());
    }
    public function withIsVariadic(): void
    {
        $parameters = new Parameters();
        $with = $parameters->withIsVariadic(true);
        $this->assertNotEquals($with, $parameters);
        $this->assertTrue($with->isVariadic());
    }
}
 
  |