<?php

declare(strict_types=1);

namespace App\Tests\Unit\Auth;

use App\Domain\Auth\AuthenticatedPrincipal;
use App\Domain\Auth\Role;
use App\Domain\Auth\TokenKind;
use App\Infrastructure\Http\Middleware\RbacMiddleware;
use App\Infrastructure\Http\Middleware\TokenAuthenticationMiddleware;
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\Response;

/**
 * Direct unit coverage for the RBAC tier table from M03 §7. Integration
 * tests in AuthMatrixTest exercise the routes that exist; these target
 * the operator/admin rungs which have no live route in M03.
 */
final class RbacMiddlewareTest extends TestCase
{
    public function testAdminPrincipalSatisfiesAdminRoute(): void
    {
        self::assertSame(200, $this->dispatch(Role::Admin, $this->principal(Role::Admin)));
    }

    public function testOperatorPrincipalSatisfiesOperatorRoute(): void
    {
        self::assertSame(200, $this->dispatch(Role::Operator, $this->principal(Role::Operator)));
    }

    public function testViewerPrincipalForbiddenFromOperatorRoute(): void
    {
        self::assertSame(403, $this->dispatch(Role::Operator, $this->principal(Role::Viewer)));
    }

    public function testOperatorPrincipalForbiddenFromAdminRoute(): void
    {
        self::assertSame(403, $this->dispatch(Role::Admin, $this->principal(Role::Operator)));
    }

    public function testReporterPrincipalUnauthorizedOnAnyAdminRoute(): void
    {
        $principal = new AuthenticatedPrincipal(
            tokenKind: TokenKind::Reporter,
            userId: null,
            role: null,
            reporterId: 1,
            consumerId: null,
            tokenId: 1,
        );

        self::assertSame(401, $this->dispatch(Role::Viewer, $principal));
    }

    public function testServicePrincipalWithoutRoleForbidden(): void
    {
        // ImpersonationMiddleware would normally have populated role; if it
        // didn't (e.g. someone mounted an admin route without it), we expect
        // a defensive 403 rather than a misleading 200.
        $principal = new AuthenticatedPrincipal(
            tokenKind: TokenKind::Service,
            userId: null,
            role: null,
            reporterId: null,
            consumerId: null,
            tokenId: 1,
        );

        self::assertSame(403, $this->dispatch(Role::Viewer, $principal));
    }

    public function testMissingPrincipalReturns401(): void
    {
        $factory = new ResponseFactory();
        $middleware = RbacMiddleware::require($factory, Role::Viewer);

        $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
        $handler = new class () implements RequestHandlerInterface {
            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                return new Response();
            }
        };

        $response = $middleware->process($request, $handler);
        self::assertSame(401, $response->getStatusCode());
    }

    private function principal(Role $role): AuthenticatedPrincipal
    {
        return new AuthenticatedPrincipal(
            tokenKind: TokenKind::Admin,
            userId: 42,
            role: $role,
            reporterId: null,
            consumerId: null,
            tokenId: 7,
        );
    }

    private function dispatch(Role $required, AuthenticatedPrincipal $principal): int
    {
        $factory = new ResponseFactory();
        $middleware = RbacMiddleware::require($factory, $required);

        $request = (new ServerRequestFactory())
            ->createServerRequest('GET', '/x')
            ->withAttribute(TokenAuthenticationMiddleware::ATTR_PRINCIPAL, $principal);

        $handler = new class () implements RequestHandlerInterface {
            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                return new Response();
            }
        };

        return $middleware->process($request, $handler)->getStatusCode();
    }
}
