<?php

declare(strict_types=1);

namespace Apexl\Io\Tests\FeatureTests;

use Apexl\Io\Routing\DeclaresRoutePermission;
use Apexl\Io\Routing\IoRoute;
use Apexl\Io\Routing\IoRouteGroup;
use Apexl\Io\TestInfrastructure\FeatureTest;
use PHPUnit\Framework\Attributes\Test;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteCollectorProxy;

/**
 * A permission on a group has to be as readable as one on a route.
 *
 * Slim attaches group middleware by handing it straight to the route's dispatcher — never
 * through `Route::add()` — and not until the route is first handled. So the obvious thing to
 * write, a group carrying the permission for everything inside it, would be enforced
 * correctly and invisible to the menu: pages offered, then refused with a 403. That is worse
 * than not supporting groups at all, because it looks right.
 */
final class RouteGroupPermissionsTest extends FeatureTest
{
    private function route(string $name): IoRoute
    {
        $route = self::$application->getRouteCollector()->getNamedRoute($name);
        self::assertInstanceOf(IoRoute::class, $route);

        return $route;
    }

    /** @return list<string> */
    private function gates(string $name): array
    {
        return array_map(
            static fn(DeclaresRoutePermission $gate): string => $gate->getPermission()->name,
            $this->route($name)->getMiddleware()->findAll(DeclaresRoutePermission::class),
        );
    }

    #[Test]
    public function aPermissionOnAGroupIsReadableFromTheRoutesInside(): void
    {
        self::$application->group('/test-group-perm', function (RouteCollectorProxy $group): void {
            $group->get('/thing', fn() => null)->setName('test.group.gated');
        })->add(new TestGate(TestGatePermission::Outer));

        self::assertSame(['Outer'], $this->gates('test.group.gated'));
    }

    /**
     * Both apply at dispatch, so both must be visible — a reader taking only the first would
     * let through a route its own permission refuses.
     */
    #[Test]
    public function aGroupAndItsRouteBothGate(): void
    {
        self::$application->group('/test-group-both', function (RouteCollectorProxy $group): void {
            $group->get('/thing', fn() => null)->setName('test.group.both')
                ->add(new TestGate(TestGatePermission::Inner));
        })->add(new TestGate(TestGatePermission::Outer));

        self::assertSame(['Outer', 'Inner'], $this->gates('test.group.both'));
    }

    /** Reported outermost first, which is the order they run in. */
    #[Test]
    public function nestedGroupsReportInExecutionOrder(): void
    {
        self::$application->group('/test-group-outer', function (RouteCollectorProxy $outer): void {
            $outer->group('/inner', function (RouteCollectorProxy $inner): void {
                $inner->get('/thing', fn() => null)->setName('test.group.nested')
                    ->add(new TestGate(TestGatePermission::Route));
            })->add(new TestGate(TestGatePermission::Inner));
        })->add(new TestGate(TestGatePermission::Outer));

        self::assertSame(['Outer', 'Inner', 'Route'], $this->gates('test.group.nested'));
    }

    /**
     * Middleware is attached to a group after its closure has already registered the routes
     * inside it, so a route that copied its groups' middleware when it was built would see
     * an empty list. Reading late is the only thing that works.
     */
    #[Test]
    public function middlewareAddedAfterTheGroupClosureIsStillSeen(): void
    {
        $group = self::$application->group('/test-group-late', function (RouteCollectorProxy $g): void {
            $g->get('/thing', fn() => null)->setName('test.group.late');
        });

        self::assertSame([], $this->gates('test.group.late'));

        $group->add(new TestGate(TestGatePermission::Outer));

        self::assertSame(['Outer'], $this->gates('test.group.late'));
    }

    #[Test]
    public function theContainerBuildsOurGroup(): void
    {
        $group = self::$application->group('/test-group-class', function (RouteCollectorProxy $g): void {
            $g->get('/thing', fn() => null)->setName('test.group.class');
        });

        self::assertInstanceOf(IoRouteGroup::class, $group);
    }
}

enum TestGatePermission
{
    case Outer;
    case Inner;
    case Route;
}

final readonly class TestGate implements DeclaresRoutePermission
{
    public function __construct(private TestGatePermission $permission) {}

    public function getPermission(): object|array
    {
        return $this->permission;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        return $handler->handle($request);
    }
}
