<?php

declare(strict_types=1);

namespace Apexl\Tests\Core\UnitTests;

use apexl\Io\includes\HookManager;
use apexl\Io\TestInfrastructure\UnitTest;
use DI\Container;
use PHPUnit\Framework\Attributes\Test;

final class HookManagerTest extends UnitTest
{
    private HookManager $hookManager;

    protected function setUp(): void
    {
        parent::setUp();
        $this->hookManager = new HookManager(new Container());
    }

    #[Test]
    public function registeredCallbackIsInvoked(): void
    {
        $invoked = false;
        $this->hookManager->registerCallback('hook', function () use (&$invoked): void {
            $invoked = true;
        });

        $this->hookManager->processHook('hook');

        $this->assertTrue($invoked);
    }

    #[Test]
    public function inputIsPipedThroughCallback(): void
    {
        $this->hookManager->registerCallback(
            'hook',
            fn(int $input): int => $input * 2,
        );

        $result = $this->hookManager->processHook('hook', 5);

        $this->assertSame(10, $result);
    }

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

        $result = $this->hookManager->processHook('hook', 1);

        $this->assertSame(20, $result);
    }

    #[Test]
    public function falsyCallbackReturnPreservesPreviousOutput(): void
    {
        $this->hookManager->registerCallback('hook', fn(string $input): string => 'kept');
        $this->hookManager->registerCallback('hook', fn(string $input): ?string => null);
        $this->hookManager->registerCallback('hook', fn(string $input): string => $input === 'kept' ? 'final' : 'lost');

        $result = $this->hookManager->processHook('hook', 'initial');

        $this->assertSame('final', $result);
    }

    #[Test]
    public function nullInputDoesNotPrependToArguments(): void
    {
        $received = null;
        $this->hookManager->registerCallback(
            'hook',
            function (string $first, string $second) use (&$received): string {
                $received = [$first, $second];

                return 'returned';
            },
        );

        $result = $this->hookManager->processHook('hook', null, 'a', 'b');

        $this->assertSame(['a', 'b'], $received);
        $this->assertSame('returned', $result);
    }

    #[Test]
    public function nonNullInputIsPrependedToArguments(): void
    {
        $received = null;
        $this->hookManager->registerCallback(
            'hook',
            function (int $input, string $tag) use (&$received): int {
                $received = [$input, $tag];

                return $input;
            },
        );

        $this->hookManager->processHook('hook', 7, 'extra');

        $this->assertSame([7, 'extra'], $received);
    }

    #[Test]
    public function disableShortCircuitsAllHooks(): void
    {
        $invoked = false;
        $this->hookManager->registerCallback('hook', function () use (&$invoked): void {
            $invoked = true;
        });

        $this->hookManager->disable();
        $result = $this->hookManager->processHook('hook', 'input');

        $this->assertFalse($invoked);
        $this->assertSame('input', $result);
    }

    #[Test]
    public function enableRestoresProcessing(): void
    {
        $count = 0;
        $this->hookManager->registerCallback('hook', function () use (&$count): void {
            $count++;
        });

        $this->hookManager->disable();
        $this->hookManager->processHook('hook');
        $this->hookManager->enable();
        $this->hookManager->processHook('hook');

        $this->assertSame(1, $count);
    }

    #[Test]
    public function disableHookSkipsTheSpecificCallback(): void
    {
        $kept = false;
        $skipped = false;

        $skippedCallback = function () use (&$skipped): void {
            $skipped = true;
        };
        $keptCallback = function () use (&$kept): void {
            $kept = true;
        };

        $this->hookManager->registerCallback('hook', $skippedCallback);
        $this->hookManager->registerCallback('hook', $keptCallback);

        $this->hookManager->disableHook('hook', $skippedCallback);
        $this->hookManager->processHook('hook');

        $this->assertFalse($skipped);
        $this->assertTrue($kept);
    }

    #[Test]
    public function enableHookRestoresASpecificCallback(): void
    {
        $invoked = false;
        $callback = function () use (&$invoked): void {
            $invoked = true;
        };

        $this->hookManager->registerCallback('hook', $callback);
        $this->hookManager->disableHook('hook', $callback);
        $this->hookManager->enableHook('hook', $callback);
        $this->hookManager->processHook('hook');

        $this->assertTrue($invoked);
    }

    #[Test]
    public function disableHookWithoutCallbackDisablesAllCallbacksForHook(): void
    {
        $firstInvoked = false;
        $secondInvoked = false;
        $otherInvoked = false;

        $this->hookManager->registerCallback('hook', function () use (&$firstInvoked): void {
            $firstInvoked = true;
        });
        $this->hookManager->registerCallback('hook', function () use (&$secondInvoked): void {
            $secondInvoked = true;
        });
        $this->hookManager->registerCallback('other', function () use (&$otherInvoked): void {
            $otherInvoked = true;
        });

        $this->hookManager->disableHook('hook');

        $this->hookManager->processHook('hook');
        $this->hookManager->processHook('other');

        $this->assertFalse($firstInvoked, 'first hook callback should be skipped');
        $this->assertFalse($secondInvoked, 'second hook callback should be skipped');
        $this->assertTrue($otherInvoked, 'unrelated hook should still run');
    }

    #[Test]
    public function disableAllAndEnableAllStackOnTopOfSpecificDisable(): void
    {
        $aInvoked = false;
        $bInvoked = false;

        $a = function () use (&$aInvoked): void {
            $aInvoked = true;
        };
        $b = function () use (&$bInvoked): void {
            $bInvoked = true;
        };

        $this->hookManager->registerCallback('hook', $a);
        $this->hookManager->registerCallback('hook', $b);

        // Specifically disable $a, then disable all callbacks for the hook on top.
        $this->hookManager->disableHook('hook', $a);
        $this->hookManager->disableHook('hook');

        $this->hookManager->processHook('hook');
        $this->assertFalse($aInvoked, '$a should be skipped (specific + all disabled)');
        $this->assertFalse($bInvoked, '$b should be skipped (all disabled)');

        // Lift the all-callbacks disable; $a should still be specifically disabled.
        $this->hookManager->enableHook('hook');

        $this->hookManager->processHook('hook');
        $this->assertFalse($aInvoked, '$a should remain disabled by its specific entry');
        $this->assertTrue($bInvoked, '$b should now run again');

        // Now lift the specific disable too — both should fire.
        $aInvoked = false;
        $bInvoked = false;
        $this->hookManager->enableHook('hook', $a);

        $this->hookManager->processHook('hook');
        $this->assertTrue($aInvoked, '$a should fire after its specific disable is lifted');
        $this->assertTrue($bInvoked, '$b should still fire');
    }

    #[Test]
    public function isHookDisabledReportsDisabledState(): void
    {
        $callback = static fn(): null => null;

        $this->assertFalse($this->hookManager->isHookDisabled('hook', $callback));

        $this->hookManager->disableHook('hook', $callback);
        $this->assertTrue($this->hookManager->isHookDisabled('hook', $callback));

        $this->hookManager->enableHook('hook', $callback);
        $this->assertFalse($this->hookManager->isHookDisabled('hook', $callback));
    }

    #[Test]
    public function enableHookForUnknownHookIsNoOp(): void
    {
        $callback = static fn(): null => null;

        $this->hookManager->enableHook('never-registered', $callback);

        $this->assertFalse($this->hookManager->isHookDisabled('never-registered', $callback));
    }

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

        $result = $this->hookManager->processMany(['first', 'second'], 2);

        $this->assertSame(9, $result);
    }

    #[Test]
    public function processManyWithEmptyArrayReturnsInputUnchanged(): void
    {
        $result = $this->hookManager->processMany([], 'untouched');

        $this->assertSame('untouched', $result);
    }

    #[Test]
    public function processManyFiltersFalsyHookNames(): void
    {
        $this->hookManager->registerCallback('real', fn(int $input): int => $input + 100);

        $result = $this->hookManager->processMany(['', null, 0, 'real'], 1);

        $this->assertSame(101, $result);
    }

    #[Test]
    public function variadicArgumentsAreForwardedToCallback(): void
    {
        $received = null;
        $this->hookManager->registerCallback(
            'hook',
            function (int $first, int $second, int $third) use (&$received): int {
                $received = [$first, $second, $third];

                return $first;
            },
        );

        $this->hookManager->processHook('hook', null, 1, 2, 3);

        $this->assertSame([1, 2, 3], $received);
    }

    #[Test]
    public function containerResolvesMissingArguments(): void
    {
        $container = new Container();
        $container->set(HookManagerTestGreeter::class, new HookManagerTestGreeter('hello'));
        $hookManager = new HookManager($container);

        $hookManager->registerCallback(
            'hook',
            fn(string $input, HookManagerTestGreeter $greeter): string => $greeter->greet($input),
        );

        $result = $hookManager->processHook('hook', 'world');

        $this->assertSame('hello world', $result);
    }
}

final readonly class HookManagerTestGreeter
{
    public function __construct(private string $greeting) {}

    public function greet(string $name): string
    {
        return sprintf('%s %s', $this->greeting, $name);
    }
}
