<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz RumiƄski <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Tokenizer\Analyzer;

use PhpCsFixer\Tests\TestCase;
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer
 */
final class AlternativeSyntaxAnalyzerTest extends TestCase
{
    /**
     * @param list<int> $expectedPositives
     *
     * @dataProvider provideBelongsToAlternativeSyntaxCases
     */
    public function testBelongsToAlternativeSyntax(array $expectedPositives, string $source): void
    {
        $tokens = Tokens::fromCode($source);

        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
            self::assertSame(
                \in_array($index, $expectedPositives, true),
                (new AlternativeSyntaxAnalyzer())->belongsToAlternativeSyntax($tokens, $index),
                '@ index: '.$index
            );
        }
    }

    public static function provideBelongsToAlternativeSyntaxCases(): iterable
    {
        yield 'declare' => [
            [7],
            '<?php declare(ticks=1):enddeclare;',
        ];

        yield 'for' => [
            [20],
            '<?php for($i = 0; $i < 10; $i++): echo $i; endfor;',
        ];

        yield 'foreach' => [
            [17],
            '<?php foreach([1, 2, 3] as $i): echo $i; endforeach;',
        ];

        yield 'if, elseif, else' => [
            [6, 17, 25],
            '<?php if ($condition): echo 1; elseif($a): echo 2; else: echo 3; endif;',
        ];

        yield 'switch' => [
            [6],
            '<?php switch ($value): default: echo 4; endswitch;',
        ];

        yield 'while' => [
            [5],
            '<?php while(true): echo "na"; endwhile;',
        ];

        yield 'multiple expressions' => [
            [7, 15, 51],
            '<?php
                if ($condition1): echo 1; else: echo 2; endif;
                somelabel: echo 3;
                echo $condition2 ? 4 : 5;
                if ($condition3): echo 6; endif;
            ',
        ];
    }

    /**
     * @dataProvider provideItFindsTheEndOfAnAlternativeSyntaxBlockCases
     */
    public function testItFindsTheEndOfAnAlternativeSyntaxBlock(string $code, int $startIndex, int $expectedResult): void
    {
        $analyzer = new AlternativeSyntaxAnalyzer();

        self::assertSame(
            $expectedResult,
            $analyzer->findAlternativeSyntaxBlockEnd(
                Tokens::fromCode($code),
                $startIndex
            )
        );
    }

    /**
     * @return iterable<array{string, int, int}>
     */
    public static function provideItFindsTheEndOfAnAlternativeSyntaxBlockCases(): iterable
    {
        yield ['<?php if ($foo): foo(); endif;', 1, 13];

        yield ['<?php if ($foo): foo(); else: bar(); endif;', 1, 13];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); endif;', 1, 13];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); endif;', 13, 25];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); else: baz(); endif;', 13, 25];

        yield ['<?php if ($foo): foo(); else: bar(); endif;', 13, 21];

        yield ['<?php for (;;): foo(); endfor;', 1, 14];

        yield ['<?php foreach ($foo as $bar): foo(); endforeach;', 1, 17];

        yield ['<?php while ($foo): foo(); endwhile;', 1, 13];

        yield ['<?php switch ($foo): case 1: foo(); endswitch;', 1, 18];

        $nested = <<<'PHP'
            <?php
            switch (foo()):
                case 1:
                    switch (foo2()):
                        case 2:
                            if (bar()) {

                            }
                            switch (foo2()):
                                case 4:
                                {
                                    switch (foo3()) {
                                        case 4:
                                        {

                                        }
                                    }
                                }
                            endswitch;
                    endswitch;
                case 2:
                    switch (foo5()) {
                        case 4:
                            echo 1;
                    }
            endswitch;
            PHP;

        yield [$nested, 1, 113];

        yield [$nested, 15, 83];

        yield [$nested, 41, 80];

        $nestedWithHtml = <<<'PHP'
            <?php if (1): ?>
                <div></div>
            <?php else: ?>
                <?php if (2): ?>
                    <div></div>
                <?php else: ?>
                    <div></div>
                <?php endif; ?>
            <?php endif; ?>
            PHP;

        yield [$nestedWithHtml, 1, 11];

        yield [$nestedWithHtml, 11, 38];

        yield [$nestedWithHtml, 17, 27];

        yield [$nestedWithHtml, 27, 33];
    }

    /**
     * @dataProvider provideItThrowsOnInvalidAlternativeSyntaxBlockStartIndexCases
     */
    public function testItThrowsOnInvalidAlternativeSyntaxBlockStartIndex(string $code, int $startIndex, string $expectedMessage): void
    {
        $tokens = Tokens::fromCode($code);

        $analyzer = new AlternativeSyntaxAnalyzer();

        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage($expectedMessage);

        $analyzer->findAlternativeSyntaxBlockEnd($tokens, $startIndex);
    }

    /**
     * @return iterable<array{string, int, string}>
     */
    public static function provideItThrowsOnInvalidAlternativeSyntaxBlockStartIndexCases(): iterable
    {
        yield ['<?php if ($foo): foo(); endif;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); endif;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); endif;', 999, 'There is no token at index 999.'];

        yield ['<?php if ($foo): foo(); else: bar(); endif;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); else: bar(); endif;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); else: bar(); endif;', 999, 'There is no token at index 999.'];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); endif;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); endif;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); endif;', 999, 'There is no token at index 999.'];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); else: baz(); endif;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); else: baz(); endif;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php if ($foo): foo(); elseif ($bar): bar(); else: baz(); endif;', 999, 'There is no token at index 999.'];

        yield ['<?php for (;;): foo(); endfor;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php for (;;): foo(); endfor;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php for (;;): foo(); endfor;', 999, 'There is no token at index 999.'];

        yield ['<?php foreach ($foo as $bar): foo(); endforeach;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php foreach ($foo as $bar): foo(); endforeach;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php foreach ($foo as $bar): foo(); endforeach;', 999, 'There is no token at index 999.'];

        yield ['<?php while ($foo): foo(); endwhile;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php while ($foo): foo(); endwhile;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php while ($foo): foo(); endwhile;', 999, 'There is no token at index 999.'];

        yield ['<?php switch ($foo): case 1: foo(); endswitch;', 0, 'Token at index 0 is not the start of an alternative syntax block.'];

        yield ['<?php switch ($foo): case 1: foo(); endswitch;', 2, 'Token at index 2 is not the start of an alternative syntax block.'];

        yield ['<?php switch ($foo): case 1: foo(); endswitch;', 999, 'There is no token at index 999.'];
    }
}