/**
 * Licensed to jclouds, Inc. (jclouds) under one or more
 * contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  jclouds 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.blobstore.integration.internal;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.jclouds.blobstore.options.ListContainerOptions.Builder.inDirectory;
import static org.jclouds.blobstore.options.ListContainerOptions.Builder.maxResults;
import static org.jclouds.blobstore.util.BlobStoreUtils.getContentAsStringOrNullAndClose;
import static org.testng.Assert.assertEquals;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.jclouds.blobstore.BlobMap;
import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.options.ListContainerOptions;
import org.jclouds.io.Payloads;
import org.jclouds.util.Strings2;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Tests to cover @{link LiveS3ObjectMap}
 * 
 * @author Adrian Cole
 */
public abstract class BaseBlobMapIntegrationTest extends BaseMapIntegrationTest<Blob> {

   private static class StringToBlob implements Function<String, Blob> {
      private final BlobMap map;

      private StringToBlob(BlobMap map) {
         this.map = map;
      }

      @Override
      public Blob apply(String arg0) {
         return map.blobBuilder().payload(arg0).build();
      }
   }

   @Override
   @Test(groups = { "integration", "live" })
   public void testValues() throws IOException, InterruptedException {
      String bucketName = getContainerName();
      try {
         BlobMap map = createMap(context, bucketName);

         putFiveStrings(map);
         putFiveStringsUnderPath(map);

         Collection<Blob> blobs = map.values();
         assertConsistencyAwareMapSize(map, 5);
         Set<String> blobsAsString = Sets.newLinkedHashSet();
         for (Blob blob : blobs) {
            blobsAsString.add(getContentAsStringOrNullAndClose(blob));
         }
         blobsAsString.removeAll(fiveStrings.values());
         assert blobsAsString.size() == 0 : blobsAsString.size() + ": " + blobs + ": " + blobsAsString;
      } finally {
         returnContainer(bucketName);
      }
   }

   @Test(groups = { "integration", "live" })
   public void testRemove() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String bucketName = getContainerName();
      try {
         Map<String, Blob> map = createMap(context, bucketName);
         putStringWithMD5(map, "one", "two");
         assertConsistencyAwareContentEquals(map, "one", "two");
         // TODO track how often this occurs and potentially update map implementation
         assertConsistencyAwareRemoveEquals(map, "one", null);
         assertConsistencyAwareGetEquals(map, "one", null);
         assertConsistencyAwareKeySize(map, 0);
      } finally {
         returnContainer(bucketName);
      }
   }

   private void assertConsistencyAwareContentEquals(final Map<String, Blob> map, final String key, final String blob)
         throws InterruptedException {
      assertConsistencyAware(new Runnable() {
         public void run() {
            Blob old = map.remove(key);
            try {
               assertEquals(getContentAsStringOrNullAndClose(old), blob);
            } catch (IOException e) {
               throw new RuntimeException(e);
            }
         }
      });
   }

   @Override
   @Test(groups = { "integration", "live" })
   public void testEntrySet() throws IOException, InterruptedException {
      String bucketName = getContainerName();
      try {
         final BlobMap map = createMap(context, bucketName);
         putFiveStrings(map);
         assertConsistencyAwareMapSize(map, 5);
         Set<Entry<String, Blob>> entries = map.entrySet();
         assertEquals(entries.size(), 5);
         for (Entry<String, Blob> entry : entries) {
            assertEquals(fiveStrings.get(entry.getKey()), getContentAsStringOrNullAndClose(entry.getValue()));
            Blob blob = entry.getValue();
            blob.setPayload("");
            Payloads.calculateMD5(blob);
            entry.setValue(blob);
         }
         assertConsistencyAware(new Runnable() {
            public void run() {
               for (Blob blob : map.values()) {
                  try {
                     assertEquals(getContentAsStringOrNullAndClose(blob), "");
                  } catch (IOException e) {
                     Throwables.propagate(e);
                  }
               }
            }
         });

      } finally {
         returnContainer(bucketName);
      }
   }

   @Test(groups = { "integration", "live" })
   public void testContains() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String bucketName = getContainerName();
      try {
         Map<String, Blob> map = createMap(context, bucketName);
         putStringWithMD5(map, "one", "apple");
         Blob blob = context.getBlobStore().blobBuilder("one").payload("apple").calculateMD5().build();
         assertConsistencyAwareContainsValue(map, blob);
      } finally {
         returnContainer(bucketName);
      }
   }

   void getOneReturnsAppleAndOldValueIsNull(Map<String, Blob> map, Blob old) throws IOException, InterruptedException {
      assert old == null;
      assertEquals(getContentAsStringOrNullAndClose(map.get("one")), "apple");
      assertConsistencyAwareMapSize(map, 1);
   }

   void getOneReturnsBearAndOldValueIsApple(Map<String, Blob> map, Blob oldValue) throws IOException,
         InterruptedException {
      assertEquals(getContentAsStringOrNullAndClose(checkNotNull(map.get("one"), "one")), "bear");
      assertEquals(getContentAsStringOrNullAndClose(oldValue), "apple");
      assertConsistencyAwareMapSize(map, 1);
   }

   @Test(groups = { "integration", "live" })
   public void testPut() throws IOException, InterruptedException {
      String bucketName = getContainerName();
      try {
         Map<String, Blob> map = createMap(context, bucketName);
         Blob blob = context.getBlobStore().blobBuilder("one").payload(Strings2.toInputStream("apple")).calculateMD5()
               .build();
         Blob old = map.put(blob.getMetadata().getName(), blob);
         getOneReturnsAppleAndOldValueIsNull(map, old);
         blob.setPayload(Strings2.toInputStream("bear"));
         Payloads.calculateMD5(blob);
         Blob apple = map.put(blob.getMetadata().getName(), blob);
         getOneReturnsBearAndOldValueIsApple(map, apple);
      } finally {
         returnContainer(bucketName);
      }
   }

   @Test(groups = { "integration", "live" })
   public void testPutAll() throws InterruptedException, ExecutionException, TimeoutException {
      String bucketName = getContainerName();
      try {
         Map<String, Blob> map = createMap(context, bucketName);
         ImmutableMap.Builder<String, Blob> newMap = ImmutableMap.<String, Blob> builder();
         for (String key : fiveInputs.keySet()) {
            newMap.put(
                  key,
                  context.getBlobStore().blobBuilder(key).payload(fiveInputs.get(key))
                        .contentLength((long) fiveBytes.get(key).length).build());
         }
         map.putAll(newMap.build());
         assertConsistencyAwareMapSize(map, 5);
         assertConsistencyAwareKeySetEquals(map, ImmutableSet.copyOf(fiveInputs.keySet()));
         fourLeftRemovingOne(map);
      } finally {
         returnContainer(bucketName);
      }
   }

   @Test(groups = { "integration", "live" })
   public void testPutMoreThanSingleListing() throws InterruptedException, ExecutionException, TimeoutException {
      if (maxResultsForTestListings() == 0)
         return;
      String bucketName = getContainerName();
      try {
         BlobMap map = createMap(context, bucketName);
         Builder<String> keySet = ImmutableSet.<String> builder();
         for (int i = 0; i < maxResultsForTestListings() + 1; i++) {
            keySet.add(i + "");
         }

         Map<String, Blob> newMap = Maps.newLinkedHashMap();
         for (String key : keySet.build()) {
            newMap.put(key, map.blobBuilder().payload(key).build());
         }
         map.putAll(newMap);
         newMap.clear();

         assertConsistencyAwareMapSize(map, maxResultsForTestListings() + 1);
         assertConsistencyAwareKeySetEquals(map, keySet.build());
         map.clear();
         assertConsistencyAwareMapSize(map, 0);
      } finally {
         returnContainer(bucketName);
      }
   }

   @Override
   protected void putStringWithMD5(Map<String, Blob> map, String key, String text) throws IOException {
      map.put(key, context.getBlobStore().blobBuilder(key).payload(text).calculateMD5().build());
   }

   protected void putFiveStrings(BlobMap map) {
      map.putAll(Maps.transformValues(fiveStrings, new StringToBlob(map)));
   }

   protected void putFiveStringsUnderPath(BlobMap map) {
      map.putAll(Maps.transformValues(fiveStringsUnderPath, new StringToBlob(map)));
   }

   protected int maxResultsForTestListings() {
      return 100;
   }

   protected BlobMap createMap(BlobStoreContext context, String bucket) {
      return createMap(context, bucket, maxResults(maxResultsForTestListings()));
   }

   protected BlobMap createMap(BlobStoreContext context, String bucket, ListContainerOptions options) {
      return context.createBlobMap(bucket, options);
   }

   @Override
   protected void addTenObjectsUnderPrefix(String containerName, String prefix) throws InterruptedException {
      BlobMap blobMap = createMap(context, containerName, inDirectory(prefix));
      for (int i = 0; i < 10; i++) {
         blobMap.put(i + "", blobMap.blobBuilder().payload(i + "content").build());
      }
   }

   @Override
   protected void addTenObjectsUnderRoot(String containerName) throws InterruptedException {
      BlobMap blobMap = createMap(context, containerName, ListContainerOptions.NONE);
      for (int i = 0; i < 10; i++) {
         blobMap.put(i + "", blobMap.blobBuilder().payload(i + "content").build());
      }
   }
}
