/*
 * 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.oauth.v2;

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.io.BaseEncoding.base64Url;
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
import static org.jclouds.oauth.v2.config.CredentialType.P12_PRIVATE_KEY_CREDENTIALS;
import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_SECRET;
import static org.jclouds.oauth.v2.config.CredentialType.CLIENT_CREDENTIALS_P12_AND_CERTIFICATE;
import static org.jclouds.oauth.v2.config.OAuthProperties.CERTIFICATE;
import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
import static org.jclouds.oauth.v2.config.OAuthProperties.RESOURCE;
import static org.jclouds.util.Strings2.toStringAndClose;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;

import java.io.IOException;
import java.net.URL;
import java.util.Properties;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;

import org.jclouds.ContextBuilder;
import org.jclouds.concurrent.config.ExecutorServiceModule;
import org.jclouds.oauth.v2.config.CredentialType;
import org.jclouds.oauth.v2.config.OAuthModule;
import org.jclouds.oauth.v2.config.OAuthScopes;
import org.jclouds.oauth.v2.config.OAuthScopes.SingleScope;
import org.jclouds.oauth.v2.domain.Claims;
import org.jclouds.oauth.v2.domain.ClientCredentialsClaims;
import org.jclouds.oauth.v2.domain.Token;
import org.jclouds.rest.AnonymousHttpApiMetadata;
import org.jclouds.rest.AuthorizationException;
import org.testng.annotations.Test;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.inject.Binder;
import com.google.inject.Module;


@Test(groups = "unit", testName = "OAuthApiMockTest")
public class AuthorizationApiMockTest {
   private static final String SCOPE = "https://www.googleapis.com/auth/prediction";

   private static final String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";

   private static final String claims = "{\"iss\":\"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer"
         + ".gserviceaccount.com\",\"scope\":\"" + SCOPE + "\",\"aud\":\"https://accounts.google"
         + ".com/o/oauth2/token\",\"exp\":1328573381,\"iat\":1328569781}";

   private static final Token TOKEN = Token.create("1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", "Bearer", 3600);

   private static final Claims CLAIMS = Claims.create(
         "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com", // iss
         SCOPE, // scope
         "https://accounts.google.com/o/oauth2/token", // aud
         1328573381, // exp
         1328569781 // iat
         );

   private static final String clientCredentialsHeader = "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"x5t\":\"RZk6mx4gGECvF6XWZWkK9qaGdHk=\"}";
   private static final String clientCredentialsClaims = "{\"iss\":\"a242b44e-2c2a-3bdd-b094-6152da263c54\"," +
         "\"sub\":\"a242b44e-2c2a-3bdd-b094-6152da263c54\",\"aud\":" +
         "\"https://login.microsoftonline.com/a242ccee-1a1a-3bdd-b094-6152da263c54/oauth2/token\"" +
         ",\"exp\":1328573381,\"nbf\":1328569781,\"jti\":\"abcdefgh\"}";

   private static final ClientCredentialsClaims CLIENT_CREDENTIALS_CLAIMS = ClientCredentialsClaims.create(
           "a242b44e-2c2a-3bdd-b094-6152da263c54", // iss
           "a242b44e-2c2a-3bdd-b094-6152da263c54", // sub
           "https://login.microsoftonline.com/a242ccee-1a1a-3bdd-b094-6152da263c54/oauth2/token", //aud
           1328573381, // exp
           1328569781, // nbf
           "abcdefgh" // jti
   );

   public void testGenerateJWTRequest() throws Exception {
      MockWebServer server = new MockWebServer();

      try {
         server.enqueue(new MockResponse().setBody("{\n"
                 + "  \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n"
                 + "  \"token_type\" : \"Bearer\",\n" + "  \"expires_in\" : 3600\n" + "}"));
         server.start();

         AuthorizationApi api = api(server.url("/").url(), P12_PRIVATE_KEY_CREDENTIALS);

         assertEquals(api.authorize(CLAIMS), TOKEN);

         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "POST");
         assertEquals(request.getHeader("Accept"), APPLICATION_JSON);
         assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded");

         assertEquals(
                 request.getBody().readUtf8(), //
                 "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&"
                         +
                         // Base64 Encoded Header
                         "assertion="
                         + Joiner.on('.').join(encoding.encode(header.getBytes(UTF_8)),
                         encoding.encode(claims.getBytes(UTF_8)),
                         // Base64 encoded {header}.{claims} signature (using
                         // SHA256)
                         "W2Lesr_98AzVYiMbzxFqmwcOjpIWlwqkC6pNn1fXND9oSDNNnFhy-AAR6DKH-x9ZmxbY80"
                                 + "R5fH-OCeWumXlVgceKN8Z2SmgQsu8ElTpypQA54j_5j8vUImJ5hsOUYPeyF1U2BUzZ3L5g"
                                 + "03PXBA0YWwRU9E1ChH28dQBYuGiUmYw"));
      } finally {
         server.shutdown();
      }
   }

   public void testAuthorizationExceptionIsPopulatedOn4xx() throws Exception {
      MockWebServer server = new MockWebServer();
      try {
         server.enqueue(new MockResponse().setResponseCode(400));
         server.start();

         AuthorizationApi api = api(server.url("/").url(), P12_PRIVATE_KEY_CREDENTIALS);
         api.authorize(CLAIMS);
         fail("An AuthorizationException should have been raised");
      } catch (AuthorizationException ex) {
         // Success
      } finally {
         server.shutdown();
      }
   }

   public void testGenerateClientSecretRequest() throws Exception {
      MockWebServer server = new MockWebServer();

      String credential = "password";
      String identity = "user";
      String resource = "http://management.azure.com/";
      String encoded_resource = "http%3A//management.azure.com/";

      try {
         server.enqueue(new MockResponse().setBody("{\n"
                 + "  \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n"
                 + "  \"token_type\" : \"Bearer\",\n" + "  \"expires_in\" : 3600\n" + "}"));
         server.start();

         AuthorizationApi api = api(server.url("/").url(), CLIENT_CREDENTIALS_SECRET);

         assertEquals(api.authorizeClientSecret(identity, credential, resource, null), TOKEN);

         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "POST");
         assertEquals(request.getHeader("Accept"), APPLICATION_JSON);
         assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded");

         assertEquals(
                 request.getBody().readUtf8(), //
                 "grant_type=client_credentials&client_id=" + identity
                         + "&client_secret=" + credential
                         + "&resource=" + encoded_resource);
      } finally {
         server.shutdown();
      }
   }

   public void testGenerateClientCredentialsJWTRequest() throws Exception {
      MockWebServer server = new MockWebServer();

      String identity = "user";
      String resource = "http://management.azure.com/";
      String encoded_resource = "http%3A//management.azure.com/";

      try {
         server.enqueue(new MockResponse().setBody("{\n"
                 + "  \"access_token\" : \"1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M\",\n"
                 + "  \"token_type\" : \"Bearer\",\n" + "  \"expires_in\" : 3600\n" + "}"));
         server.start();

         AuthorizationApi api = api(server.url("/").url(), CLIENT_CREDENTIALS_P12_AND_CERTIFICATE);
         assertEquals(api.authorize(identity, CLIENT_CREDENTIALS_CLAIMS, resource, null), TOKEN);

         RecordedRequest request = server.takeRequest();
         assertEquals(request.getMethod(), "POST");
         assertEquals(request.getHeader("Accept"), APPLICATION_JSON);
         assertEquals(request.getHeader("Content-Type"), "application/x-www-form-urlencoded");

         assertEquals(
                 request.getBody().readUtf8(),
                 "grant_type=client_credentials&" +
                         "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&" +
                         "client_id=" + identity + "&" +
                         "client_assertion=" +
                         Joiner.on(".").join(encoding.encode(clientCredentialsHeader.getBytes(UTF_8)),
                                 encoding.encode(clientCredentialsClaims.getBytes(UTF_8)),
                                 "ip3i0YLlunb4iq8sUMlpYDKnEuzmvlLpF4NQvn_aiysO5cuT5QHuGREq" +
                                         "gyEa-UMhfZoW49ggUWjS7YBT00r64cFE3dovaNMiZYZuVWu_" +
                                         "FpqO2QlwV7uXqhaRIE0cyabbKG44YJwA-NE4rtFZedFMo94F" +
                                         "6aOz2FN3en8zS9UVqmM"
                                 ) + "&" +
                         "resource=" + encoded_resource);
      } finally {
         server.shutdown();
      }
   }

   private final BaseEncoding encoding = base64Url().omitPadding();

   private AuthorizationApi api(URL url, CredentialType credentialType) throws IOException {
      Properties overrides = new Properties();
      overrides.setProperty("oauth.endpoint", url.toString());
      overrides.setProperty(JWS_ALG, "RS256");
      overrides.setProperty(CREDENTIAL_TYPE, credentialType.toString());
      overrides.setProperty(AUDIENCE, "https://accounts.google.com/o/oauth2/token");
      overrides.setProperty(RESOURCE, "https://management.azure.com/");
      overrides.setProperty(PROPERTY_MAX_RETRIES, "1");
      overrides.setProperty(CERTIFICATE, toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testcert.pem")));

      return ContextBuilder.newBuilder(AnonymousHttpApiMetadata.forApi(AuthorizationApi.class))
            .credentials("foo", toStringAndClose(OAuthTestUtils.class.getResourceAsStream("/testpk.pem")))
            .endpoint(url.toString())
            .overrides(overrides)
            .modules(ImmutableSet.of(new ExecutorServiceModule(newDirectExecutorService()), new OAuthModule(), new Module() {
               @Override public void configure(Binder binder) {
                  binder.bind(OAuthScopes.class).toInstance(SingleScope.create(SCOPE));
               }
            }))
            .buildApi(AuthorizationApi.class);
   }
}
