<?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\Fixer\Phpdoc;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
 * @author Graham Campbell <hello@gjcampbell.co.uk>
 *
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\Phpdoc\PhpdocVarWithoutNameFixer
 */
final class PhpdocVarWithoutNameFixerTest extends AbstractFixerTestCase
{
    /**
     * @dataProvider provideFixVarCases
     */
    public function testFixVar(string $expected, ?string $input = null): void
    {
        $this->doTest($expected, $input);
    }

    /**
     * @dataProvider provideFixVarCases
     */
    public function testFixType(string $expected, ?string $input = null): void
    {
        $expected = str_replace('@var', '@type', $expected);
        if (null !== $input) {
            $input = str_replace('@var', '@type', $input);
        }

        $this->doTest($expected, $input);
    }

    public static function provideFixVarCases(): iterable
    {
        yield 'testFixVar' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var string Hello!
                     */
                    public $foo;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var string $foo Hello!
                     */
                    public $foo;
                }
                EOF
            ,
        ];

        yield 'testFixType' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var int|null
                     */
                    public $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var int|null $bar
                     */
                    public $bar;
                }
                EOF
            ,
        ];

        yield 'testDoNothing' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var Foo\Bar This is a variable.
                     */
                    public $bar;
                }
                EOF
        ];

        yield 'testFixVarWithNestedKeys' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var array {
                     *     @var bool   $required Whether this element is required
                     *     @var string $label    The display name for this element
                     * }
                     */
                     public $options;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var array $options {
                     *     @var bool   $required Whether this element is required
                     *     @var string $label    The display name for this element
                     * }
                     */
                     public $options;
                }
                EOF
        ];

        yield 'testSingleLine' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar */
                    public $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar $bar */
                    public $bar;
                }
                EOF
            ,
        ];

        yield 'testSingleLineProtected' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar */
                    protected $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar $bar */
                    protected $bar;
                }
                EOF
            ,
        ];

        yield 'testSingleLinePrivate' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar */
                    private $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar $bar */
                    private $bar;
                }
                EOF
            ,
        ];

        yield 'testSingleLineVar' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar */
                    var $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar $bar */
                    var $bar;
                }
                EOF
            ,
        ];

        yield 'testSingleLineStatic' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar */
                    static public $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar $bar */
                    static public $bar;
                }
                EOF
            ,
        ];

        yield 'testSingleLineNoSpace' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar*/
                    public $bar;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var Foo\Bar $bar*/
                    public $bar;
                }
                EOF
            ,
        ];

        yield 'testInlineDoc' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * Initializes this class with the given options.
                     *
                     * @param array $options {
                     *     @var bool   $required Whether this element is required
                     *     @var string $label    The display name for this element
                     * }
                     */
                    public function init($options)
                    {
                        // Do something
                    }
                }
                EOF
            ,
        ];

        yield 'testSingleLineNoProperty' => [
            <<<'EOF'
                <?php

                /** @var Foo\Bar $bar */
                $bar;
                EOF
        ];

        yield 'testMultiLineNoProperty' => [
            <<<'EOF'
                <?php

                /**
                 * @var Foo\Bar $bar
                 */
                $bar;
                EOF
        ];

        yield 'testVeryNestedInlineDoc' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var array {
                     *     @var array $secondLevelOne   {
                     *         {@internal This should not break}
                     *         @var int $thirdLevel
                     *     }
                     *     @var array $secondLevelTwo   {
                     *         @var array $thirdLevel     {
                     *             @var string $fourthLevel
                     *         }
                     *         @var int   $moreThirdLevel
                     *     }
                     *     @var int   $secondLevelThree
                     * }
                     */
                    public $nestedFoo;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var array $nestedFoo {
                     *     @var array $secondLevelOne   {
                     *         {@internal This should not break}
                     *         @var int $thirdLevel
                     *     }
                     *     @var array $secondLevelTwo   {
                     *         @var array $thirdLevel     {
                     *             @var string $fourthLevel
                     *         }
                     *         @var int   $moreThirdLevel
                     *     }
                     *     @var int   $secondLevelThree
                     * }
                     */
                    public $nestedFoo;
                }
                EOF
        ];

        yield [
            '<?php
class Foo
{
    /**
     * @no_candidate string Hello!
     */
    public $foo;
}
',
        ];

        yield [
            '<?php
class Foo{}
/**  */',
        ];

        yield 'anonymousClass' => [
            <<<'EOF'
                <?php

                class Anon
                {
                    public function getNewAnon()
                    {
                        return new class()
                        {
                            /**
                             * @var string
                             */
                            public $stringVar;

                            public function getNewAnon()
                            {
                                return new class()
                                {
                                    /**
                                     * @var string
                                     */
                                    public $stringVar;
                                };
                            }
                        };
                    }
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Anon
                {
                    public function getNewAnon()
                    {
                        return new class()
                        {
                            /**
                             * @var $stringVar string
                             */
                            public $stringVar;

                            public function getNewAnon()
                            {
                                return new class()
                                {
                                    /**
                                     * @var $stringVar string
                                     */
                                    public $stringVar;
                                };
                            }
                        };
                    }
                }
                EOF
            ,
        ];

        yield [
            '<?php
/**
 * Header
 */

class A {} // for the candidate check

/**
 * @var ClassLoader $loader
 */
$loader = require __DIR__.\'/../vendor/autoload.php\';

/**
 * @var \Foo\Bar $bar
 */
$bar->doSomething(1);

/**
 * @var $bar \Foo\Bar
 */
$bar->doSomething(2);

/**
 * @var User $bar
 */
($bar = tmp())->doSomething(3);

/**
 * @var User $bar
 */
list($bar) = a();
                ',
        ];

        yield 'const are not handled by this fixer' => [
            '<?php
class A
{
    /**
     * @var array<string, true> SKIPPED_TYPES
     */
    private const SKIPPED_TYPES = ["a" => true];
}
',
        ];

        yield 'trait' => [
            '<?php
 trait StaticExample {
    /**
     * @var string Hello!
     */
    public static $static = "foo";
}',
            '<?php
 trait StaticExample {
    /**
     * @var string $static Hello!
     */
    public static $static = "foo";
}',
        ];

        yield 'complex type with union containing callable that has `$this` in signature' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var array<string, string|array{ string|\Closure(mixed, string, $this): int|float }>|false Hello!
                     */
                    public $foo;

                    /** @var int Hello! */
                    public $foo2;

                    /** @var int Hello! */
                    public $foo3;

                    /** @var int Hello! */
                    public $foo4;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /**
                     * @var array<string, string|array{ string|\Closure(mixed, string, $this): int|float }>|false $foo Hello!
                     */
                    public $foo;

                    /** @var int $thi Hello! */
                    public $foo2;

                    /** @var int $thiss Hello! */
                    public $foo3;

                    /** @var int $this2 Hello! */
                    public $foo4;
                }
                EOF
            ,
        ];

        yield 'testFixMultibyteVariableName' => [
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var int Hello! */
                    public $foo;

                    /** @var 🚀 🚀 */
                    public $foo2;
                }
                EOF
            ,
            <<<'EOF'
                <?php

                class Foo
                {
                    /** @var int $my🚀 Hello! */
                    public $foo;

                    /** @var 🚀 $my 🚀 */
                    public $foo2;
                }
                EOF
            ,
        ];
    }

    /**
     * @dataProvider provideFix81Cases
     *
     * @requires PHP 8.1
     */
    public function testFix81(string $expected, ?string $input = null): void
    {
        $this->doTest($expected, $input);
    }

    public static function provideFix81Cases(): iterable
    {
        yield 'readonly' => [
            '<?php

class Foo
{
    /** @var Foo */
    public $bar1;

    /** @var Foo */
    public readonly int $bar2;

    /** @var Foo */
    readonly public int $bar3;

    /** @var Foo */
    readonly int $bar4;
}',
            '<?php

class Foo
{
    /** @var Foo $bar1 */
    public $bar1;

    /** @var Foo $bar2 */
    public readonly int $bar2;

    /** @var Foo $bar3 */
    readonly public int $bar3;

    /** @var Foo $bar4 */
    readonly int $bar4;
}',
        ];

        yield 'final public const are not handled by this fixer' => [
            '<?php
class A
{
    /**
     * @var array<string, true> SKIPPED_TYPES
     */
    final public const SKIPPED_TYPES = ["a" => true];
}
',
        ];
    }
}
