<?php

declare(strict_types=1);

namespace Apexl\Tests\Core\UnitTests;

use apexl\Io\includes\Entity;
use apexl\Io\includes\HookManager;
use DI\Container;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionProperty;

/**
 * Regression cover for {@see Entity::callHooks()}, which used to hand every hook
 * the ORIGINAL input instead of the accumulated result — silently discarding
 * every hook's work but the last. `store()` calls it with
 * [BEFORE_FIRST_STORE, BEFORE_STORE], so io-user's created_by hook was dropped
 * on the floor from 20 Jan 2026 until this was fixed.
 *
 * Plain TestCase (not the UnitTest base): these assertions need no container
 * bootstrap, and Entity is built without its constructor so no DB is touched.
 */
final class EntityCallHooksTest extends TestCase
{
    private HookManager $hookManager;
    private Entity $entity;

    protected function setUp(): void
    {
        parent::setUp();

        $this->hookManager = new HookManager(new Container());

        // Entity's constructor reaches for the container, the Vault and the
        // table schema. None of that is relevant to hook dispatch.
        $this->entity = new ReflectionClass(Entity::class)->newInstanceWithoutConstructor();
        $property = new ReflectionProperty(Entity::class, 'hookManager');
        $property->setValue($this->entity, $this->hookManager);
    }

    private function callHooks(array $hooks, mixed $input, mixed ...$args): mixed
    {
        $method = new ReflectionClass(Entity::class)->getMethod('callHooks');

        return $method->invoke($this->entity, $hooks, $input, ...$args);
    }

    #[Test]
    public function accumulatedResultIsThreadedThroughEveryHookName(): void
    {
        $this->hookManager->registerCallback('first', fn(int $input): int => $input + 1);
        $this->hookManager->registerCallback('second', fn(int $input): int => $input * 3);

        // Pre-fix this returned 6: 'second' was handed the original 2, not 3.
        $this->assertSame(9, $this->callHooks(['first', 'second'], 2));
    }

    #[Test]
    public function accumulatedResultIsThreadedFromEntitySpecificToGeneric(): void
    {
        $this->hookManager->registerCallback(
            Entity::entityHook('hook', Entity::class),
            fn(int $input): int => $input + 1,
        );
        $this->hookManager->registerCallback('hook', fn(int $input): int => $input * 3);

        $this->assertSame(9, $this->callHooks(['hook'], 2));
    }

    #[Test]
    public function dispatchOrderIsEntitySpecificThenGenericForEachHook(): void
    {
        $order = [];
        $record = function (string $name) use (&$order): callable {
            return function (string $input) use ($name, &$order): string {
                $order[] = $name;

                return $input;
            };
        };

        foreach (['first', 'second'] as $hook) {
            $this->hookManager->registerCallback(
                Entity::entityHook($hook, Entity::class),
                $record($hook . '.entity'),
            );
            $this->hookManager->registerCallback($hook, $record($hook . '.generic'));
        }

        $this->callHooks(['first', 'second'], 'input');

        $this->assertSame(
            ['first.entity', 'first.generic', 'second.entity', 'second.generic'],
            $order,
        );
    }

    #[Test]
    public function nullHookNamesAreSkipped(): void
    {
        // store() passes `$inserting ? HOOK__BEFORE_FIRST_STORE : null`.
        $this->hookManager->registerCallback('real', fn(int $input): int => $input + 100);

        $this->assertSame(101, $this->callHooks([null, 'real'], 1));
    }

    #[Test]
    public function voidHookDoesNotNullTheAccumulator(): void
    {
        $this->hookManager->registerCallback('first', fn(int $input): int => $input + 1);
        $this->hookManager->registerCallback('second', function (int $input): void {});
        $this->hookManager->registerCallback('third', fn(int $input): int => $input * 3);

        $this->assertSame(9, $this->callHooks(['first', 'second', 'third'], 2));
    }

    #[Test]
    public function variadicArgumentsReachEveryHook(): void
    {
        $received = [];
        $capture = function (string $input, string $tag) use (&$received): string {
            $received[] = $tag;

            return $input;
        };

        $this->hookManager->registerCallback(Entity::entityHook('hook', Entity::class), $capture);
        $this->hookManager->registerCallback('hook', $capture);

        $this->callHooks(['hook'], 'input', 'the-tag');

        $this->assertSame(['the-tag', 'the-tag'], $received);
    }
}
