/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.gobblin.tunnel;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpForward;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;


/**
 * Tests for HTTP traffic through Tunnel via an HTTP proxy.
 *
 * @author navteniev@linkedin.com
 */
@Test(enabled=false, groups = { "gobblin.tunnel", "disabledOnCI" })
public class TunnelTest {

  private ClientAndServer mockServer;
  int PORT = 0;

  @BeforeClass
  public void startProxy()
      throws IOException {
    mockServer = ClientAndServer.startClientAndServer(0);
    PORT = mockServer.getPort();
  }

  @AfterClass
  public void stopProxy() {
    mockServer.stop();
  }

  @AfterMethod
  public void reset(){
    mockServer.reset();
  }

  @Test (enabled=false)
  public void mustBuildTunnelAndStartAcceptingConnections()
      throws Exception {
    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);

    try {
      int tunnelPort = tunnel.getPort();
      assertTrue(tunnelPort > 0);
    } finally {
      tunnel.close();
    }
  }

  @Test (enabled=false)
  public void mustHandleClientDisconnectingWithoutClosingTunnel()
      throws Exception {
    mockExample();
    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);

    try {
      int tunnelPort = tunnel.getPort();
      SocketChannel client = SocketChannel.open();

      client.connect(new InetSocketAddress("localhost", tunnelPort));
      client.write(ByteBuffer.wrap("GET / HTTP/1.1%nUser-Agent: GobblinTunnel%nConnection:keep - alive %n%n".getBytes()));
      client.close();

      assertNotNull(fetchContent(tunnelPort));
    } finally {
      tunnel.close();
    }
  }

  @Test (enabled=false)
  public void mustHandleConnectionToExternalResource()
      throws Exception {

    mockExample();
    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);

    try {
      String content = fetchContent(tunnel.getPort());

      assertNotNull(content);
    } finally {
      tunnel.close();
    }
  }

  @Test (enabled=false)
  public void mustHandleMultipleConnections()
      throws Exception {
    mockExample();
    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);
    int clients = 5;

    final CountDownLatch startSignal = new CountDownLatch(1);
    final CountDownLatch doneSignal = new CountDownLatch(clients);

    ExecutorService executor = Executors.newFixedThreadPool(clients);
    try {
      final int tunnelPort = tunnel.getPort();

      List<Future<String>> results = new ArrayList<Future<String>>();

      for (int i = 0; i < clients; i++) {
        Future<String> result = executor.submit(new Callable<String>() {
          @Override
          public String call()
              throws Exception {
            startSignal.await();

            try {
              return fetchContent(tunnelPort);
            } finally {
              doneSignal.countDown();
            }
          }
        });

        results.add(result);
      }

      startSignal.countDown();
      doneSignal.await();

      for (Future<String> result : results) {
        assertNotNull(result.get());
      }
    } finally {
      tunnel.close();
    }
  }

  @Test(enabled=false, expectedExceptions = SocketException.class)
  public void mustRefuseConnectionWhenProxyIsUnreachable()
      throws Exception {

    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", 1);

    try {
      int tunnelPort = tunnel.getPort();

      fetchContent(tunnelPort);
    } finally {
      tunnel.close();
    }
  }

  @Test(enabled=false, expectedExceptions = SocketException.class)
  public void mustRefuseConnectionWhenProxyRefuses() throws Exception{
    mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("www.us.apache.org:80"))
        .respond(HttpResponse.response().withStatusCode(403));

    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);

    try {
      int tunnelPort = tunnel.getPort();

      fetchContent(tunnelPort);
    } finally {
      tunnel.close();
    }
  }

  @Test(enabled=false, expectedExceptions = SocketException.class)
  public void mustRefuseConnectionWhenProxyTimesOut() throws Exception{
    mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("www.us.apache.org:80"))
        .respond(HttpResponse.response().withDelay(TimeUnit.SECONDS,2).withStatusCode(200));

    Tunnel tunnel = Tunnel.build("example.org", 80, "localhost", PORT);

    try {
      int tunnelPort = tunnel.getPort();

      fetchContent(tunnelPort);
    } finally {
      tunnel.close();
    }
  }

  @Test(enabled = false)
  public void mustDownloadLargeFiles()
      throws Exception {

    mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("www.us.apache.org:80"))
        .respond(HttpResponse.response().withStatusCode(200));
    mockServer.when(HttpRequest.request().withMethod("GET")
        .withPath("/dist//httpcomponents/httpclient/binary/httpcomponents-client-4.5.1-bin.tar.gz"))
        .forward(HttpForward.forward().withHost("www.us.apache.org").withPort(80));

    Tunnel tunnel = Tunnel.build("www.us.apache.org", 80, "localhost", PORT);
    try {
      IOUtils.copyLarge((InputStream) new URL("http://localhost:" + tunnel.getPort()
              + "/dist//httpcomponents/httpclient/binary/httpcomponents-client-4.5.1-bin.tar.gz")
              .getContent(new Class[]{InputStream.class}),
          new FileOutputStream(File.createTempFile("httpcomponents-client-4.5.1-bin", "tar.gz")));
    } finally {
      tunnel.close();
    }
  }

  private String fetchContent(int tunnelPort)
      throws IOException {
    InputStream content = (InputStream) new URL(String.format("http://localhost:%s/", tunnelPort)).openConnection()
        .getContent(new Class[]{InputStream.class});
    return IOUtils.toString(content);
  }

  private void mockExample()
      throws IOException {
    mockServer.when(HttpRequest.request().withMethod("CONNECT").withPath("example.org:80"))
        .respond(HttpResponse.response().withStatusCode(200));
    mockServer.when(HttpRequest.request().withMethod("GET").withPath("/"))
        .respond(HttpResponse.response(IOUtils.toString(getClass().getResourceAsStream("/example.org.html"))));
  }

}
