<?php

declare(strict_types=1);

namespace Apexl\Io\Tests\UnitTests;

use Apexl\Io\Interfaces\AbstractHook;
use Apexl\Io\Interfaces\HookInterface;
use Apexl\Io\System\Services\HookKeyResolver;
use DI\Container;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
 * Boot asks every hook in every module which keys it subscribes to, before it can know
 * whether any of them will be reached. Asked of an instance, that question cost a
 * constructed object per hook — plus everything the container had to build to make it —
 * on every request, including the requests where the hook never ran.
 *
 * The assertion that matters is therefore not "the right keys come back": it is that
 * nothing was constructed to find them. Plain TestCase with a bare container; this
 * decision needs no bootstrap.
 */
final class HookKeyResolverTest extends TestCase
{
    private function resolver(): HookKeyResolver
    {
        return new HookKeyResolver(new Container());
    }

    protected function setUp(): void
    {
        ExpensiveHook::$constructed = 0;
        LegacyHook::$constructed = 0;
    }

    #[Test]
    public function aStaticHookIsReadWithoutBeingBuilt(): void
    {
        self::assertSame(['collectMiddleware'], $this->resolver()->keys(ExpensiveHook::class));
        self::assertSame(0, ExpensiveHook::$constructed, 'Reading a class constant of a class costs nothing.');
    }

    /** The deprecated contract still works — it just pays for the answer. */
    #[Test]
    public function aLegacyHookIsConstructedToBeAsked(): void
    {
        self::assertSame(['initialiseModule'], $this->resolver()->keys(LegacyHook::class));
        self::assertSame(1, LegacyHook::$constructed);
    }

    /**
     * Modules may hand over a ready-made instance rather than a class-string, which is the
     * one case where construction has already happened and there is nothing to save.
     */
    #[Test]
    public function anInstanceIsAskedDirectly(): void
    {
        self::assertSame(['collectMiddleware'], $this->resolver()->keys(new ExpensiveHook()));
        self::assertSame(['initialiseModule'], $this->resolver()->keys(new LegacyHook()));
    }

    #[Test]
    public function somethingThatIsNotAHookIsRejected(): void
    {
        $this->expectException(InvalidArgumentException::class);

        $this->resolver()->keys(NotAHook::class);
    }
}

/** Stands in for the real thing: a hook whose construction is the cost being avoided. */
final class ExpensiveHook implements HookInterface
{
    public static int $constructed = 0;

    public function __construct()
    {
        self::$constructed++;
    }

    public static function hooks(): array
    {
        return ['collectMiddleware'];
    }

    public function __invoke(mixed $input): mixed
    {
        return $input;
    }
}

final class LegacyHook implements AbstractHook
{
    public static int $constructed = 0;

    public function __construct()
    {
        self::$constructed++;
    }

    public function hooks(): array
    {
        return ['initialiseModule'];
    }

    public function __invoke(mixed $input): mixed
    {
        return $input;
    }
}

final class NotAHook {}
