package com.baeldung.httpclient.conn;

import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.conn.ConnectionRequest;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.util.EntityUtils;
import org.junit.Ignore;
import org.junit.Test;

public class HttpClientConnectionManagementLiveTest {

    // Example 2.1. Getting a Connection Request for a Low Level Connection (HttpClientConnection)
    @Test
    public final void whenLowLevelConnectionIsEstablished_thenNoExceptions() throws ConnectionPoolTimeoutException, InterruptedException, ExecutionException {
        try (BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager()) {
            HttpRoute route = new HttpRoute(new HttpHost("www.baeldung.com", 80));
            final ConnectionRequest connRequest = connManager.requestConnection(route, null);
            assertNotNull(connRequest.get(1000, TimeUnit.SECONDS));
        }
    }

    // Example 3.1. Setting the PoolingHttpClientConnectionManager on a HttpClient
    @Test
    public final void whenPollingConnectionManagerIsConfiguredOnHttpClient_thenNoExceptions() throws ClientProtocolException, IOException {
        PoolingHttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(poolingConnManager)
            .build();
        client.execute(new HttpGet("https://www.baeldung.com"));

        assertTrue(poolingConnManager.getTotalStats()
            .getLeased() == 1);
    }

    // Example 3.2. Using Two HttpClients to Connect to One Target Host Each
    @Test
    public final void whenTwoConnectionsForTwoRequests_thenNoExceptions() throws InterruptedException {
        HttpGet get1 = new HttpGet("https://www.baeldung.com");
        HttpGet get2 = new HttpGet("https://www.google.com");
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        CloseableHttpClient client1 = HttpClients.custom()
            .setConnectionManager(connManager)
            .build();
        CloseableHttpClient client2 = HttpClients.custom()
            .setConnectionManager(connManager)
            .build();

        MultiHttpClientConnThread thread1 = new MultiHttpClientConnThread(client1, get1);
        MultiHttpClientConnThread thread2 = new MultiHttpClientConnThread(client2, get2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        assertTrue(connManager.getTotalStats()
            .getLeased() == 0);
    }

    // Example 4.1. Increasing the Number of Connections that Can be Open and Managed Beyond the default Limits
    @Test
    public final void whenIncreasingConnectionPool_thenNoEceptions() {
        try (PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager()) {
            connManager.setMaxTotal(5);
            connManager.setDefaultMaxPerRoute(4);
            HttpHost host = new HttpHost("www.baeldung.com", 80);
            connManager.setMaxPerRoute(new HttpRoute(host), 5);
        }
    }

    // Example 4.2. Using Threads to Execute Connections
    @Test
    public final void whenExecutingSameRequestsInDifferentThreads_thenExecuteReuqest() throws InterruptedException {
        HttpGet get = new HttpGet("http://www.baeldung.com");
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(connManager)
            .build();
        MultiHttpClientConnThread thread1 = new MultiHttpClientConnThread(client, get);
        MultiHttpClientConnThread thread2 = new MultiHttpClientConnThread(client, get);
        MultiHttpClientConnThread thread3 = new MultiHttpClientConnThread(client, get);
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join();
        thread2.join();
        thread3.join();
    }

    // Example 5.1. A Custom Keep Alive Strategy
    @Test
    public final void whenCustomizingKeepAliveStrategy_thenNoExceptions() {
        final ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(final HttpResponse myResponse, final HttpContext myContext) {
                final HeaderElementIterator it = new BasicHeaderElementIterator(myResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    final HeaderElement he = it.nextElement();
                    final String param = he.getName();
                    final String value = he.getValue();
                    if ((value != null) && param.equalsIgnoreCase("timeout")) {
                        return Long.parseLong(value) * 1000;
                    }
                }
                final HttpHost target = (HttpHost) myContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
                if ("localhost".equalsIgnoreCase(target.getHostName())) {
                    return 10 * 1000;
                } else {
                    return 5 * 1000;
                }
            }

        };
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        HttpClients.custom()
            .setKeepAliveStrategy(myStrategy)
            .setConnectionManager(connManager)
            .build();
    }

    // Example 6.1. BasicHttpClientConnectionManager Connection Reuse
    @Test
    public final void givenBasicHttpClientConnManager_whenConnectionReuse_thenNoExceptions() throws IOException, HttpException, InterruptedException, ExecutionException {
        BasicHttpClientConnectionManager basicConnManager = new BasicHttpClientConnectionManager();
        HttpClientContext context = HttpClientContext.create();

        // low level
        HttpRoute route = new HttpRoute(new HttpHost("www.baeldung.com", 443));
        ConnectionRequest connRequest = basicConnManager.requestConnection(route, null);
        HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
        basicConnManager.connect(conn, route, 1000, context);
        basicConnManager.routeComplete(conn, route, context);

        HttpRequestExecutor exeRequest = new HttpRequestExecutor();
        context.setTargetHost((new HttpHost("www.baeldung.com", 80)));
        HttpGet get = new HttpGet("http://www.baeldung.com");
        exeRequest.execute(get, conn, context);

        basicConnManager.releaseConnection(conn, null, 1, TimeUnit.SECONDS);

        // high level
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(basicConnManager)
            .build();
        client.execute(get);
    }

    // Example 6.2. PoolingHttpClientConnectionManager: Re-Using Connections with Threads
    @Test
    public final void whenConnectionsNeededGreaterThanMaxTotal_thenLeaseMasTotalandReuse() throws InterruptedException {
        HttpGet get = new HttpGet("http://echo.200please.com");
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        connManager.setDefaultMaxPerRoute(5);
        connManager.setMaxTotal(5);
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(connManager)
            .build();
        MultiHttpClientConnThread[] threads = new MultiHttpClientConnThread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new MultiHttpClientConnThread(client, get, connManager);
        }
        for (MultiHttpClientConnThread thread : threads) {
            thread.start();
        }
        for (MultiHttpClientConnThread thread : threads) {
            thread.join(1000);
        }
    }

    // Example 7.1. Setting Socket Timeout to 5 Seconds
    @Test
    public final void whenConfiguringTimeOut_thenNoExceptions() {
        HttpRoute route = new HttpRoute(new HttpHost("www.baeldung.com", 80));
        try (PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager()) {
            connManager.setSocketConfig(route.getTargetHost(), SocketConfig.custom()
                .setSoTimeout(5000)
                .build());
            assertTrue(connManager.getSocketConfig(route.getTargetHost())
                .getSoTimeout() == 5000);
        }
    }

    // Example 8.1. Setting the HttpClient to Check for Stale Connections
    @Test
    public final void whenHttpClientChecksStaleConns_thenNoExceptions() {
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        HttpClients.custom()
            .setDefaultRequestConfig(RequestConfig.custom()
                .setStaleConnectionCheckEnabled(true)
                .build())
            .setConnectionManager(connManager)
            .build();
    }

    // Example 8.2. Using a Stale Connection Monitor Thread
    @Test
    public final void whenCustomizedIdleConnMonitor_thenNoExceptions() throws InterruptedException {
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        HttpClients.custom()
            .setConnectionManager(connManager)
            .build();
        IdleConnectionMonitorThread staleMonitor = new IdleConnectionMonitorThread(connManager);
        staleMonitor.start();
        staleMonitor.join(1000);
    }

    // Example 9.1. Closing Connection and Releasing Resources
    @Test(expected = IllegalStateException.class)
    public final void whenClosingConnectionsandManager_thenCloseWithNoExceptions1() throws InterruptedException, ExecutionException, IOException, HttpException {
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(connManager)
            .build();
        final HttpGet get = new HttpGet("http://google.com");
        CloseableHttpResponse response = client.execute(get);

        EntityUtils.consume(response.getEntity());
        response.close();
        client.close();
        connManager.close();
        connManager.shutdown();

        client.execute(get);

        assertTrue(response.getEntity() == null);
    }

    @Test
    // Example 3.2. TESTER VERSION
    public final void whenTwoConnectionsForTwoRequests_thenTwoConnectionsAreLeased() throws InterruptedException {
        HttpGet get1 = new HttpGet("https://www.baeldung.com");
        HttpGet get2 = new HttpGet("https://www.google.com");

        PoolingHttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();
        final CloseableHttpClient client1 = HttpClients.custom()
            .setConnectionManager(poolingConnManager)
            .build();
        final CloseableHttpClient client2 = HttpClients.custom()
            .setConnectionManager(poolingConnManager)
            .build();

        final TesterVersion_MultiHttpClientConnThread thread1 = new TesterVersion_MultiHttpClientConnThread(client1, get1, poolingConnManager);
        final TesterVersion_MultiHttpClientConnThread thread2 = new TesterVersion_MultiHttpClientConnThread(client2, get2, poolingConnManager);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join(1000);
        assertTrue(poolingConnManager.getTotalStats()
            .getLeased() == 2);
    }

    @Test
    // Example 4.2 Tester Version
    public final void whenExecutingSameRequestsInDifferentThreads_thenUseDefaultConnLimit() throws InterruptedException {
        PoolingHttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(poolingConnManager)
            .build();
        final TesterVersion_MultiHttpClientConnThread thread1 = new TesterVersion_MultiHttpClientConnThread(client, new HttpGet("http://www.google.com"), poolingConnManager);
        final TesterVersion_MultiHttpClientConnThread thread2 = new TesterVersion_MultiHttpClientConnThread(client, new HttpGet("http://www.google.com"), poolingConnManager);
        final TesterVersion_MultiHttpClientConnThread thread3 = new TesterVersion_MultiHttpClientConnThread(client, new HttpGet("http://www.google.com"), poolingConnManager);
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(10000);
        thread2.join(10000);
        thread3.join(10000);
    }

    @Test
    // 6.2 TESTER VERSION
    public final void whenConnectionsNeededGreaterThanMaxTotal_thenReuseConnections() throws InterruptedException {
        PoolingHttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();
        poolingConnManager.setDefaultMaxPerRoute(5);
        poolingConnManager.setMaxTotal(5);
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(poolingConnManager)
            .build();
        final MultiHttpClientConnThread[] threads = new MultiHttpClientConnThread[10];
        int countConnMade = 0;
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new MultiHttpClientConnThread(client, new HttpGet("http://www.baeldung.com/"), poolingConnManager);
        }
        for (final MultiHttpClientConnThread thread : threads) {
            thread.start();
        }
        for (final MultiHttpClientConnThread thread : threads) {
            thread.join(10000);
            countConnMade++;
            if (countConnMade == 0) {
                assertTrue(thread.getLeasedConn() == 5);
            }
        }
    }

    @Test
    @Ignore("Very Long Running")
    // 8.2 TESTER VERSION
    public final void whenCustomizedIdleConnMonitor_thenEliminateIdleConns() throws InterruptedException {
        PoolingHttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();
        CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(poolingConnManager)
            .build();
        final IdleConnectionMonitorThread staleMonitor = new IdleConnectionMonitorThread(poolingConnManager);
        final HttpGet get = new HttpGet("http://google.com");
        final TesterVersion_MultiHttpClientConnThread thread1 = new TesterVersion_MultiHttpClientConnThread(client, get, poolingConnManager);
        final TesterVersion_MultiHttpClientConnThread thread2 = new TesterVersion_MultiHttpClientConnThread(client, get, poolingConnManager);
        final TesterVersion_MultiHttpClientConnThread thread3 = new TesterVersion_MultiHttpClientConnThread(client, get, poolingConnManager);
        staleMonitor.start();
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        assertTrue(poolingConnManager.getTotalStats()
            .getAvailable() == 1);
        thread3.join(32000);
        assertTrue(poolingConnManager.getTotalStats()
            .getAvailable() == 0);
    }
}
