<?php

namespace React\Tests\Socket;

use React\EventLoop\Factory;
use React\Socket\TcpServer;
use React\Socket\ConnectionInterface;
use React\Socket\TcpConnector;
use Clue\React\Block;

class FunctionalTcpServerTest extends TestCase
{
    public function testEmitsConnectionForNewConnection()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $server->on('connection', $this->expectCallableOnce());

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);
    }

    public function testEmitsNoConnectionForNewConnectionWhenPaused()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $server->on('connection', $this->expectCallableNever());
        $server->pause();

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);
    }

    public function testEmitsConnectionForNewConnectionWhenResumedAfterPause()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $server->on('connection', $this->expectCallableOnce());
        $server->pause();
        $server->resume();

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);
    }

    public function testEmitsConnectionWithRemoteIp()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $peer = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$peer) {
            $peer = $conn->getRemoteAddress();
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertContains('127.0.0.1:', $peer);
    }

    public function testEmitsConnectionWithLocalIp()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $local = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$local) {
            $local = $conn->getLocalAddress();
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertContains('127.0.0.1:', $local);
        $this->assertEquals($server->getAddress(), $local);
    }

    public function testEmitsConnectionWithLocalIpDespiteListeningOnAll()
    {
        $loop = Factory::create();

        $server = new TcpServer('0.0.0.0:0', $loop);
        $local = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$local) {
            $local = $conn->getLocalAddress();
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertContains('127.0.0.1:', $local);
    }

    public function testEmitsConnectionWithRemoteIpAfterConnectionIsClosedByPeer()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $peer = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$peer) {
            $conn->on('close', function () use ($conn, &$peer) {
                $peer = $conn->getRemoteAddress();
            });
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $client = Block\await($promise, $loop, 0.1);
        $client->end();

        Block\sleep(0.1, $loop);

        $this->assertContains('127.0.0.1:', $peer);
    }

    public function testEmitsConnectionWithRemoteNullAddressAfterConnectionIsClosedLocally()
    {
        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $peer = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$peer) {
            $conn->close();
            $peer = $conn->getRemoteAddress();
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertNull($peer);
    }

    public function testEmitsConnectionEvenIfConnectionIsCancelled()
    {
        if (PHP_OS !== 'Linux') {
            $this->markTestSkipped('Linux only (OS is ' . PHP_OS . ')');
        }

        $loop = Factory::create();

        $server = new TcpServer(0, $loop);
        $server->on('connection', $this->expectCallableOnce());

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());
        $promise->cancel();

        $promise->then(null, $this->expectCallableOnce());

        Block\sleep(0.1, $loop);
    }

    public function testEmitsConnectionForNewIpv6Connection()
    {
        $loop = Factory::create();

        try {
            $server = new TcpServer('[::1]:0', $loop);
        } catch (\RuntimeException $e) {
            $this->markTestSkipped('Unable to start IPv6 server socket (not available on your platform?)');
        }

        $server->on('connection', $this->expectCallableOnce());

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);
    }

    public function testEmitsConnectionWithRemoteIpv6()
    {
        $loop = Factory::create();

        try {
            $server = new TcpServer('[::1]:0', $loop);
        } catch (\RuntimeException $e) {
            $this->markTestSkipped('Unable to start IPv6 server socket (not available on your platform?)');
        }

        $peer = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$peer) {
            $peer = $conn->getRemoteAddress();
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertContains('[::1]:', $peer);
    }

    public function testEmitsConnectionWithLocalIpv6()
    {
        $loop = Factory::create();

        try {
            $server = new TcpServer('[::1]:0', $loop);
        } catch (\RuntimeException $e) {
            $this->markTestSkipped('Unable to start IPv6 server socket (not available on your platform?)');
        }

        $local = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$local) {
            $local = $conn->getLocalAddress();
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertContains('[::1]:', $local);
        $this->assertEquals($server->getAddress(), $local);
    }

    public function testEmitsConnectionWithInheritedContextOptions()
    {
        if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.13', '<')) {
            // https://3v4l.org/hB4Tc
            $this->markTestSkipped('Not supported on legacy HHVM < 3.13');
        }

        $loop = Factory::create();

        $server = new TcpServer(0, $loop, array(
            'backlog' => 4
        ));

        $all = null;
        $server->on('connection', function (ConnectionInterface $conn) use (&$all) {
            $all = stream_context_get_options($conn->stream);
        });

        $connector = new TcpConnector($loop);
        $promise = $connector->connect($server->getAddress());

        $promise->then($this->expectCallableOnce());

        Block\sleep(0.1, $loop);

        $this->assertEquals(array('socket' => array('backlog' => 4)), $all);
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testFailsToListenOnInvalidUri()
    {
        $loop = Factory::create();

        new TcpServer('///', $loop);
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testFailsToListenOnUriWithoutPort()
    {
        $loop = Factory::create();

        new TcpServer('127.0.0.1', $loop);
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testFailsToListenOnUriWithWrongScheme()
    {
        $loop = Factory::create();

        new TcpServer('udp://127.0.0.1:0', $loop);
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testFailsToListenOnUriWIthHostname()
    {
        $loop = Factory::create();

        new TcpServer('localhost:8080', $loop);
    }
}