<?php

namespace GuzzleHttp\Tests\Adapter\Curl;

require_once __DIR__ . '/AbstractCurl.php';

use GuzzleHttp\Adapter\Curl\MultiAdapter;
use GuzzleHttp\Adapter\Transaction;
use GuzzleHttp\Client;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\NoSeekStream;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Tests\Server;

/**
 * @covers GuzzleHttp\Adapter\Curl\MultiAdapter
 */
class MultiAdapterTest extends AbstractCurl
{
    protected function getAdapter($factory = null, $options = [])
    {
        return new MultiAdapter($factory ?: new MessageFactory(), $options);
    }

    public function testSendsSingleRequest()
    {
        Server::flush();
        Server::enqueue("HTTP/1.1 200 OK\r\nFoo: bar\r\nContent-Length: 0\r\n\r\n");
        $t = new Transaction(new Client(), new Request('GET', Server::$url));
        $a = new MultiAdapter(new MessageFactory());
        $response = $a->send($t);
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('bar', $response->getHeader('Foo'));
    }

    public function testCanSetSelectTimeout()
    {
        $current = isset($_SERVER[MultiAdapter::ENV_SELECT_TIMEOUT])
            ? $_SERVER[MultiAdapter::ENV_SELECT_TIMEOUT]: null;
        unset($_SERVER[MultiAdapter::ENV_SELECT_TIMEOUT]);
        $a = new MultiAdapter(new MessageFactory());
        $this->assertEquals(1, $this->readAttribute($a, 'selectTimeout'));
        $a = new MultiAdapter(new MessageFactory(), ['select_timeout' => 10]);
        $this->assertEquals(10, $this->readAttribute($a, 'selectTimeout'));
        $_SERVER[MultiAdapter::ENV_SELECT_TIMEOUT] = 2;
        $a = new MultiAdapter(new MessageFactory());
        $this->assertEquals(2, $this->readAttribute($a, 'selectTimeout'));
        $_SERVER[MultiAdapter::ENV_SELECT_TIMEOUT] = $current;
    }

    public function testCanSetMaxHandles()
    {
        $a = new MultiAdapter(new MessageFactory());
        $this->assertEquals(3, $this->readAttribute($a, 'maxHandles'));
        $a = new MultiAdapter(new MessageFactory(), ['max_handles' => 10]);
        $this->assertEquals(10, $this->readAttribute($a, 'maxHandles'));
    }

    /**
     * @expectedException \GuzzleHttp\Exception\AdapterException
     * @expectedExceptionMessage cURL error -2:
     */
    public function testChecksCurlMultiResult()
    {
        MultiAdapter::throwMultiError(-2);
    }

    public function testChecksForCurlException()
    {
        $mh = curl_multi_init();
        $request = new Request('GET', 'http://httbin.org');
        $transaction = $this->getMockBuilder('GuzzleHttp\Adapter\Transaction')
            ->setMethods(['getRequest'])
            ->disableOriginalConstructor()
            ->getMock();
        $transaction->expects($this->exactly(2))
            ->method('getRequest')
            ->will($this->returnValue($request));
        $context = $this->getMockBuilder('GuzzleHttp\Adapter\Curl\BatchContext')
            ->setMethods(['throwsExceptions'])
            ->setConstructorArgs([$mh, true])
            ->getMock();
        $context->expects($this->once())
            ->method('throwsExceptions')
            ->will($this->returnValue(true));
        $a = new MultiAdapter(new MessageFactory());
        $r = new \ReflectionMethod($a, 'isCurlException');
        $r->setAccessible(true);
        try {
            $r->invoke($a, $transaction, ['result' => -10], $context, []);
            curl_multi_close($mh);
            $this->fail('Did not throw');
        } catch (RequestException $e) {
            curl_multi_close($mh);
            $this->assertSame($request, $e->getRequest());
            $this->assertContains('[curl] (#-10) ', $e->getMessage());
            $this->assertContains($request->getUrl(), $e->getMessage());
        }
    }

    public function testSendsParallelRequestsFromQueue()
    {
        $c = new Client();
        Server::flush();
        Server::enqueue([
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
        ]);
        $transactions = [
            new Transaction($c, new Request('GET', Server::$url)),
            new Transaction($c, new Request('PUT', Server::$url)),
            new Transaction($c, new Request('HEAD', Server::$url)),
            new Transaction($c, new Request('GET', Server::$url))
        ];
        $a = new MultiAdapter(new MessageFactory());
        $a->sendAll(new \ArrayIterator($transactions), 2);
        foreach ($transactions as $t) {
            $response = $t->getResponse();
            $this->assertNotNull($response);
            $this->assertEquals(200, $response->getStatusCode());
        }
    }

    public function testCreatesAndReleasesHandlesWhenNeeded()
    {
        $a = new MultiAdapter(new MessageFactory());
        $c = new Client([
            'adapter'  => $a,
            'base_url' => Server::$url
        ]);

        Server::flush();
        Server::enqueue([
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
            "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
        ]);

        $ef = function (ErrorEvent $e) { throw $e->getException(); };

        $request1 = $c->createRequest('GET', '/');
        $request1->getEmitter()->on('headers', function () use ($a, $c, $ef) {
            $a->send(new Transaction($c, $c->createRequest('GET', '/', [
                'events' => [
                    'headers' => function () use ($a, $c, $ef) {
                        $r = $c->createRequest('GET', '/', [
                            'events' => ['error' => ['fn' => $ef, 'priority' => 9999]]
                        ]);
                        $r->getEmitter()->once('headers', function () use ($a, $c, $r) {
                            $a->send(new Transaction($c, $r));
                        });
                        $a->send(new Transaction($c, $r));
                        // Now, reuse an existing handle
                        $a->send(new Transaction($c, $r));
                        },
                    'error' => ['fn' => $ef, 'priority' => 9999]
                ]
            ])));
        });

        $request1->getEmitter()->on('error', $ef);

        $transactions = [
            new Transaction($c, $request1),
            new Transaction($c, $c->createRequest('PUT')),
            new Transaction($c, $c->createRequest('HEAD'))
        ];

        $a->sendAll(new \ArrayIterator($transactions), 2);

        foreach ($transactions as $index => $t) {
            $response = $t->getResponse();
            $this->assertInstanceOf(
                'GuzzleHttp\\Message\\ResponseInterface',
                $response,
                'Transaction at index ' . $index . ' did not populate response'
            );
            $this->assertEquals(200, $response->getStatusCode());
        }
    }

    public function testThrowsAndReleasesWhenErrorDuringCompleteEvent()
    {
        Server::flush();
        Server::enqueue("HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n");
        $request = new Request('GET', Server::$url);
        $request->getEmitter()->on('complete', function (CompleteEvent $e) {
            throw new RequestException('foo', $e->getRequest());
        });
        $t = new Transaction(new Client(), $request);
        $a = new MultiAdapter(new MessageFactory());
        try {
            $a->send($t);
            $this->fail('Did not throw');
        } catch (RequestException $e) {
            $this->assertSame($request, $e->getRequest());
        }
    }

    public function testEnsuresResponseWasSetForGet()
    {
        $client = new Client();
        $request = $client->createRequest('GET', Server::$url);
        $response = new Response(200, []);
        $er = null;

        $request->getEmitter()->on(
            'error',
            function (ErrorEvent $e) use (&$er, $response) {
                $er = $e;
            }
        );

        $transaction = $this->getMockBuilder('GuzzleHttp\Adapter\Transaction')
            ->setMethods(['getResponse', 'setResponse'])
            ->setConstructorArgs([$client, $request])
            ->getMock();
        $transaction->expects($this->any())->method('setResponse');
        $transaction->expects($this->any())
            ->method('getResponse')
            ->will($this->returnCallback(function () use ($response) {
                $caller = debug_backtrace()[6]['function'];
                return $caller == 'addHandle' ||
                    $caller == 'validateResponseWasSet'
                    ? null
                    : $response;
            }));

        $a = new MultiAdapter(new MessageFactory());
        Server::flush();
        Server::enqueue(["HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"]);
        $a->sendAll(new \ArrayIterator([$transaction]), 10);
        $this->assertNotNull($er);

        $this->assertContains(
            'No response was received',
            $er->getException()->getMessage()
        );
    }

    private function runConnectionTest(
        $queue,
        $stream,
        $msg,
        $statusCode = null
    ) {
        $obj = new \stdClass();
        $er = null;
        $client = new Client();
        $request = $client->createRequest('PUT', Server::$url, [
            'body' => $stream
        ]);

        $request->getEmitter()->on(
            'error',
            function (ErrorEvent $e) use (&$er) {
                $er = $e;
            }
        );

        $transaction = $this->getMockBuilder('GuzzleHttp\Adapter\Transaction')
            ->setMethods(['getResponse', 'setResponse'])
            ->setConstructorArgs([$client, $request])
            ->getMock();

        $transaction->expects($this->any())
            ->method('setResponse')
            ->will($this->returnCallback(function ($r) use (&$obj) {
                $obj->res = $r;
            }));

        $transaction->expects($this->any())
            ->method('getResponse')
            ->will($this->returnCallback(function () use ($obj, &$called) {
                $caller = debug_backtrace()[6]['function'];
                if ($caller == 'addHandle') {
                    return null;
                } elseif ($caller == 'validateResponseWasSet') {
                    return ++$called == 2 ? $obj->res : null;
                } else {
                    return $obj->res;
                }
            }));

        $a = new MultiAdapter(new MessageFactory());
        Server::flush();
        Server::enqueue($queue);
        $a->sendAll(new \ArrayIterator([$transaction]), 10);

        if ($msg) {
            $this->assertNotNull($er);
            $this->assertContains($msg, $er->getException()->getMessage());
        } else {
            $this->assertEquals(
                $statusCode,
                $transaction->getResponse()->getStatusCode()
            );
        }
    }

    public function testThrowsWhenTheBodyCannotBeRewound()
    {
        $this->runConnectionTest(
            ["HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"],
            new NoSeekStream(Stream::factory('foo')),
            'attempting to rewind the request body failed'
        );
    }

    public function testRetriesRewindableStreamsWhenClosedConnectionErrors()
    {
        $this->runConnectionTest(
            [
                "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
                "HTTP/1.1 201 OK\r\nContent-Length: 0\r\n\r\n",
            ],
            Stream::factory('foo'),
            false,
            201
        );
    }

    public function testThrowsImmediatelyWhenInstructed()
    {
        Server::flush();
        Server::enqueue(["HTTP/1.1 501\r\nContent-Length: 0\r\n\r\n"]);
        $c = new Client(['base_url' => Server::$url]);
        $request = $c->createRequest('GET', '/');
        $request->getEmitter()->on('error', function (ErrorEvent $e) {
            $e->throwImmediately(true);
        });
        $transactions = [new Transaction($c, $request)];
        $a = new MultiAdapter(new MessageFactory());
        try {
            $a->sendAll(new \ArrayIterator($transactions), 1);
            $this->fail('Did not throw');
        } catch (RequestException $e) {
            $this->assertSame($request, $e->getRequest());
        }
    }

    public function testRewindsStreamOnComplete()
    {
        Server::flush();
        Server::enqueue("HTTP/1.1 200 OK\r\nFoo: bar\r\nContent-Length: 4\r\n\r\ntest");
        $t = new Transaction(new Client(), new Request('GET', Server::$url));
        $a = new MultiAdapter(new MessageFactory());
        $response = $a->send($t);
        $this->assertEquals('test', $response->getBody()->read(4));
    }
}
