<?php

declare(strict_types=1);

namespace App\Tests\Unit\Http;

use App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware;
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;
use Slim\Psr7\Factory\StreamFactory;

/**
 * SEC_REVIEW F69 — bound inbound request body size before Slim's
 * `BodyParsingMiddleware` reads it into memory. Two layers:
 * `Content-Length` header check (catches the well-behaved client) and
 * `getSize()` fallback (catches a stream that knows its length even
 * if the header lied).
 */
final class RequestBodySizeLimitMiddlewareTest extends TestCase
{
    public function testRequestUnderCapPasses(): void
    {
        $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
        $request = (new ServerRequestFactory())
            ->createServerRequest('POST', '/x')
            ->withBody((new StreamFactory())->createStream(str_repeat('a', 512)))
            ->withHeader('Content-Length', '512');

        $response = $mw->process($request, $this->okHandler());

        self::assertSame(200, $response->getStatusCode());
    }

    public function testRequestOverCapByContentLengthIs413(): void
    {
        $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
        $request = (new ServerRequestFactory())
            ->createServerRequest('POST', '/x')
            ->withBody((new StreamFactory())->createStream(str_repeat('a', 4096)))
            ->withHeader('Content-Length', '4096');

        $response = $mw->process($request, $this->shouldNotRunHandler());

        self::assertSame(413, $response->getStatusCode());
        $body = json_decode((string) $response->getBody(), true);
        self::assertSame('payload_too_large', $body['error'] ?? null);
    }

    public function testRequestOverCapByStreamSizeIs413(): void
    {
        // No Content-Length header (chunked-style request); fall back
        // to the stream's reported size.
        $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
        $request = (new ServerRequestFactory())
            ->createServerRequest('POST', '/x')
            ->withBody((new StreamFactory())->createStream(str_repeat('a', 4096)));
        // Strip Content-Length the factory might have set.
        $request = $request->withoutHeader('Content-Length');

        $response = $mw->process($request, $this->shouldNotRunHandler());

        self::assertSame(413, $response->getStatusCode());
    }

    public function testEmptyBodyPasses(): void
    {
        $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
        $request = (new ServerRequestFactory())->createServerRequest('GET', '/healthz');

        $response = $mw->process($request, $this->okHandler());

        self::assertSame(200, $response->getStatusCode());
    }

    public function testNonNumericContentLengthDoesNotShortCircuitButStreamSizeStillCaught(): void
    {
        // A garbage Content-Length is ignored (not a digit string), but
        // the stream-size fallback catches the actual oversize body.
        $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
        $body = (new StreamFactory())->createStream(str_repeat('a', 4096));
        $request = (new ServerRequestFactory())
            ->createServerRequest('POST', '/x')
            ->withBody($body)
            ->withHeader('Content-Length', 'garbage');

        $response = $mw->process($request, $this->shouldNotRunHandler());

        self::assertSame(413, $response->getStatusCode());
    }

    private function okHandler(): RequestHandlerInterface
    {
        return new class () implements RequestHandlerInterface {
            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                return (new ResponseFactory())->createResponse(200);
            }
        };
    }

    private function shouldNotRunHandler(): RequestHandlerInterface
    {
        return new class () implements RequestHandlerInterface {
            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                throw new \LogicException('handler must not be invoked when middleware rejects oversize');
            }
        };
    }
}
