<?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.
 */

namespace League\Uri;

use ArrayIterator;
use League\Uri\Components\Fragment;
use League\Uri\Exceptions\SyntaxError;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use stdClass;
use Stringable;
use TypeError;
use ValueError;

use function date_create;
use function http_build_query;
use function tmpfile;

use const PHP_QUERY_RFC1738;
use const PHP_QUERY_RFC3986;
use const PHP_VERSION_ID;

final class QueryStringTest extends TestCase
{
    public function testEncodingThrowsExceptionWithQueryParser(): void
    {
        $this->expectException(SyntaxError::class);

        QueryString::parse('foo=bar', '&', 42);
    }

    public function testSyntaxErrorThrowsExceptionWithQueryParser(): void
    {
        $this->expectException(SyntaxError::class);

        QueryString::parse("foo=bar\0");
    }

    public function testSyntaxErrorThrowsExceptionWithQueryParserAndAnEmptySeparator(): void
    {
        $this->expectException(SyntaxError::class);

        QueryString::parse('foo=bar', ''); /* @phpstan-ignore-line */
    }

    public function testEncodingThrowsExceptionWithQueryBuilder(): void
    {
        $this->expectException(SyntaxError::class);

        QueryString::build([['foo', 'bar']], '&', 42);
    }

    public function testBuildThrowsExceptionWithQueryBuilder(): void
    {
        $this->expectException(SyntaxError::class);
        QueryString::build([['foo', 'boo' => 'bar']]); /* @phpstan-ignore-line */
    }

    #[DataProvider('extractQueryProvider')]
    public function testExtractQuery(Stringable|string|null|bool $query, array $expected): void
    {
        self::assertSame($expected, QueryString::extract($query));
    }

    public static function extractQueryProvider(): array
    {
        return [
            [
                'query' => null,
                'expected' => [],
            ],
            [
                'query' => false,
                'expected' => ['0' => ''],
            ],
            [
                'query' => '%25car=%25car',
                'expected' => ['%car' => '%car'],
            ],
            [
                'query' => '&&',
                'expected' => [],
            ],
            [
                'query' => true,
                'expected' => ['1' => ''],
            ],
            [
                'query' => false,
                'expected' => ['0' => ''],
            ],
            [
                'query' => 'arr[1=sid&arr[4][2=fred',
                'expected' => [
                    'arr[1' => 'sid',
                    'arr' => ['4' => 'fred'],
                ],
            ],
            [
                'query' => 'arr1]=sid&arr[4]2]=fred',
                'expected' => [
                    'arr1]' => 'sid',
                    'arr' => ['4' => 'fred'],
                ],
            ],
            [
                'query' => 'arr[one=sid&arr[4][two=fred',
                'expected' => [
                    'arr[one' => 'sid',
                    'arr' => ['4' => 'fred'],
                ],
            ],
            [
                'query' => 'first=%41&second=%a&third=%b',
                'expected' => [
                    'first' => 'A',
                    'second' => '%a',
                    'third' => '%b',
                ],
            ],
            [
                'query' => 'arr.test[1]=sid&arr test[4][two]=fred',
                'expected' => [
                    'arr.test' => ['1' => 'sid'],
                    'arr test' => ['4' => ['two' => 'fred']],
                ],
            ],
            [
                'query' => 'foo&bar=&baz=bar&fo.o',
                'expected' => [
                    'foo' => '',
                    'bar' => '',
                    'baz' => 'bar',
                    'fo.o' => '',
                ],
            ],
            [
                'query' => 'foo[]=bar&foo[]=baz',
                'expected' => [
                    'foo' => ['bar', 'baz'],
                ],
            ],
        ];
    }

    /**
     * @param non-empty-string $separator
     */
    #[DataProvider('parserProvider')]
    public function testParse(Stringable|string|null|bool $query, string $separator, array $expected, int $encoding): void
    {
        self::assertSame($expected, QueryString::parse($query, $separator, $encoding));
    }

    public static function parserProvider(): array
    {
        return [
            'URI Component Object object' => [
                Fragment::new('a=1&a=2'),
                '&',
                [['a', '1'], ['a', '2']],
                PHP_QUERY_RFC3986,
            ],
            'stringable object' => [
                new class () {
                    public function __toString(): string
                    {
                        return 'a=1&a=2';
                    }
                },
                '&',
                [['a', '1'], ['a', '2']],
                PHP_QUERY_RFC3986,
            ],
            'rfc1738 without hexaencoding' => [
                'to+to=foo%2bbar',
                '&',
                [['to to', 'foo+bar']],
                PHP_QUERY_RFC1738,
            ],
            'null value' => [
                null,
                '&',
                [],
                PHP_QUERY_RFC3986,
            ],
            'empty string' => [
                '',
                '&',
                [['', null]],
                PHP_QUERY_RFC3986,
            ],
            'bool value' => [
                false,
                '&',
                [['0', null]],
                PHP_QUERY_RFC1738,
            ],
            'identical keys' => [
                'a=1&a=2',
                '&',
                [['a', '1'], ['a', '2']],
                PHP_QUERY_RFC3986,
            ],
            'no value' => [
                'a&b',
                '&',
                [['a', null], ['b', null]],
                PHP_QUERY_RFC3986,
            ],
            'empty value' => [
                'a=&b=',
                '&',
                [['a', ''], ['b', '']],
                PHP_QUERY_RFC3986,
            ],
            'php array' => [
                'a[]=1&a[]=2',
                '&',
                [['a[]', '1'], ['a[]', '2']],
                PHP_QUERY_RFC3986,
            ],
            'preserve dot' => [
                'a.b=3',
                '&',
                [['a.b', '3']],
                PHP_QUERY_RFC3986,
            ],
            'decode' => [
                'a%20b=c%20d',
                '&',
                [['a b', 'c d']],
                PHP_QUERY_RFC3986,
            ],
            'no key stripping' => [
                'a=&b',
                '&',
                [['a', ''], ['b', null]],
                PHP_QUERY_RFC3986,
            ],
            'no value stripping' => [
                'a=b=',
                '&',
                [['a', 'b=']],
                PHP_QUERY_RFC3986,
            ],
            'key only' => [
                'a',
                '&',
                [['a', null]],
                PHP_QUERY_RFC3986,
            ],
            'preserve falsey 1' => [
                '0',
                '&',
                [['0', null]],
                PHP_QUERY_RFC3986,
            ],
            'preserve falsey 2' => [
                '0=',
                '&',
                [['0', '']],
                PHP_QUERY_RFC3986,
            ],
            'preserve falsey 3' => [
                'a=0',
                '&',
                [['a', '0']],
                PHP_QUERY_RFC3986,
            ],
            'different separator' => [
                'a=0;b=0&c=4',
                ';',
                [['a', '0'], ['b', '0&c=4']],
                PHP_QUERY_RFC3986,
            ],
            'numeric key only' => [
                '42',
                '&',
                [['42', null]],
                PHP_QUERY_RFC3986,
            ],
            'numeric key' => [
                '42=l33t',
                '&',
                [['42', 'l33t']],
                PHP_QUERY_RFC3986,
            ],
            'rfc1738' => [
                '42=l3+3t',
                '&',
                [['42', 'l3 3t']],
                PHP_QUERY_RFC1738,
            ],
        ];
    }

    #[DataProvider('buildProvider')]
    public function testBuild(
        iterable $pairs,
        ?string $expected_rfc1738,
        ?string $expected_rfc3986
    ): void {
        self::assertSame($expected_rfc1738, QueryString::build($pairs, '&', PHP_QUERY_RFC1738));
        self::assertSame($expected_rfc3986, QueryString::build($pairs, '&', PHP_QUERY_RFC3986));
    }

    public static function buildProvider(): array
    {
        return [
            'empty string' => [
                'pairs' => [],
                'expected_rfc1738' => null,
                'expected_rfc3986' => null,
            ],
            'identical keys' => [
                'pairs' => new ArrayIterator([['a', true] , [true, 'a']]),
                'expected_rfc1738' => 'a=1&1=a',
                'expected_rfc3986' => 'a=1&1=a',
            ],
            'no value' => [
                'pairs' => [['a', null], ['b', null]],
                'expected_rfc1738' => 'a&b',
                'expected_rfc3986' => 'a&b',
            ],
            'empty value' => [
                'pairs' => [['a', ''], ['b', 1.3]],
                'expected_rfc1738' => 'a=&b=1.3',
                'expected_rfc3986' => 'a=&b=1.3',
            ],
            'php array (1)' => [
                'pairs' => [['a[]', '1%a6'], ['a[]', '2']],
                'expected_rfc1738' => 'a%5B%5D=1%25a6&a%5B%5D=2',
                'expected_rfc3986' => 'a%5B%5D=1%25a6&a%5B%5D=2',
            ],
            'php array (2)' => [
                'pairs' => [['module', 'home'], ['action', 'show'], ['page', '😓']],
                'expected_rfc1738' => 'module=home&action=show&page=%F0%9F%98%93',
                'expected_rfc3986' => 'module=home&action=show&page=%F0%9F%98%93',
            ],
            'php array (3)' => [
                'pairs' => [['module', 'home'], ['action', 'v%61lue']],
                'expected_rfc1738' => 'module=home&action=v%2561lue',
                'expected_rfc3986' => 'module=home&action=v%2561lue',
            ],
            'preserve dot' => [
                'pairs' => [['a.b', '3']],
                'expected_rfc1738' => 'a.b=3',
                'expected_rfc3986' => 'a.b=3',
            ],
            'no key stripping' => [
                'pairs' => [['a', ''], ['b', null]],
                'expected_rfc1738' => 'a=&b',
                'expected_rfc3986' => 'a=&b',
            ],
            'no value stripping' => [
                'pairs' => [['a', 'b=']],
                'expected_rfc1738' => 'a=b%3D',
                'expected_rfc3986' => 'a=b%3D',
            ],
            'key only' => [
                'pairs' => [['a', null]],
                'expected_rfc1738' => 'a',
                'expected_rfc3986' => 'a',
            ],
            'preserve falsey 1' => [
                'pairs' => [['0', null]],
                'expected_rfc1738' => '0',
                'expected_rfc3986' => '0',
            ],
            'preserve falsey 2' => [
                'pairs' => [['0', '']],
                'expected_rfc1738' => '0=',
                'expected_rfc3986' => '0=',
            ],
            'preserve falsey 3' => [
                'pairs' => [['0', '0']],
                'expected_rfc1738' => '0=0',
                'expected_rfc3986' => '0=0',
            ],
            'rcf1738' => [
                'pairs' => [['toto', 'foo+bar toto']],
                'expected_rfc1738' => 'toto=foo%2Bbar+toto',
                'expected_rfc3986' => 'toto=foo%2Bbar%20toto',
            ],
            'utf-8 characters' => [
                'pairs' => [["\v\xED", "\v\xED"]],
                'expected_rfc1738' => '%0B%ED=%0B%ED',
                'expected_rfc3986' => '%0B%ED=%0B%ED',
            ],
            'uri in value' => [
                'pairs' => [['url', 'https://uri.thephpleague.com/components/2.0/?module=home#what-you-will-be-able-to-do with space']],
                'expected_rfc1738' => 'url=https%3A%2F%2Furi.thephpleague.com%2Fcomponents%2F2.0%2F%3Fmodule%3Dhome%23what-you-will-be-able-to-do+with+space',
                'expected_rfc3986' => 'url=https%3A%2F%2Furi.thephpleague.com%2Fcomponents%2F2.0%2F%3Fmodule%3Dhome%23what-you-will-be-able-to-do%20with%20space',
            ],
        ];
    }

    #[DataProvider('failedBuilderProvider')]
    public function testBuildQueryThrowsException(iterable $pairs, string $separator, int $enc_type): void
    {
        $this->expectException(SyntaxError::class);
        QueryString::build($pairs, $separator, $enc_type); /* @phpstan-ignore-line */
    }

    public static function failedBuilderProvider(): array
    {
        return [
            'The collection cannot contain empty pair' => [
                [[]],
                '&',
                PHP_QUERY_RFC1738,
            ],
            'The pair key must be stringable' => [
                [[date_create(), 'bar']],
                '&',
                PHP_QUERY_RFC1738,
            ],
            'The pair value must be stringable or null - rfc3986/rfc1738' => [
                [['foo', date_create()]],
                '&',
                PHP_QUERY_RFC3986,
            ],
            'identical keys with associative array' => [
                new ArrayIterator([['key' => 'a', 'value' => true] , ['key' => 'a', 'value' => '2']]),
                '&',
                PHP_QUERY_RFC3986,
            ],
            'Object' => [
                [['a[]', (object) '1']],
                '&',
                PHP_QUERY_RFC1738,
            ],
            'the separator cannot be the empty string' => [
                [['foo', 'bar']],
                '',
                PHP_QUERY_RFC3986,
            ],
        ];
    }

    #[DataProvider('queryProvider')]
    public function testStringRepresentationComponent(string|array $input, string|null $expected): void
    {
        $query = is_array($input) ? QueryString::build($input) : QueryString::build(QueryString::parse($input));

        self::assertSame($expected, $query);
    }

    public static function queryProvider(): array
    {
        $unreserved = 'a-zA-Z0-9.-_~!$&\'()*,;=:@';

        return [
            'bug fix issue 84' => ['fào=?%25bar&q=v%61lue', 'f%C3%A0o=%3F%25bar&q=value'],
            'string' => ['kingkong=toto', 'kingkong=toto'],
            'query object' => ['kingkong=toto', 'kingkong=toto'],
            'empty string' => ['', ''],
            'empty array' => [[], null],
            'non empty array' => [[['', null]], ''],
            'contains a reserved word #' => ['foo%23bar', 'foo%23bar'],
            'contains a delimiter ?' => ['?foo%23bar', '%3Ffoo%23bar'],
            'key-only' => ['k^ey', 'k%5Eey'],
            'key-value' => ['k^ey=valu`', 'k%5Eey=valu%60'],
            'array-key-only' => ['key[]', 'key%5B%5D'],
            'array-key-value' => ['key[]=valu`', 'key%5B%5D=valu%60'],
            'complex' => ['k^ey&key[]=valu`&f<>=`bar', 'k%5Eey&key%5B%5D=valu%60&f%3C%3E=%60bar'],
            'Percent encode spaces' => ['q=va lue', 'q=va%20lue'],
            'Percent encode multibyte' => ['€', '%E2%82%AC'],
            "Don't encode something that's already encoded" => ['q=va%20lue', 'q=va%20lue'],
            'Percent encode invalid percent encodings' => ['q=va%2-lue', 'q=va%252-lue'],
            "Don't encode path segments" => ['q=va/lue', 'q=va%2Flue'],
            "Don't encode unreserved chars or sub-delimiters" => [$unreserved, 'a-zA-Z0-9.-_~%21%24&%27%28%29%2A%2C%3B=%3A%40'],
            'Encoded unreserved chars are not decoded' => ['q=v%61lue', 'q=value'],
        ];
    }

    #[Test]
    #[DataProvider('queryWithInnerEmptyBracetsProvider')]
    public function it_should_parse_empty_bracets_issue_146(string $query, string $expected): void
    {
        $data = QueryString::extract($query);
        parse_str($query, $result);

        self::assertSame($data, $result);
        self::assertSame($expected, http_build_query($data, '', '&', PHP_QUERY_RFC3986));
    }

    public static function queryWithInnerEmptyBracetsProvider(): iterable
    {
        yield 'query with on level empty bracets' => [
            'query' => 'foo[]=bar',
            'expected' => 'foo%5B0%5D=bar',
        ];

        yield 'query with two level bracets' => [
            'query' => 'key[][][foo][9]=bar',
            'expected' => 'key%5B0%5D%5B0%5D%5Bfoo%5D%5B9%5D=bar',
        ];

        yield 'query with invalid remaining; close bracet without an opening bracet' => [
            'query' => 'key[][]foo][9]=bar',
            'expected' => 'key%5B0%5D%5B0%5D=bar',
        ];

        yield 'query with invalid remaining; no opening bracet' => [
            'query' => 'key[]9=bar',
            'expected' => 'key%5B0%5D=bar',
        ];

        yield 'query with invalid remaining; opening bracet no at the start of the remaining string' => [
            'query' => 'key[]9[]=bar',
            'expected' => 'key%5B0%5D=bar',
        ];
    }

    /**
     * @param non-empty-string $separator
     */
    #[DataProvider('providesVariablesToCompose')]
    public function test_it_can_compose_a_query_string(
        object|array $variable,
        string $separator,
        int $encoding,
        ?string $expected,
        QueryComposeMode $mode,
    ): void {
        self::assertSame($expected, QueryString::compose($variable, $separator, $encoding, $mode));
    }

    public static function providesVariablesToCompose(): iterable
    {
        yield 'empty string if the variable is empty' => [
            'variable' => [],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => '',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'null if the variable is empty' => [
            'variable' => [],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => null,
            'mode' => QueryComposeMode::Safe,
        ];

        yield 'null if the object properties are not accessible' => [
            'variable' => new stdClass(),
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => '',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 1' => [
            'variable' =>  ['foo' => 'bar', 'baz' => 1, 'test' => "a ' \" ", 'abc', 'float' => 10.42, 'true' => true, 'false' => false],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => 'foo=bar&baz=1&test=a+%27+%22+&0=abc&float=10.42&true=1&false=0',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 2 - with a different separator' => [
            'variable' =>  ['foo' => 'bar', 'baz' => 1, 'test' => "a ' \" ", 'abc', 'float' => 10.42, 'true' => true, 'false' => false],
            'separator' => ';',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => 'foo=bar;baz=1;test=a+%27+%22+;0=abc;float=10.42;true=1;false=0',
            'mode' => QueryComposeMode::Native,
        ];

        $data = new class () implements Stringable {
            public string $public = 'input';
            protected string $protected = 'hello';
            private string $private = 'world';
            public function __toString(): string
            {
                return $this->private;
            }
        };

        yield 'basic encoding from php-src tests 3 - with object' => [
            'variable' =>  $data,
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => 'public=input',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 3 - with null object' => [
            'variable' =>  new stdClass(),
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => '',
            'mode' => QueryComposeMode::Native,
        ];

        $data = new class () implements Stringable {
            public function __toString(): string
            {
                return 'Stringable';
            }
        };

        yield 'basic encoding from php-src tests 4 - with stringable object without public property' => [
            'variable' =>  ['hello', $data],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => '0=hello',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 5 -  with stringable object without public property' => [
            'variable' =>  $data,
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => '',
            'mode' => QueryComposeMode::Native,
        ];

        $o = new class () {
            public mixed $public = 'input';
        };
        $nested = clone $o;
        $o->public = $nested;

        yield 'basic encoding from php-src tests 6 - nested object' => [
            'variable' =>  $o,
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => 'public%5Bpublic%5D=input',
            'mode' => QueryComposeMode::Native,
        ];

        $obj = new stdClass();
        $obj->name = 'homepage';
        $obj->page = 1;
        $obj->sort = 'desc,name';

        yield 'basic encoding from php-src tests 7 - stdClass' => [
            'variable' =>  $obj,
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => 'name=homepage&page=1&sort=desc%2Cname',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 8 - array' => [
            'variable' =>  [
                20,
                5 => 13,
                '9' => [
                    1 => 'val1',
                    3 => 'val2',
                    'string' => 'string',
                ],
                'name' => 'homepage',
                'page' => 10,
                'sort' => [
                    'desc',
                    'admin' => [
                        'admin1',
                        'admin2' => [
                            'who' => 'admin2',
                            2 => 'test',
                        ],
                    ],
                ],
            ],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => '0=20&5=13&9%5B1%5D=val1&9%5B3%5D=val2&9%5Bstring%5D=string&name=homepage&page=10&sort%5B0%5D=desc&sort%5Badmin%5D%5B0%5D=admin1&sort%5Badmin%5D%5Badmin2%5D%5Bwho%5D=admin2&sort%5Badmin%5D%5Badmin2%5D%5B2%5D=test',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 8 - array with rfc1738 encoding' => [
            'variable' =>  [
                'name' => 'main page',
                'sort' => 'desc,admin',
                'equation' => '10 + 10 - 5',
            ],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC1738,
            'expected' => 'name=main+page&sort=desc%2Cadmin&equation=10+%2B+10+-+5',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 8 - array with rfc3986 encoding' => [
            'variable' =>  [
                'name' => 'main page',
                'sort' => 'desc,admin',
                'equation' => '10 + 10 - 5',
            ],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => 'name=main%20page&sort=desc%2Cadmin&equation=10%20%2B%2010%20-%205',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 8 - with null in default mode' => [
            'variable' =>  [null],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => '',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests 8 - with null in conservative mode' => [
            'variable' =>  [null],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => '',
            'mode' => QueryComposeMode::Safe,
        ];

        $v = 'value';
        $ref = &$v;

        yield 'basic encoding from php-src tests 8 - with reference' => [
            'variable' =>  [$ref],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => '0=value',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'bug resolution in php-src tests 9 - float conversion' => [
            'variable' => ['x' => 1E+14, 'y' => '1E+14'],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => 'x=1.0E%2B14&y=1E%2B14',
            'mode' => QueryComposeMode::Native,
        ];

        yield 'basic encoding from php-src tests backed enum' => [
            'variable' => ['backed' => EnumBacked::Two],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => 'backed%5Bname%5D=Two&backed%5Bvalue%5D=Kabiri',
            'mode' => QueryComposeMode::Compatible,
        ];

        yield 'basic encoding from php-src tests backed enum in modern handled form' => [
            'variable' => ['backed' => EnumBacked::Two],
            'separator' => '&',
            'encoding' => PHP_QUERY_RFC3986,
            'expected' => 'backed=Kabiri',
            'mode' => QueryComposeMode::EnumCompatible,
        ];
    }

    public function test_it_throws_if_a_object_recursion_is_detected(): void
    {
        $recursive = new class () {
            public mixed $public = 'input';
        };

        $recursive->public = $recursive;

        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Native));
        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Compatible));
        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::EnumCompatible));

        $this->expectException(TypeError::class);
        QueryString::compose($recursive, composeMode: QueryComposeMode::Safe);
    }

    public function test_it_throws_if_a_array_recursion_is_detected(): void
    {
        $recursive = [];
        $recursive['self'] = &$recursive;

        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Native));
        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Compatible));
        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::EnumCompatible));

        $this->expectException(ValueError::class);
        self::assertSame('', QueryString::compose($recursive, composeMode: QueryComposeMode::Safe));
    }

    public function test_it_throws_if_a_resource_is_present(): void
    {
        $tmpfile = [tmpfile()];
        self::assertSame('', QueryString::compose($tmpfile, composeMode: QueryComposeMode::Native));
        self::assertSame('', QueryString::compose($tmpfile, composeMode: QueryComposeMode::Compatible));
        self::assertSame('', QueryString::compose($tmpfile, composeMode: QueryComposeMode::EnumCompatible));

        $this->expectException(TypeError::class);

        QueryString::compose($tmpfile, composeMode: QueryComposeMode::Safe);
    }

    public function test_it_throws_if_a_non_backed_enum_is_given_in_enum_native_mode(): void
    {
        $this->expectException(TypeError::class);

        QueryString::compose(['pure' => PureEnum::One], composeMode: QueryComposeMode::EnumCompatible);
    }

    public function test_it_silently_ignore_if_a_non_backed_enum_is_given_in_enum_lenient_mode(): void
    {
        self::assertSame('foo=bar', QueryString::compose(['pure' => PureEnum::One, 'foo' => 'bar'], composeMode: QueryComposeMode::EnumLenient));
    }

    public function test_it_throws_if_a_non_backed_enum_is_given_in_strict_mode(): void
    {
        $this->expectException(TypeError::class);

        QueryString::compose(['pure' => PureEnum::One], composeMode: QueryComposeMode::Safe);
    }

    public function test_it_does_not_fail_if_a_non_backed_enum_is_given_in_compatible_mode(): void
    {
        self::assertSame('pure%5Bname%5D=One', QueryString::compose(['pure' => PureEnum::One], composeMode: QueryComposeMode::Compatible));
    }

    public function test_it_handles_backed_enums(): void
    {
        $params = ['bar' => EnumBacked::One, 'baz' => 1];
        $compatible = 'bar%5Bname%5D=One&bar%5Bvalue%5D=Rimwe';
        $enumNative = 'bar=Rimwe';

        self::assertSame((PHP_VERSION_ID < 80400 ? $compatible : $enumNative).'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::Native));
        self::assertSame($compatible.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::Compatible));
        self::assertSame($enumNative.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::EnumLenient));
        self::assertSame($enumNative.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::EnumCompatible));
        self::assertSame($enumNative.'&baz=1', QueryString::compose($params, composeMode: QueryComposeMode::Safe));

    }

    public function test_it_can_handles_empty_array(): void
    {
        self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::Native));
        self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::Compatible));
        self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::EnumLenient));
        self::assertSame('', QueryString::compose([], composeMode: QueryComposeMode::EnumCompatible));
        self::assertNull(QueryString::compose([], composeMode: QueryComposeMode::Safe));
    }

    public function test_it_can_convert_list_without_indices_in_safe_mode(): void
    {
        $data = ['a' => ['foo', false, 1.23]];

        self::assertSame('a%5B%5D=foo&a%5B%5D=0&a%5B%5D=1.23', QueryString::compose($data, composeMode: QueryComposeMode::Safe));
        self::assertSame('a%5B0%5D=foo&a%5B1%5D=0&a%5B2%5D=1.23', QueryString::compose($data, composeMode: QueryComposeMode::Native));
    }

    public function test_it_can_handle_null_value_differently_with_composed_mode(): void
    {
        $data = ['module' => null, 'action' => '', 'page' => true];

        self::assertSame('module&action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::Safe));
        self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::EnumLenient));
        self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::EnumCompatible));
        self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::Compatible));
        self::assertSame('action=&page=1', QueryString::compose($data, composeMode: QueryComposeMode::Native));
        self::assertSame('action=&page=1', http_build_query($data));
    }
}

enum PureEnum
{
    case One;
    case Two;
}

enum EnumBacked: string
{
    case One = 'Rimwe';
    case Two = 'Kabiri';
}
