<?php

declare(strict_types=1);

namespace Apexl\Io\Tests\FeatureTests;

use function Apexl\Io\container;
use Apexl\Io\Routing\DeclaresRoutePermission;
use Apexl\Io\Routing\IoRoute;
use Apexl\Io\System\Services\RouteDebugHelper;
use Apexl\Io\TestInfrastructure\FeatureTest;
use PHPUnit\Framework\Attributes\Test;

use Slim\Interfaces\RouteInterface;

/**
 * The application's routes, seen the way the menu and `routes:dump` see them.
 *
 * A permission is the middleware that enforces it. That is only workable because routes are
 * IoRoute, which records what is attached to it — Slim's own Route keeps middleware as a
 * chain of anonymous handlers over private state, so the same permission on a plain route
 * would be enforced and unreadable.
 *
 * Which makes the binding a load-bearing thing worth asserting: if the container ever stops
 * handing out IoRouteCollector, nothing throws. Routes still register, requests are still
 * refused, and the menu quietly stops hiding pages the caller cannot reach.
 */
final class RoutePermissionsTest extends FeatureTest
{
    /** @return list<RouteInterface> */
    private function routes(): array
    {
        return array_values(self::$application->getRouteCollector()->getRoutes());
    }

    #[Test]
    public function everyRouteCanBeAskedWhatIsAttachedToIt(): void
    {
        $routes = $this->routes();

        self::assertNotEmpty($routes, 'The application registered no routes.');

        $plain = array_filter($routes, static fn($route): bool => !$route instanceof IoRoute);

        self::assertSame(
            [],
            array_map(static fn($route): string => (string) $route->getName(), $plain),
            'A route Slim built itself cannot be asked what gates it.',
        );
    }

    /**
     * Slim deprecated route arguments in 4.14.1; they hold strings only, and they are merged
     * into `getArguments()`, which io-validation spreads into the data a request is validated
     * against. Metadata there becomes phantom fields on every validated request.
     */
    #[Test]
    public function noRouteCarriesMetadataAsASlimArgument(): void
    {
        $offenders = [];

        foreach ($this->routes() as $route) {
            $leaked = array_intersect(
                array_keys($route->getArguments()),
                ['permission', 'entity', 'moduleName'],
            );

            if ($leaked !== []) {
                $offenders[(string) $route->getName()] = $leaked;
            }
        }

        self::assertSame([], $offenders, 'Route arguments are for path placeholders only.');
    }

    /** The application protects routes, and the protection can be read back off them. */
    #[Test]
    public function protectedRoutesReportWhatTheyRequire(): void
    {
        $permissions = [];

        foreach ($this->routes() as $route) {
            if (!$route instanceof IoRoute) {
                continue;
            }

            $declared = $route->getMiddleware()->find(DeclaresRoutePermission::class)?->getPermission();

            if ($declared !== null) {
                $permissions[(string) $route->getName()] = $declared;
            }
        }

        self::assertNotEmpty($permissions, 'The application protects no routes at all.');

        foreach ($permissions as $name => $permission) {
            self::assertTrue(
                is_object($permission) || is_array($permission),
                sprintf('Route "%s" should report the permission object it was given.', $name),
            );
        }
    }

    /** `routes:dump` groups by the module that registered the route, read off the route. */
    #[Test]
    public function theRouteDumpGroupsByTheRoutesOwnModule(): void
    {
        $grouped = \Apexl\Io\container()->get(RouteDebugHelper::class)->routes();

        self::assertNotEmpty($grouped);

        foreach ($grouped as $module => $rows) {
            foreach ($rows as $row) {
                // A route need not be named, and an unnamed one cannot be looked up by name.
                if (($row['name'] ?? null) === null || $row['name'] === '') {
                    continue;
                }

                $route = self::$application->getRouteCollector()->getNamedRoute((string) $row['name']);

                self::assertSame(
                    $module === 'Core' ? null : $module,
                    $route instanceof IoRoute ? $route->getModule() : null,
                    sprintf('Route "%s" is grouped under "%s".', $row['name'], $module),
                );
            }
        }
    }
}
