/*-
 * -\-\-
 * async-google-pubsub-client
 * --
 * Copyright (C) 2016 - 2017 Spotify AB
 * --
 * 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.
 * -/-/-
 */

/*
 * Copyright (c) 2011-2015 Spotify AB
 *
 * 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.spotify.google.cloud.pubsub.client.integration;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.util.Utils;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.Futures;

import com.spotify.google.cloud.pubsub.client.Message;
import com.spotify.google.cloud.pubsub.client.MessageBuilder;
import com.spotify.google.cloud.pubsub.client.Pubsub;
import com.spotify.google.cloud.pubsub.client.PubsubFuture;
import com.spotify.google.cloud.pubsub.client.ReceivedMessage;
import com.spotify.google.cloud.pubsub.client.Subscription;
import com.spotify.google.cloud.pubsub.client.SubscriptionList;
import com.spotify.google.cloud.pubsub.client.Topic;
import com.spotify.google.cloud.pubsub.client.TopicList;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.net.ssl.SSLContext;

import static com.spotify.google.cloud.pubsub.client.integration.Util.TEST_NAME_PREFIX;
import static java.lang.Long.toHexString;
import static java.lang.System.out;
import static java.util.stream.Collectors.toList;
import static java.util.zip.Deflater.BEST_SPEED;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

/**
 * Tests talking to the real Google Cloud Pub/Sub service.
 */
public class PubsubIT {

  private static final int CONCURRENCY = 128;

  private static final String PROJECT = Util.defaultProject();

  private static final String TOPIC = TEST_NAME_PREFIX + toHexString(ThreadLocalRandom.current().nextLong());
  private static final String SUBSCRIPTION = TEST_NAME_PREFIX + toHexString(ThreadLocalRandom.current().nextLong());

  private static GoogleCredential CREDENTIAL;

  private Pubsub pubsub;

  @BeforeClass
  public static void setUpCredentials() throws IOException {
    CREDENTIAL = GoogleCredential.getApplicationDefault(
        Utils.getDefaultTransport(), Utils.getDefaultJsonFactory());
  }

  @Before
  public void setUp() {
    pubsub = Pubsub.builder()
        .maxConnections(CONCURRENCY)
        .credential(CREDENTIAL)
        .build();
  }

  @After
  public void tearDown() throws ExecutionException, InterruptedException {
    if (pubsub != null) {
      pubsub.deleteSubscription(PROJECT, SUBSCRIPTION).exceptionally(t -> null).get();
      pubsub.deleteTopic(PROJECT, TOPIC).exceptionally(t -> null).get();
      pubsub.close();
    }
  }

  @Test
  public void testCreateGetListDeleteTopics() throws Exception {
    testCreateGetListDeleteTopics(pubsub);
  }

  private static void testCreateGetListDeleteTopics(final Pubsub pubsub) throws Exception {

    // Create topic
    final Topic expected = Topic.of(PROJECT, TOPIC);
    {
      final Topic topic = pubsub.createTopic(PROJECT, TOPIC).get();
      assertThat(topic, is(expected));
    }

    // Get topic
    {
      final Topic topic = pubsub.getTopic(PROJECT, TOPIC).get();
      assertThat(topic, is(expected));
    }

    // Verify that the topic is listed
    {
      final List<Topic> topics = topics(pubsub);
      assertThat(topics, hasItem(expected));
    }

    // Delete topic
    {
      pubsub.deleteTopic(PROJECT, TOPIC).get();
    }

    // Verify that topic is gone
    {
      final Topic topic = pubsub.getTopic(PROJECT, TOPIC).get();
      assertThat(topic, is(nullValue()));
    }
    {
      final List<Topic> topics = topics(pubsub);
      assertThat(topics, not(contains(expected)));
    }
  }

  private static List<Topic> topics(final Pubsub pubsub) throws ExecutionException, InterruptedException {
    final List<Topic> topics = new ArrayList<>();
    Optional<String> pageToken = Optional.empty();
    while (true) {
      final TopicList response = pubsub.listTopics(PROJECT, pageToken.orElse(null)).get();
      topics.addAll(response.topics());
      pageToken = response.nextPageToken();
      if (!pageToken.isPresent()) {
        break;
      }
    }
    return topics;
  }

  @Test
  public void testCreateGetListDeleteSubscriptions() throws Exception {
    // Create topic to subscribe to
    final Topic topic = pubsub.createTopic(PROJECT, TOPIC).get();

    // Create subscription
    final Subscription expected = Subscription.of(PROJECT, SUBSCRIPTION, TOPIC);
    {
      final Subscription subscription = pubsub.createSubscription(PROJECT, SUBSCRIPTION, TOPIC).get();
      assertThat(subscription.name(), is(expected.name()));
      assertThat(subscription.topic(), is(expected.topic()));
    }

    // Get subscription
    {
      final Subscription subscription = pubsub.getSubscription(PROJECT, SUBSCRIPTION).get();
      assertThat(subscription.name(), is(expected.name()));
      assertThat(subscription.topic(), is(expected.topic()));
    }

    // Verify that the subscription is listed
    {
      final List<Subscription> subscriptions = subscriptions(pubsub);
      assertThat(subscriptions.stream()
                     .anyMatch(s -> s.name().equals(expected.name()) &&
                                    s.topic().equals(expected.topic())),
                 is(true));
    }

    // Delete subscription
    {
      pubsub.deleteSubscription(PROJECT, SUBSCRIPTION).get();
    }

    // Verify that subscription is gone
    {
      final Subscription subscription = pubsub.getSubscription(PROJECT, SUBSCRIPTION).get();
      assertThat(subscription, is(nullValue()));
    }
    {
      final List<Subscription> subscriptions = subscriptions(pubsub);
      assertThat(subscriptions.stream()
                     .noneMatch(s -> s.name().equals(expected.name())),
                 is(true));
    }
  }

  private static List<Subscription> subscriptions(final Pubsub pubsub) throws ExecutionException, InterruptedException {
    final List<Subscription> subscriptions = new ArrayList<>();
    Optional<String> pageToken = Optional.empty();
    while (true) {
      final SubscriptionList response = pubsub.listSubscriptions(PROJECT, pageToken.orElse(null)).get();
      subscriptions.addAll(response.subscriptions());
      pageToken = response.nextPageToken();
      if (!pageToken.isPresent()) {
        break;
      }
    }
    return subscriptions;
  }

  @Test
  public void testPublish() throws IOException, ExecutionException, InterruptedException {
    pubsub.createTopic(PROJECT, TOPIC).get();
    final String data = BaseEncoding.base64().encode("hello world".getBytes("UTF-8"));
    final Message message = new MessageBuilder().data(data).build();
    final List<String> response = pubsub.publish(PROJECT, TOPIC, message).get();
    out.println(response);
  }

  @Test
  public void testPullSingle() throws IOException, ExecutionException, InterruptedException {
    // Create topic and subscription
    pubsub.createTopic(PROJECT, TOPIC).get();
    pubsub.createSubscription(PROJECT, SUBSCRIPTION, TOPIC).get();

    // Publish a message
    final String data = BaseEncoding.base64().encode("hello world".getBytes("UTF-8"));
    final Message message = Message.of(data);
    final List<String> ids = pubsub.publish(PROJECT, TOPIC, message).get();
    final String id = ids.get(0);
    final List<ReceivedMessage> response = pubsub.pull(PROJECT, SUBSCRIPTION, false).get();

    // Verify received message
    assertThat(response.size(), is(1));
    assertThat(response.get(0).message().data(), is(data));
    assertThat(response.get(0).message().messageId().get(), is(id));
    assertThat(response.get(0).message().publishTime().get(), is(notNullValue()));
    assertThat(response.get(0).ackId(), not(isEmptyOrNullString()));

    // Modify ack deadline
    pubsub.modifyAckDeadline(PROJECT, SUBSCRIPTION, 30, response.get(0).ackId()).get();

    // Ack message
    pubsub.acknowledge(PROJECT, SUBSCRIPTION, response.get(0).ackId()).get();
  }

  @Test
  public void testPullBatch() throws IOException, ExecutionException, InterruptedException {
    pubsub.createTopic(PROJECT, TOPIC).get();
    pubsub.createSubscription(PROJECT, SUBSCRIPTION, TOPIC).get();
    final List<Message> messages = ImmutableList.of(Message.ofEncoded("m0"),
                                                    Message.ofEncoded("m1"),
                                                    Message.ofEncoded("m2"));
    final List<String> ids = pubsub.publish(PROJECT, TOPIC, messages).get();
    final Map<String, ReceivedMessage> received = new HashMap<>();

    // Pull until we've received 3 messages or time out. Store received messages in a map as they might be out of order.
    final long deadlineNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
    while (true) {
      final List<ReceivedMessage> response = pubsub.pull(PROJECT, SUBSCRIPTION).get();
      for (final ReceivedMessage message : response) {
        received.put(message.message().messageId().get(), message);
      }
      if (received.size() >= 3) {
        break;
      }
      if (System.nanoTime() > deadlineNanos) {
        fail("timeout");
      }
    }

    // Verify received messages
    assertThat(received.size(), is(3));
    for (int i = 0; i < 3; i++) {
      final String id = ids.get(i);
      final ReceivedMessage receivedMessage = received.get(id);
      assertThat(receivedMessage.message().data(), is(messages.get(i).data()));
      assertThat(receivedMessage.message().messageId().get(), is(id));
      assertThat(receivedMessage.ackId(), not(isEmptyOrNullString()));
    }
    final List<String> ackIds = received.values().stream()
        .map(ReceivedMessage::ackId)
        .collect(Collectors.toList());

    // Batch modify ack deadline
    pubsub.modifyAckDeadline(PROJECT, SUBSCRIPTION, 30, ackIds).get();

    // Batch ack the messages
    pubsub.acknowledge(PROJECT, SUBSCRIPTION, ackIds).get();
  }

  @Test
  public void testBestSpeedCompressionPublish() throws IOException, ExecutionException, InterruptedException {
    pubsub = Pubsub.builder()
        .maxConnections(CONCURRENCY)
        .credential(CREDENTIAL)
        .compressionLevel(BEST_SPEED)
        .build();
    pubsub.createTopic(PROJECT, TOPIC).get();
    final String data = BaseEncoding.base64().encode(Strings.repeat("hello world", 100).getBytes("UTF-8"));
    final Message message = new MessageBuilder().data(data).build();
    final PubsubFuture<List<String>> future = pubsub.publish(PROJECT, TOPIC, message);
    out.println("raw size: " + data.length());
    out.println("payload size: " + future.payloadSize());
  }

  @Test
  public void testEnabledCipherSuites() throws Exception {
    pubsub.close();

    final String[] defaultCiphers = SSLContext.getDefault().getDefaultSSLParameters().getCipherSuites();
    final List<String> nonGcmCiphers = Stream.of(defaultCiphers)
        .filter(cipher -> !cipher.contains("GCM"))
        .collect(Collectors.toList());

    pubsub = Pubsub.builder()
        .maxConnections(CONCURRENCY)
        .credential(CREDENTIAL)
        .enabledCipherSuites(nonGcmCiphers)
        .build();

    testCreateGetListDeleteTopics(pubsub);
  }

  @Test
  @Ignore
  public void listAllTopics() throws ExecutionException, InterruptedException {
    topics(pubsub).stream().map(Topic::name).forEach(System.out::println);
  }

  @Test
  @Ignore
  public void listAllSubscriptions() throws ExecutionException, InterruptedException {
    subscriptions(pubsub).stream().map(s -> s.name() + ", topic=" + s.topic()).forEach(System.out::println);
  }

  @Test
  @Ignore
  public void cleanUpTestTopics() throws ExecutionException, InterruptedException {
    final ExecutorService executor = Executors.newFixedThreadPool(CONCURRENCY / 2);
    Optional<String> pageToken = Optional.empty();
    while (true) {
      final TopicList response = pubsub.listTopics(PROJECT, pageToken.orElse(null)).get();
      response.topics().stream()
          .map(Topic::name)
          .filter(t -> t.contains(TEST_NAME_PREFIX))
          .map(t -> executor.submit(() -> {
            System.out.println("Removing topic: " + t);
            return pubsub.deleteTopic(t).get();
          }))
          .collect(toList())
          .forEach(Futures::getUnchecked);
      pageToken = response.nextPageToken();
      if (!pageToken.isPresent()) {
        break;
      }
    }
  }

  @Test
  @Ignore
  public void cleanUpTestSubscriptions() throws ExecutionException, InterruptedException {
    final ExecutorService executor = Executors.newFixedThreadPool(CONCURRENCY / 2);
    Optional<String> pageToken = Optional.empty();
    while (true) {
      final SubscriptionList response = pubsub.listSubscriptions(PROJECT, pageToken.orElse(null)).get();
      response.subscriptions().stream()
          .map(Subscription::name)
          .filter(s -> s.contains(TEST_NAME_PREFIX))
          .map(s -> executor.submit(() -> {
            System.out.println("Removing subscription: " + s);
            return pubsub.deleteSubscription(s).get();
          }))
          .collect(toList())
          .forEach(Futures::getUnchecked);
      pageToken = response.nextPageToken();
      if (!pageToken.isPresent()) {
        break;
      }
    }
  }
}