<?php

declare(strict_types=1);

namespace App\Tests\Unit\Http;

use App\Domain\Auth\AuthenticatedPrincipal;
use App\Domain\Auth\TokenKind;
use App\Domain\Time\FixedClock;
use App\Infrastructure\Http\Middleware\RateLimitMiddleware;
use App\Infrastructure\Http\Middleware\TokenAuthenticationMiddleware;
use App\Infrastructure\Http\RateLimiter;
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;

/**
 * SEC_REVIEW F27. Two invariants:
 *
 * 1. With a principal: bucket key is `token:<tokenId>`, no IP draw.
 * 2. Without a principal: bucket key falls back to `ip:<REMOTE_ADDR>`
 *    so 401 paths and pre-auth flooding are throttled instead of
 *    bypassing the limiter.
 *
 * The `token:` and `ip:` namespaces draw on independent buckets — see
 * {@see RateLimiterTest::testTokenAndIpNamespacesDoNotShareABucket}.
 */
final class RateLimitMiddlewareTest extends TestCase
{
    public function testAuthenticatedRequestKeysOnTokenId(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        // capacity=2, refill=1/s. Token 42 should drain in two requests.
        $limiter = new RateLimiter($clock, 1.0, 2.0);
        $middleware = new RateLimitMiddleware($limiter, new ResponseFactory());

        $principal = new AuthenticatedPrincipal(
            tokenKind: TokenKind::Reporter,
            userId: null,
            role: null,
            reporterId: 1,
            consumerId: null,
            tokenId: 42,
        );

        $request = $this->request('1.2.3.4')
            ->withAttribute(TokenAuthenticationMiddleware::ATTR_PRINCIPAL, $principal);

        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(429, $middleware->process($request, $this->okHandler())->getStatusCode());

        // Confirm the bucket actually was `token:42` — a different IP-only
        // request still has a full ip-bucket and must succeed.
        $unauth = $this->request('1.2.3.4');
        self::assertSame(200, $middleware->process($unauth, $this->okHandler())->getStatusCode());
    }

    public function testUnauthenticatedRequestKeysOnRemoteAddr(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        $limiter = new RateLimiter($clock, 1.0, 2.0);
        $middleware = new RateLimitMiddleware($limiter, new ResponseFactory());

        $request = $this->request('1.2.3.4');

        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());

        // Third request from same IP, still no principal — bucket is drained.
        $third = $middleware->process($request, $this->okHandler());
        self::assertSame(429, $third->getStatusCode());
        self::assertSame('1', $third->getHeaderLine('Retry-After'));
        self::assertSame('application/json', $third->getHeaderLine('Content-Type'));
        self::assertStringContainsString('rate_limited', (string) $third->getBody());
    }

    public function testDifferentRemoteIpsHaveIndependentBuckets(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        $limiter = new RateLimiter($clock, 1.0, 2.0);
        $middleware = new RateLimitMiddleware($limiter, new ResponseFactory());

        // Drain 1.2.3.4.
        $a = $this->request('1.2.3.4');
        $middleware->process($a, $this->okHandler());
        $middleware->process($a, $this->okHandler());
        self::assertSame(429, $middleware->process($a, $this->okHandler())->getStatusCode());

        // 5.6.7.8 is unaffected.
        $b = $this->request('5.6.7.8');
        self::assertSame(200, $middleware->process($b, $this->okHandler())->getStatusCode());
        self::assertSame(200, $middleware->process($b, $this->okHandler())->getStatusCode());
    }

    public function testMissingRemoteAddrFallsBackToUnknownBucket(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        $limiter = new RateLimiter($clock, 1.0, 2.0);
        $middleware = new RateLimitMiddleware($limiter, new ResponseFactory());

        $request = (new ServerRequestFactory())->createServerRequest('GET', 'https://api.test/x');

        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(429, $middleware->process($request, $this->okHandler())->getStatusCode());
    }

    public function testEmptyRemoteAddrFallsBackToUnknownBucket(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        $limiter = new RateLimiter($clock, 1.0, 2.0);
        $middleware = new RateLimitMiddleware($limiter, new ResponseFactory());

        $request = $this->request('');

        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(200, $middleware->process($request, $this->okHandler())->getStatusCode());
        self::assertSame(429, $middleware->process($request, $this->okHandler())->getStatusCode());
    }

    private function request(string $remoteAddr): ServerRequestInterface
    {
        return (new ServerRequestFactory())
            ->createServerRequest('GET', 'https://api.test/x', ['REMOTE_ADDR' => $remoteAddr]);
    }

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