<?php

declare(strict_types=1);

namespace App\Tests\Unit\Http;

use App\Domain\Ip\InvalidCidrException;
use App\Infrastructure\Http\Middleware\InternalNetworkMiddleware;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Factory\ResponseFactory;
use Slim\Psr7\Factory\ServerRequestFactory;

/**
 * Network gate must let only loopback through by default (SEC_REVIEW F25),
 * 404 everything else, and never leak via 403 (which would tell attackers
 * the endpoint exists). The handler is stubbed to a marker response so we
 * can confirm whether the middleware short-circuited or passed through.
 *
 * RFC1918 is no longer in the default allowlist: an operator who needs
 * non-loopback sources must opt in via `INTERNAL_CIDR_ALLOWLIST`.
 */
final class InternalNetworkMiddlewareTest extends TestCase
{
    /**
     * @return iterable<string, array{string, bool}>
     */
    public static function defaultAddressProvider(): iterable
    {
        yield 'loopback v4' => ['127.0.0.1', true];
        yield 'loopback v6' => ['::1', true];
        // RFC1918 sources are no longer allowed by default — F25.
        yield 'rfc1918 10/8 rejected by default' => ['10.5.6.7', false];
        yield 'rfc1918 172.16/12 rejected by default' => ['172.16.42.1', false];
        yield 'rfc1918 172.31/12 (boundary) rejected by default' => ['172.31.255.255', false];
        yield 'just outside 172.16/12' => ['172.32.0.1', false];
        yield 'rfc1918 192.168/16 rejected by default' => ['192.168.1.1', false];
        yield 'public 1.1.1.1' => ['1.1.1.1', false];
        yield 'public v4' => ['203.0.113.4', false];
        yield 'public v6' => ['2001:db8::1', false];
        yield 'malformed' => ['not-an-ip', false];
        yield 'empty' => ['', false];
    }

    #[DataProvider('defaultAddressProvider')]
    public function testDefaultLoopbackOnlyGate(string $remoteAddr, bool $shouldPass): void
    {
        $this->assertGate(new InternalNetworkMiddleware(new ResponseFactory()), $remoteAddr, $shouldPass);
    }

    public function testNullAllowedCidrsFallsBackToLoopbackDefault(): void
    {
        $middleware = new InternalNetworkMiddleware(new ResponseFactory(), null);
        $this->assertGate($middleware, '127.0.0.1', true);
        $this->assertGate($middleware, '10.0.0.1', false);
    }

    public function testEmptyAllowedCidrsFallsBackToLoopbackDefault(): void
    {
        $middleware = new InternalNetworkMiddleware(new ResponseFactory(), []);
        $this->assertGate($middleware, '::1', true);
        $this->assertGate($middleware, '192.168.1.1', false);
    }

    public function testCustomAllowlistAdmitsConfiguredSourcesOnly(): void
    {
        // Operator opt-in: extend allowlist to a single bridge IP.
        $middleware = new InternalNetworkMiddleware(
            new ResponseFactory(),
            ['127.0.0.1/32', '::1/128', '172.20.0.5/32'],
        );
        $this->assertGate($middleware, '127.0.0.1', true);
        $this->assertGate($middleware, '::1', true);
        $this->assertGate($middleware, '172.20.0.5', true);
        // Still narrow — 172.20.0.6 is one off and rejected.
        $this->assertGate($middleware, '172.20.0.6', false);
        // The wider 10/8 block was not configured.
        $this->assertGate($middleware, '10.0.0.1', false);
    }

    public function testInvalidCidrInConstructorFailsClosed(): void
    {
        $this->expectException(\Throwable::class);
        new InternalNetworkMiddleware(new ResponseFactory(), ['not-a-cidr']);
    }

    public function testParseCidrListAcceptsMixedSeparators(): void
    {
        self::assertSame(
            ['127.0.0.1/32', '::1/128', '10.0.0.0/8'],
            InternalNetworkMiddleware::parseCidrList('127.0.0.1/32, ::1/128 10.0.0.0/8'),
        );
    }

    public function testParseCidrListReturnsEmptyForBlank(): void
    {
        self::assertSame([], InternalNetworkMiddleware::parseCidrList(''));
        self::assertSame([], InternalNetworkMiddleware::parseCidrList('   '));
    }

    public function testParseCidrListThrowsOnInvalidEntry(): void
    {
        $this->expectException(InvalidCidrException::class);
        InternalNetworkMiddleware::parseCidrList('127.0.0.1/32, garbage');
    }

    private function assertGate(InternalNetworkMiddleware $middleware, string $remoteAddr, bool $shouldPass): void
    {
        $req = (new ServerRequestFactory())->createServerRequest(
            'POST',
            '/internal/jobs/tick',
            ['REMOTE_ADDR' => $remoteAddr],
        );
        $passthrough = new class () implements RequestHandlerInterface {
            public bool $reached = false;

            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                $this->reached = true;

                return (new ResponseFactory())->createResponse(204);
            }
        };

        $response = $middleware->process($req, $passthrough);

        if ($shouldPass) {
            self::assertSame(204, $response->getStatusCode(), $remoteAddr . ' should be allowed');
            self::assertTrue($passthrough->reached);
        } else {
            self::assertSame(404, $response->getStatusCode(), $remoteAddr . ' should be denied');
            self::assertFalse($passthrough->reached, 'handler must not see disallowed sources');
        }
    }
}
