/*
 * 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.gogrid;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.jclouds.util.Predicates2.retry;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jclouds.apis.BaseApiLiveTest;
import org.jclouds.domain.Credentials;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.gogrid.domain.Ip;
import org.jclouds.gogrid.domain.IpPortPair;
import org.jclouds.gogrid.domain.Job;
import org.jclouds.gogrid.domain.LoadBalancer;
import org.jclouds.gogrid.domain.LoadBalancerPersistenceType;
import org.jclouds.gogrid.domain.LoadBalancerType;
import org.jclouds.gogrid.domain.PowerCommand;
import org.jclouds.gogrid.domain.Server;
import org.jclouds.gogrid.domain.ServerImage;
import org.jclouds.gogrid.domain.ServerImageType;
import org.jclouds.gogrid.options.AddLoadBalancerOptions;
import org.jclouds.gogrid.options.AddServerOptions;
import org.jclouds.gogrid.options.GetImageListOptions;
import org.jclouds.gogrid.predicates.LoadBalancerLatestJobCompleted;
import org.jclouds.gogrid.predicates.ServerLatestJobCompleted;
import org.jclouds.javax.annotation.Nullable;
import org.jclouds.predicates.SocketOpen;
import org.testng.SkipException;
import org.testng.TestException;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeGroups;
import org.testng.annotations.Test;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.net.HostAndPort;
import com.google.inject.Guice;

/**
 * End to end live test for GoGrid
 * <p/>
 * Takes too long to execute. Please split into multiple tests
 */
@Test(enabled = false, groups = "live", singleThreaded = true, testName = "GoGridLiveTestDisabled")
public class GoGridLiveTestDisabled extends BaseApiLiveTest<GoGridApi> {

   public GoGridLiveTestDisabled() {
      provider = "gogrid";
   }

   private Predicate<Server> serverLatestJobCompleted;
   private Predicate<LoadBalancer> loadBalancerLatestJobCompleted;
   /**
    * Keeps track of the servers, created during the tests, to remove them after all tests complete
    */
   private List<String> serversToDeleteAfterTheTests = Lists.newArrayList();
   private List<String> loadBalancersToDeleteAfterTest = Lists.newArrayList();


   @BeforeGroups(groups = { "integration", "live" })
   @Override
   public void setup() {
      super.setup();
      serverLatestJobCompleted = retry(new ServerLatestJobCompleted(api.getJobServices()), 800, 20, SECONDS);
      loadBalancerLatestJobCompleted = retry(new LoadBalancerLatestJobCompleted(api.getJobServices()), 800, 20,
            SECONDS);
   }

   @Test(enabled = true)
   public void testDescriptionIs500Characters() {
      final String nameOfServer = "Description" + String.valueOf(new Date().getTime()).substring(6);
      serversToDeleteAfterTheTests.add(nameOfServer);

      Set<Ip> availableIps = api.getIpServices().getUnassignedPublicIpList();
      Ip availableIp = Iterables.getLast(availableIps);

      String ram = Iterables.get(api.getServerServices().getRamSizes(), 0).getName();
      StringBuilder builder = new StringBuilder();

      for (int i = 0; i < 500; i++)
         builder.append('a');

      String description = builder.toString();

      Server createdServer = api.getServerServices().addServer(nameOfServer,
               "GSI-f8979644-e646-4711-ad58-d98a5fa3612c", ram, availableIp.getIp(),
               new AddServerOptions().withDescription(description));
      assertNotNull(createdServer);
      assert serverLatestJobCompleted.apply(createdServer);

      assertEquals(Iterables.getLast(api.getServerServices().getServersByName(nameOfServer)).getDescription(),
               description);

   }

   /**
    * Tests server start, reboot and deletion. Also verifies IP services and job services.
    */
   @Test(enabled = true)
   public void testServerLifecycle() {
      int serverCountBeforeTest = api.getServerServices().getServerList().size();

      final String nameOfServer = "Server" + String.valueOf(new Date().getTime()).substring(6);
      serversToDeleteAfterTheTests.add(nameOfServer);

      Set<Ip> availableIps = api.getIpServices().getUnassignedPublicIpList();
      Ip availableIp = Iterables.getLast(availableIps);

      String ram = Iterables.get(api.getServerServices().getRamSizes(), 0).getName();

      Server createdServer = api.getServerServices().addServer(nameOfServer,
               "GSI-f8979644-e646-4711-ad58-d98a5fa3612c", ram, availableIp.getIp());
      assertNotNull(createdServer);
      assert serverLatestJobCompleted.apply(createdServer);

      // get server by name
      Set<Server> response = api.getServerServices().getServersByName(nameOfServer);
      assert response.size() == 1;

      // restart the server
      api.getServerServices().power(nameOfServer, PowerCommand.RESTART);

      Set<Job> jobs = api.getJobServices().getJobsForObjectName(nameOfServer);
      assert "RestartVirtualServer".equals(Iterables.getLast(jobs).getCommand().getName());

      assert serverLatestJobCompleted.apply(createdServer);

      int serverCountAfterAddingOneServer = api.getServerServices().getServerList().size();
      assert serverCountAfterAddingOneServer == serverCountBeforeTest + 1 : "There should be +1 increase in the number of servers since the test started";

      // delete the server
      api.getServerServices().deleteByName(nameOfServer);

      jobs = api.getJobServices().getJobsForObjectName(nameOfServer);
      assert "DeleteVirtualServer".equals(Iterables.getLast(jobs).getCommand().getName());

      assert serverLatestJobCompleted.apply(createdServer);

      int serverCountAfterDeletingTheServer = api.getServerServices().getServerList().size();
      assert serverCountAfterDeletingTheServer == serverCountBeforeTest : "There should be the same # of servers as since the test started";

      // make sure that IP is put back to "unassigned"
      assert api.getIpServices().getUnassignedIpList().contains(availableIp);
   }

   /**
    * Starts a servers, verifies that jobs are created correctly and an be retrieved from the job
    * services
    */
   @Test(dependsOnMethods = "testServerLifecycle", enabled = true)
   public void testJobs() {
      final String nameOfServer = "Server" + String.valueOf(new Date().getTime()).substring(6);
      serversToDeleteAfterTheTests.add(nameOfServer);

      Set<Ip> availableIps = api.getIpServices().getUnassignedPublicIpList();

      String ram = Iterables.get(api.getServerServices().getRamSizes(), 0).getName();

      Server createdServer = api.getServerServices().addServer(nameOfServer,
               "GSI-f8979644-e646-4711-ad58-d98a5fa3612c", ram, Iterables.getLast(availableIps).getIp());

      assert serverLatestJobCompleted.apply(createdServer);

      // restart the server
      api.getServerServices().power(nameOfServer, PowerCommand.RESTART);

      Set<Job> jobs = api.getJobServices().getJobsForObjectName(nameOfServer);

      Job latestJob = Iterables.getLast(jobs);
      Long latestJobId = latestJob.getId();

      Job latestJobFetched = Iterables.getOnlyElement(api.getJobServices().getJobsById(latestJobId));

      assert latestJob.equals(latestJobFetched) : "Job and its representation found by ID don't match";

      long[] idsOfAllJobs = new long[jobs.size()];
      int i = 0;
      for (Job job : jobs) {
         idsOfAllJobs[i++] = job.getId();
      }

      Set<Job> jobsFetched = api.getJobServices().getJobsById(idsOfAllJobs);
      assert jobsFetched.size() == jobs.size() : format(
               "Number of jobs fetched by ids doesn't match the number of jobs "
                        + "requested. Requested/expected: %d. Found: %d.", jobs.size(), jobsFetched.size());

      // delete the server
      api.getServerServices().deleteByName(nameOfServer);
   }

   /**
    * Tests common load balancer operations. Also verifies IP services and job services.
    */
   @Test(enabled = true)
   public void testLoadBalancerLifecycle() {
      int lbCountBeforeTest = api.getLoadBalancerServices().getLoadBalancerList().size();

      final String nameOfLoadBalancer = "LoadBalancer" + String.valueOf(new Date().getTime()).substring(6);
      loadBalancersToDeleteAfterTest.add(nameOfLoadBalancer);

      Set<Ip> availableIps = api.getIpServices().getUnassignedPublicIpList();

      if (availableIps.size() < 4)
         throw new SkipException("Not enough available IPs (4 needed) to run the test");
      Iterator<Ip> ipIterator = availableIps.iterator();
      Ip vip = ipIterator.next();
      Ip realIp1 = ipIterator.next();
      Ip realIp2 = ipIterator.next();
      Ip realIp3 = ipIterator.next();

      AddLoadBalancerOptions options = new AddLoadBalancerOptions.Builder().create(LoadBalancerType.LEAST_CONNECTED,
               LoadBalancerPersistenceType.SOURCE_ADDRESS);
      LoadBalancer createdLoadBalancer = api.getLoadBalancerServices().addLoadBalancer(nameOfLoadBalancer,
               IpPortPair.builder().ip(vip).port(80).build(), Arrays.asList(IpPortPair.builder().ip(realIp1).port(80).build(),
               IpPortPair.builder().ip(realIp2).port(80).build()),
               options);
      assertNotNull(createdLoadBalancer);
      assert loadBalancerLatestJobCompleted.apply(createdLoadBalancer);

      // get load balancer by name
      Set<LoadBalancer> response = api.getLoadBalancerServices().getLoadBalancersByName(nameOfLoadBalancer);
      assert response.size() == 1;
      createdLoadBalancer = Iterables.getOnlyElement(response);
      assertNotNull(createdLoadBalancer.getRealIpList());
      assertEquals(createdLoadBalancer.getRealIpList().size(), 2);
      assertNotNull(createdLoadBalancer.getVirtualIp());
      assertEquals(createdLoadBalancer.getVirtualIp().getIp().getIp(), vip.getIp());

      LoadBalancer editedLoadBalancer = api.getLoadBalancerServices().editLoadBalancerNamed(nameOfLoadBalancer,
               Arrays.asList(IpPortPair.builder().ip(realIp3).port(8181).build()));
      assert loadBalancerLatestJobCompleted.apply(editedLoadBalancer);
      assertNotNull(editedLoadBalancer.getRealIpList());
      assertEquals(editedLoadBalancer.getRealIpList().size(), 1);
      assertEquals(Iterables.getOnlyElement(editedLoadBalancer.getRealIpList()).getIp().getIp(), realIp3.getIp());

      int lbCountAfterAddingOneServer = api.getLoadBalancerServices().getLoadBalancerList().size();
      assert lbCountAfterAddingOneServer == lbCountBeforeTest + 1 : "There should be +1 increase in the number of load balancers since the test started";

      // delete the load balancer
      api.getLoadBalancerServices().deleteByName(nameOfLoadBalancer);

      Set<Job> jobs = api.getJobServices().getJobsForObjectName(nameOfLoadBalancer);
      assert "DeleteLoadBalancer".equals(Iterables.getLast(jobs).getCommand().getName());

      assert loadBalancerLatestJobCompleted.apply(createdLoadBalancer);

      int lbCountAfterDeletingTheServer = api.getLoadBalancerServices().getLoadBalancerList().size();
      assert lbCountAfterDeletingTheServer == lbCountBeforeTest : "There should be the same # of load balancers as since the test started";
   }

   /**
    * Tests common server image operations.
    */
   @Test(enabled = true)
   public void testImageLifecycle() {
      GetImageListOptions options = GetImageListOptions.Builder.publicDatabaseServers();
      Set<ServerImage> images = api.getImageServices().getImageList(options);

      Predicate<ServerImage> isDatabaseServer = new Predicate<ServerImage>() {
         @Override
         public boolean apply(@Nullable ServerImage serverImage) {
            return checkNotNull(serverImage).getType() == ServerImageType.DATABASE_SERVER;
         }
      };

      assert Iterables.all(images, isDatabaseServer) : "All of the images should've been of database type";

      ServerImage image = Iterables.getLast(images);
      ServerImage imageFromServer = Iterables
               .getOnlyElement(api.getImageServices().getImagesByName(image.getName()));
      assertEquals(image, imageFromServer);

      try {
         api.getImageServices().editImageDescription(image.getName(), "newDescription");
         throw new TestException("An exception hasn't been thrown where expected; expected GoGridResponseException");
      } catch (GoGridResponseException e) {
         // expected situation - check and proceed
         assertTrue(e.getMessage().contains("GoGridIllegalArgumentException"));
      }

   }

   @Test(enabled = true)
   public void testShellAccess() throws IOException {
      final String nameOfServer = "Server" + String.valueOf(new Date().getTime()).substring(6);
      serversToDeleteAfterTheTests.add(nameOfServer);

      Set<Ip> availableIps = api.getIpServices().getUnassignedIpList();
      Ip availableIp = Iterables.getLast(availableIps);

      Server createdServer = api.getServerServices().addServer(nameOfServer,
               "GSI-f8979644-e646-4711-ad58-d98a5fa3612c", "1", availableIp.getIp());
      assertNotNull(createdServer);
      assert serverLatestJobCompleted.apply(createdServer);

      // get server by name
      Set<Server> response = api.getServerServices().getServersByName(nameOfServer);
      assert response.size() == 1;
      createdServer = Iterables.getOnlyElement(response);

      Map<String, Credentials> credsMap = api.getServerServices().getServerCredentialsList();
      LoginCredentials instanceCredentials = LoginCredentials.fromCredentials(credsMap.get(createdServer.getName()));
      assertNotNull(instanceCredentials);

      HostAndPort socket = HostAndPort.fromParts(createdServer.getIp().getIp(), 22);
      SocketOpen socketOpen = Guice.createInjector().getInstance(SocketOpen.class);
      Predicate<HostAndPort> socketTester = retry(socketOpen, 180, 5, SECONDS);
      socketTester.apply(socket);

      // check that the get credentials call is the same as this
      assertEquals(api.getServerServices().getServerCredentials(createdServer.getId()), instanceCredentials);

      try {
         assertEquals(api.getServerServices().getServerCredentials(Long.MAX_VALUE), null);
      } catch (AssertionError e) {
         e.printStackTrace();
      }

      // delete the server
      api.getServerServices().deleteByName(nameOfServer);
   }

   /**
    * In case anything went wrong during the tests, removes the objects created in the tests.
    */
   @AfterTest
   public void cleanup() {
      for (String serverName : serversToDeleteAfterTheTests) {
         try {
            api.getServerServices().deleteByName(serverName);
         } catch (Exception e) {
            // it's already been deleted - proceed
         }
      }
      for (String loadBalancerName : loadBalancersToDeleteAfterTest) {
         try {
            api.getLoadBalancerServices().deleteByName(loadBalancerName);
         } catch (Exception e) {
            // it's already been deleted - proceed
         }
      }
   }
}
