<?php

declare(strict_types=1);

namespace Tests\Unit\Framework\Async;

use App\Framework\Async\AsyncPromise;
use App\Framework\Async\FiberManager;
use App\Framework\DateTime\SystemClock;
use App\Framework\DateTime\SystemTimer;

describe('AsyncPromise', function () {
    beforeEach(function () {
        $this->clock = new SystemClock();
        $this->timer = new SystemTimer($this->clock);
        $this->fiberManager = new FiberManager($this->clock, $this->timer);
    });

    it('creates a resolved promise', function () {
        $promise = AsyncPromise::resolve('test-value', $this->fiberManager);

        expect($promise->isResolved())->toBeTrue();
        expect($promise->await())->toBe('test-value');
    });

    it('creates a rejected promise', function () {
        $exception = new \RuntimeException('test error');
        $promise = AsyncPromise::reject($exception, $this->fiberManager);

        expect($promise->isResolved())->toBeTrue();

        try {
            $promise->await();
            throw new \Exception('Should have thrown');
        } catch (\RuntimeException $e) {
            expect($e->getMessage())->toBe('test error');
        }
    });

    it('creates a promise from callable', function () {
        $promise = AsyncPromise::create(
            fn() => 'computed-value',
            $this->fiberManager
        );

        // Give fiber time to execute
        usleep(10000); // 10ms

        expect($promise->await())->toBe('computed-value');
    });

    it('chains then callbacks', function () {
        $promise = AsyncPromise::resolve(5, $this->fiberManager)
            ->then(fn($value) => $value * 2)
            ->then(fn($value) => $value + 3);

        usleep(10000); // Give time for chaining

        expect($promise->await())->toBe(13);
    });

    it('catches exceptions', function () {
        $promise = AsyncPromise::create(
            fn() => throw new \RuntimeException('error'),
            $this->fiberManager
        )->catch(fn($e) => 'caught: ' . $e->getMessage());

        usleep(10000);

        expect($promise->await())->toBe('caught: error');
    });

    it('executes finally callback', function () {
        $finallyCalled = false;

        $promise = AsyncPromise::resolve('value', $this->fiberManager)
            ->finally(function () use (&$finallyCalled) {
                $finallyCalled = true;
            });

        usleep(10000);
        $promise->await();

        expect($finallyCalled)->toBeTrue();
    });

    it('waits for all promises', function () {
        $promise1 = AsyncPromise::resolve(1, $this->fiberManager);
        $promise2 = AsyncPromise::resolve(2, $this->fiberManager);
        $promise3 = AsyncPromise::resolve(3, $this->fiberManager);

        $allPromise = AsyncPromise::all(
            [$promise1, $promise2, $promise3],
            $this->fiberManager
        );

        usleep(10000);

        $results = $allPromise->await();
        expect($results)->toBe([1, 2, 3]);
    });

    it('races promises and returns first', function () {
        $slowPromise = AsyncPromise::create(function () {
            usleep(100000); // 100ms
            return 'slow';
        }, $this->fiberManager);

        $fastPromise = AsyncPromise::create(function () {
            // No sleep - immediate return
            return 'fast';
        }, $this->fiberManager);

        $racePromise = AsyncPromise::race(
            [$slowPromise, $fastPromise],
            $this->fiberManager
        );

        usleep(20000); // Wait a bit for fast to complete

        $result = $racePromise->await();
        // Either fast wins or first to complete
        expect(['fast', 'slow'])->toContain($result);
    });

    it('handles promise rejection in all', function () {
        $promise1 = AsyncPromise::resolve(1, $this->fiberManager);
        $promise2 = AsyncPromise::reject(
            new \RuntimeException('failed'),
            $this->fiberManager
        );

        $allPromise = AsyncPromise::all(
            [$promise1, $promise2],
            $this->fiberManager
        );

        usleep(10000);

        try {
            $allPromise->await();
            throw new \Exception('Should have thrown');
        } catch (\RuntimeException $e) {
            expect($e->getMessage())->toBe('failed');
        }
    });

    it('provides promise statistics', function () {
        $promise = AsyncPromise::create(
            fn() => 'value',
            $this->fiberManager
        );

        usleep(10000);

        $stats = $promise->getStats();

        expect($stats)->toHaveKey('resolved');
        expect($stats)->toHaveKey('has_result');
        expect($stats)->toHaveKey('has_exception');
    });

    it('chains multiple then callbacks with different return types', function () {
        $promise = AsyncPromise::resolve(10, $this->fiberManager)
            ->then(fn($x) => $x * 2)           // 20
            ->then(fn($x) => (string) $x)      // "20"
            ->then(fn($x) => $x . '!')         // "20!"
            ->then(fn($x) => strlen($x));      // 3

        usleep(10000);

        expect($promise->await())->toBe(3);
    });

    it('handles nested promises', function () {
        $innerPromise = AsyncPromise::create(
            fn() => 'inner',
            $this->fiberManager
        );

        $outerPromise = AsyncPromise::create(
            fn() => $innerPromise->await() . '-outer',
            $this->fiberManager
        );

        usleep(20000);

        expect($outerPromise->await())->toBe('inner-outer');
    });
});
