<?php

/**
 * @see       https://github.com/laminas/laminas-code for the canonical source repository
 * @copyright https://github.com/laminas/laminas-code/blob/master/COPYRIGHT.md
 * @license   https://github.com/laminas/laminas-code/blob/master/LICENSE.md New BSD License
 */

namespace LaminasTest\Code\Generator;

use Laminas\Code\Exception\InvalidArgumentException;
use Laminas\Code\Generator\GeneratorInterface;
use Laminas\Code\Generator\TypeGenerator;
use PHPUnit\Framework\TestCase;

use function array_combine;
use function array_filter;
use function array_map;
use function class_implements;
use function ltrim;
use function str_replace;
use function strpos;

/**
 * @group zendframework/zend-code#29
 * @covers \Laminas\Code\Generator\TypeGenerator
 * @covers \Laminas\Code\Generator\TypeGenerator\AtomicType
 */
class TypeGeneratorTest extends TestCase
{
    public function testIsAGenerator()
    {
        self::assertContains(GeneratorInterface::class, class_implements(TypeGenerator::class));
    }

    /**
     * @dataProvider validType
     * @param string $typeString
     * @param string $expectedReturnType
     */
    public function testFromValidTypeString($typeString, $expectedReturnType)
    {
        $generator = TypeGenerator::fromTypeString($typeString);

        self::assertSame($expectedReturnType, $generator->generate());
    }

    /**
     * @dataProvider validType
     * @param string $typeString
     * @param string $expectedReturnType
     */
    public function testStringCastFromValidTypeString($typeString, $expectedReturnType)
    {
        $generator = TypeGenerator::fromTypeString($typeString);

        self::assertSame(
            str_replace('|\\', '|', ltrim($expectedReturnType, '?\\')),
            $generator->__toString()
        );
    }

    /**
     * @dataProvider invalidType
     * @param string $typeString
     */
    public function testRejectsInvalidTypeString($typeString)
    {
        $this->expectException(InvalidArgumentException::class);

        TypeGenerator::fromTypeString($typeString);
    }

    /**
     * @return string[][]
     *
     * IMPORTANT: the reason why we don't convert `foo|null` into `?foo` or `?foo` into `foo|null`
     *            is that this library still supports generating code that is compatible with PHP 7,
     *            and therefore we cannot normalize nullable types to use `|null`, for now.
     */
    public function validType()
    {
        $valid = [
            ['\\foo', '\\foo'],
            ['foo', '\\foo'],
            ['foo', '\\foo'],
            ['foo1', '\\foo1'],
            ['foo\\bar', '\\foo\\bar'],
            ['\\foo\\bar', '\\foo\\bar'],
            ['a\\b\\c', '\\a\\b\\c'],
            ['foo\\bar\\baz', '\\foo\\bar\\baz'],
            ['foo\\bar\\baz1', '\\foo\\bar\\baz1'],
            ['FOO', '\\FOO'],
            ['FOO1', '\\FOO1'],
            ['void', 'void'],
            ['Void', 'void'],
            ['VOID', 'void'],
            ['array', 'array'],
            ['Array', 'array'],
            ['ARRAY', 'array'],
            ['callable', 'callable'],
            ['Callable', 'callable'],
            ['CALLABLE', 'callable'],
            ['string', 'string'],
            ['String', 'string'],
            ['STRING', 'string'],
            ['int', 'int'],
            ['Int', 'int'],
            ['INT', 'int'],
            ['float', 'float'],
            ['Float', 'float'],
            ['FLOAT', 'float'],
            ['bool', 'bool'],
            ['Bool', 'bool'],
            ['BOOL', 'bool'],
            ['iterable', 'iterable'],
            ['Iterable', 'iterable'],
            ['ITERABLE', 'iterable'],
            ['object', 'object'],
            ['Object', 'object'],
            ['OBJECT', 'object'],
            ['mixed', 'mixed'],
            ['Mixed', 'mixed'],
            ['MIXED', 'mixed'],
            ['resource', '\\resource'],
            ['Resource', '\\Resource'],
            ['RESOURCE', '\\RESOURCE'],
            ['foo_bar', '\\foo_bar'],
            ['?foo', '?\\foo'],
            ['?foo', '?\\foo'],
            ['?foo1', '?\\foo1'],
            ['?foo\\bar', '?\\foo\\bar'],
            ['?a\\b\\c', '?\\a\\b\\c'],
            ['?foo\\bar\\baz', '?\\foo\\bar\\baz'],
            ['?foo\\bar\\baz1', '?\\foo\\bar\\baz1'],
            ['?FOO', '?\\FOO'],
            ['?FOO1', '?\\FOO1'],
            ['?array', '?array'],
            ['?Array', '?array'],
            ['?ARRAY', '?array'],
            ['?callable', '?callable'],
            ['?Callable', '?callable'],
            ['?CALLABLE', '?callable'],
            ['?string', '?string'],
            ['?String', '?string'],
            ['?STRING', '?string'],
            ['?int', '?int'],
            ['?Int', '?int'],
            ['?INT', '?int'],
            ['?float', '?float'],
            ['?Float', '?float'],
            ['?FLOAT', '?float'],
            ['?bool', '?bool'],
            ['?Bool', '?bool'],
            ['?BOOL', '?bool'],
            ['?iterable', '?iterable'],
            ['?Iterable', '?iterable'],
            ['?ITERABLE', '?iterable'],
            ['?object', '?object'],
            ['?Object', '?object'],
            ['?OBJECT', '?object'],
            ['?resource', '?\\resource'],
            ['?Resource', '?\\Resource'],
            ['?RESOURCE', '?\\RESOURCE'],
            ['?foo_bar', '?\\foo_bar'],
            ["\x80", "\\\x80"],
            ["\x80\\\x80", "\\\x80\\\x80"],

            // Basic union types
            ['foo|bar', '\\bar|\\foo'],
            ['\\foo|\\bar', '\\bar|\\foo'],
            ['foo|string', '\\foo|string'],

            // Capitalization of given types must be preserved
            ['Foo\\Bar|Baz\\Tab', '\\Baz\\Tab|\\Foo\\Bar'],
            ['\\Foo\\Bar|\\Baz\\Tab', '\\Baz\\Tab|\\Foo\\Bar'],

            // Union types are sorted
            ['C|B|D|A', '\\A|\\B|\\C|\\D'],
            ['string|int|bool|null|float|\\Foo', '\\Foo|bool|int|float|string|null'],

            // Union types may be composed by FQCN and non-FQCN
            ['\\Foo\\Bar|Baz\\Tab', '\\Baz\\Tab|\\Foo\\Bar'],
            ['Foo\\Bar|\\Baz\\Tab', '\\Baz\\Tab|\\Foo\\Bar'],

            // Nullable types using `|null` should be equivalent to their `?` counterparts, but
            // we cannot normalize them until PHP 7 support is dropped.
            ['foo|null', '\\foo|null'],
            ['null|foo', '\\foo|null'],
            ['foo|bar|null', '\\bar|\\foo|null'],

            // The `false` type can only be used in combination with other types
            ['foo|false', '\\foo|false'],
            ['string|false', 'string|false'],
            ['string|false|null', 'string|false|null'],

            // `false` + `null` requires a third type
            ['Foo|false|null', '\\Foo|false|null'],

            // The `static` type should not be turned into a FQCN
            ['static', 'static'],
            ['?static', '?static'],
            ['static|null', 'static|null'],
        ];

        return array_combine(
            array_map('current', $valid),
            $valid
        );
    }

    /**
     * Valid class names - just the same as validType, but with only those elements prefixed by '\\'
     *
     * @return string[][]
     */
    public function validClassName()
    {
        return array_filter(
            $this->validType(),
            function (array $pair) {
                return 0 === strpos($pair[1], '\\');
            }
        );
    }

    /**
     * @return string[][]
     */
    public function invalidType()
    {
        $invalid = [
            [''],
            ['\\'],
            ['\\\\'],
            ['\\\\foo'],
            ["\x7f"],
            ["foo\\\x7f"],
            ["foo\x7f\\foo"],
            ['1'],
            ['\\1'],
            ['\\1\\2'],
            ['foo\\1'],
            ['foo\\bar\\1'],
            ['1foo'],
            ['foo\\1foo'],
            ['?foo\\bar|null'],
            ['*'],
            ["\0"],
            ['\\array'],
            ['\\Array'],
            ['\\ARRAY'],
            ['\\array|null'],
            ['null|\\array'],
            ['?array|null'],
            ['\\callable'],
            ['\\Callable'],
            ['\\CALLABLE'],
            ['\\callable|null'],
            ['null|\\callable'],
            ['?callable|null'],
            ['\\string'],
            ['\\String'],
            ['\\STRING'],
            ['\\string|null'],
            ['null|\\string'],
            ['?string|null'],
            ['\\int'],
            ['\\Int'],
            ['\\INT'],
            ['\\int|null'],
            ['null|\\int'],
            ['?int|null'],
            ['\\float'],
            ['\\Float'],
            ['\\FLOAT'],
            ['\\float|null'],
            ['null|\\float'],
            ['?float|null'],
            ['\\false'],
            ['\\FALSE'],
            ['\\False'],
            ['?false|null'],
            ['\\bool'],
            ['\\Bool'],
            ['\\BOOL'],
            ['\\bool|null'],
            ['null|\\bool'],
            ['?bool|null'],
            ['\\void'],
            ['\\Void'],
            ['\\VOID'],
            ['\\void|null'],
            ['null|\\void'],
            ['?void'],
            ['?Void'],
            ['?VOID'],
            ['void|null'],
            ['?void|null'],
            ['\\iterable'],
            ['\\Iterable'],
            ['\\ITERABLE'],
            ['\\iterable|null'],
            ['null|\\iterable'],
            ['?iterable|null'],
            ['\\object'],
            ['\\Object'],
            ['\\OBJECT'],
            ['\\object|null'],
            ['null|\\object'],
            ['?object|null'],
            ['\\static'],
            ['\\STATIC'],
            ['\\Static'],
            ['\\static|null'],
            ['null|\\static'],
            ['?static|null'],
            ['\\mixed'],
            ['\\MIXED'],
            ['\\Mixed'],
            ['\\mixed|null'],
            ['null|\\mixed'],

            // `mixed` can not be union-ed with anything
            ['?mixed'],
            ['mixed|null'],
            ['mixed|Foo'],
            ['mixed|\\foo'],

            // `false` and `null` must always be used as part of a union type
            ['null'],
            ['false'],
            ['?null'],
            ['?false'],
            ['false|null'],
            ['null|false'],

            // Duplicate types are rejected
            ['A|A'],
            ['A|\A'],
            ['\A|A'],
            ['A|A|null'],
            ['A|null|A'],
            ['null|A|A'],
            ['string|string'],
            ['string|string|null'],
            ['string|null|string'],
            ['null|string|string'],
        ];

        return array_combine(
            array_map('current', $invalid),
            $invalid
        );
    }
}
