<?php

namespace React\Tests\HttpClient;

use React\HttpClient\Request;
use React\HttpClient\RequestData;
use React\Stream\Stream;
use React\Promise\FulfilledPromise;
use React\Promise\RejectedPromise;
use React\Promise;
use React\Promise\Deferred;

class RequestTest extends TestCase
{
    private $connector;
    private $stream;

    public function setUp()
    {
        $this->stream = $this->getMockBuilder('React\Stream\Stream')
            ->disableOriginalConstructor()
            ->getMock();

        $this->connector = $this->getMockBuilder('React\SocketClient\ConnectorInterface')
            ->getMock();

        $this->response = $this->getMockBuilder('React\HttpClient\Response')
            ->disableOriginalConstructor()
            ->getMock();
    }

    /** @test */
    public function requestShouldBindToStreamEventsAndUseconnector()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $this->stream
            ->expects($this->at(0))
            ->method('on')
            ->with('drain', $this->identicalTo(array($request, 'handleDrain')));
        $this->stream
            ->expects($this->at(1))
            ->method('on')
            ->with('data', $this->identicalTo(array($request, 'handleData')));
        $this->stream
            ->expects($this->at(2))
            ->method('on')
            ->with('end', $this->identicalTo(array($request, 'handleEnd')));
        $this->stream
            ->expects($this->at(3))
            ->method('on')
            ->with('error', $this->identicalTo(array($request, 'handleError')));
        $this->stream
            ->expects($this->at(5))
            ->method('removeListener')
            ->with('drain', $this->identicalTo(array($request, 'handleDrain')));
        $this->stream
            ->expects($this->at(6))
            ->method('removeListener')
            ->with('data', $this->identicalTo(array($request, 'handleData')));
        $this->stream
            ->expects($this->at(7))
            ->method('removeListener')
            ->with('end', $this->identicalTo(array($request, 'handleEnd')));
        $this->stream
            ->expects($this->at(8))
            ->method('removeListener')
            ->with('error', $this->identicalTo(array($request, 'handleError')));

        $response = $this->response;

        $this->stream->expects($this->once())
            ->method('emit')
            ->with('data', $this->identicalTo(array('body')));

        $response->expects($this->at(0))
            ->method('on')
            ->with('end', $this->anything())
            ->will($this->returnCallback(function ($event, $cb) use (&$endCallback) {
                $endCallback = $cb;
            }));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
            ->method('__invoke')
            ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain'))
            ->will($this->returnValue($response));

        $request->setResponseFactory($factory);

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with($response);

        $request->on('response', $handler);
        $request->on('close', $this->expectCallableNever());

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                null,
                $this->isInstanceof('React\HttpClient\Response'),
                $this->isInstanceof('React\HttpClient\Request')
            );

        $request->on('end', $handler);
        $request->end();

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("\r\nbody");

        $this->assertNotNull($endCallback);
        call_user_func($endCallback);
    }

    /** @test */
    public function requestShouldEmitErrorIfConnectionFails()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->rejectedConnectionMock();

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                $this->isInstanceOf('RuntimeException'),
                $this->isInstanceOf('React\HttpClient\Request')
            );

        $request->on('error', $handler);

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                $this->isInstanceOf('RuntimeException'),
                null,
                $this->isInstanceOf('React\HttpClient\Request')
            );

        $request->on('end', $handler);
        $request->on('close', $this->expectCallableNever());

        $request->end();
    }

    /** @test */
    public function requestShouldEmitErrorIfConnectionEndsBeforeResponseIsParsed()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                $this->isInstanceOf('RuntimeException'),
                $this->isInstanceOf('React\HttpClient\Request')
            );

        $request->on('error', $handler);

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                $this->isInstanceOf('RuntimeException'),
                null,
                $this->isInstanceOf('React\HttpClient\Request')
            );

        $request->on('end', $handler);
        $request->on('close', $this->expectCallableNever());

        $request->end();
        $request->handleEnd();
    }

    /** @test */
    public function requestShouldEmitErrorIfConnectionEmitsError()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                $this->isInstanceOf('Exception'),
                $this->isInstanceOf('React\HttpClient\Request')
            );

        $request->on('error', $handler);

        $handler = $this->createCallableMock();
        $handler->expects($this->once())
            ->method('__invoke')
            ->with(
                $this->isInstanceOf('Exception'),
                null,
                $this->isInstanceOf('React\HttpClient\Request')
            );

        $request->on('end', $handler);
        $request->on('close', $this->expectCallableNever());

        $request->end();
        $request->handleError(new \Exception('test'));
    }

    /**
     * @test
     * @expectedException Exception
     * @expectedExceptionMessage something failed
     */
    public function requestDoesNotHideErrors()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->rejectedConnectionMock();

        $request->on('error', function () {
            throw new \Exception('something failed');
        });

        $request->end();
    }

    /** @test */
    public function postRequestShouldSendAPostRequest()
    {
        $requestData = new RequestData('POST', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $this->stream
            ->expects($this->at(4))
            ->method('write')
            ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#"));
        $this->stream
            ->expects($this->at(5))
            ->method('write')
            ->with($this->identicalTo("some post data"));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
            ->method('__invoke')
            ->will($this->returnValue($this->response));

        $request->setResponseFactory($factory);
        $request->end('some post data');

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("\r\nbody");
    }

    /** @test */
    public function writeWithAPostRequestShouldSendToTheStream()
    {
        $requestData = new RequestData('POST', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $this->stream
            ->expects($this->at(4))
            ->method('write')
            ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#"));
        $this->stream
            ->expects($this->at(5))
            ->method('write')
            ->with($this->identicalTo("some"));
        $this->stream
            ->expects($this->at(6))
            ->method('write')
            ->with($this->identicalTo("post"));
        $this->stream
            ->expects($this->at(7))
            ->method('write')
            ->with($this->identicalTo("data"));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
            ->method('__invoke')
            ->will($this->returnValue($this->response));

        $request->setResponseFactory($factory);

        $request->write("some");
        $request->write("post");
        $request->end("data");

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("\r\nbody");
    }

    /** @test */
    public function writeWithAPostRequestShouldSendBodyAfterHeadersAndEmitDrainEvent()
    {
        $requestData = new RequestData('POST', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $resolveConnection = $this->successfulAsyncConnectionMock();

        $this->stream
            ->expects($this->at(4))
            ->method('write')
            ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#"));
        $this->stream
            ->expects($this->at(5))
            ->method('write')
            ->with($this->identicalTo("some"));
        $this->stream
            ->expects($this->at(6))
            ->method('write')
            ->with($this->identicalTo("post"));
        $this->stream
            ->expects($this->at(7))
            ->method('write')
            ->with($this->identicalTo("data"));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
            ->method('__invoke')
            ->will($this->returnValue($this->response));

        $request->setResponseFactory($factory);

        $this->assertFalse($request->write("some"));
        $this->assertFalse($request->write("post"));

        $request->once('drain', function () use ($request) {
            $request->write("data");
            $request->end();
        });

        $resolveConnection();

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("\r\nbody");
    }

    /** @test */
    public function pipeShouldPipeDataIntoTheRequestBody()
    {
        $requestData = new RequestData('POST', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $this->stream
            ->expects($this->at(4))
            ->method('write')
            ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#"));
        $this->stream
            ->expects($this->at(5))
            ->method('write')
            ->with($this->identicalTo("some"));
        $this->stream
            ->expects($this->at(6))
            ->method('write')
            ->with($this->identicalTo("post"));
        $this->stream
            ->expects($this->at(7))
            ->method('write')
            ->with($this->identicalTo("data"));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
            ->method('__invoke')
            ->will($this->returnValue($this->response));

        $loop = $this
            ->getMockBuilder('React\EventLoop\LoopInterface')
            ->getMock();

        $request->setResponseFactory($factory);

        $stream = fopen('php://memory', 'r+');
        $stream = new Stream($stream, $loop);

        $stream->pipe($request);
        $stream->emit('data', array('some'));
        $stream->emit('data', array('post'));
        $stream->emit('data', array('data'));

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("\r\nbody");
    }

    /**
     * @test
     * @expectedException InvalidArgumentException
     * @expectedExceptionMessage $data must be null or scalar
     */
    public function endShouldOnlyAcceptScalars()
    {
        $requestData = new RequestData('POST', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $request->end(array());
    }

    /** @test */
    public function requestShouldRelayErrorEventsFromResponse()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $response = $this->response;

        $response->expects($this->at(0))
            ->method('on')
            ->with('end', $this->anything());
        $response->expects($this->at(1))
            ->method('on')
            ->with('error', $this->anything())
            ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) {
                $errorCallback = $cb;
            }));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
            ->method('__invoke')
            ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain'))
            ->will($this->returnValue($response));

        $request->setResponseFactory($factory);
        $request->end();

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("\r\nbody");

        $this->assertNotNull($errorCallback);
        call_user_func($errorCallback, new \Exception('test'));
    }

    /** @test */
    public function requestShouldRemoveAllListenerAfterClosed()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $request->on('end', function () {});
        $this->assertCount(1, $request->listeners('end'));

        $request->close();
        $this->assertCount(0, $request->listeners('end'));
    }

    private function successfulConnectionMock()
    {
        call_user_func($this->successfulAsyncConnectionMock());
    }

    private function successfulAsyncConnectionMock()
    {
        $deferred = new Deferred();

        $this->connector
            ->expects($this->once())
            ->method('create')
            ->with('www.example.com', 80)
            ->will($this->returnValue($deferred->promise()));

        return function () use ($deferred) {
            $deferred->resolve($this->stream);
        };
    }

    private function rejectedConnectionMock()
    {
        $this->connector
            ->expects($this->once())
            ->method('create')
            ->with('www.example.com', 80)
            ->will($this->returnValue(new RejectedPromise(new \RuntimeException())));
    }

    /** @test */
    public function multivalueHeader()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $response = $this->response;

        $response->expects($this->at(0))
        ->method('on')
        ->with('end', $this->anything());
        $response->expects($this->at(1))
        ->method('on')
        ->with('error', $this->anything())
        ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) {
            $errorCallback = $cb;
        }));

        $factory = $this->createCallableMock();
        $factory->expects($this->once())
        ->method('__invoke')
        ->with('HTTP', '1.0', '200', 'OK', array('Content-Type' => 'text/plain', 'X-Xss-Protection' => '1; mode=block', 'Cache-Control' => 'public, must-revalidate, max-age=0'))
        ->will($this->returnValue($response));

        $request->setResponseFactory($factory);
        $request->end();

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Content-Type: text/plain\r\n");
        $request->handleData("X-Xss-Protection:1; mode=block\r\n");
        $request->handleData("Cache-Control:public, must-revalidate, max-age=0\r\n");
        $request->handleData("\r\nbody");

        $this->assertNotNull($errorCallback);
        call_user_func($errorCallback, new \Exception('test'));
    }

    /** @test */
    public function chunkedStreamDecoder()
    {
        $requestData = new RequestData('GET', 'http://www.example.com');
        $request = new Request($this->connector, $requestData);

        $this->successfulConnectionMock();

        $request->end();

        $this->stream->expects($this->once())
            ->method('emit')
            ->with('data', ["1\r\nb\r"]);

        $request->handleData("HTTP/1.0 200 OK\r\n");
        $request->handleData("Transfer-Encoding: chunked\r\n");
        $request->handleData("\r\n1\r\nb\r");
        $request->handleData("\n3\t\nody\r\n0\t\n\r\n");

    }
}
