/*
 * Copyright © 2017 Google Inc.
 *
 * Licensed 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 com.google.enterprise.cloudsearch.sdk;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient;
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest;
import com.google.api.client.googleapis.testing.auth.oauth2.MockGoogleCredential;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.Json;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.client.util.Key;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

/** Unit tests for validating functionality provided by {@link BaseApiService} */
public class BaseApiServiceTest {

  private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

  @Rule public ExpectedException thrown = ExpectedException.none();

  private static class TestingHttpTransport extends MockHttpTransport {

    private final Iterator<MockLowLevelHttpRequest> requests;

    private TestingHttpTransport(Collection<MockLowLevelHttpRequest> requests) {
      this.requests = checkNotNull(requests).iterator();
    }

    @Override
    public MockLowLevelHttpRequest buildRequest(String method, String url) throws IOException {
      if (!requests.hasNext()) {
        throw new UnsupportedOperationException("Unexpected request for URL " + url);
      }
      return requests.next();
    }
  }

  private static class TestApi extends AbstractGoogleJsonClient {
    protected TestApi(Builder builder) {
      super(builder);
    }

    private static class Builder extends AbstractGoogleJsonClient.Builder {

      protected Builder(
          HttpTransport transport,
          JsonFactory jsonFactory,
          HttpRequestInitializer httpRequestInitializer) {
        super(
            transport,
            jsonFactory,
            "https://www.googleapis.com/",
            "mock/v1/",
            httpRequestInitializer,
            false);
      }

      @Override
      public TestApi build() {
        return new TestApi(this);
      }
    }
  }

  private static class TestRequest extends AbstractGoogleJsonClientRequest<TestItem> {

    protected TestRequest(TestApi client) {
      super(client, "GET", "/mock/v1", null, TestItem.class);
    }
  }

  /** Public for json parsing */
  public static class TestItem extends GenericJson {
    @Key private Boolean flag;
    @Key private Integer number;
    @Key private String text;
    @Key private List<String> collection;
    @Key private TestItem child;
  }

  private static class TestApiService extends BaseApiService<TestApi> {

    private final AtomicBoolean isRunning = new AtomicBoolean();

    private TestApiService(Builder builder) {
      super(builder);
    }

    private static class Builder extends BaseApiService.AbstractBuilder<Builder, TestApi> {
      @Override
      public Builder getThis() {
        return this;
      }

      @Override
      public Set<String> getApiScopes() {
        return ImmutableSet.of("https://www.googleapis.com/mock.read");
      }

      @Override
      public TestApi.Builder getServiceBuilder(
          HttpTransport transport,
          JsonFactory jsonFactory,
          HttpRequestInitializer requestInitializer) {
        return new TestApi.Builder(transport, jsonFactory, requestInitializer);
      }

      @Override
      public TestApiService build() throws GeneralSecurityException, IOException {
        setupServiceAndCredentials();
        return new TestApiService(this);
      }
    }

    @Override
    protected void startUp() throws Exception {
      isRunning.compareAndSet(false, true);
    }

    @Override
    protected void shutDown() throws Exception {
      isRunning.compareAndSet(true, false);
    }

    private TestItem get() throws IOException {
      return executeRequest(new TestRequest(service), StatsManager.getComponent("TestApi"), true);
    }
  }

  @Test
  public void testRequestWithInitializePrimitiveTypes() throws Exception {
    HttpTransport transport =
        new TestingHttpTransport(ImmutableList.of(buildRequest(200, new GenericJson())));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    validateEmptyItem(apiService.get());
  }

  @Test
  public void testRequestWithNonEmptyResponse() throws Exception {
    TestItem expected = new TestItem();
    expected.number = 10;
    expected.collection = ImmutableList.of("item1");
    expected.flag = true;
    expected.child = new TestItem();
    expected.text = "golden";

    HttpTransport transport =
        new TestingHttpTransport(ImmutableList.of(buildRequest(200, expected)));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    TestItem result = apiService.get();
    assertTrue(result.flag);
    assertEquals(expected.number, result.number);
    assertEquals(expected.text, result.text);
    assertEquals(expected.collection, result.collection);
    validateEmptyItem(result.child);
  }

  @Test
  public void testRequestWithNonRetryableError() throws Exception {
    HttpTransport transport =
        new TestingHttpTransport(ImmutableList.of(buildRequest(400, new GenericJson())));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    thrown.expect(GoogleJsonResponseException.class);
    apiService.get();
  }

  @Test
  public void testRequestWithRetryableError() throws Exception {
    TestingHttpTransport transport =
        new TestingHttpTransport(
            ImmutableList.of(
                buildRequest(503, new GenericJson()), buildRequest(200, new GenericJson())));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    validateEmptyItem(apiService.get());
    assertFalse(transport.requests.hasNext());
  }

  @Test
  public void testRequestWithRetryableErrorFor502() throws Exception {
    TestingHttpTransport transport =
        new TestingHttpTransport(
            ImmutableList.of(
                buildRequest(502, new GenericJson()), buildRequest(200, new GenericJson())));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    validateEmptyItem(apiService.get());
    assertFalse(transport.requests.hasNext());
  }

  @Test
  public void testRequestWithTimeouts() throws Exception {
    AtomicInteger connectionTimeoutHolder = new AtomicInteger();
    AtomicInteger readTimeoutHolder = new AtomicInteger();
    MockLowLevelHttpRequest request =
        new MockLowLevelHttpRequest("https://www.googleapis.com/mock/v1") {
          @Override
          public MockLowLevelHttpResponse execute() throws IOException {
            MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
            response
                .setStatusCode(200)
                .setContentType(Json.MEDIA_TYPE)
                .setContent(JSON_FACTORY.toString(new GenericJson()));
            return response;
          }

          @Override
          public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
            connectionTimeoutHolder.set(connectTimeout);
            readTimeoutHolder.set(readTimeout);
          }
        };
    HttpTransport transport =
        new TestingHttpTransport(ImmutableList.of(request));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .setRequestTimeout(10, 15)
            .build();
    validateEmptyItem(apiService.get());
    assertEquals(10000, connectionTimeoutHolder.get());
    assertEquals(15000, readTimeoutHolder.get());
  }

  @Test
  public void testRequestWithNonDefaultRootUrl() throws Exception {
    MockLowLevelHttpRequest request = buildRequest(200, new GenericJson());
    TestingHttpTransport transport = spy(new TestingHttpTransport(ImmutableList.of(request)));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .setRootUrl("https://staging.googleapis.com")
            .build();
    validateEmptyItem(apiService.get());
    verify(transport).buildRequest("GET", "https://staging.googleapis.com/mock/v1");
  }

  @Test
  public void testApiServiceWithPrebuiltClient() throws Exception {
    MockLowLevelHttpRequest request = buildRequest(200, new GenericJson());
    HttpTransport transport = new TestingHttpTransport(ImmutableList.of(request));
    TestApi apiClient =
        (TestApi)
            new TestApi.Builder(transport, JSON_FACTORY, null)
                .setApplicationName("bring your own client")
                .build();
    TestApiService apiService =
        new TestApiService.Builder()
            .setService(apiClient)
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    validateEmptyItem(apiService.get());
    assertThat(
        request.getHeaderValues("user-agent").get(0), containsString("bring your own client"));
  }

  @Test
  public void testIOExceptionRetry() throws Exception {
    MockLowLevelHttpRequest request =
        new MockLowLevelHttpRequest("https://www.googleapis.com/mock/v1") {
          @Override
          public MockLowLevelHttpResponse execute() throws IOException {
            throw new IOException("something is wrong");
          }
        };
    MockLowLevelHttpRequest retryRequest = buildRequest(200, new GenericJson());
    HttpTransport transport = new TestingHttpTransport(ImmutableList.of(request, retryRequest));
    TestApiService apiService =
        new TestApiService.Builder()
            .setTransport(transport)
            .setCredentialFactory(scopes -> new MockGoogleCredential.Builder().build())
            .build();
    validateEmptyItem(apiService.get());
  }

  private static MockLowLevelHttpRequest buildRequest(int responseCode, GenericJson apiResponse) {
    return new MockLowLevelHttpRequest("https://www.googleapis.com/mock/v1") {
      @Override
      public MockLowLevelHttpResponse execute() throws IOException {
        MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
        response
            .setStatusCode(responseCode)
            .setContentType(Json.MEDIA_TYPE)
            .setContent(JSON_FACTORY.toString(apiResponse));
        return response;
      }
    };
  }

  private static void validateEmptyItem(TestItem result) {
    assertFalse(result.flag);
    assertEquals(Integer.valueOf(0), result.number);
    assertNull(result.text);
    assertNull(result.child);
    assertEquals(Collections.emptyList(), result.collection);
  }
}