<?php

/**
 * League.Uri (https://uri.thephpleague.com)
 *
 * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace League\Uri\UriTemplate;

use League\Uri\Exceptions\SyntaxError;
use League\Uri\Exceptions\TemplateCanNotBeExpanded;
use PHPUnit\Framework\TestCase;
use function var_export;

/**
 * @coversDefaultClass \League\Uri\UriTemplate\Expression
 */
final class ExpressionTest extends TestCase
{
    /**
     * @covers ::createFromString
     * @covers ::__construct
     * @covers ::toString
     * @covers ::variableNames
     * @covers ::setExpressionString
     * @covers ::setVariableNames
     *
     * @dataProvider providesValidNotation
     */
    public function testItCanBeInstantiatedWithAValidNotation(string $notation, array $variableNames): void
    {
        $expression = Expression::createFromString($notation);
        self::assertSame($notation, $expression->toString());
        self::assertSame($variableNames, $expression->variableNames());
    }

    public function providesValidNotation(): iterable
    {
        return [
            'level 1' => ['notation' => '{var}', 'variableNames' => ['var']],
            'level 2' => ['notation' => '{+var}', 'variableNames' => ['var']],
            'level 3.1' => ['notation' => '{#var}', 'variableNames' => ['var']],
            'level 3.2' => ['notation' => '{x,y}', 'variableNames' => ['x', 'y']],
            'level 3.3' => ['notation' => '{+x,hello,y}', 'variableNames' => ['x', 'hello', 'y']],
            'level 3.4' => ['notation' => '{#path,x}', 'variableNames' => ['path', 'x']],
            'level 3.5' => ['notation' => '{.x,y}', 'variableNames' => ['x', 'y']],
            'level 3.6' => ['notation' => '{/var,x}', 'variableNames' => ['var', 'x']],
            'level 3.7' => ['notation' => '{;x,y,empty}', 'variableNames' => ['x', 'y', 'empty']],
            'level 3.8' => ['notation' => '{?x,y,undef}', 'variableNames' => ['x', 'y', 'undef']],
            'level 3.9' => ['notation' => '{&x,y,empty}', 'variableNames' => ['x', 'y', 'empty']],
            'level 4.1' => ['notation' => '{+path:6}', 'variableNames' => ['path']],
            'level 4.2' => ['notation' => '{var:3}', 'variableNames' => ['var']],
            'level 4.3' => ['notation' => '{#keys*}', 'variableNames' => ['keys']],
            'level 4.4' => ['notation' => '{/var:1,var}', 'variableNames' => ['var']],
            'level 4.5' => ['notation' => '{;keys*}', 'variableNames' => ['keys']],
            'level 4.6' => ['notation' => '{?var:3}', 'variableNames' => ['var']],
            'level 4.7' => ['notation' => '{.null}', 'variableNames' => ['null']],
        ];
    }

    /**
     * @covers ::__set_state
     * @covers \League\Uri\UriTemplate\Template
     */
    public function testSetState(): void
    {
        $expressionString = '{;keys*}';

        $expression = Expression::createFromString($expressionString);

        self::assertEquals($expression, eval('return '.var_export($expression, true).';'));
    }

    /**
     * @covers ::createFromString
     *
     * @dataProvider providesInvalidExpression
     */
    public function testExpressionConstructFailsWithInvalidString(string $expression): void
    {
        self::expectException(SyntaxError::class);

        Expression::createFromString($expression);
    }

    public function providesInvalidExpression(): iterable
    {
        return [
            'missing content' => ['{}'],
            'missing delimiter' => ['foobar'],
            'reserved operator' => ['{|var*}'],
            'missing ending braces' => ['{/id*'],
            'missing starting braces' => ['/id*}'],
            'multiple starting operators' => ['{/?id}'],
            'invalid prefix' => ['{var:prefix}'],
            'multiple operator modifiers (1)' => ['{hello:2*}'] ,
            'duplicate operator' => ['{??hello}'] ,
            'reserved operator !' => ['{!hello}'] ,
            'space inside variable name' => ['{with space}'],
            'leading space in variable name (1)' => ['{ leading_space}'],
            'trailing space in variable name' => ['{trailing_space }'],
            'reserved operator =' => ['{=path}'] ,
            'forbidden operator $' => ['{$var}'],
            'reserved operator |' => ['{|var*}'],
            'using an operator modifier as an operator' => ['{*keys?}'],
            'variable name contains a reserved character (1)' => ['{?empty=default,var}'],
            'variable name contains invalid character (1)' => ['{-prefix|/-/|var}'],
            'variable name contains invalid prefix' => ['{example:color?}'],
            'variable name contains a reserved character (2)' => ['{?empty|foo=none}'],
            'variable name contains a reserved character (3)' => ['{#hello+}'],
            'variable name contains a reserved character (4)' => ['{hello+}'],
            'multiple operator modifiers (2)' => ['{;keys:1*}'],
            'variable name contains invalid character (2)' => ['{-join|&|var,list}'],
            'variable name contains invalid character (3)' => ['{~thing}'],
            'variable name contains invalid character (4)' => ['{default-graph-uri}'],
            'variable name contains invalid character (5)' => ['{?query,default-graph-uri}'],
            'variable name contains invalid character (6)' => ['{?query){&default-graph-uri*}'],
            'leading space in variable name (2)' => ['{?x, y}'],
        ];
    }

    /**
     * @covers ::expand
     * @covers ::replace
     * @covers ::inject
     * @covers ::replaceString
     * @covers ::replaceList
     * @covers ::decodeReserved
     * @covers ::isAssoc
     *
     * @dataProvider templateExpansionProvider
     */
    public function testExpandsUriTemplates(string $template, string $expectedUriString, array $variables): void
    {
        self::assertSame($expectedUriString, Expression::createFromString($template)->expand(new VariableBag($variables)));
    }

    public function templateExpansionProvider(): iterable
    {
        $variables = [
            'var'   => 'value',
            'hello' => 'Hello World!',
            'empty' => '',
            'path'  => '/foo/bar',
            'x'     => '1024',
            'y'     => '768',
            'null'  => null,
            'list'  => ['red', 'green', 'blue'],
            'keys'  => [
                'semi'  => ';',
                'dot'   => '.',
                'comma' => ',',
            ],
            'empty_keys' => [],
            'bool' => true,
        ];

        $templateAndExpansionData = [
            'level 1' => [
                ['{var}',          'value'],
                ['{hello}',        'Hello%20World%21'],
                ['{bool}',         '1'],
            ],
            'level 2' => [
                ['{+var}',         'value'],
                ['{+hello}',       'Hello%20World!'],
                ['{+path}',        '/foo/bar'],
            ],
            'level 3' => [
                ['{#var}',          '#value'],
                ['{#hello}',        '#Hello%20World!'],
                ['{x,y}',           '1024,768'],
                ['{x,hello,y}',     '1024,Hello%20World%21,768'],
                ['{+x,hello,y}',    '1024,Hello%20World!,768'],
                ['{+path,x}',       '/foo/bar,1024'],
                ['{#x,hello,y}',    '#1024,Hello%20World!,768'],
                ['{#path,x}',       '#/foo/bar,1024'],
                ['{.var}',          '.value'],
                ['{.x,y}',          '.1024.768'],
                ['{/var}',          '/value'],
                ['{/var,x}',        '/value/1024'],
                ['{;x,y}',          ';x=1024;y=768'],
                ['{;x,y,empty}',    ';x=1024;y=768;empty'],
                ['{?x,y}',          '?x=1024&y=768'],
                ['{?x,y,empty}',    '?x=1024&y=768&empty='],
                ['{?x,y,undef}',    '?x=1024&y=768'],
                ['{&x}',            '&x=1024'],
                ['{&x,y,empty}',    '&x=1024&y=768&empty='],
            ],
            'level 4' => [
                ['{var:3}',         'val'],
                ['{var:30}',        'value'],
                ['{list}',          'red,green,blue'],
                ['{list*}',         'red,green,blue'],
                ['{keys}',          'semi,%3B,dot,.,comma,%2C'],
                ['{keys*}',         'semi=%3B,dot=.,comma=%2C'],
                ['{+path:6}',       '/foo/b'],
                ['{+list}',         'red,green,blue'],
                ['{+list*}',        'red,green,blue'],
                ['{+keys}',         'semi,;,dot,.,comma,,'],
                ['{+keys*}',        'semi=;,dot=.,comma=,'],
                ['{#path:6}',       '#/foo/b'],
                ['{#list}',         '#red,green,blue'],
                ['{#list*}',        '#red,green,blue'],
                ['{#keys}',         '#semi,;,dot,.,comma,,'],
                ['{#keys*}',        '#semi=;,dot=.,comma=,'],
                ['{.var:3}',       '.val'],
                ['{.list}',        '.red,green,blue'],
                ['{.list*}',       '.red.green.blue'],
                ['{.keys}',        '.semi,%3B,dot,.,comma,%2C'],
                ['{.keys*}',       '.semi=%3B.dot=..comma=%2C'],
                ['{/var:1,var}',    '/v/value'],
                ['{/list}',         '/red,green,blue'],
                ['{/list*}',        '/red/green/blue'],
                ['{/list*,path:4}', '/red/green/blue/%2Ffoo'],
                ['{/keys}',             '/semi,%3B,dot,.,comma,%2C'],
                ['{/keys*}',            '/semi=%3B/dot=./comma=%2C'],
                ['{;hello:5}',          ';hello=Hello'],
                ['{;list}',             ';list=red,green,blue'],
                ['{;list*}',            ';list=red;list=green;list=blue'],
                ['{;keys}',             ';keys=semi,%3B,dot,.,comma,%2C'],
                ['{;keys*}',            ';semi=%3B;dot=.;comma=%2C'],
                ['{?var:3}',            '?var=val'],
                ['{?list}',             '?list=red,green,blue'],
                ['{?list*}',            '?list=red&list=green&list=blue'],
                ['{?keys}',             '?keys=semi,%3B,dot,.,comma,%2C'],
                ['{?keys*}',            '?semi=%3B&dot=.&comma=%2C'],
                ['{&var:3}',            '&var=val'],
                ['{&list}',             '&list=red,green,blue'],
                ['{&list*}',            '&list=red&list=green&list=blue'],
                ['{&keys}',             '&keys=semi,%3B,dot,.,comma,%2C'],
                ['{&keys*}',            '&semi=%3B&dot=.&comma=%2C'],
                ['{.null}',            ''],
                ['{.null,var}',        '.value'],
                ['{.empty_keys*}',     ''],
                ['{.empty_keys}',      ''],
            ],
            'extra' => [
                // Test that missing expansions are skipped
                ['{&missing*}', ''],
            ],
        ];

        foreach ($templateAndExpansionData as $specification => $tests) {
            foreach ($tests as $offset => $test) {
                yield $specification.' test '.$offset => [
                    'template' => $test[0],
                    'expectedUriString' => $test[1],
                    'variables' => $variables,
                ];
            }
        }
    }

    /**
     * @covers ::replaceList
     * @covers \League\Uri\Exceptions\TemplateCanNotBeExpanded
     *
     * @dataProvider invalidModifierToApply
     */
    public function testExpandThrowsExceptionIfTheModifierCanNotBeApplied(string $expression, array $variables): void
    {
        self::expectException(TemplateCanNotBeExpanded::class);

        Expression::createFromString($expression)->expand(new VariableBag($variables));
    }

    /**
     * Following negative tests with wrong variable can only be detected at runtime.
     *
     * @see https://github.com/uri-templates/uritemplate-test/blob/master/negative-tests.json
     */
    public function invalidModifierToApply(): iterable
    {
        return [
            'can not apply a modifier on a hash value (1)' => [
                'expression' => '{keys:1}',
                'variables' => [
                    'keys' => [
                        'semi' => ';',
                        'dot' => '.',
                        'comma' => ',',
                    ],
                ],
            ],
            'can not apply a modifier on a hash value (2)' => [
                'expression' => '{+keys:1}',
                'variables' => [
                    'keys' => [
                        'semi' => ';',
                        'dot' => '.',
                        'comma' => ',',
                    ],
                ],
            ],
        ];
    }
}
