/*
 * 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.compute.extensions.internal;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.compute.predicates.NodePredicates.inGroup;
import static org.jclouds.util.Predicates2.retry;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import java.util.NoSuchElementException;
import java.util.concurrent.ExecutionException;

import javax.annotation.Resource;
import javax.inject.Named;

import org.jclouds.compute.ComputeService;
import org.jclouds.compute.RunNodesException;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.ImageTemplate;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.domain.TemplateBuilder;
import org.jclouds.compute.extensions.ImageExtension;
import org.jclouds.compute.internal.BaseComputeServiceContextLiveTest;
import org.jclouds.compute.reference.ComputeServiceConstants;
import org.jclouds.logging.Logger;
import org.jclouds.ssh.SshClient;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;


/**
 * Base test for {@link ImageExtension} implementations.
 */
public abstract class BaseImageExtensionLiveTest extends BaseComputeServiceContextLiveTest {

   @Resource
   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
   protected Logger logger = Logger.NULL;

   protected final String imageGroup = "test-create-image";

   protected String imageId;

   /**
    * Returns the template for the base node, override to test different templates.
    * 
    * @return
    */
   public TemplateBuilder getNodeTemplate() {
      TemplateBuilder templateBuilder = view.getComputeService().templateBuilder();
      if (templateBuilderSpec != null) {
          templateBuilder = templateBuilder.from(templateBuilderSpec);
      }
      return templateBuilder;
   }

   /**
    * Returns the maximum amount of time (in seconds) to wait for a node spawned from the new image
    * to become available, override to increase this time.
    * 
    * @return
    */
   public long getSpawnNodeMaxWait() {
      return 600L;
   }

   /**
    * Lists the images found in the {@link ComputeService}, subclasses may override to constrain
    * search.
    * 
    * @return
    */
   protected Iterable<? extends Image> listImages() {
      return view.getComputeService().listImages();
   }

   @Test(groups = { "integration", "live" }, singleThreaded = true)
   public void testCreateImage() throws RunNodesException, InterruptedException, ExecutionException {
      ComputeService computeService = view.getComputeService();
      Optional<ImageExtension> imageExtension = computeService.getImageExtension();
      assertTrue(imageExtension.isPresent(), "image extension was not present");

      Template template = getNodeTemplate().build();
      NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup(imageGroup, 1, template));
      checkReachable(node);
      
      prepareNodeBeforeCreatingImage(node);

      logger.info("Creating image from node %s, started with template: %s", node, template);
      ImageTemplate newImageTemplate = imageExtension.get().buildImageTemplateFromNode(imageGroup,
              node.getId());
      Image image = imageExtension.get().createImage(newImageTemplate).get();
      logger.info("Image created: %s", image);

      assertEquals(imageGroup, image.getName());

      imageId = image.getId();
      computeService.destroyNode(node.getId());

      Optional<? extends Image> optImage = getImage();
      assertTrue(optImage.isPresent());
   }
   
   /**
    * Subclasses can override this to prepare the node before creating an image (run cleanup scripts, etc)
    */
   protected void prepareNodeBeforeCreatingImage(NodeMetadata node) {
      
   }

   @Test(groups = { "integration", "live" }, singleThreaded = true, dependsOnMethods = "testCreateImage")
   public void testImageIsCachedAfterBeingCreated() {
      Optional<Image> imageInCache = findImageWithNameInCache(imageGroup);
      assertTrue(imageInCache.isPresent());
      assertEquals(imageInCache.get().getId(), imageId);
   }

   @Test(groups = { "integration", "live" }, singleThreaded = true, dependsOnMethods = "testCreateImage")
   public void testSpawnNodeFromImage() throws RunNodesException {
      ComputeService computeService = view.getComputeService();
      Optional<? extends Image> optImage = getImage();
      assertTrue(optImage.isPresent());

      NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup(imageGroup, 1, getNodeTemplate()
               // fromImage does not use the arg image's id (but we do need to set location)
               .imageId(optImage.get().getId()).fromImage(optImage.get()).build()));

      checkReachable(node);
      view.getComputeService().destroyNode(node.getId());
   }

   @Test(groups = { "integration", "live" }, singleThreaded = true, dependsOnMethods = { "testCreateImage",
            "testSpawnNodeFromImage", "testImageIsCachedAfterBeingCreated" })
   public void testDeleteImage() {
      ComputeService computeService = view.getComputeService();

      Optional<ImageExtension> imageExtension = computeService.getImageExtension();
      assertTrue(imageExtension.isPresent(), "image extension was not present");

      Optional<? extends Image> optImage = getImage();
      assertTrue(optImage.isPresent());

      Image image = optImage.get();
      assertTrue(imageExtension.get().deleteImage(image.getId()));
   }
   
   @Test(groups = { "integration", "live" }, singleThreaded = true, dependsOnMethods = "testDeleteImage")
   public void testImageIsRemovedFromCacheAfterDeletion() {
      Optional<Image> imageInCache = findImageWithNameInCache(imageGroup);
      assertFalse(imageInCache.isPresent());
      assertFalse(getImage().isPresent());
   }

   private Optional<? extends Image> getImage() {
      return Optional.fromNullable(view.getComputeService().getImage(imageId));
   }

   private void checkReachable(NodeMetadata node) {
      SshClient client = view.utils().sshForNode().apply(node);
      assertTrue(retry(new Predicate<SshClient>() {
         @Override
         public boolean apply(SshClient input) {
            input.connect();
            if (input.exec("id").getExitStatus() == 0) {
               return true;
            }
            return false;
         }
      }, getSpawnNodeMaxWait(), 1L, SECONDS).apply(client));
   }

   protected Optional<Image> findImageWithNameInCache(String name) {
      try {
         Template template = view.getComputeService().templateBuilder().imageNameMatches(name).build();
         return Optional.of(template.getImage());
      } catch (NoSuchElementException ex) {
         return Optional.absent();
      }
   }

   @AfterClass(groups = { "integration", "live" })
   @Override
   protected void tearDownContext() {
      try {
         view.getComputeService().destroyNodesMatching(inGroup(imageGroup));

         Optional<? extends Image> image = getImage();

         if (image.isPresent() && image.get().getStatus() != Image.Status.DELETED) {
            Optional<ImageExtension> imageExtension = view.getComputeService().getImageExtension();
            if (imageExtension.isPresent()) {
               imageExtension.get().deleteImage(image.get().getId());
            }
         }
      } catch (Exception e) {
         // Any exception is most likely due to nodes/images not existing, which is the desired result
         // anyway, so discarding the exception.
      }

      super.tearDownContext();
   }
}
