/*
 * 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.jclouds.http.okhttp;

import static org.jclouds.Constants.PROPERTY_MAX_CONNECTIONS_PER_CONTEXT;
import static org.jclouds.Constants.PROPERTY_MAX_CONNECTIONS_PER_HOST;
import static org.jclouds.Constants.PROPERTY_USER_THREADS;
import static org.jclouds.util.Closeables2.closeQuietly;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Properties;

import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.TlsVersion;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;

import org.jclouds.http.BaseHttpCommandExecutorServiceIntegrationTest;
import org.jclouds.http.HttpResponseException;
import org.jclouds.http.config.ConfiguresHttpCommandExecutorService;
import org.jclouds.http.okhttp.config.OkHttpCommandExecutorServiceModule;
import org.jclouds.rest.annotations.BinderParam;
import org.jclouds.rest.annotations.PATCH;
import org.jclouds.rest.binders.BindToStringPayload;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableList;
import com.google.inject.AbstractModule;
import com.google.inject.Module;


/**
 * Tests the functionality of the {@link OkHttpCommandExecutorService}
 */
@Test
public class OkHttpCommandExecutorServiceTest extends BaseHttpCommandExecutorServiceIntegrationTest {

   @Override
   protected Module createConnectionModule() {
      return new OkHttpCommandExecutorServiceModule();
   }

   @Override
   protected void addOverrideProperties(final Properties props) {
      props.setProperty(PROPERTY_MAX_CONNECTIONS_PER_CONTEXT, 50 + "");
      props.setProperty(PROPERTY_MAX_CONNECTIONS_PER_HOST, 0 + "");
      props.setProperty(PROPERTY_USER_THREADS, 5 + "");
   }

   private interface PatchApi extends Closeable {
      @PATCH
      @Path("/objects/{id}")
      @Produces("text/plain")
      String patch(@PathParam("id") String id, @BinderParam(BindToStringPayload.class) String body);

      @PATCH
      @Path("/objects/{id}")
      String patchNothing(@PathParam("id") String id);
   }

   @Test
   public void testPatch() throws Exception {
      MockWebServer server = mockWebServer(new MockResponse().setBody("fooPATCH"));
      PatchApi api = api(PatchApi.class, server.url("/").toString());
      try {
         String result = api.patch("", "foo");
         // Verify that the body is properly populated
         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "PATCH");
         assertEquals(request.getBody().readUtf8(), "foo");
         assertEquals(result, "fooPATCH");
         // Verify content headers are sent
         assertNotNull(request.getHeader("Content-Type"));
         assertNotNull(request.getHeader("Content-Length"));
         assertEquals(request.getHeader("Content-Type"), "text/plain");
         assertEquals(Integer.valueOf(request.getHeader("Content-Length")).intValue(), "foo".getBytes().length);
      } finally {
         closeQuietly(api);
         server.shutdown();
      }
   }

   @Test
   public void testPatchIsRetriedOnFailure() throws Exception {
      MockWebServer server = mockWebServer(new MockResponse().setResponseCode(500),
            new MockResponse().setBody("fooPATCH"));
      PatchApi api = api(PatchApi.class, server.url("/").toString());
      try {
         String result = api.patch("", "foo");
         assertEquals(server.getRequestCount(), 2);
         assertEquals(result, "fooPATCH");
         // Verify that the body was properly sent in the two requests
         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "PATCH");
         assertEquals(request.getBody().readUtf8(), "foo");
         request = server.takeRequest();
         assertEquals(request.getMethod(), "PATCH");
         assertEquals(request.getBody().readUtf8(), "foo");
      } finally {
         closeQuietly(api);
         server.shutdown();
      }
   }

   @Test
   public void testPatchRedirect() throws Exception {
      MockWebServer redirectTarget = mockWebServer(new MockResponse().setBody("fooPATCHREDIRECT"));
      redirectTarget.useHttps(sslSocketFactory(), false);
      MockWebServer server = mockWebServer(new MockResponse().setResponseCode(302).setHeader("Location",
            redirectTarget.url("/").toString()));
      PatchApi api = api(PatchApi.class, server.url("/").toString());
      try {
         String result = api.patch("", "foo");
         assertEquals(result, "fooPATCHREDIRECT");
         assertEquals(server.getRequestCount(), 1);
         assertEquals(redirectTarget.getRequestCount(), 1);
         // Verify that the body was populated after the redirect
         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "PATCH");
         assertEquals(request.getBody().readUtf8(), "foo");
         request = redirectTarget.takeRequest();
         assertEquals(request.getMethod(), "PATCH");
         assertEquals(request.getBody().readUtf8(), "foo");
      } finally {
         closeQuietly(api);
         redirectTarget.shutdown();
         server.shutdown();
      }
   }

   @Test
   public void testZeroLengthPatch() throws Exception {
      MockWebServer server = mockWebServer(new MockResponse());
      PatchApi api = api(PatchApi.class, server.url("/").toString());
      try {
         api.patchNothing("");
         assertEquals(server.getRequestCount(), 1);
         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "PATCH");
         assertEquals(request.getBody().readUtf8(), "");
      } finally {
         closeQuietly(api);
         server.shutdown();
      }
   }

   @Test(expectedExceptions = HttpResponseException.class, expectedExceptionsMessageRegExp = "Unable to find acceptable protocols.*")
   public void testSSLConnectionFailsIfOnlyHttpConfigured() throws Exception {
      MockWebServer server = mockWebServer(new MockResponse());
      server.useHttps(sslSocketFactory(), false);
      Module httpConfigModule = new ConnectionSpecModule(ConnectionSpec.CLEARTEXT);
      PatchApi api = api(PatchApi.class, server.url("/").toString(), httpConfigModule);
      try {
         api.patchNothing("");
      } finally {
         closeQuietly(api);
         server.shutdown();
      }
   }

   @Test(expectedExceptions = HttpResponseException.class, expectedExceptionsMessageRegExp = "CLEARTEXT communication not enabled for client.*")
   public void testHTTPConnectionFailsIfOnlySSLConfigured() throws Exception {
      MockWebServer server = mockWebServer(new MockResponse());
      Module httpConfigModule = new ConnectionSpecModule(ConnectionSpec.MODERN_TLS);
      PatchApi api = api(PatchApi.class, server.url("/").toString(), httpConfigModule);
      try {
         api.patchNothing("");
      } finally {
         closeQuietly(api);
         server.shutdown();
      }
   }

   @Test
   public void testBothProtocolsSucceedIfSSLAndHTTPConfigured() throws Exception {
      MockWebServer redirectTarget = mockWebServer(new MockResponse());
      MockWebServer server = mockWebServer(new MockResponse().setResponseCode(302).setHeader("Location",
            redirectTarget.url("/").toString()));
      server.useHttps(sslSocketFactory(), false);
      Module httpConfigModule = new ConnectionSpecModule(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS);
      PatchApi api = api(PatchApi.class, server.url("/").toString(), httpConfigModule);
      try {
         api.patchNothing("");
         assertEquals(server.getRequestCount(), 1);
         assertEquals(redirectTarget.getRequestCount(), 1);
      } finally {
         closeQuietly(api);
         server.shutdown();
         redirectTarget.shutdown();
      }
   }

   @Test
   public void testRestrictedSSLProtocols() throws Exception {
      MockWebServer server = mockWebServer(new MockResponse());
      server.useHttps(sslSocketFactory(), false);
      ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS).tlsVersions(TlsVersion.TLS_1_2)
            .build();
      PatchApi api = api(PatchApi.class, server.url("/").toString(), new ConnectionSpecModule(spec));
      try {
         api.patchNothing("");
         assertEquals(server.getRequestCount(), 1);
         RecordedRequest request = server.takeRequest();
         assertEquals(request.getTlsVersion().javaName(), "TLSv1.2");
      } finally {
         closeQuietly(api);
         server.shutdown();
      }
   }

   protected static MockWebServer mockWebServer(MockResponse... responses) throws IOException {
      MockWebServer server = new MockWebServer();
      server.start(null, 0);
      for (MockResponse response : responses) {
         server.enqueue(response);
      }
      return server;
   }

   @ConfiguresHttpCommandExecutorService
   private static final class ConnectionSpecModule extends AbstractModule {
      private final List<ConnectionSpec> connectionSpecs;

      public ConnectionSpecModule(ConnectionSpec... connectionSpecs) {
         this.connectionSpecs = ImmutableList.copyOf(connectionSpecs);
      }

      @Override
      protected void configure() {
         install(new OkHttpCommandExecutorServiceModule());
         bind(OkHttpClientSupplier.class).toInstance(new OkHttpClientSupplier() {
            @Override
            public OkHttpClient get() {
               return new OkHttpClient.Builder()
                   .connectionSpecs(connectionSpecs)
                   .build();
            }
         });
      }
   }

}
