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

import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeoutException;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.test.TestingServer;
import org.apache.helix.HelixManager;
import org.apache.helix.HelixManagerFactory;
import org.apache.helix.InstanceType;
import org.apache.helix.model.Message;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.amazonaws.services.autoscaling.model.AutoScalingGroup;
import com.amazonaws.services.autoscaling.model.Tag;
import com.amazonaws.services.autoscaling.model.TagDescription;
import com.amazonaws.services.ec2.model.AvailabilityZone;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.io.Closer;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

import org.apache.gobblin.cluster.GobblinClusterConfigurationKeys;
import org.apache.gobblin.cluster.GobblinHelixConstants;
import org.apache.gobblin.cluster.HelixMessageSubTypes;
import org.apache.gobblin.cluster.HelixMessageTestBase;
import org.apache.gobblin.cluster.HelixUtils;
import org.apache.gobblin.cluster.TestHelper;
import org.apache.gobblin.cluster.TestShutdownMessageHandlerFactory;
import org.apache.gobblin.testing.AssertWithBackoff;


/**
 * Unit tests for {@link GobblinAWSClusterLauncher}.
 *
 * @author Abhishek Tiwari
 */
@Test(groups = { "gobblin.aws" })
public class GobblinAWSClusterLauncherTest implements HelixMessageTestBase  {
  public final static Logger LOG = LoggerFactory.getLogger(GobblinAWSClusterLauncherTest.class);

  private CuratorFramework curatorFramework;
  private Config config;

  private GobblinAWSClusterLauncher gobblinAwsClusterLauncher;
  private HelixManager helixManager;

  private String gobblinClusterName = "testCluster";
  private String helixClusterName;
  private String clusterId;

  private TagDescription clusterNameTag = new TagDescription()
      .withKey(GobblinAWSClusterLauncher.CLUSTER_NAME_ASG_TAG).withValue(gobblinClusterName);
  private TagDescription clusterIdTag = new TagDescription()
      .withKey(GobblinAWSClusterLauncher.CLUSTER_ID_ASG_TAG).withValue("dummy");
  private TagDescription masterTypeTag = new TagDescription()
      .withKey(GobblinAWSClusterLauncher.ASG_TYPE_ASG_TAG).withValue(GobblinAWSClusterLauncher.ASG_TYPE_MASTER);
  private TagDescription workerTypeTag = new TagDescription()
      .withKey(GobblinAWSClusterLauncher.ASG_TYPE_ASG_TAG).withValue(GobblinAWSClusterLauncher.ASG_TYPE_WORKERS);
  private AutoScalingGroup masterASG = new AutoScalingGroup()
      .withAutoScalingGroupName("AutoScalingGroup_master")
      .withLaunchConfigurationName("LaunchConfiguration_master")
      .withTags(clusterNameTag, clusterIdTag, masterTypeTag);
  private AutoScalingGroup workerASG = new AutoScalingGroup()
      .withAutoScalingGroupName("AutoScalingGroup_worker")
      .withLaunchConfigurationName("LaunchConfiguration_worker")
      .withTags(clusterNameTag, clusterIdTag, workerTypeTag);
  private AvailabilityZone availabilityZone = new AvailabilityZone().withZoneName("A");
  private Instance instance = new Instance().withPublicIpAddress("0.0.0.0");

  private final Closer closer = Closer.create();

  @Mock
  private AWSSdkClient awsSdkClient;

  @BeforeClass
  public void setUp() throws Exception {

    // Mock AWS SDK calls
    MockitoAnnotations.openMocks(this);

    Mockito.doNothing()
        .when(awsSdkClient)
        .createSecurityGroup(Mockito.anyString(), Mockito.anyString());
    Mockito.doReturn(Lists.<AvailabilityZone>newArrayList(availabilityZone))
        .when(awsSdkClient)
        .getAvailabilityZones();
    Mockito.doReturn("dummy")
        .when(awsSdkClient)
        .createKeyValuePair(Mockito.anyString());
    Mockito.doReturn(Lists.<AutoScalingGroup>newArrayList(masterASG, workerASG))
        .when(awsSdkClient)
        .getAutoScalingGroupsWithTag(Mockito.any(Tag.class));
    Mockito.doReturn(Lists.<Instance>newArrayList(instance))
        .when(awsSdkClient)
        .getInstancesForGroup(Mockito.anyString(), Mockito.anyString());
    Mockito.doReturn(Lists.<S3ObjectSummary>newArrayList())
        .when(awsSdkClient)
        .listS3Bucket(Mockito.anyString(), Mockito.anyString());
    Mockito.doNothing()
        .when(awsSdkClient)
        .addPermissionsToSecurityGroup(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class),
            Mockito.any(Integer.class), Mockito.any(Integer.class));
    Mockito.doNothing()
        .when(awsSdkClient)
        .createAutoScalingGroup(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Integer.class),
            Mockito.any(Integer.class), Mockito.any(Integer.class), Mockito.any(Optional.class),
            Mockito.any(Optional.class), Mockito.any(Optional.class), Mockito.any(Optional.class),
            Mockito.any(Optional.class), Mockito.any(Optional.class), Mockito.any(List.class));
    Mockito.doNothing()
        .when(awsSdkClient)
        .createLaunchConfig(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class),
            Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Optional.class),
            Mockito.any(Optional.class), Mockito.any(Optional.class), Mockito.any(Optional.class),
            Mockito.any(Optional.class), Mockito.any(String.class));
    Mockito
        .doNothing()
        .when(awsSdkClient)
        .deleteAutoScalingGroup(Mockito.any(String.class), Mockito.any(boolean.class));
    Mockito
        .doNothing()
        .when(awsSdkClient)
        .deleteLaunchConfiguration(Mockito.any(String.class));
    Mockito.doNothing()
        .when(awsSdkClient)
        .addPermissionsToSecurityGroup(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class),
            Mockito.any(Integer.class), Mockito.any(Integer.class));

    // Local test Zookeeper
    final TestingServer testingZKServer = this.closer.register(new TestingServer(-1));
    LOG.info("Testing ZK Server listening on: " + testingZKServer.getConnectString());
    this.curatorFramework = TestHelper.createZkClient(testingZKServer, this.closer);

    // Load configuration
    final URL url = GobblinAWSClusterLauncherTest.class.getClassLoader().getResource(
        GobblinAWSClusterLauncherTest.class.getSimpleName() + ".conf");
    Assert.assertNotNull(url, "Could not find resource " + url);
    this.config = ConfigFactory.parseURL(url)
        .withValue("gobblin.cluster.zk.connection.string",
                   ConfigValueFactory.fromAnyRef(testingZKServer.getConnectString()))
        .resolve();
    this.helixClusterName = this.config.getString(GobblinClusterConfigurationKeys.HELIX_CLUSTER_NAME_KEY);

    final String zkConnectionString = this.config.getString(GobblinClusterConfigurationKeys.ZK_CONNECTION_STRING_KEY);
    this.helixManager = HelixManagerFactory
        .getZKHelixManager(this.config.getString(GobblinClusterConfigurationKeys.HELIX_CLUSTER_NAME_KEY),
            TestHelper.TEST_HELIX_INSTANCE_NAME, InstanceType.CONTROLLER, zkConnectionString);

    // Gobblin AWS Cluster Launcher to test
    this.gobblinAwsClusterLauncher = new TestGobblinAWSClusterLauncher(this.config);
  }

  @Test
  public void testCreateHelixCluster() throws Exception {
    // This is tested here instead of in HelixUtilsTest to avoid setting up yet another testing ZooKeeper server.
    HelixUtils
        .createGobblinHelixCluster(this.config.getString(GobblinClusterConfigurationKeys.ZK_CONNECTION_STRING_KEY),
            this.config.getString(GobblinClusterConfigurationKeys.HELIX_CLUSTER_NAME_KEY));

    // Assert to check if there is no pre-existing cluster
    Assert.assertEquals(this.curatorFramework.checkExists().forPath(String.format("/%s",
        this.helixClusterName)).getVersion(), 0);
    Assert.assertEquals(this.curatorFramework.checkExists().forPath(String.format("/%s/CONTROLLER",
        this.helixClusterName)).getVersion(), 0);
  }

  @Test(dependsOnMethods = "testCreateHelixCluster")
  public void testSetupAndSubmitApplication() throws Exception {
    // Setup new cluster
    this.clusterId = this.gobblinAwsClusterLauncher.setupGobblinCluster();
    this.clusterIdTag.setValue(this.clusterId);
  }

  @Test(dependsOnMethods = "testSetupAndSubmitApplication")
  public void testGetReconnectableApplicationId() throws Exception {
    // Assert to check if cluster was created correctly by trying to reconnect to it
    Assert.assertEquals(this.gobblinAwsClusterLauncher.getReconnectableClusterId().get(), this.clusterId);
  }

  @Test(dependsOnMethods = "testGetReconnectableApplicationId")
  public void testSendShutdownRequest() throws Exception {
    // Connect to Helix as Controller and register a shutdown request handler
    this.helixManager.connect();
    this.helixManager.getMessagingService().registerMessageHandlerFactory(GobblinHelixConstants.SHUTDOWN_MESSAGE_TYPE,
        new TestShutdownMessageHandlerFactory(this));

    // Make Gobblin AWS Cluster launcher start a shutdown
    this.gobblinAwsClusterLauncher.connectHelixManager();
    this.gobblinAwsClusterLauncher.sendShutdownRequest();

    Assert.assertEquals(this.curatorFramework.checkExists()
        .forPath(String.format("/%s/CONTROLLER/MESSAGES", this.helixClusterName)).getVersion(), 0);
    GetControllerMessageNumFunc getCtrlMessageNum =
        new GetControllerMessageNumFunc(this.helixClusterName, this.curatorFramework);

    // Assert to check if shutdown message was issued
    AssertWithBackoff assertWithBackoff =
        AssertWithBackoff.create().logger(LoggerFactory.getLogger("testSendShutdownRequest")).timeoutMs(20000);
    assertWithBackoff.assertEquals(getCtrlMessageNum, 1, "1 controller message queued");

    // Assert to check if shutdown message was processed
    // Give Helix sometime to handle the message
    assertWithBackoff.assertEquals(getCtrlMessageNum, 0, "all controller messages processed");
  }

  @AfterClass
  public void tearDown() throws IOException, TimeoutException {
    try {
      this.gobblinAwsClusterLauncher.stop();

      if (this.helixManager.isConnected()) {
        this.helixManager.disconnect();
      }

      this.gobblinAwsClusterLauncher.disconnectHelixManager();
    } finally {
      this.closer.close();
    }
  }

  @Test(enabled = false)
  @Override
  public void assertMessageReception(Message message) {
    Assert.assertEquals(message.getMsgType(), GobblinHelixConstants.SHUTDOWN_MESSAGE_TYPE);
    Assert.assertEquals(message.getMsgSubType(), HelixMessageSubTypes.APPLICATION_MASTER_SHUTDOWN.toString());
  }

  class TestGobblinAWSClusterLauncher extends GobblinAWSClusterLauncher {
    public TestGobblinAWSClusterLauncher(Config config) throws IOException {
      super(config);
    }

    protected AWSSdkClient createAWSSdkClient() {
      return awsSdkClient;
    }
  }

  static class GetControllerMessageNumFunc implements Function<Void, Integer> {
    private final CuratorFramework curatorFramework;
    private final String testName;

    public GetControllerMessageNumFunc(String testName, CuratorFramework curatorFramework) {
      this.curatorFramework = curatorFramework;
      this.testName = testName;
    }

    @Override
    public Integer apply(Void input) {
      try {
        return this.curatorFramework.getChildren().forPath(String.format("/%s/CONTROLLER/MESSAGES",
            this.testName)).size();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
  }
}
