/*
 * 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.apache.samza.table.remote.couchbase;

import com.couchbase.client.core.CouchbaseException;
import com.couchbase.client.java.AsyncBucket;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.document.Document;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.error.TemporaryFailureException;
import com.couchbase.client.java.error.TemporaryLockFailureException;

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.samza.SamzaException;
import org.apache.samza.context.Context;
import org.apache.samza.serializers.Serde;
import org.apache.samza.serializers.StringSerde;
import org.apache.samza.table.AsyncReadWriteTable;

import org.junit.Test;
import org.junit.runner.RunWith;

import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import rx.Observable;

import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.powermock.api.mockito.PowerMockito.*;


/**
 * This class performs unit tests for CouchbaseTableWriteFunction.
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest(CouchbaseBucketRegistry.class)
public class TestCouchbaseTableWriteFunction {
  private static final String DEFAULT_BUCKET_NAME = "default-bucket-name";
  private static final String DEFAULT_CLUSTER_NODE = "localhost";

  @Test(expected = IllegalArgumentException.class)
  public void testConstructorInvalidBucketName() {
    new CouchbaseTableWriteFunction<>("", String.class, DEFAULT_CLUSTER_NODE);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testConstructorInvalidClusterNodes() {
    new CouchbaseTableWriteFunction<>(DEFAULT_BUCKET_NAME, String.class);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testConstructorInvalidValueClass() {
    new CouchbaseTableWriteFunction<>(DEFAULT_BUCKET_NAME, null, DEFAULT_CLUSTER_NODE);
  }

  @Test
  public void testIsNotRetriable() {
    CouchbaseTableWriteFunction writeFunction = createAndInit();
    assertFalse(writeFunction.isRetriable(null));
    assertFalse(writeFunction.isRetriable(new SamzaException()));
    assertFalse(writeFunction.isRetriable(new SamzaException("", new IllegalArgumentException())));
  }

  @Test
  public void testIsRetriable() {
    CouchbaseTableWriteFunction writeFunction = createAndInit();
    assertTrue(writeFunction.isRetriable(new TemporaryFailureException()));
    assertTrue(writeFunction.isRetriable(new TemporaryLockFailureException()));
    assertTrue(writeFunction.isRetriable(new SamzaException(new TemporaryLockFailureException())));
    assertTrue(writeFunction.isRetriable(new SamzaException(new TemporaryFailureException())));
    assertTrue(writeFunction.isRetriable(
        new RuntimeException(new SamzaException(new RuntimeException(new TemporaryFailureException())))));
  }

  @Test(expected = IllegalArgumentException.class)
  public void testPutAsyncNullKey() {
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit();
    writeFunction.putAsync(null, JsonObject.create());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testPutAsyncKeyWithSpaces() {
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit();
    writeFunction.putAsync("k e y", JsonObject.create());
  }

  @Test(expected = NullPointerException.class)
  public void testPutAsyncNullValue() {
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit();
    writeFunction.putAsync("key", null);
  }

  @Test
  public void testPutAsyncException() {
    String key = "throwExceptionKey";
    JsonObject value = JsonObject.create();
    Bucket bucket = mock(Bucket.class);
    AsyncBucket asyncBucket = mock(AsyncBucket.class);
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit(bucket, asyncBucket);
    when(asyncBucket.upsert(any(Document.class), anyLong(), any(TimeUnit.class))).thenReturn(
        Observable.error(new CouchbaseException()));
    assertTrue(writeFunction.putAsync(key, value).isCompletedExceptionally());
  }

  @Test
  public void testPutAsyncJsonObjectValue() throws Exception {
    String key = "key";
    JsonObject value = JsonObject.fromJson("{\"id\": 1}");
    Bucket bucket = mock(Bucket.class);
    AsyncBucket asyncBucket = mock(AsyncBucket.class);
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit(bucket, asyncBucket);
    when(asyncBucket.upsert(any(Document.class), anyLong(), any(TimeUnit.class))).thenReturn(Observable.just(null));
    assertNull(writeFunction.putAsync(key, value).get());
  }

  @Test
  public void testPutAsyncStringValue() throws Exception {
    String key = "key";
    String value = "value";
    Bucket bucket = mock(Bucket.class);
    AsyncBucket asyncBucket = mock(AsyncBucket.class);
    CouchbaseTableWriteFunction<String> writeFunction =
        createAndInit(String.class, new StringSerde(), bucket, asyncBucket);
    when(asyncBucket.upsert(any(Document.class), anyLong(), any(TimeUnit.class))).thenReturn(Observable.just(null));
    assertNull(writeFunction.putAsync(key, value).get());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testDeleteAsyncNullKey() {
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit();
    writeFunction.deleteAsync(null);
  }

  @Test
  public void testDeleteAsyncException() {
    String key = "throwExceptionKey";
    Bucket bucket = mock(Bucket.class);
    AsyncBucket asyncBucket = mock(AsyncBucket.class);
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit(bucket, asyncBucket);
    when(asyncBucket.remove(eq(key), anyLong(), any(TimeUnit.class))).thenReturn(
        Observable.error(new CouchbaseException()));
    assertTrue(writeFunction.deleteAsync(key).isCompletedExceptionally());
  }

  @Test
  public void testDeleteAsyncJsonObjectValue() throws Exception {
    String key = "key";
    Bucket bucket = mock(Bucket.class);
    AsyncBucket asyncBucket = mock(AsyncBucket.class);
    CouchbaseTableWriteFunction<JsonObject> writeFunction = createAndInit(bucket, asyncBucket);
    when(asyncBucket.remove(eq(key), anyLong(), any(TimeUnit.class))).thenReturn(Observable.just(null));
    assertNull(writeFunction.deleteAsync(key).get());
  }

  @Test
  public void testDeleteAsyncStringValue() throws Exception {
    String key = "key";
    Bucket bucket = mock(Bucket.class);
    AsyncBucket asyncBucket = mock(AsyncBucket.class);
    CouchbaseTableWriteFunction<String> writeFunction =
        createAndInit(String.class, new StringSerde(), bucket, asyncBucket);
    when(asyncBucket.remove(eq(key), anyLong(), any(TimeUnit.class))).thenReturn(Observable.just(null));
    assertNull(writeFunction.deleteAsync(key).get());
  }

  private CouchbaseTableWriteFunction<JsonObject> createAndInit() {
    return createAndInit(mock(Bucket.class), mock(AsyncBucket.class));
  }

  private CouchbaseTableWriteFunction<JsonObject> createAndInit(Bucket bucket, AsyncBucket asyncBucket) {
    return createAndInit(JsonObject.class, null, bucket, asyncBucket);
  }

  private <V> CouchbaseTableWriteFunction<V> createAndInit(Class<V> valueClass, Serde<V> serde, Bucket bucket,
      AsyncBucket asyncBucket) {
    when(bucket.async()).thenReturn(asyncBucket);
    PowerMockito.stub(PowerMockito.method(CouchbaseBucketRegistry.class, "getBucket", String.class, List.class,
        CouchbaseEnvironmentConfigs.class)).toReturn(bucket);
    CouchbaseTableWriteFunction<V> writeFunction =
        new CouchbaseTableWriteFunction<>(DEFAULT_BUCKET_NAME, valueClass, DEFAULT_CLUSTER_NODE).withSerde(serde);
    writeFunction.init(mock(Context.class), mock(AsyncReadWriteTable.class));
    return writeFunction;
  }
}