<?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\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
 * @author Graham Campbell <hello@gjcampbell.co.uk>
 * @author Jakub Kwaśniewski <jakub@zero-85.pl>
 *
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\Phpdoc\PhpdocSeparationFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\Phpdoc\PhpdocSeparationFixer>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\Phpdoc\PhpdocSeparationFixer
 */
final class PhpdocSeparationFixerTest extends AbstractFixerTestCase
{
    public function testFix(): void
    {
        $this->doTest('<?php
/** @param EngineInterface $templating
*@return void
*/');

        $expected = <<<'EOF'
            <?php
                /**
                 * @param EngineInterface $templating
                 *
                 * @return void
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * @param EngineInterface $templating
                 * @return void
                 */

            EOF;

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

    public function testFixMoreTags(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * Hello there!
                 *
                 * @internal
                 *
                 * @param string $foo
                 *
                 * @throws Exception
                 *
                 * @return bool
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Hello there!
                 * @internal
                 * @param string $foo
                 * @throws Exception
                 *
                 *
                 *
                 * @return bool
                 */

            EOF;

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

    public function testFixSpreadOut(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * Hello there!
                 *
                 * Long description
                 * goes here.
                 *
                 * @param string $foo
                 * @param bool   $bar Bar
                 *
                 * @throws Exception|RuntimeException
                 *
                 * @return bool
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Hello there!
                 *
                 * Long description
                 * goes here.
                 * @param string $foo
                 *
                 *
                 * @param bool   $bar Bar
                 *
                 *
                 *
                 * @throws Exception|RuntimeException
                 *
                 *
                 *
                 *
                 * @return bool
                 */

            EOF;

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

    public function testMultiLineComments(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * Hello there!
                 *
                 * Long description
                 * goes here.
                 *
                 * @param string $foo test 123
                 *                    asdasdasd
                 * @param bool  $bar qwerty
                 *
                 * @throws Exception|RuntimeException
                 *
                 * @return bool
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Hello there!
                 *
                 * Long description
                 * goes here.
                 * @param string $foo test 123
                 *                    asdasdasd
                 * @param bool  $bar qwerty
                 * @throws Exception|RuntimeException
                 * @return bool
                 */

            EOF;

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

    public function testCrazyMultiLineComments(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * Clients accept an array of constructor parameters.
                 *
                 * Here's an example of creating a client using a URI template for the
                 * client's base_url and an array of default request options to apply
                 * to each request:
                 *
                 *     $client = new Client([
                 *         'base_url' => [
                 *              'https://www.foo.com/{version}/',
                 *              ['version' => '123']
                 *          ],
                 *         'defaults' => [
                 *             'timeout'         => 10,
                 *             'allow_redirects' => false,
                 *             'proxy'           => '192.168.16.1:10'
                 *         ]
                 *     ]);
                 *
                 * @param _AutogeneratedInputConfiguration $config Client configuration settings
                 *     - base_url: Base URL of the client that is merged into relative URLs.
                 *       Can be a string or an array that contains a URI template followed
                 *       by an associative array of expansion variables to inject into the
                 *       URI template.
                 *     - handler: callable RingPHP handler used to transfer requests
                 *     - message_factory: Factory used to create request and response object
                 *     - defaults: Default request options to apply to each request
                 *     - emitter: Event emitter used for request events
                 *     - fsm: (internal use only) The request finite state machine. A
                 *       function that accepts a transaction and optional final state. The
                 *       function is responsible for transitioning a request through its
                 *       lifecycle events.
                 * @param string $foo
                 */

            EOF;

        $this->doTest($expected);
    }

    public function testDoctrineExample(): void
    {
        $expected = <<<'EOF'
            <?php
            /**
             * PersistentObject base class that implements getter/setter methods for all mapped fields and associations
             * by overriding __call.
             *
             * This class is a forward compatible implementation of the PersistentObject trait.
             *
             * Limitations:
             *
             * 1. All persistent objects have to be associated with a single ObjectManager, multiple
             *    ObjectManagers are not supported. You can set the ObjectManager with `PersistentObject#setObjectManager()`.
             * 2. Setters and getters only work if a ClassMetadata instance was injected into the PersistentObject.
             *    This is either done on `postLoad` of an object or by accessing the global object manager.
             * 3. There are no hooks for setters/getters. Just implement the method yourself instead of relying on __call().
             * 4. Slower than handcoded implementations: An average of 7 method calls per access to a field and 11 for an association.
             * 5. Only the inverse side associations get autoset on the owning side as well. Setting objects on the owning side
             *    will not set the inverse side associations.
             *
             * @example
             *
             *  PersistentObject::setObjectManager($em);
             *
             *  class Foo extends PersistentObject
             *  {
             *      private $id;
             *  }
             *
             *  $foo = new Foo();
             *  $foo->getId(); // method exists through __call
             *
             * @author Benjamin Eberlei <kontakt@beberlei.de>
             */

            EOF;

        $this->doTest($expected);
    }

    public function testSymfonyExample(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * Constructor.
                 *
                 * Depending on how you want the storage driver to behave you probably
                 * want to override this constructor entirely.
                 *
                 * List of options for $options array with their defaults.
                 *
                 * @see https://php.net/session.configuration for options
                 *
                 * but we omit 'session.' from the beginning of the keys for convenience.
                 *
                 * ("auto_start", is not supported as it tells PHP to start a session before
                 * PHP starts to execute user-land code. Setting during runtime has no effect).
                 *
                 * cache_limiter, "nocache" (use "0" to prevent headers from being sent entirely).
                 * cookie_domain, ""
                 * cookie_httponly, ""
                 * cookie_lifetime, "0"
                 * cookie_path, "/"
                 * cookie_secure, ""
                 * entropy_file, ""
                 * entropy_length, "0"
                 * gc_divisor, "100"
                 * gc_maxlifetime, "1440"
                 * gc_probability, "1"
                 * hash_bits_per_character, "4"
                 * hash_function, "0"
                 * name, "PHPSESSID"
                 * referer_check, ""
                 * serialize_handler, "php"
                 * use_cookies, "1"
                 * use_only_cookies, "1"
                 * use_trans_sid, "0"
                 * upload_progress.enabled, "1"
                 * upload_progress.cleanup, "1"
                 * upload_progress.prefix, "upload_progress_"
                 * upload_progress.name, "PHP_SESSION_UPLOAD_PROGRESS"
                 * upload_progress.freq, "1%"
                 * upload_progress.min-freq, "1"
                 * url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset="
                 *
                 * @param array                                                            $options Session configuration options.
                 * @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $handler
                 * @param MetadataBag                                                      $metaBag MetadataBag.
                 */

            EOF;

        $this->doTest($expected);
    }

    public function testDeprecatedAndSeeTags(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * Hi!
                 *
                 * @author Bar Baz <foo@example.com>
                 *
                 * @deprecated As of some version.
                 * @see Replacement
                 *      described here.
                 *
                 * @param string $foo test 123
                 * @param bool  $bar qwerty
                 *
                 * @return void
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Hi!
                 *
                 * @author Bar Baz <foo@example.com>
                 * @deprecated As of some version.
                 *
                 * @see Replacement
                 *      described here.
                 * @param string $foo test 123
                 * @param bool  $bar qwerty
                 *
                 * @return void
                 */

            EOF;

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

    public function testPropertyTags(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * @author Bar Baz <foo@example.com>
                 *
                 * @property int $foo
                 * @property-read int $foo
                 * @property-write int $bar
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * @author Bar Baz <foo@example.com>
                 * @property int $foo
                 *
                 * @property-read int $foo
                 *
                 * @property-write int $bar
                 */

            EOF;

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

    public function testClassDocBlock(): void
    {
        $expected = <<<'EOF'
            <?php

            namespace Foo;

            /**
             * This is a class that does classy things.
             *
             * @internal
             *
             * @package Foo
             * @subpackage Foo\Bar
             *
             * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
             * @author Graham Campbell <hello@gjcampbell.co.uk>
             * @copyright Foo Bar
             * @license MIT
             */
            class Bar {}

            EOF;

        $input = <<<'EOF'
            <?php

            namespace Foo;

            /**
             * This is a class that does classy things.
             * @internal
             * @package Foo
             *
             *
             * @subpackage Foo\Bar
             * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
             *
             * @author Graham Campbell <hello@gjcampbell.co.uk>
             *
             * @copyright Foo Bar
             *
             *
             * @license MIT
             */
            class Bar {}

            EOF;

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

    public function testPoorAlignment(): void
    {
        $expected = <<<'EOF'
            <?php

            namespace Foo;

            /**
            *      This is a class that does classy things.
                *
            *    @internal
            *
             *          @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
                *@author Graham Campbell <hello@gjcampbell.co.uk>
             */
            class Bar {}

            EOF;

        $input = <<<'EOF'
            <?php

            namespace Foo;

            /**
            *      This is a class that does classy things.
                *
            *    @internal
               *
            *
            *
             *          @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
                 *
                                         *
                *@author Graham Campbell <hello@gjcampbell.co.uk>
             */
            class Bar {}

            EOF;

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

    public function testMoveUnknownAnnotations(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 * @expectedException Exception
                 *
                 * @expectedExceptionMessage Oh Noes!
                 * Something when wrong!
                 *
                 * @Hello\Test\Foo(asd)
                 *
                 * @Method("GET")
                 *
                 * @param string $expected
                 * @param string $input
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * @expectedException Exception
                 * @expectedExceptionMessage Oh Noes!
                 * Something when wrong!
                 *
                 *
                 * @Hello\Test\Foo(asd)
                 * @Method("GET")
                 *
                 * @param string $expected
                 *
                 * @param string $input
                 */

            EOF;

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

    /**
     * @dataProvider provideInheritDocCases
     */
    public function testInheritDoc(string $expected, string $input): void
    {
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<array{string, string}>
     */
    public static function provideInheritDocCases(): iterable
    {
        yield [
            '<?php
    /**
     * {@inheritdoc}
     *
     * @param string $expected
     * @param string $input
     */
',
            '<?php
    /**
     * {@inheritdoc}
     * @param string $expected
     * @param string $input
     */
',
        ];

        yield [
            '<?php
    /**
     * {@inheritDoc}
     *
     * @param string $expected
     * @param string $input
     */
',
            '<?php
    /**
     * {@inheritDoc}
     * @param string $expected
     * @param string $input
     */
',
        ];
    }

    public function testEmptyDocBlock(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 *
                 */

            EOF;

        $this->doTest($expected);
    }

    public function testLargerEmptyDocBlock(): void
    {
        $expected = <<<'EOF'
            <?php
                /**
                 *
                 *
                 *
                 *
                 */

            EOF;

        $this->doTest($expected);
    }

    public function testOneLineDocBlock(): void
    {
        $expected = <<<'EOF'
            <?php
                /** Foo */
                const Foo = 1;

            EOF;

        $this->doTest($expected);
    }

    public function testMessyWhitespaces(): void
    {
        $expected = "<?php\t/**\r\n\t * @param string \$text\r\n\t *\r\n\t * @return string\r\n\t */";
        $input = "<?php\t/**\r\n\t * @param string \$text\r\n\t * @return string\r\n\t */";

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

    public function testWithSpacing(): void
    {
        $expected = '<?php
    /**
     * Foo
     *
     * @bar 123
     *
     * {@inheritdoc}       '.'
     *
     *   @param string $expected
     * @param string $input
     */';

        $input = '<?php
    /**
     * Foo
     * @bar 123
     *
     * {@inheritdoc}       '.'
     *   @param string $expected
     * @param string $input
     */';

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

    public function testTagInTwoGroupsConfiguration(): void
    {
        $this->expectException(InvalidFixerConfigurationException::class);
        $this->expectExceptionMessage(
            'The option "groups" value is invalid. '.
            'The "param" tag belongs to more than one group.'
        );

        $this->fixer->configure(['groups' => [['param', 'return'], ['param', 'throws']]]);
    }

    public function testTagSpecifiedTwoTimesInGroupConfiguration(): void
    {
        $this->expectException(InvalidFixerConfigurationException::class);
        $this->expectExceptionMessage(
            'The option "groups" value is invalid. '.
            'The "param" tag is specified more than once.'
        );

        $this->fixer->configure(['groups' => [['param', 'return', 'param', 'throws']]]);
    }

    public function testLaravelGroups(): void
    {
        $this->fixer->configure(['groups' => [
            ['param', 'return'],
            ['throws'],
            ['deprecated', 'link', 'see', 'since'],
            ['author', 'copyright', 'license'],
            ['category', 'package', 'subpackage'],
            ['property', 'property-read', 'property-write'],
        ]]);

        $expected = <<<'EOF'
            <?php
                /**
                 * Attempt to authenticate using HTTP Basic Auth.
                 *
                 * @param  string  $field
                 * @param  array  $extraConditions
                 * @return \Symfony\Component\HttpFoundation\Response|null
                 *
                 * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Attempt to authenticate using HTTP Basic Auth.
                 *
                 * @param  string  $field
                 * @param  array  $extraConditions
                 * @return \Symfony\Component\HttpFoundation\Response|null
                 * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
                 */

            EOF;

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

    public function testVariousGroups(): void
    {
        $this->fixer->configure([
            'groups' => [
                ['deprecated', 'link', 'see', 'since', 'author', 'copyright', 'license'],
                ['category', 'package', 'subpackage'],
                ['property', 'property-read', 'property-write'],
                ['return', 'param'],
            ],
        ]);

        $expected = <<<'EOF'
            <?php
                /**
                 * Attempt to authenticate using HTTP Basic Auth.
                 *
                 * @link https://example.com/link
                 * @see https://doc.example.com/link
                 * @copyright by John Doe 2001
                 * @author John Doe
                 *
                 * @property-custom string $prop
                 *
                 * @param  string  $field
                 * @param  array  $extraConditions
                 * @return \Symfony\Component\HttpFoundation\Response|null
                 *
                 * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Attempt to authenticate using HTTP Basic Auth.
                 *
                 * @link https://example.com/link
                 *
                 *
                 * @see https://doc.example.com/link
                 * @copyright by John Doe 2001
                 * @author John Doe
                 * @property-custom string $prop
                 * @param  string  $field
                 * @param  array  $extraConditions
                 *
                 * @return \Symfony\Component\HttpFoundation\Response|null
                 * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
                 */

            EOF;

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

    public function testVariousAdditionalGroups(): void
    {
        $this->fixer->configure([
            'groups' => [
                ['deprecated', 'link', 'see', 'since', 'author', 'copyright', 'license'],
                ['category', 'package', 'subpackage'],
                ['property', 'property-read', 'property-write'],
                ['return', 'param'],
            ],
        ]);

        $expected = <<<'EOF'
            <?php
                /**
                 * Attempt to authenticate using HTTP Basic Auth.
                 *
                 * @link https://example.com/link
                 * @see https://doc.example.com/link
                 * @copyright by John Doe 2001
                 * @author John Doe
                 *
                 * @param  string  $field
                 * @param  array  $extraConditions
                 * @return \Symfony\Component\HttpFoundation\Response|null
                 *
                 * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
                 */

            EOF;

        $input = <<<'EOF'
            <?php
                /**
                 * Attempt to authenticate using HTTP Basic Auth.
                 *
                 * @link https://example.com/link
                 *
                 *
                 * @see https://doc.example.com/link
                 * @copyright by John Doe 2001
                 * @author John Doe
                 * @param  string  $field
                 * @param  array  $extraConditions
                 *
                 * @return \Symfony\Component\HttpFoundation\Response|null
                 * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
                 */

            EOF;

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

    /**
     * @dataProvider provideDocCodeCases
     *
     * @param _AutogeneratedInputConfiguration $config
     */
    public function testDocCode(string $expected, ?string $input = null, ?array $config = null): void
    {
        if (null !== $config) {
            $this->fixer->configure($config);
        }

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

    /**
     * @return iterable<string, array{0: string, 1?: string, 2?: array<string, mixed>}>
     */
    public static function provideDocCodeCases(): iterable
    {
        $input = <<<'EOF'
            <?php
            /**
             * Hello there!
             *
             * @author John Doe
             * @custom Test!
             * @throws Exception|RuntimeException foo
             * @param string $foo
             * @param bool   $bar Bar
             *
             * @return int  Return the number of changes.
             */

            EOF;

        yield 'laravel' => [
            <<<'EOF'
                <?php
                /**
                 * Hello there!
                 *
                 * @author John Doe
                 *
                 * @custom Test!
                 *
                 * @throws Exception|RuntimeException foo
                 *
                 * @param string $foo
                 * @param bool   $bar Bar
                 * @return int  Return the number of changes.
                 */

                EOF,
            $input,
            ['groups' => [
                ['param', 'return'],
                ['throws'],
                ['deprecated', 'link', 'see', 'since'],
                ['author', 'copyright', 'license'],
                ['category', 'package', 'subpackage'],
                ['property', 'property-read', 'property-write'],
            ]],
        ];

        yield 'all_tags' => [
            <<<'EOF'
                <?php
                /**
                 * Hello there!
                 *
                 * @author John Doe
                 * @custom Test!
                 * @throws Exception|RuntimeException foo
                 *
                 * @param string $foo
                 * @param bool   $bar Bar
                 * @return int  Return the number of changes.
                 */

                EOF,
            $input,
            ['groups' => [['author', 'throws', 'custom'], ['return', 'param']]],
        ];

        yield 'default_groups_standard_tags' => [
            <<<'EOF'
                <?php
                /**
                 * Hello there!
                 *
                 * @author John Doe
                 *
                 * @throws Exception|RuntimeException foo
                 *
                 * @custom Test!
                 *
                 * @param string $foo
                 * @param bool   $bar Bar
                 *
                 * @return int  Return the number of changes.
                 */

                EOF,
            <<<'EOF'
                <?php
                /**
                 * Hello there!
                 * @author John Doe
                 * @throws Exception|RuntimeException foo
                 * @custom Test!
                 * @param string $foo
                 * @param bool   $bar Bar
                 * @return int  Return the number of changes.
                 */

                EOF,
        ];

        yield 'Separated unlisted tags with default config' => [
            <<<'EOF'
                <?php
                /**
                 * @not-in-any-group1
                 *
                 * @not-in-any-group2
                 *
                 * @not-in-any-group3
                 */

                EOF,
            <<<'EOF'
                <?php
                /**
                 * @not-in-any-group1
                 * @not-in-any-group2
                 * @not-in-any-group3
                 */

                EOF,
        ];

        yield 'Skip unlisted tags' => [
            <<<'EOF'
                <?php
                /**
                 * @in-group-1
                 * @in-group-1-too
                 *
                 * @not-in-any-group1
                 *
                 * @not-in-any-group2
                 * @not-in-any-group3
                 */

                EOF,
            <<<'EOF'
                <?php
                /**
                 * @in-group-1
                 *
                 * @in-group-1-too
                 * @not-in-any-group1
                 *
                 * @not-in-any-group2
                 * @not-in-any-group3
                 */

                EOF,
            [
                'groups' => [['in-group-1', 'in-group-1-too']],
                'skip_unlisted_annotations' => true,
            ],
        ];

        yield 'Doctrine annotations' => [
            <<<'EOF'
                <?php
                /**
                 * @ORM\Id
                 * @ORM\Column(type="integer")
                 * @ORM\GeneratedValue
                 */

                EOF,
            <<<'EOF'
                <?php
                /**
                 * @ORM\Id
                 *
                 * @ORM\Column(type="integer")
                 *
                 * @ORM\GeneratedValue
                 */

                EOF,
            ['groups' => [
                ['ORM\Id', 'ORM\Column', 'ORM\GeneratedValue'],
            ]],
        ];

        yield 'With wildcard' => [
            <<<'EOF'
                <?php
                /**
                 * @ORM\Id
                 * @ORM\Column(type="integer")
                 * @ORM\GeneratedValue
                 *
                 * @Assert\NotNull
                 * @Assert\Type("string")
                 */

                EOF,
            <<<'EOF'
                <?php
                /**
                 * @ORM\Id
                 *
                 * @ORM\Column(type="integer")
                 *
                 * @ORM\GeneratedValue
                 * @Assert\NotNull
                 *
                 * @Assert\Type("string")
                 */

                EOF,
            ['groups' => [
                ['ORM\*'],
                ['Assert\*'],
            ]],
        ];
    }
}