<?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 GuzzleHttp\Psr7\Utils;
use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\UriInterface as Psr7UriInterface;

#[CoversClass(BaseUri::class)]
#[Group('modifier')]
final class BaseUriTest extends TestCase
{
    private const BASE_URI = 'http://a/b/c/d;p?q';

    public function testItCanBeJsonSerialized(): void
    {
        self::assertSame(
            json_encode(Uri::new('http://example.com')),
            json_encode(BaseUri::from('http://example.com'))
        );
    }

    #[DataProvider('resolveProvider')]
    public function testCreateResolve(string $baseUri, string $uri, string $expected): void
    {
        $uriResolved = BaseUri::from($baseUri)->resolve($uri);

        self::assertInstanceOf(Uri::class, $uriResolved->getUri());
        self::assertSame($expected, $uriResolved->getUriString());

        $psr7Resolved = BaseUri::from($baseUri, new Psr17Factory())->resolve($uri);

        self::assertInstanceOf(\Nyholm\Psr7\Uri::class, $psr7Resolved->getUri());
        self::assertSame($expected, $psr7Resolved->getUriString());
    }

    public static function resolveProvider(): array
    {
        return [
            'base uri'                => [self::BASE_URI, '',              self::BASE_URI],
            'scheme'                  => [self::BASE_URI, 'http://d/e/f',  'http://d/e/f'],
            'path 1'                  => [self::BASE_URI, 'g',             'http://a/b/c/g'],
            'path 2'                  => [self::BASE_URI, './g',           'http://a/b/c/g'],
            'path 3'                  => [self::BASE_URI, 'g/',            'http://a/b/c/g/'],
            'path 4'                  => [self::BASE_URI, '/g',            'http://a/g'],
            'authority'               => [self::BASE_URI, '//g',           'http://g'],
            'query'                   => [self::BASE_URI, '?y',            'http://a/b/c/d;p?y'],
            'path + query'            => [self::BASE_URI, 'g?y',           'http://a/b/c/g?y'],
            'fragment'                => [self::BASE_URI, '#s',            'http://a/b/c/d;p?q#s'],
            'path + fragment'         => [self::BASE_URI, 'g#s',           'http://a/b/c/g#s'],
            'path + query + fragment' => [self::BASE_URI, 'g?y#s',         'http://a/b/c/g?y#s'],
            'single dot 1'            => [self::BASE_URI, '.',             'http://a/b/c/'],
            'single dot 2'            => [self::BASE_URI, './',            'http://a/b/c/'],
            'single dot 3'            => [self::BASE_URI, './g/.',         'http://a/b/c/g/'],
            'single dot 4'            => [self::BASE_URI, 'g/./h',         'http://a/b/c/g/h'],
            'double dot 1'            => [self::BASE_URI, '..',            'http://a/b/'],
            'double dot 2'            => [self::BASE_URI, '../',           'http://a/b/'],
            'double dot 3'            => [self::BASE_URI, '../g',          'http://a/b/g'],
            'double dot 4'            => [self::BASE_URI, '../..',         'http://a/'],
            'double dot 5'            => [self::BASE_URI, '../../',        'http://a/'],
            'double dot 6'            => [self::BASE_URI, '../../g',       'http://a/g'],
            'double dot 7'            => [self::BASE_URI, '../../../g',    'http://a/g'],
            'double dot 8'            => [self::BASE_URI, '../../../../g', 'http://a/g'],
            'double dot 9'            => [self::BASE_URI, 'g/../h' ,       'http://a/b/c/h'],
            'mulitple slashes'        => [self::BASE_URI, 'foo////g',      'http://a/b/c/foo////g'],
            'complex path 1'          => [self::BASE_URI, ';x',            'http://a/b/c/;x'],
            'complex path 2'          => [self::BASE_URI, 'g;x',           'http://a/b/c/g;x'],
            'complex path 3'          => [self::BASE_URI, 'g;x?y#s',       'http://a/b/c/g;x?y#s'],
            'complex path 4'          => [self::BASE_URI, 'g;x=1/./y',     'http://a/b/c/g;x=1/y'],
            'complex path 5'          => [self::BASE_URI, 'g;x=1/../y',    'http://a/b/c/y'],
            'dot segments presence 1' => [self::BASE_URI, '/./g',          'http://a/g'],
            'dot segments presence 2' => [self::BASE_URI, '/../g',         'http://a/g'],
            'dot segments presence 3' => [self::BASE_URI, 'g.',            'http://a/b/c/g.'],
            'dot segments presence 4' => [self::BASE_URI, '.g',            'http://a/b/c/.g'],
            'dot segments presence 5' => [self::BASE_URI, 'g..',           'http://a/b/c/g..'],
            'dot segments presence 6' => [self::BASE_URI, '..g',           'http://a/b/c/..g'],
            'origin uri without path' => ['http://h:b@a', 'b/../y',        'http://h:b@a/y'],
            'not same origin'         => [self::BASE_URI, 'ftp://a/b/c/d', 'ftp://a/b/c/d'],
        ];
    }

    public function testRelativizeIsNotMade(): void
    {
        $uri = '//path#fragment';

        self::assertEquals($uri, BaseUri::from('https://example.com/path')->relativize($uri)->getUriString());
    }

    #[DataProvider('relativizeProvider')]
    public function testRelativize(string $uri, string $resolved, string $expected): void
    {
        self::assertSame(
            $expected,
            BaseUri::from(Http::new($uri))->relativize($resolved)->getUriString()
        );
    }

    public static function relativizeProvider(): array
    {
        return [
            'different scheme'        => [self::BASE_URI,       'https://a/b/c/d;p?q',   'https://a/b/c/d;p?q'],
            'different authority'     => [self::BASE_URI,       'https://g/b/c/d;p?q',   'https://g/b/c/d;p?q'],
            'empty uri'               => [self::BASE_URI,       '',                      ''],
            'same uri'                => [self::BASE_URI,       self::BASE_URI,          ''],
            'same path'               => [self::BASE_URI,       'http://a/b/c/d;p',      'd;p'],
            'parent path 1'           => [self::BASE_URI,       'http://a/b/c/',         './'],
            'parent path 2'           => [self::BASE_URI,       'http://a/b/',           '../'],
            'parent path 3'           => [self::BASE_URI,       'http://a/',             '../../'],
            'parent path 4'           => [self::BASE_URI,       'http://a',              '../../'],
            'sibling path 1'          => [self::BASE_URI,       'http://a/b/c/g',        'g'],
            'sibling path 2'          => [self::BASE_URI,       'http://a/b/c/g/h',      'g/h'],
            'sibling path 3'          => [self::BASE_URI,       'http://a/b/g',          '../g'],
            'sibling path 4'          => [self::BASE_URI,       'http://a/g',            '../../g'],
            'query'                   => [self::BASE_URI,       'http://a/b/c/d;p?y',    '?y'],
            'fragment'                => [self::BASE_URI,       'http://a/b/c/d;p?q#s',  '#s'],
            'path + query'            => [self::BASE_URI,       'http://a/b/c/g?y',      'g?y'],
            'path + fragment'         => [self::BASE_URI,       'http://a/b/c/g#s',      'g#s'],
            'path + query + fragment' => [self::BASE_URI,       'http://a/b/c/g?y#s',    'g?y#s'],
            'empty segments'          => [self::BASE_URI,       'http://a/b/c/foo////g', 'foo////g'],
            'empty segments 1'        => [self::BASE_URI,       'http://a/b////c/foo/g', '..////c/foo/g'],
            'relative single dot 1'   => [self::BASE_URI,       '.',                     '.'],
            'relative single dot 2'   => [self::BASE_URI,       './',                    './'],
            'relative double dot 1'   => [self::BASE_URI,       '..',                    '..'],
            'relative double dot 2'   => [self::BASE_URI,       '../',                   '../'],
            'path with colon 1'       => ['http://a/',          'http://a/d:p',          './d:p'],
            'path with colon 2'       => [self::BASE_URI,       'http://a/b/c/g/d:p',    'g/d:p'],
            'scheme + auth 1'         => ['http://a',           'http://a?q#s',          '?q#s'],
            'scheme + auth 2'         => ['http://a/',          'http://a?q#s',          '/?q#s'],
            '2 relative paths 1'      => ['a/b',                '../..',                 '../..'],
            '2 relative paths 2'      => ['a/b',                './.',                   './.'],
            '2 relative paths 3'      => ['a/b',                '../c',                  '../c'],
            '2 relative paths 4'      => ['a/b',                'c/..',                  'c/..'],
            '2 relative paths 5'      => ['a/b',                'c/.',                   'c/.'],
            'baseUri with query'      => ['/a/b/?q',            '/a/b/#h',               './#h'],
            'targetUri with fragment' => ['/',                  '/#h',                   '#h'],
            'same document'           => ['/',                  '/',                     ''],
            'same URI normalized'     => ['http://a',           'http://a/',             ''],
        ];
    }

    #[DataProvider('relativizeAndResolveProvider')]
    public function testRelativizeAndResolve(
        string $baseUri,
        string $uri,
        string $expectedRelativize,
        string $expectedRelative,
    ): void {
        self::assertSame(
            $expectedRelativize,
            (string) BaseUri::from($baseUri)->relativize($uri)
        );
    }

    public static function relativizeAndResolveProvider(): array
    {
        return [
            'empty path'            => [self::BASE_URI, 'http://a/', '../../',   'http://a/'],
            'absolute empty path'   => [self::BASE_URI, 'http://a',  '../../',   'http://a/'],
            'relative single dot 1' => [self::BASE_URI, '.',         '.',        'http://a/b/c/'],
            'relative single dot 2' => [self::BASE_URI, './',        './',       'http://a/b/c/'],
            'relative double dot 1' => [self::BASE_URI, '..',        '..',       'http://a/b/'],
            'relative double dot 2' => [self::BASE_URI, '../',       '../',      'http://a/b/'],
            '2 relative paths 1'    => ['a/b',          '../..',     '../..',    '/'],
            '2 relative paths 2'    => ['a/b',          './.',       './.',      'a/'],
            '2 relative paths 3'    => ['a/b',          '../c',      '../c',     'c'],
            '2 relative paths 4'    => ['a/b',          'c/..',      'c/..',     'a/'],
            '2 relative paths 5'    => ['a/b',          'c/.',       'c/.',      'a/c/'],
            'path with colon'       => ['http://a/',    'http://a/d:p', './d:p', 'http://a/d:p'],
        ];
    }

    #[DataProvider('uriProvider')]
    public function testInfo(
        Psr7UriInterface|Uri $uri,
        Psr7UriInterface|Uri|null $base_uri,
        bool $absolute_uri,
        bool $network_path,
        bool $absolute_path,
        bool $relative_path,
        bool $same_document,
    ): void {
        if (null !== $base_uri) {
            self::assertSame($same_document, BaseUri::from($base_uri)->isSameDocument($uri));
        }
        self::assertSame($relative_path, BaseUri::from($uri)->isRelativePath());
        self::assertSame($absolute_path, BaseUri::from($uri)->isAbsolutePath());
        self::assertSame($absolute_uri, BaseUri::from($uri)->isAbsolute());
        self::assertSame($network_path, BaseUri::from($uri)->isNetworkPath());
    }

    public static function uriProvider(): array
    {
        return [
            'absolute uri' => [
                'uri' => Http::new('http://a/p?q#f'),
                'base_uri' => null,
                'absolute_uri' => true,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => false,
                'same_document' => false,
            ],
            'network relative uri' => [
                'uri' => Http::new('//스타벅스코리아.com/p?q#f'),
                'base_uri' => Http::new('//xn--oy2b35ckwhba574atvuzkc.com/p?q#z'),
                'absolute_uri' => false,
                'network_path' => true,
                'absolute_path' => false,
                'relative_path' => false,
                'same_document' => true,
            ],
            'path relative uri with non empty path' => [
                'uri' => Http::new('p?q#f'),
                'base_uri' => null,
                'absolute_uri' => false,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => true,
                'same_document' => false,
            ],
            'path relative uri with empty' => [
                'uri' => Http::new('?q#f'),
                'base_uri' => null,
                'absolute_uri' => false,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => true,
                'same_document' => false,
            ],
            'same URI after query sorting' => [
                'uri' => Uri::new('http://a/p?a=1&q=1&a=2#f'),
                'base_uri' => Uri::new('http://a/p?a=2&q=1&a=1#f'),
                'absolute_uri' => true,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => false,
                'same_document' => true,
            ],
            'same URI after query sorting and different host' => [
                'uri' => Utils::uriFor('http://bébé.be/p?a=1&q=1&a=2#f'),
                'base_uri' => Uri::new('http://xn--bb-bjab.be/p?a=2&q=1&a=1#f'),
                'absolute_uri' => true,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => false,
                'same_document' => true,
            ],

            'same URI after path normalization' => [
                'uri' => Utils::uriFor('http://bébé.be/'),
                'base_uri' => Uri::new('http://xn--bb-bjab.be'),
                'absolute_uri' => true,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => false,
                'same_document' => true,
            ],
            'same URI after port normalization' => [
                'uri' => Utils::uriFor('https://bébé.be/'),
                'base_uri' => Uri::new('HtTps://xn--bb-bjab.be:443'),
                'absolute_uri' => true,
                'network_path' => false,
                'absolute_path' => false,
                'relative_path' => false,
                'same_document' => true,
            ],
        ];
    }

    public function testIsFunctionsThrowsTypeError(): void
    {
        self::assertTrue(BaseUri::from('http://example.com')->isAbsolute());
        self::assertFalse(BaseUri::from('http://example.com')->isNetworkPath());
        self::assertTrue(BaseUri::from('/example.com')->isAbsolutePath());
        self::assertTrue(BaseUri::from('example.com#foobar')->isRelativePath());
    }

    #[DataProvider('sameValueAsProvider')]
    public function testSameValueAs(Psr7UriInterface|Uri $uri1, Psr7UriInterface|Uri $uri2, bool $expected): void
    {
        self::assertSame($expected, BaseUri::from($uri2)->isSameDocument($uri1));
    }

    public static function sameValueAsProvider(): array
    {
        return [
            '2 disctincts URIs' => [
                Http::new('http://example.com'),
                Uri::new('ftp://example.com'),
                false,
            ],
            '2 identical URIs' => [
                Http::new('http://example.com'),
                Http::new('http://example.com'),
                true,
            ],
            '2 identical URIs after removing dot segment' => [
                Http::new('http://example.org/~foo/'),
                Http::new('http://example.ORG/bar/./../~foo/'),
                true,
            ],
            '2 distincts relative URIs' => [
                Http::new('~foo/'),
                Http::new('../~foo/'),
                false,
            ],
            '2 identical relative URIs' => [
                Http::new('../%7efoo/'),
                Http::new('../~foo/'),
                true,
            ],
            '2 identical URIs after normalization (1)' => [
                Http::new('HtTp://مثال.إختبار:80/%7efoo/%7efoo/'),
                Http::new('http://xn--mgbh0fb.xn--kgbechtv/%7Efoo/~foo/'),
                true,
            ],
            '2 identical URIs after normalization (2)' => [
                Http::new('http://www.example.com'),
                Http::new('http://www.example.com/'),
                true,
            ],
            '2 identical URIs after normalization (3)' => [
                Http::new('http://www.example.com'),
                Http::new('http://www.example.com:/'),
                true,
            ],
            '2 identical URIs after normalization (4)' => [
                Http::new('http://www.example.com'),
                Http::new('http://www.example.com:80/'),
                true,
            ],
        ];
    }

    #[DataProvider('getOriginProvider')]
    public function testGetOrigin(Psr7UriInterface|Uri|string $uri, ?string $expectedOrigin): void
    {
        self::assertSame($expectedOrigin, BaseUri::from($uri)->origin()?->__toString());
    }

    public static function getOriginProvider(): array
    {
        return [
            'http uri' => [
                'uri' => Uri::new('https://example.com/path?query#fragment'),
                'expectedOrigin' => 'https://example.com',
            ],
            'http uri with non standard port' => [
                'uri' => Uri::new('https://example.com:81/path?query#fragment'),
                'expectedOrigin' => 'https://example.com:81',
            ],
            'relative uri' => [
                'uri' => Uri::new('//example.com:81/path?query#fragment'),
                'expectedOrigin' => null,
            ],
            'absolute uri with user info' => [
                'uri' => Uri::new('https://user:pass@example.com:81/path?query#fragment'),
                'expectedOrigin' => 'https://example.com:81',
            ],
            'opaque URI' => [
                'uri' => Uri::new('mailto:info@thephpleague.com'),
                'expectedOrigin' => null,
            ],
            'file URI' => [
                'uri' => Uri::new('file:///usr/bin/test'),
                'expectedOrigin' => null,
            ],
            'blob' => [
                'uri' => Uri::new('blob:https://mozilla.org:443/1f6a188e-c21b-11f0-8de9-0242ac120002'),
                'expectedOrigin' => 'https://mozilla.org',
            ],
            'normalized ipv4' => [
                'uri' => 'https://0:443/',
                'expectedOrigin' => 'https://0.0.0.0',
            ],
            'normalized ipv4 with object' => [
                'uri' => Uri::new('https://0:443/'),
                'expectedOrigin' => 'https://0.0.0.0',
            ],
            'compressed ipv6' => [
                'uri' => 'https://[1050:0000:0000:0000:0005:0000:300c:326b]:443/',
                'expectedOrigin' => 'https://[1050::5:0:300c:326b]',
            ],
            'normalized ipv4 non decimal notation' => [
                'uri' => 'https://0xc0a821/foo/bar/',
                'expectedOrigin' => 'https://0.192.168.33',
            ],
        ];
    }

    #[DataProvider('getCrossOriginExamples')]
    public function testIsCrossOrigin(string $original, string $modified, bool $expected): void
    {
        self::assertSame($expected, BaseUri::from($original)->isCrossOrigin($modified));
    }

    /**
     * @return array<string, array{0:string, 1:string, 2:bool}>
     */
    public static function getCrossOriginExamples(): array
    {
        return [
            'different path' => ['http://example.com/123', 'http://example.com/', false],
            'same port with default value (1)' => ['https://example.com/123', 'https://example.com:443/', false],
            'same port with default value (2)' => ['ws://example.com:80/123', 'ws://example.com/', false],
            'same explicit port' => ['wss://example.com:443/123', 'wss://example.com:443/', false],
            'same origin with i18n host' => ['https://xn--bb-bjab.be./path', 'https://Bébé.BE./path', false],
            'same origin using a blob' => ['blob:https://mozilla.org:443/1f6a188e-c21b-11f0-8de9-0242ac120002', 'https://mozilla.org/123', false],
            'different scheme' => ['https://example.com/123', 'ftp://example.com/', true],
            'different host' => ['ftp://example.com/123', 'ftp://www.example.com/123', true],
            'different port implicit' => ['https://example.com/123', 'https://example.com:81/', true],
            'different port explicit' => ['https://example.com:80/123', 'https://example.com:81/', true],
            'same scheme different port' => ['https://example.com:443/123', 'https://example.com:444/', true],
            'comparing two opaque URI' => ['ldap://ldap.example.net', 'ldap://ldap.example.net', true],
            'comparing a URI with an origin and one with an opaque origin' => ['https://example.com:443/123', 'ldap://ldap.example.net', true],
            'cross origin using a blob' => ['blob:http://mozilla.org:443/1f6a188e-c21b-11f0-8de9-0242ac120002', 'https://mozilla.org/123', true],
        ];
    }

    #[DataProvider('resolveProvider')]
    public function testResolveWithPsr7Implementation(string $baseUri, string $uri, string $expected): void
    {
        $resolvedUri = BaseUri::from(Utils::uriFor($baseUri))->resolve($uri);

        self::assertInstanceOf(Uri::class, $resolvedUri->getUri());
        self::assertSame($expected, (string) $resolvedUri);
    }

    #[DataProvider('relativizeProvider')]
    public function testRelativizeWithPsr7Implementation(string $uriString, string $resolved, string $expected): void
    {
        $uri = Utils::uriFor($uriString);
        $baseUri = BaseUri::from($uri);

        $relativizeUri = $baseUri->withUriFactory(new \Nyholm\Psr7\Factory\Psr17Factory())->relativize($resolved);
        self::assertInstanceOf(\Nyholm\Psr7\Uri::class, $relativizeUri->getUri());
        self::assertSame($expected, (string) $relativizeUri);

        $guzzleBaseUri = $baseUri->withUriFactory(new \GuzzleHttp\Psr7\HttpFactory());
        $relativizeUri = $guzzleBaseUri->relativize($resolved);
        self::assertInstanceOf(\GuzzleHttp\Psr7\Uri::class, $relativizeUri->getUri());
        self::assertSame($expected, (string) $relativizeUri);

        $relativizeUri = $guzzleBaseUri->withoutUriFactory()->relativize($resolved);
        self::assertInstanceOf(Uri::class, $relativizeUri->getUri());
        self::assertSame($expected, (string) $relativizeUri);
    }

    #[DataProvider('getOriginProvider')]
    public function testGetOriginWithPsr7Implementation(Psr7UriInterface|Uri|string $uri, ?string $expectedOrigin): void
    {
        $origin = BaseUri::from(Utils::uriFor((string) $uri), new \GuzzleHttp\Psr7\HttpFactory())->origin();
        if (null !== $origin) {
            self::assertInstanceOf(\GuzzleHttp\Psr7\Uri::class, $origin->getUri());
            self::assertSame($expectedOrigin, $origin->__toString());

            return;
        }

        self::assertSame($expectedOrigin, $origin);
    }

    #[DataProvider('provideIDNUri')]
    public function testHostIsIDN(string $uri, bool $expected): void
    {
        self::assertSame($expected, BaseUri::from($uri)->hasIdn());
        self::assertSame($expected, BaseUri::from(Utils::uriFor($uri), new \GuzzleHttp\Psr7\HttpFactory())->hasIdn());
    }

    public static function provideIDNUri(): iterable
    {
        yield 'ascii uri (1)' => [
            'uri' => 'https://www.example.com',
            'expected' => false,
        ];

        yield 'ascii uri with invalid converted i18n' => [
            'uri' => 'https://www.xn--ample.com',
            'expected' => false,
        ];

        yield 'idn uri' => [
            'uri' => 'https://www.bébé.be',
            'expected' => true,
        ];

        yield 'uri without host' => [
            'uri' => '/path/to/the?sky=1',
            'expected' => false,
        ];

        yield 'uri without empty host' => [
            'uri' => 'file:///path/to/the/sky',
            'expected' => false,
        ];
    }

    #[DataProvider('unixpathProvider')]
    public function testReturnsUnixPath(?string $expected, string $input): void
    {
        self::assertSame($expected, BaseUri::from($input)->unixPath());
        self::assertSame($expected, BaseUri::from(Utils::uriFor($input))->unixPath());
    }

    public static function unixpathProvider(): array
    {
        return [
            'relative path' => [
                'expected' => 'path',
                'input' => 'path',
            ],
            'absolute path' => [
                'expected' => '/path',
                'input' => 'file:///path',
            ],
            'path with empty char' => [
                'expected' => '/path empty/bar',
                'input' => 'file:///path%20empty/bar',
            ],
            'relative path with dot segments' => [
                'expected' => 'path/./relative',
                'input' => 'path/./relative',
            ],
            'absolute path with dot segments' => [
                'expected' => '/path/./../relative',
                'input' => 'file:///path/./../relative',
            ],
            'unsupported scheme' => [
                'expected' => null,
                'input' => 'http://example.com/foo/bar',
            ],
        ];
    }

    #[DataProvider('windowLocalPathProvider')]
    public function testReturnsWindowsPath(?string $expected, string $input): void
    {
        self::assertSame($expected, BaseUri::from($input)->windowsPath());
        self::assertSame($expected, BaseUri::from(Utils::uriFor($input))->windowsPath());
    }

    public static function windowLocalPathProvider(): array
    {
        return [
            'relative path' => [
                'expected' => 'path',
                'input' => 'path',
            ],
            'relative path with dot segments' => [
                'expected' => 'path\.\relative',
                'input' => 'path/./relative',
            ],
            'absolute path' => [
                'expected' => 'c:\windows\My Documents 100%20\foo.txt',
                'input' => 'file:///c:/windows/My%20Documents%20100%2520/foo.txt',
            ],
            'windows relative path' => [
                'expected' => 'c:My Documents 100%20\foo.txt',
                'input' => 'file:///c:My%20Documents%20100%2520/foo.txt',
            ],
            'absolute path with `|`' => [
                'expected' => 'c:\windows\My Documents 100%20\foo.txt',
                'input' => 'file:///c:/windows/My%20Documents%20100%2520/foo.txt',
            ],
            'windows relative path with `|`' => [
                'expected' => 'c:My Documents 100%20\foo.txt',
                'input' => 'file:///c:My%20Documents%20100%2520/foo.txt',
            ],
            'absolute path with dot segments' => [
                'expected' => '\path\.\..\relative',
                'input' => '/path/./../relative',
            ],
            'absolute UNC path' => [
                'expected' => '\\\\server\share\My Documents 100%20\foo.txt',
                'input' => 'file://server/share/My%20Documents%20100%2520/foo.txt',
            ],
            'unsupported scheme' => [
                'expected' => null,
                'input' => 'http://example.com/foo/bar',
            ],
        ];
    }

    #[DataProvider('rfc8089UriProvider')]
    public function testReturnsRFC8089UriString(?string $expected, string $input): void
    {
        self::assertSame($expected, BaseUri::from($input)->toRfc8089());
        self::assertSame($expected, BaseUri::from(Utils::uriFor($input))->toRfc8089());
    }

    public static function rfc8089UriProvider(): iterable
    {
        return [
            'localhost' => [
                'expected' => 'file:/etc/fstab',
                'input' => 'file://localhost/etc/fstab',
            ],
            'empty authority' => [
                'expected' => 'file:/etc/fstab',
                'input' => 'file:///etc/fstab',
            ],
            'file with authority' => [
                'expected' => 'file://yesman/etc/fstab',
                'input' => 'file://yesman/etc/fstab',
            ],
            'invalid scheme' => [
                'expected' => null,
                'input' => 'foobar://yesman/etc/fstab',
            ],
        ];
    }

    #[DataProvider('opaqueUriProvider')]
    #[Test]
    public function it_tells_if_an_uri_is_opaque(bool $expected, string $uri): void
    {
        self::assertSame($expected, BaseUri::from($uri)->isOpaque());
    }

    public static function opaqueUriProvider(): iterable
    {
        yield 'empty URI' => [
            'expected' => false,
            'uri' => '',
        ];

        yield 'relative URI' => [
            'expected' => false,
            'uri' => 'path?query#fragment',
        ];

        yield 'URI with authority' => [
            'expected' => false,
            'uri' => '//authority/path?query#fragment',
        ];

        yield 'absolute HTTP URI' => [
            'expected' => false,
            'uri' => 'https://authority/path?query#fragment',
        ];

        yield 'absolute mail URI' => [
            'expected' => true,
            'uri' => 'mail:foo@example.com',
        ];

        yield 'data URI' => [
            'expected' => true,
            'uri' => 'data:',
        ];
    }
}
