//
//  ========================================================================
//  Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.client.http;

import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.client.AbstractHttpClientServerTest;
import org.eclipse.jetty.client.ConnectionPool;
import org.eclipse.jetty.client.DuplexConnectionPool;
import org.eclipse.jetty.client.EmptyServerHandler;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
{
    public HttpDestinationOverHTTPTest(SslContextFactory sslContextFactory)
    {
        super(sslContextFactory);
    }

    @Before
    public void init() throws Exception
    {
        start(new EmptyServerHandler());
    }

    @Test
    public void test_FirstAcquire_WithEmptyQueue() throws Exception
    {
        HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()));
        destination.start();
        DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
        Connection connection = connectionPool.acquire();
        if (connection == null)
        {
            // There are no queued requests, so the newly created connection will be idle
            connection = timedPoll(connectionPool.getIdleConnections(), 5, TimeUnit.SECONDS);
        }
        Assert.assertNotNull(connection);
    }

    @Test
    public void test_SecondAcquire_AfterFirstAcquire_WithEmptyQueue_ReturnsSameConnection() throws Exception
    {
        HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()));
        destination.start();
        DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
        Connection connection1 = connectionPool.acquire();
        if (connection1 == null)
        {
            // There are no queued requests, so the newly created connection will be idle
            long start = System.nanoTime();
            while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
            {
                TimeUnit.MILLISECONDS.sleep(50);
                connection1 = connectionPool.getIdleConnections().peek();
            }
            Assert.assertNotNull(connection1);

            Connection connection2 = connectionPool.acquire();
            Assert.assertSame(connection1, connection2);
        }
    }

    @Test
    public void test_SecondAcquire_ConcurrentWithFirstAcquire_WithEmptyQueue_CreatesTwoConnections() throws Exception
    {
        final CountDownLatch idleLatch = new CountDownLatch(1);
        final CountDownLatch latch = new CountDownLatch(1);
        HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()))
        {
            @Override
            protected ConnectionPool newConnectionPool(HttpClient client)
            {
                return new DuplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this)
                {
                    @Override
                    protected void onCreated(Connection connection)
                    {
                        try
                        {
                            idleLatch.countDown();
                            latch.await(5, TimeUnit.SECONDS);
                            super.onCreated(connection);
                        }
                        catch (InterruptedException x)
                        {
                            x.printStackTrace();
                        }
                    }
                };
            }
        };
        destination.start();
        DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
        Connection connection1 = connectionPool.acquire();

        // Make sure we entered idleCreated().
        Assert.assertTrue(idleLatch.await(5, TimeUnit.SECONDS));

        // There are no available existing connections, so acquire()
        // returns null because we delayed idleCreated() above
        Assert.assertNull(connection1);

        // Second attempt also returns null because we delayed idleCreated() above.
        Connection connection2 = connectionPool.acquire();
        Assert.assertNull(connection2);

        latch.countDown();

        // There must be 2 idle connections.
        Queue<Connection> idleConnections = connectionPool.getIdleConnections();
        Connection connection = timedPoll(idleConnections, 5, TimeUnit.SECONDS);
        Assert.assertNotNull(connection);
        connection = timedPoll(idleConnections, 5, TimeUnit.SECONDS);
        Assert.assertNotNull(connection);
    }

    @Test
    public void test_Acquire_Process_Release_Acquire_ReturnsSameConnection() throws Exception
    {
        HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()));
        destination.start();
        DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
        HttpConnectionOverHTTP connection1 = (HttpConnectionOverHTTP)connectionPool.acquire();

        long start = System.nanoTime();
        while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
        {
            TimeUnit.MILLISECONDS.sleep(50);
            connection1 = (HttpConnectionOverHTTP)connectionPool.getIdleConnections().peek();
        }
        Assert.assertNotNull(connection1);

        // Acquire the connection to make it active
        Assert.assertSame(connection1, connectionPool.acquire());

        destination.process(connection1);
        destination.release(connection1);

        Connection connection2 = connectionPool.acquire();
        Assert.assertSame(connection1, connection2);
    }

    @Test
    public void test_IdleConnection_IdleTimeout() throws Exception
    {
        long idleTimeout = 1000;
        client.setIdleTimeout(idleTimeout);

        HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()));
        destination.start();
        DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
        Connection connection1 = connectionPool.acquire();
        if (connection1 == null)
        {
            // There are no queued requests, so the newly created connection will be idle
            long start = System.nanoTime();
            while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
            {
                TimeUnit.MILLISECONDS.sleep(50);
                connection1 = connectionPool.getIdleConnections().peek();
            }
            Assert.assertNotNull(connection1);

            TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);

            connection1 = connectionPool.getIdleConnections().poll();
            Assert.assertNull(connection1);
        }
    }

    @Test
    public void test_Request_Failed_If_MaxRequestsQueuedPerDestination_Exceeded() throws Exception
    {
        int maxQueued = 1;
        client.setMaxRequestsQueuedPerDestination(maxQueued);
        client.setMaxConnectionsPerDestination(1);

        // Make one request to open the connection and be sure everything is setup properly
        ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
                .scheme(scheme)
                .send();
        Assert.assertEquals(200, response.getStatus());

        // Send another request that is sent immediately
        final CountDownLatch successLatch = new CountDownLatch(1);
        final CountDownLatch failureLatch = new CountDownLatch(1);
        client.newRequest("localhost", connector.getLocalPort())
                .scheme(scheme)
                .path("/one")
                .onRequestQueued(request ->
                {
                    // This request exceeds the maximum queued, should fail
                    client.newRequest("localhost", connector.getLocalPort())
                            .scheme(scheme)
                            .path("/two")
                            .send(result ->
                            {
                                Assert.assertTrue(result.isFailed());
                                Assert.assertThat(result.getRequestFailure(), Matchers.instanceOf(RejectedExecutionException.class));
                                failureLatch.countDown();
                            });
                })
                .send(result ->
                {
                    if (result.isSucceeded())
                        successLatch.countDown();
                });

        Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
        Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
    }

    @Test
    public void testDestinationIsRemoved() throws Exception
    {
        String host = "localhost";
        int port = connector.getLocalPort();
        Destination destinationBefore = client.getDestination(scheme, host, port);

        ContentResponse response = client.newRequest(host, port)
                .scheme(scheme)
                .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString())
                .send();

        Assert.assertEquals(200, response.getStatus());

        Destination destinationAfter = client.getDestination(scheme, host, port);
        Assert.assertSame(destinationBefore, destinationAfter);

        client.setRemoveIdleDestinations(true);

        response = client.newRequest(host, port)
                .scheme(scheme)
                .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString())
                .send();

        Assert.assertEquals(200, response.getStatus());

        destinationAfter = client.getDestination(scheme, host, port);
        Assert.assertNotSame(destinationBefore, destinationAfter);
    }

    private Connection timedPoll(Queue<Connection> connections, long time, TimeUnit unit) throws InterruptedException
    {
        long start = System.nanoTime();
        while (unit.toNanos(time) > System.nanoTime() - start)
        {
            Connection connection = connections.poll();
            if (connection != null)
                return connection;
            TimeUnit.MILLISECONDS.sleep(5);
        }
        return connections.poll();
    }
}
