<?php

declare(strict_types=1);

use App\Framework\HttpClient\Curl\Handle;
use App\Framework\HttpClient\Curl\HandleOption;
use App\Framework\HttpClient\Curl\Info;
use App\Framework\HttpClient\Curl\MultiHandle;
use App\Framework\HttpClient\Curl\Exception\MultiHandleException;

describe('Curl\MultiHandle', function () {
    it('can be initialized empty', function () {
        $multi = new MultiHandle();

        expect($multi)->toBeInstanceOf(MultiHandle::class);
        expect($multi->count())->toBe(0);
    });

    it('can be initialized with handles', function () {
        $handle1 = new Handle('https://httpbin.org/get');
        $handle2 = new Handle('https://httpbin.org/uuid');

        $multi = new MultiHandle($handle1, $handle2);

        expect($multi)->toBeInstanceOf(MultiHandle::class);
        expect($multi->count())->toBe(2);
        expect($multi->getHandles())->toHaveCount(2);
    });

    it('can add handles', function () {
        $multi = new MultiHandle();
        $handle = new Handle('https://httpbin.org/get');

        $result = $multi->addHandle($handle);

        expect($result)->toBe($multi); // Fluent API
        expect($multi->count())->toBe(1);
    });

    it('can remove handles', function () {
        $multi = new MultiHandle();
        $handle = new Handle('https://httpbin.org/get');

        $multi->addHandle($handle);
        expect($multi->count())->toBe(1);

        $multi->removeHandle($handle);
        expect($multi->count())->toBe(0);
    });

    it('can get all attached handles', function () {
        $multi = new MultiHandle();
        $handle1 = new Handle('https://httpbin.org/get');
        $handle2 = new Handle('https://httpbin.org/uuid');

        $multi->addHandle($handle1);
        $multi->addHandle($handle2);

        $handles = $multi->getHandles();

        expect($handles)->toHaveCount(2);
        expect($handles)->toContain($handle1);
        expect($handles)->toContain($handle2);
    });

    it('can execute parallel requests with executeAll()', function () {
        $multi = new MultiHandle();

        $urls = [
            'https://httpbin.org/delay/1',
            'https://httpbin.org/uuid',
            'https://httpbin.org/get',
        ];

        $handles = [];
        foreach ($urls as $url) {
            $handle = new Handle($url);
            $handle->setOption(HandleOption::Timeout, 10);
            $handles[] = $handle;
            $multi->addHandle($handle);
        }

        expect($multi->count())->toBe(3);

        $startTime = microtime(true);
        $completed = $multi->executeAll();
        $duration = microtime(true) - $startTime;

        // Parallel execution should be faster than sequential (3+ seconds)
        expect($duration)->toBeLessThan(2.5);
        expect($completed)->toHaveCount(3);

        // Verify all requests completed successfully
        foreach ($handles as $handle) {
            expect($handle->getInfo(Info::ResponseCode))->toBe(200);
        }
    });

    it('executes requests in parallel not sequentially', function () {
        $multi = new MultiHandle();

        // Create 3 requests with 1 second delay each
        $handles = [];
        for ($i = 0; $i < 3; $i++) {
            $handle = new Handle('https://httpbin.org/delay/1');
            $handle->setOption(HandleOption::Timeout, 10);
            $handles[] = $handle;
            $multi->addHandle($handle);
        }

        $startTime = microtime(true);
        $multi->executeAll();
        $duration = microtime(true) - $startTime;

        // If sequential: ~3 seconds
        // If parallel: ~1 second
        expect($duration)->toBeLessThan(2.0);
        expect($duration)->toBeGreaterThan(0.8);
    });

    it('can handle mixed success and failure', function () {
        $multi = new MultiHandle();

        $handle1 = new Handle('https://httpbin.org/get');
        $handle1->setOption(HandleOption::Timeout, 10);

        $handle2 = new Handle('https://httpbin.org/status/404');
        $handle2->setOption(HandleOption::Timeout, 10);

        $handle3 = new Handle('https://httpbin.org/status/500');
        $handle3->setOption(HandleOption::Timeout, 10);

        $multi->addHandle($handle1);
        $multi->addHandle($handle2);
        $multi->addHandle($handle3);

        $completed = $multi->executeAll();

        expect($completed)->toHaveCount(3);

        // Check individual status codes
        expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
        expect($handle2->getInfo(Info::ResponseCode))->toBe(404);
        expect($handle3->getInfo(Info::ResponseCode))->toBe(500);
    });

    it('can execute with manual control loop', function () {
        $multi = new MultiHandle();

        $handle = new Handle('https://httpbin.org/get');
        $handle->setOption(HandleOption::Timeout, 10);
        $multi->addHandle($handle);

        $stillRunning = 0;
        $iterations = 0;

        do {
            $multi->execute($stillRunning);

            if ($stillRunning > 0) {
                $multi->select();
            }

            $iterations++;
        } while ($stillRunning > 0 && $iterations < 100);

        expect($stillRunning)->toBe(0);
        expect($handle->getInfo(Info::ResponseCode))->toBe(200);
    });

    it('can handle many parallel requests', function () {
        $multi = new MultiHandle();

        $numRequests = 10;
        $handles = [];

        for ($i = 0; $i < $numRequests; $i++) {
            $handle = new Handle('https://httpbin.org/get');
            $handle->setOption(HandleOption::Timeout, 10);
            $handles[] = $handle;
            $multi->addHandle($handle);
        }

        expect($multi->count())->toBe($numRequests);

        $startTime = microtime(true);
        $completed = $multi->executeAll();
        $duration = microtime(true) - $startTime;

        expect($completed)->toHaveCount($numRequests);

        // Should complete much faster than sequential
        expect($duration)->toBeLessThan(5.0);

        // Verify all completed
        foreach ($handles as $handle) {
            expect($handle->getInfo(Info::ResponseCode))->toBe(200);
        }
    });

    it('provides access to underlying resource', function () {
        $multi = new MultiHandle();

        $resource = $multi->getResource();

        expect($resource)->toBeInstanceOf(CurlMultiHandle::class);
    });

    it('can set multi handle options', function () {
        $multi = new MultiHandle();

        // CURLMOPT_PIPELINING enable HTTP/1.1 pipelining
        if (defined('CURLMOPT_PIPELINING')) {
            $result = $multi->setOption(CURLMOPT_PIPELINING, 1);
            expect($result)->toBe($multi); // Fluent API
        }

        expect($multi)->toBeInstanceOf(MultiHandle::class);
    });

    it('handles empty multi handle gracefully', function () {
        $multi = new MultiHandle();

        // Execute without any handles
        $completed = $multi->executeAll();

        expect($completed)->toHaveCount(0);
        expect($multi->count())->toBe(0);
    });

    it('can add and remove handles multiple times', function () {
        $multi = new MultiHandle();
        $handle1 = new Handle('https://httpbin.org/get');
        $handle2 = new Handle('https://httpbin.org/uuid');

        $multi->addHandle($handle1);
        expect($multi->count())->toBe(1);

        $multi->addHandle($handle2);
        expect($multi->count())->toBe(2);

        $multi->removeHandle($handle1);
        expect($multi->count())->toBe(1);

        $multi->removeHandle($handle2);
        expect($multi->count())->toBe(0);
    });

    it('select returns number of handles with activity', function () {
        $multi = new MultiHandle();

        $handle = new Handle('https://httpbin.org/delay/1');
        $handle->setOption(HandleOption::Timeout, 10);
        $multi->addHandle($handle);

        $stillRunning = 0;
        $multi->execute($stillRunning);

        if ($stillRunning > 0) {
            $result = $multi->select(1.0);
            // Result can be -1 (error), 0 (timeout), or >0 (activity)
            expect($result)->toBeGreaterThanOrEqual(-1);
        }

        expect($stillRunning)->toBeGreaterThanOrEqual(0);
    });

    it('getInfo returns transfer information', function () {
        $multi = new MultiHandle();

        $handle = new Handle('https://httpbin.org/get');
        $handle->setOption(HandleOption::Timeout, 10);
        $multi->addHandle($handle);

        $stillRunning = 0;
        $messagesInQueue = 0;

        do {
            $multi->execute($stillRunning);

            if ($stillRunning > 0) {
                $multi->select();
            }

            // Check for completed transfers
            while ($info = $multi->getInfo($messagesInQueue)) {
                expect($info)->toBeArray();
                expect($info)->toHaveKey('handle');
                expect($info)->toHaveKey('result');
            }
        } while ($stillRunning > 0);

        expect($stillRunning)->toBe(0);
    });

    it('can handle POST requests in parallel', function () {
        $multi = new MultiHandle();

        $handles = [];
        $data = ['name' => 'Test', 'timestamp' => time()];

        for ($i = 0; $i < 3; $i++) {
            $handle = new Handle('https://httpbin.org/post');
            $handle->setOptions([
                HandleOption::Post->value => true,
                HandleOption::PostFields->value => json_encode($data),
                HandleOption::HttpHeader->value => ['Content-Type: application/json'],
                HandleOption::Timeout->value => 10,
            ]);

            $handles[] = $handle;
            $multi->addHandle($handle);
        }

        $completed = $multi->executeAll();

        expect($completed)->toHaveCount(3);

        foreach ($handles as $handle) {
            expect($handle->getInfo(Info::ResponseCode))->toBe(200);
        }
    });

    it('automatically cleans up on destruction', function () {
        $multi = new MultiHandle();

        $handle1 = new Handle('https://httpbin.org/get');
        $handle2 = new Handle('https://httpbin.org/uuid');

        $multi->addHandle($handle1);
        $multi->addHandle($handle2);

        expect($multi->count())->toBe(2);

        unset($multi);

        // If we reach here without errors, cleanup worked
        expect(true)->toBeTrue();
    });

    it('can register onComplete callback', function () {
        $multi = new MultiHandle();
        $completedUrls = [];

        $multi->onComplete(function(Handle $handle) use (&$completedUrls) {
            $completedUrls[] = $handle->getInfo(Info::EffectiveUrl);
        });

        $handle1 = new Handle('https://httpbin.org/get');
        $handle2 = new Handle('https://httpbin.org/uuid');
        $handle1->setOption(HandleOption::Timeout, 10);
        $handle2->setOption(HandleOption::Timeout, 10);

        $multi->addHandle($handle1);
        $multi->addHandle($handle2);

        $multi->executeAll();

        expect($completedUrls)->toHaveCount(2);
        expect($completedUrls[0])->toContain('httpbin.org');
        expect($completedUrls[1])->toContain('httpbin.org');
    });

    it('can register onError callback', function () {
        $multi = new MultiHandle();
        $errors = [];

        $multi->onError(function(Handle $handle, int $errorCode) use (&$errors) {
            $errors[] = [
                'url' => $handle->getInfo(Info::EffectiveUrl),
                'code' => $errorCode
            ];
        });

        // Create a handle that will timeout
        $handle = new Handle('https://httpbin.org/delay/10');
        $handle->setOption(HandleOption::Timeout, 1); // 1 second timeout
        $multi->addHandle($handle);

        $multi->executeAll();

        expect($errors)->toHaveCount(1);
        expect($errors[0]['code'])->toBe(CURLE_OPERATION_TIMEDOUT);
    });

    it('can register onProgress callback', function () {
        $multi = new MultiHandle();
        $progressUpdates = [];

        $multi->onProgress(function(int $completed, int $total) use (&$progressUpdates) {
            $progressUpdates[] = ['completed' => $completed, 'total' => $total];
        });

        $handle1 = new Handle('https://httpbin.org/get');
        $handle2 = new Handle('https://httpbin.org/uuid');
        $handle3 = new Handle('https://httpbin.org/delay/1');

        $handle1->setOption(HandleOption::Timeout, 10);
        $handle2->setOption(HandleOption::Timeout, 10);
        $handle3->setOption(HandleOption::Timeout, 10);

        $multi->addHandle($handle1);
        $multi->addHandle($handle2);
        $multi->addHandle($handle3);

        $multi->executeAll();

        // Should have progress updates
        expect($progressUpdates)->not->toBeEmpty();
        expect($progressUpdates)->toHaveCount(3);

        // Final update should show all completed
        $finalUpdate = end($progressUpdates);
        expect($finalUpdate['completed'])->toBe(3);
        expect($finalUpdate['total'])->toBe(3);
    });

    it('can chain callback registrations', function () {
        $multi = new MultiHandle();

        $result = $multi
            ->onComplete(fn($handle) => null)
            ->onError(fn($handle, $code) => null)
            ->onProgress(fn($completed, $total) => null);

        expect($result)->toBe($multi);
    });

    it('triggers callbacks in correct order', function () {
        $multi = new MultiHandle();
        $events = [];

        $multi->onComplete(function(Handle $handle) use (&$events) {
            $events[] = 'complete';
        });

        $multi->onProgress(function(int $completed, int $total) use (&$events) {
            $events[] = 'progress';
        });

        $handle = new Handle('https://httpbin.org/get');
        $handle->setOption(HandleOption::Timeout, 10);
        $multi->addHandle($handle);

        $multi->executeAll();

        expect($events)->toContain('complete');
        expect($events)->toContain('progress');
    });
});

describe('Curl\MultiHandle edge cases', function () {
    it('handles timeout in parallel requests', function () {
        $multi = new MultiHandle();

        $handle1 = new Handle('https://httpbin.org/delay/1');
        $handle1->setOption(HandleOption::Timeout, 10);

        $handle2 = new Handle('https://httpbin.org/delay/20');
        $handle2->setOption(HandleOption::Timeout, 2);

        $multi->addHandle($handle1);
        $multi->addHandle($handle2);

        $completed = $multi->executeAll();

        // Handle1 should succeed
        expect($handle1->getInfo(Info::ResponseCode))->toBe(200);

        // Handle2 should timeout (response code 0 or error)
        $handle2Code = $handle2->getInfo(Info::ResponseCode);
        expect($handle2Code)->toBeIn([0, 28]); // 0 = timeout, 28 = operation timeout
    });

    it('can reuse multi handle for multiple batches', function () {
        $multi = new MultiHandle();

        // First batch
        $handle1 = new Handle('https://httpbin.org/get');
        $handle1->setOption(HandleOption::Timeout, 10);
        $multi->addHandle($handle1);

        $multi->executeAll();
        expect($handle1->getInfo(Info::ResponseCode))->toBe(200);

        $multi->removeHandle($handle1);

        // Second batch
        $handle2 = new Handle('https://httpbin.org/uuid');
        $handle2->setOption(HandleOption::Timeout, 10);
        $multi->addHandle($handle2);

        $multi->executeAll();
        expect($handle2->getInfo(Info::ResponseCode))->toBe(200);
    });
});
