package org.jboss.resteasy.test.client.other;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
import org.jboss.resteasy.client.jaxrs.engines.URLConnectionEngine;
import org.jboss.resteasy.test.client.other.resource.ApacheHttpClient4Resource;
import org.jboss.resteasy.test.client.other.resource.ApacheHttpClient4ResourceImpl;
import org.jboss.resteasy.util.HttpResponseCodes;
import org.jboss.resteasy.utils.PortProviderUtil;
import org.jboss.resteasy.utils.TestUtil;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import javax.ws.rs.NotFoundException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @tpSubChapter Resteasy-client
 * @tpChapter Integration tests
 * @tpTestCaseDetails Test connection cleanup for ApacheHttpClient4Engine and URLConnectionEngine
 * @tpSince RESTEasy 3.0.16
 */
@RunWith(Arquillian.class)
@RunAsClient
public class ApacheHttpClient4Test {

   protected static final Logger logger = LogManager.getLogger(ApacheHttpClient4Test.class.getName());

   private Class engine1 = ApacheHttpClient4Engine.class;
   private Class engine2 = URLConnectionEngine.class;

   private AtomicLong counter = new AtomicLong();

   @Deployment
   public static Archive<?> deploySimpleResource() {
      WebArchive war = TestUtil.prepareArchive(ApacheHttpClient4Test.class.getSimpleName());
      war.addClass(ApacheHttpClient4Resource.class);
      return TestUtil.finishContainerPrepare(war, null, ApacheHttpClient4ResourceImpl.class);
   }

   /**
    * @tpTestDetails Create 3 threads and test GC with correct request. System.gc is called directly. Proxy is not used.
    * @tpSince RESTEasy 3.0.16
    */
   @Test
   public void testConnectionCleanupGCBase() throws Exception {
      testConnectionCleanupGC(engine1);
      testConnectionCleanupGC(engine2);
   }

   public void testConnectionCleanupGC(Class engine) throws Exception {
      final Client client = createEngine(engine);
      counter.set(0);

      Thread[] threads = new Thread[3];

      for (int i = 0; i < 3; i++) {
         threads[i] = new Thread() {
            @Override
            public void run() {
               for (int j = 0; j < 10; j++) {
                  runit(client, false);
                  System.gc();
               }
            }
         };
      }

      for (int i = 0; i < 3; i++) {
         threads[i].start();
      }
      for (int i = 0; i < 3; i++) {
         threads[i].join();
      }

      Assert.assertEquals("Wrong count of requests", 30L, counter.get());
   }

   /**
    * @tpTestDetails Create 3 threads and test GC with correct request. System.gc is not called directly. Proxy is not used.
    * @tpSince RESTEasy 3.0.16
    */
   @Test
   public void testConnectionCleanupAuto() throws Exception {
      testConnectionCleanupAuto(engine1);
      testConnectionCleanupAuto(engine2);
   }

   public void testConnectionCleanupAuto(Class engine) throws Exception {
      final Client client = createEngine(engine);
      counter.set(0);

      Thread[] threads = new Thread[3];

      for (int i = 0; i < 3; i++) {
         threads[i] = new Thread() {
            @Override
            public void run() {
               for (int j = 0; j < 10; j++) {
                  runit(client, true);
               }
            }
         };
      }

      for (int i = 0; i < 3; i++) {
         threads[i].start();
      }
      for (int i = 0; i < 3; i++) {
         threads[i].join();
      }

      Assert.assertEquals("Wrong count of requests", 30L, counter.get());
   }

   /**
    * @tpTestDetails Create 3 threads and test GC with correct request. System.gc is not called directly. Proxy is used.
    * @tpSince RESTEasy 3.0.16
    */
   @Test
   public void testConnectionCleanupProxy() throws Exception {
      testConnectionCleanupProxy(engine1);
      testConnectionCleanupProxy(engine2);
   }

   public void testConnectionCleanupProxy(Class engine) throws Exception {
      final ResteasyClient client = createEngine(engine);
      final ApacheHttpClient4Resource proxy = client.target(PortProviderUtil.generateBaseUrl(ApacheHttpClient4Test.class.getSimpleName())).proxy(ApacheHttpClient4Resource.class);
      counter.set(0);

      Thread[] threads = new Thread[3];


      for (int i = 0; i < 3; i++) {
         threads[i] = new Thread() {
            @Override
            public void run() {
               for (int j = 0; j < 10; j++) {
                  String str = proxy.get();
                  Assert.assertEquals("Wrong response", "hello world", str);
                  counter.incrementAndGet();
               }
            }
         };
      }

      for (int i = 0; i < 3; i++) {
         threads[i].start();
      }
      for (int i = 0; i < 3; i++) {
         threads[i].join();
      }

      Assert.assertEquals("Wrong count of requests", 30L, counter.get());
   }

   /**
    * @tpTestDetails Create 3 threads and test GC with incorrect request. System.gc is called directly. Proxy is not used.
    * @tpSince RESTEasy 3.0.16
    */
   @Test
   public void testConnectionCleanupErrorGC() throws Exception {
      testConnectionCleanupErrorGC(engine1);
      testConnectionCleanupErrorGC(engine2);
   }

   /**
    * This is regression test for RESTEASY-1273
    */
   public void testConnectionCleanupErrorGC(Class engine) throws Exception {
      final ResteasyClient client = createEngine(engine);
      final ApacheHttpClient4Resource proxy = client.target(PortProviderUtil.generateBaseUrl(ApacheHttpClient4Test.class.getSimpleName())).proxy(ApacheHttpClient4Resource.class);
      counter.set(0);


      Thread[] threads = new Thread[3];

      for (int i = 0; i < 3; i++) {
         threads[i] = new Thread() {
            @Override
            public void run() {
               for (int j = 0; j < 10; j++) {
                  callProxy(proxy);
                  System.gc();
               }
            }
         };
      }

      for (int i = 0; i < 3; i++) {
         threads[i].start();
      }
      for (int i = 0; i < 3; i++) {
         threads[i].join();
      }

      Assert.assertEquals("Wrong count of requests", 30L, counter.get());
   }

   /**
    * @tpTestDetails Create 3 threads and test GC with incorrect request. System.gc is not called directly. Proxy is not used.
    * @tpSince RESTEasy 3.0.16
    */
   @Test
   public void testConnectionCleanupErrorNoGC() throws Exception {
      testConnectionCleanupErrorNoGC(engine1);
      testConnectionCleanupErrorNoGC(engine2);
   }

   /**
    * This is regression test for RESTEASY-1273
    */
   public void testConnectionCleanupErrorNoGC(Class engine) throws Exception {
      final ResteasyClient client = createEngine(engine);
      final ApacheHttpClient4Resource proxy = client.target(PortProviderUtil.generateBaseUrl(ApacheHttpClient4Test.class.getSimpleName())).proxy(ApacheHttpClient4Resource.class);
      counter.set(0);

      Thread[] threads = new Thread[3];

      for (int i = 0; i < 3; i++) {
         threads[i] = new Thread() {
            @Override
            public void run() {
               for (int j = 0; j < 10; j++) {
                  try {
                     proxy.error();
                  } catch (NotFoundException e) {
                     Assert.assertEquals(e.getResponse().getStatus(), 404);
                     e.getResponse().close();
                     counter.incrementAndGet();
                  }
               }
            }
         };
      }

      for (int i = 0; i < 3; i++) {
         threads[i].start();
      }
      for (int i = 0; i < 3; i++) {
         threads[i].join();
      }

      Assert.assertEquals("Wrong count of requests", 30L, counter.get());
   }

   /**
    * @tpTestDetails Create 3 threads and test GC with incorrect request. System.gc is not called directly.
    *                Proxy is used. Data is sent during request.
    * @tpSince RESTEasy 3.0.16
    */
   @Test
   public void testConnectionWithRequestBody() throws InterruptedException {
      testConnectionWithRequestBody(engine1);
      testConnectionWithRequestBody(engine2);
   }

   public void testConnectionWithRequestBody(Class engine) throws InterruptedException {
      final ResteasyClient client = createEngine(engine);
      final ApacheHttpClient4Resource proxy = client.target(PortProviderUtil.generateBaseUrl(ApacheHttpClient4Test.class.getSimpleName())).proxy(ApacheHttpClient4Resource.class);
      counter.set(0);

      Thread[] threads = new Thread[3];

      for (int i = 0; i < 3; i++) {
         threads[i] = new Thread() {
            @Override
            public void run() {
               for (int j = 0; j < 10; j++) {
                  String res = proxy.getData(String.valueOf(j));
                  Assert.assertNotNull("Response should not be null", res);
                  counter.incrementAndGet();
               }
            }
         };
      }

      for (int i = 0; i < 3; i++) {
         threads[i].start();
      }
      for (int i = 0; i < 3; i++) {
         threads[i].join();
      }

      Assert.assertEquals("Wrong count of requests", 30L, counter.get());
   }

   private void callProxy(ApacheHttpClient4Resource proxy) {
      try {
         proxy.error();
      } catch (NotFoundException e) {
         Assert.assertEquals(e.getResponse().getStatus(), 404);
         counter.incrementAndGet();
      }
   }

   @SuppressWarnings(value = "unchecked")
   private ResteasyClient createEngine(Class engine) {
      RequestConfig reqConfig = RequestConfig.custom()   // apache HttpClient specific
            .setConnectTimeout(5000)
            .setSocketTimeout(5000)
            .setConnectionRequestTimeout(5000)
            .build();
      CloseableHttpClient httpClient = HttpClientBuilder.create()
            .setDefaultRequestConfig(reqConfig)
            .setMaxConnTotal(3)
            .build();

      final ClientHttpEngine executor;

      if (engine.isAssignableFrom(ApacheHttpClient4Engine.class)) {
         executor = new ApacheHttpClient4Engine(httpClient);
      } else {
         executor = new URLConnectionEngine();
      }


      ResteasyClient client = new ResteasyClientBuilder().httpEngine(executor).build();
      return client;
   }

   private void runit(Client client, boolean release) {
      WebTarget target = client.target(PortProviderUtil.generateBaseUrl(ApacheHttpClient4Test.class.getSimpleName() + "/test"));
      try {
         Response response = target.request().get();
         Assert.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
         Assert.assertEquals("Wrong response", "hello world", response.readEntity(String.class));
         if (release) {
            response.close();
         }
      } catch (Exception e) {
         throw new RuntimeException(e);
      }
      counter.incrementAndGet();
   }
}
