<?php
/**
 * Tests the backfilling of the T_MATCH token to PHP < 8.0, as well as the
 * setting of parenthesis/scopes for match control structures across PHP versions.
 *
 * @author    Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
 * @copyright 2020-2021 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
 */

namespace PHP_CodeSniffer\Tests\Core\Tokenizer;

use PHP_CodeSniffer\Util\Tokens;

final class BackfillMatchTokenTest extends AbstractTokenizerTestCase
{


    /**
     * Test tokenization of match expressions.
     *
     * @param string $testMarker   The comment prefacing the target token.
     * @param int    $openerOffset The expected offset of the scope opener in relation to the testMarker.
     * @param int    $closerOffset The expected offset of the scope closer in relation to the testMarker.
     * @param string $testContent  The token content to look for.
     *
     * @dataProvider dataMatchExpression
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::tokenize
     *
     * @return void
     */
    public function testMatchExpression($testMarker, $openerOffset, $closerOffset, $testContent='match')
    {
        $tokens = $this->phpcsFile->getTokens();

        $token      = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent);
        $tokenArray = $tokens[$token];

        $this->assertSame(T_MATCH, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (code)');
        $this->assertSame('T_MATCH', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (type)');

        $this->scopeTestHelper($token, $openerOffset, $closerOffset);
        $this->parenthesisTestHelper($token);

    }//end testMatchExpression()


    /**
     * Data provider.
     *
     * @see testMatchExpression()
     *
     * @return array<string, array<string, string|int>>
     */
    public static function dataMatchExpression()
    {
        return [
            'simple_match'                              => [
                'testMarker'   => '/* testMatchSimple */',
                'openerOffset' => 6,
                'closerOffset' => 33,
            ],
            'no_trailing_comma'                         => [
                'testMarker'   => '/* testMatchNoTrailingComma */',
                'openerOffset' => 6,
                'closerOffset' => 24,
            ],
            'with_default_case'                         => [
                'testMarker'   => '/* testMatchWithDefault */',
                'openerOffset' => 6,
                'closerOffset' => 33,
            ],
            'expression_in_condition'                   => [
                'testMarker'   => '/* testMatchExpressionInCondition */',
                'openerOffset' => 6,
                'closerOffset' => 77,
            ],
            'multicase'                                 => [
                'testMarker'   => '/* testMatchMultiCase */',
                'openerOffset' => 6,
                'closerOffset' => 40,
            ],
            'multicase_trailing_comma_in_case'          => [
                'testMarker'   => '/* testMatchMultiCaseTrailingCommaInCase */',
                'openerOffset' => 6,
                'closerOffset' => 47,
            ],
            'in_closure_not_lowercase'                  => [
                'testMarker'   => '/* testMatchInClosureNotLowercase */',
                'openerOffset' => 6,
                'closerOffset' => 36,
                'testContent'  => 'Match',
            ],
            'in_arrow_function'                         => [
                'testMarker'   => '/* testMatchInArrowFunction */',
                'openerOffset' => 5,
                'closerOffset' => 36,
            ],
            'arrow_function_in_match_no_trailing_comma' => [
                'testMarker'   => '/* testArrowFunctionInMatchNoTrailingComma */',
                'openerOffset' => 6,
                'closerOffset' => 44,
            ],
            'in_function_call_param_not_lowercase'      => [
                'testMarker'   => '/* testMatchInFunctionCallParamNotLowercase */',
                'openerOffset' => 8,
                'closerOffset' => 32,
                'testContent'  => 'MATCH',
            ],
            'in_method_call_param'                      => [
                'testMarker'   => '/* testMatchInMethodCallParam */',
                'openerOffset' => 5,
                'closerOffset' => 13,
            ],
            'discard_result'                            => [
                'testMarker'   => '/* testMatchDiscardResult */',
                'openerOffset' => 6,
                'closerOffset' => 18,
            ],
            'duplicate_conditions_and_comments'         => [
                'testMarker'   => '/* testMatchWithDuplicateConditionsWithComments */',
                'openerOffset' => 12,
                'closerOffset' => 59,
            ],
            'nested_match_outer'                        => [
                'testMarker'   => '/* testNestedMatchOuter */',
                'openerOffset' => 6,
                'closerOffset' => 33,
            ],
            'nested_match_inner'                        => [
                'testMarker'   => '/* testNestedMatchInner */',
                'openerOffset' => 6,
                'closerOffset' => 14,
            ],
            'ternary_condition'                         => [
                'testMarker'   => '/* testMatchInTernaryCondition */',
                'openerOffset' => 6,
                'closerOffset' => 21,
            ],
            'ternary_then'                              => [
                'testMarker'   => '/* testMatchInTernaryThen */',
                'openerOffset' => 6,
                'closerOffset' => 21,
            ],
            'ternary_else'                              => [
                'testMarker'   => '/* testMatchInTernaryElse */',
                'openerOffset' => 6,
                'closerOffset' => 21,
            ],
            'array_value'                               => [
                'testMarker'   => '/* testMatchInArrayValue */',
                'openerOffset' => 6,
                'closerOffset' => 21,
            ],
            'array_key'                                 => [
                'testMarker'   => '/* testMatchInArrayKey */',
                'openerOffset' => 6,
                'closerOffset' => 21,
            ],
            'returning_array'                           => [
                'testMarker'   => '/* testMatchreturningArray */',
                'openerOffset' => 6,
                'closerOffset' => 125,
            ],
            'nested_in_switch_case_1'                   => [
                'testMarker'   => '/* testMatchWithDefaultNestedInSwitchCase1 */',
                'openerOffset' => 6,
                'closerOffset' => 25,
            ],
            'nested_in_switch_case_2'                   => [
                'testMarker'   => '/* testMatchWithDefaultNestedInSwitchCase2 */',
                'openerOffset' => 6,
                'closerOffset' => 25,
            ],
            'nested_in_switch_default'                  => [
                'testMarker'   => '/* testMatchWithDefaultNestedInSwitchDefault */',
                'openerOffset' => 6,
                'closerOffset' => 25,
            ],
            'match_with_nested_switch'                  => [
                'testMarker'   => '/* testMatchContainingSwitch */',
                'openerOffset' => 6,
                'closerOffset' => 180,
            ],
            'no_cases'                                  => [
                'testMarker'   => '/* testMatchNoCases */',
                'openerOffset' => 6,
                'closerOffset' => 7,
            ],
            'multi_default'                             => [
                'testMarker'   => '/* testMatchMultiDefault */',
                'openerOffset' => 6,
                'closerOffset' => 40,
            ],
        ];

    }//end dataMatchExpression()


    /**
     * Verify that "match" keywords which are not match control structures get tokenized as T_STRING
     * and don't have the extra token array indexes.
     *
     * @param string $testMarker  The comment prefacing the target token.
     * @param string $testContent The token content to look for.
     *
     * @dataProvider dataNotAMatchStructure
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::tokenize
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::processAdditional
     *
     * @return void
     */
    public function testNotAMatchStructure($testMarker, $testContent='match')
    {
        $tokens = $this->phpcsFile->getTokens();

        $token      = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent);
        $tokenArray = $tokens[$token];

        $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
        $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');

        $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set');
        $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set');
        $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set');
        $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set');
        $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set');
        $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set');

        $next = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($token + 1), null, true);
        if ($next !== false && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) {
            $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set for opener after');
        }

    }//end testNotAMatchStructure()


    /**
     * Data provider.
     *
     * @see testNotAMatchStructure()
     *
     * @return array<string, array<string, string>>
     */
    public static function dataNotAMatchStructure()
    {
        return [
            'static_method_call'                   => [
                'testMarker' => '/* testNoMatchStaticMethodCall */',
            ],
            'class_constant_access'                => [
                'testMarker'  => '/* testNoMatchClassConstantAccess */',
                'testContent' => 'MATCH',
            ],
            'class_constant_array_access'          => [
                'testMarker'  => '/* testNoMatchClassConstantArrayAccessMixedCase */',
                'testContent' => 'Match',
            ],
            'method_call'                          => [
                'testMarker' => '/* testNoMatchMethodCall */',
            ],
            'method_call_uppercase'                => [
                'testMarker'  => '/* testNoMatchMethodCallUpper */',
                'testContent' => 'MATCH',
            ],
            'property_access'                      => [
                'testMarker' => '/* testNoMatchPropertyAccess */',
            ],
            'namespaced_function_call'             => [
                'testMarker' => '/* testNoMatchNamespacedFunctionCall */',
            ],
            'namespace_operator_function_call'     => [
                'testMarker' => '/* testNoMatchNamespaceOperatorFunctionCall */',
            ],
            'interface_method_declaration'         => [
                'testMarker' => '/* testNoMatchInterfaceMethodDeclaration */',
            ],
            'class_constant_declaration'           => [
                'testMarker' => '/* testNoMatchClassConstantDeclarationLower */',
            ],
            'class_method_declaration'             => [
                'testMarker' => '/* testNoMatchClassMethodDeclaration */',
            ],
            'property_assigment'                   => [
                'testMarker' => '/* testNoMatchPropertyAssignment */',
            ],
            'class_instantiation'                  => [
                'testMarker'  => '/* testNoMatchClassInstantiation */',
                'testContent' => 'Match',
            ],
            'anon_class_method_declaration'        => [
                'testMarker'  => '/* testNoMatchAnonClassMethodDeclaration */',
                'testContent' => 'maTCH',
            ],
            'class_declaration'                    => [
                'testMarker'  => '/* testNoMatchClassDeclaration */',
                'testContent' => 'Match',
            ],
            'interface_declaration'                => [
                'testMarker'  => '/* testNoMatchInterfaceDeclaration */',
                'testContent' => 'Match',
            ],
            'trait_declaration'                    => [
                'testMarker'  => '/* testNoMatchTraitDeclaration */',
                'testContent' => 'Match',
            ],
            'constant_declaration'                 => [
                'testMarker'  => '/* testNoMatchConstantDeclaration */',
                'testContent' => 'MATCH',
            ],
            'function_declaration'                 => [
                'testMarker' => '/* testNoMatchFunctionDeclaration */',
            ],
            'namespace_declaration'                => [
                'testMarker'  => '/* testNoMatchNamespaceDeclaration */',
                'testContent' => 'Match',
            ],
            'class_extends_declaration'            => [
                'testMarker'  => '/* testNoMatchExtendedClassDeclaration */',
                'testContent' => 'Match',
            ],
            'class_implements_declaration'         => [
                'testMarker'  => '/* testNoMatchImplementedClassDeclaration */',
                'testContent' => 'Match',
            ],
            'use_statement'                        => [
                'testMarker'  => '/* testNoMatchInUseStatement */',
                'testContent' => 'Match',
            ],
            'unsupported_inline_control_structure' => [
                'testMarker' => '/* testNoMatchMissingCurlies */',
            ],
            'unsupported_alternative_syntax'       => [
                'testMarker' => '/* testNoMatchAlternativeSyntax */',
            ],
            'live_coding'                          => [
                'testMarker' => '/* testLiveCoding */',
            ],
        ];

    }//end dataNotAMatchStructure()


    /**
     * Verify that the tokenization of switch structures is not affected by the backfill.
     *
     * @param string $testMarker   The comment prefacing the target token.
     * @param int    $openerOffset The expected offset of the scope opener in relation to the testMarker.
     * @param int    $closerOffset The expected offset of the scope closer in relation to the testMarker.
     *
     * @dataProvider dataSwitchExpression
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::tokenize
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::processAdditional
     *
     * @return void
     */
    public function testSwitchExpression($testMarker, $openerOffset, $closerOffset)
    {
        $token = $this->getTargetToken($testMarker, T_SWITCH);

        $this->scopeTestHelper($token, $openerOffset, $closerOffset);
        $this->parenthesisTestHelper($token);

    }//end testSwitchExpression()


    /**
     * Data provider.
     *
     * @see testSwitchExpression()
     *
     * @return array<string, array<string, string|int>>
     */
    public static function dataSwitchExpression()
    {
        return [
            'switch_containing_match'   => [
                'testMarker'   => '/* testSwitchContainingMatch */',
                'openerOffset' => 6,
                'closerOffset' => 174,
            ],
            'match_containing_switch_1' => [
                'testMarker'   => '/* testSwitchNestedInMatch1 */',
                'openerOffset' => 5,
                'closerOffset' => 63,
            ],
            'match_containing_switch_2' => [
                'testMarker'   => '/* testSwitchNestedInMatch2 */',
                'openerOffset' => 5,
                'closerOffset' => 63,
            ],
        ];

    }//end dataSwitchExpression()


    /**
     * Verify that the tokenization of a switch case/default structure containing a match structure
     * or contained *in* a match structure is not affected by the backfill.
     *
     * @param string $testMarker   The comment prefacing the target token.
     * @param int    $openerOffset The expected offset of the scope opener in relation to the testMarker.
     * @param int    $closerOffset The expected offset of the scope closer in relation to the testMarker.
     *
     * @dataProvider dataSwitchCaseVersusMatch
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::tokenize
     * @covers       PHP_CodeSniffer\Tokenizers\PHP::processAdditional
     *
     * @return void
     */
    public function testSwitchCaseVersusMatch($testMarker, $openerOffset, $closerOffset)
    {
        $token = $this->getTargetToken($testMarker, [T_CASE, T_DEFAULT]);

        $this->scopeTestHelper($token, $openerOffset, $closerOffset);

    }//end testSwitchCaseVersusMatch()


    /**
     * Data provider.
     *
     * @see testSwitchCaseVersusMatch()
     *
     * @return array<string, array<string, string|int>>
     */
    public static function dataSwitchCaseVersusMatch()
    {
        return [
            'switch_with_nested_match_case_1'       => [
                'testMarker'   => '/* testMatchWithDefaultNestedInSwitchCase1 */',
                'openerOffset' => 3,
                'closerOffset' => 55,
            ],
            'switch_with_nested_match_case_2'       => [
                'testMarker'   => '/* testMatchWithDefaultNestedInSwitchCase2 */',
                'openerOffset' => 4,
                'closerOffset' => 21,
            ],
            'switch_with_nested_match_default_case' => [
                'testMarker'   => '/* testMatchWithDefaultNestedInSwitchDefault */',
                'openerOffset' => 1,
                'closerOffset' => 38,
            ],
            'match_with_nested_switch_case'         => [
                'testMarker'   => '/* testSwitchDefaultNestedInMatchCase */',
                'openerOffset' => 1,
                'closerOffset' => 18,
            ],
            'match_with_nested_switch_default_case' => [
                'testMarker'   => '/* testSwitchDefaultNestedInMatchDefault */',
                'openerOffset' => 1,
                'closerOffset' => 20,
            ],
        ];

    }//end dataSwitchCaseVersusMatch()


    /**
     * Helper function to verify that all scope related array indexes for a control structure
     * are set correctly.
     *
     * @param string $token                The control structure token to check.
     * @param int    $openerOffset         The expected offset of the scope opener in relation to
     *                                     the control structure token.
     * @param int    $closerOffset         The expected offset of the scope closer in relation to
     *                                     the control structure token.
     * @param bool   $skipScopeCloserCheck Whether to skip the scope closer check.
     *                                     This should be set to "true" when testing nested arrow functions,
     *                                     where the "inner" arrow function shares a scope closer with the
     *                                     "outer" arrow function, as the 'scope_condition' for the scope closer
     *                                     of the "inner" arrow function will point to the "outer" arrow function.
     *
     * @return void
     */
    private function scopeTestHelper($token, $openerOffset, $closerOffset, $skipScopeCloserCheck=false)
    {
        $tokens     = $this->phpcsFile->getTokens();
        $tokenArray = $tokens[$token];
        $tokenType  = $tokenArray['type'];
        $expectedScopeOpener = ($token + $openerOffset);
        $expectedScopeCloser = ($token + $closerOffset);

        $this->assertArrayHasKey('scope_condition', $tokenArray, 'Scope condition is not set');
        $this->assertArrayHasKey('scope_opener', $tokenArray, 'Scope opener is not set');
        $this->assertArrayHasKey('scope_closer', $tokenArray, 'Scope closer is not set');
        $this->assertSame($token, $tokenArray['scope_condition'], 'Scope condition is not the '.$tokenType.' token');
        $this->assertSame($expectedScopeOpener, $tokenArray['scope_opener'], 'Scope opener of the '.$tokenType.' token incorrect');
        $this->assertSame($expectedScopeCloser, $tokenArray['scope_closer'], 'Scope closer of the '.$tokenType.' token incorrect');

        $opener = $tokenArray['scope_opener'];
        $this->assertArrayHasKey('scope_condition', $tokens[$opener], 'Opener scope condition is not set');
        $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Opener scope opener is not set');
        $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Opener scope closer is not set');
        $this->assertSame($token, $tokens[$opener]['scope_condition'], 'Opener scope condition is not the '.$tokenType.' token');
        $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], $tokenType.' opener scope opener token incorrect');
        $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], $tokenType.' opener scope closer token incorrect');

        $closer = $tokenArray['scope_closer'];
        $this->assertArrayHasKey('scope_condition', $tokens[$closer], 'Closer scope condition is not set');
        $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Closer scope opener is not set');
        $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Closer scope closer is not set');
        if ($skipScopeCloserCheck === false) {
            $this->assertSame($token, $tokens[$closer]['scope_condition'], 'Closer scope condition is not the '.$tokenType.' token');
        }

        $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], $tokenType.' closer scope opener token incorrect');
        $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], $tokenType.' closer scope closer token incorrect');

        if (($opener + 1) !== $closer) {
            for ($i = ($opener + 1); $i < $closer; $i++) {
                $this->assertArrayHasKey(
                    $token,
                    $tokens[$i]['conditions'],
                    $tokenType.' condition not added for token belonging to the '.$tokenType.' structure'
                );
            }
        }

    }//end scopeTestHelper()


    /**
     * Helper function to verify that all parenthesis related array indexes for a control structure
     * token are set correctly.
     *
     * @param int $token The position of the control structure token.
     *
     * @return void
     */
    private function parenthesisTestHelper($token)
    {
        $tokens     = $this->phpcsFile->getTokens();
        $tokenArray = $tokens[$token];
        $tokenType  = $tokenArray['type'];

        $this->assertArrayHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is not set');
        $this->assertArrayHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is not set');
        $this->assertArrayHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is not set');
        $this->assertSame($token, $tokenArray['parenthesis_owner'], 'Parenthesis owner is not the '.$tokenType.' token');

        $opener = $tokenArray['parenthesis_opener'];
        $this->assertArrayHasKey('parenthesis_owner', $tokens[$opener], 'Opening parenthesis owner is not set');
        $this->assertSame($token, $tokens[$opener]['parenthesis_owner'], 'Opening parenthesis owner is not the '.$tokenType.' token');

        $closer = $tokenArray['parenthesis_closer'];
        $this->assertArrayHasKey('parenthesis_owner', $tokens[$closer], 'Closing parenthesis owner is not set');
        $this->assertSame($token, $tokens[$closer]['parenthesis_owner'], 'Closing parenthesis owner is not the '.$tokenType.' token');

    }//end parenthesisTestHelper()


}//end class
