<?php

namespace Tests\Feature;

use App\Support\PublicHttp;
use GuzzleHttp\Psr7\Request as PsrRequest;
use GuzzleHttp\Psr7\Response as PsrResponse;
use GuzzleHttp\Psr7\Uri;
use PHPUnit\Framework\Attributes\DataProvider;
use RuntimeException;
use Tests\TestCase;

/**
 * The address check survives a redirect.
 *
 * Every one of these guards ran once, against the host the user typed, and then
 * the client followed redirects unchecked. The user never types a private
 * address, so the guard never fired on anything that mattered: a page on an
 * ordinary public domain answered 302 to http://169.254.169.254/latest/meta-data/
 * and the body came back into the chat as the landing page analysis, up to 3000
 * characters of it. Reachable by anyone who can send a chat message.
 *
 * Three call sites had their own copy of the check and all three stopped at the
 * first hop, which is why the guard now lives in one place.
 */
class ARedirectCannotReachInsideTheNetworkTest extends TestCase
{
    /** Runs the on_redirect callback the client is actually configured with. */
    private function followTo(string $url): void
    {
        $onRedirect = PublicHttp::redirectOptions()['allow_redirects']['on_redirect'];

        $onRedirect(
            new PsrRequest('GET', 'https://example.com/'),
            new PsrResponse(302),
            new Uri($url),
        );
    }

    public static function privateTargets(): array
    {
        return [
            'cloud metadata' => ['http://169.254.169.254/latest/meta-data/'],
            'loopback redis' => ['http://127.0.0.1:6379/'],
            'loopback by name' => ['http://localhost/admin'],
            'private range' => ['http://10.0.0.5:8500/v1/kv/?recurse'],
            'private range 192' => ['http://192.168.30.7/'],
            'ipv6 loopback' => ['http://[::1]/'],
        ];
    }

    #[DataProvider('privateTargets')]
    public function test_a_redirect_to_a_private_address_is_refused(string $url): void
    {
        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('non-public address');

        $this->followTo($url);
    }

    /** A redirect that stays outside is still followed. */
    public function test_a_redirect_to_a_public_address_is_allowed(): void
    {
        $this->followTo('https://example.com/moved');

        $this->assertTrue(true, 'a public redirect target must not throw');
    }

    /**
     * The chain cannot step out of http.
     *
     * Without this a redirect to file:///etc/passwd is a scheme change Guzzle
     * would otherwise make on request.
     */
    public function test_the_redirect_chain_is_pinned_to_http_and_https(): void
    {
        $this->assertSame(
            ['http', 'https'],
            PublicHttp::redirectOptions()['allow_redirects']['protocols'],
        );
    }

    /** And the chain is bounded, so a redirect loop is not an open connection. */
    public function test_the_redirect_chain_is_bounded(): void
    {
        $this->assertSame(5, PublicHttp::redirectOptions()['allow_redirects']['max']);
    }

    /**
     * Puppeteer gets a URL, not a guarded client, so its check is up front.
     *
     * FILTER_VALIDATE_URL was the whole gate there and it accepts every one of
     * these.
     */
    public function test_puppeteer_only_accepts_public_http_urls(): void
    {
        foreach ([
            'file:///etc/passwd',
            'http://169.254.169.254/latest/meta-data/',
            'http://10.0.0.5:8500/v1/kv/?recurse',
            'ftp://internal/backup',
        ] as $url) {
            $this->assertFalse(PublicHttp::isPublicUrl($url), "{$url} should be refused");
        }

        $this->assertTrue(PublicHttp::isPublicUrl('https://example.com/collections/chairs'));
    }

    /**
     * A name that resolves nowhere is not treated as private.
     *
     * It cannot reach anything, and refusing it would turn a DNS blip into
     * "use a public landing page URL", which the buyer has already done.
     */
    public function test_an_unresolvable_name_is_not_refused_as_private(): void
    {
        $this->assertTrue(PublicHttp::isPublicHost('shop.example.com'));
    }
}
