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

import java.net.URL;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.test.TestingServer;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.helix.HelixManager;
import org.apache.helix.HelixManagerFactory;
import org.apache.helix.InstanceType;
import org.apache.helix.model.ClusterConfig;
import org.apache.helix.model.Message;
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.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
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.event.ClusterManagerShutdownRequest;
import org.apache.gobblin.testing.AssertWithBackoff;


/**
 * Unit tests for {@link GobblinClusterManager}.
 *
 * <p>
 *   This class uses a {@link TestingServer} as an embedded ZooKeeper server for testing. The Curator
 *   framework is used to provide a ZooKeeper client. This class also uses the {@link HelixManager} to
 *   act as a testing Helix participant to receive the container (running the {@link GobblinTaskRunner})
 *   shutdown request message.
 * </p>
 *
 * @author Yinan Li
 */
@Test(groups = { "gobblin.cluster" })
public class GobblinClusterManagerTest implements HelixMessageTestBase {
  public final static Logger LOG = LoggerFactory.getLogger(GobblinClusterManagerTest.class);
  public static final String HADOOP_OVERRIDE_PROPERTY_NAME = "prop";

  private TestingServer testingZKServer;

  private HelixManager helixManager;

  private GobblinClusterManager gobblinClusterManager;

  @BeforeClass
  public void setUp() throws Exception {
    // Use a random ZK port
    this.testingZKServer = new TestingServer(-1);
    LOG.info("Testing ZK Server listening on: " + testingZKServer.getConnectString());

    URL url = GobblinClusterManagerTest.class.getClassLoader().getResource(
        GobblinClusterManager.class.getSimpleName() + ".conf");
    Assert.assertNotNull(url, "Could not find resource " + url);

    Config config = ConfigFactory.parseURL(url)
        .withValue("gobblin.cluster.zk.connection.string",
                   ConfigValueFactory.fromAnyRef(testingZKServer.getConnectString()))
        .withValue(GobblinClusterConfigurationKeys.HELIX_TASK_QUOTA_CONFIG_KEY,
            ConfigValueFactory.fromAnyRef("DEFAULT:1,OTHER:10"))
        .withValue(GobblinClusterConfigurationKeys.HADOOP_CONFIG_OVERRIDES_PREFIX + "." + HADOOP_OVERRIDE_PROPERTY_NAME,
            ConfigValueFactory.fromAnyRef("value"))
        .withValue(GobblinClusterConfigurationKeys.HADOOP_CONFIG_OVERRIDES_PREFIX + "." + "fs.file.impl.disable.cache",
            ConfigValueFactory.fromAnyRef("true"))
        .resolve();

    String zkConnectionString = config.getString(GobblinClusterConfigurationKeys.ZK_CONNECTION_STRING_KEY);
    HelixUtils.createGobblinHelixCluster(zkConnectionString,
        config.getString(GobblinClusterConfigurationKeys.HELIX_CLUSTER_NAME_KEY));

    this.helixManager = HelixManagerFactory
        .getZKHelixManager(config.getString(GobblinClusterConfigurationKeys.HELIX_CLUSTER_NAME_KEY),
            TestHelper.TEST_HELIX_INSTANCE_NAME, InstanceType.PARTICIPANT, zkConnectionString);
    this.helixManager.connect();
    this.helixManager.getMessagingService().registerMessageHandlerFactory(GobblinHelixConstants.SHUTDOWN_MESSAGE_TYPE,
        new TestShutdownMessageHandlerFactory(this));

    this.gobblinClusterManager =
        new GobblinClusterManager(GobblinClusterManagerTest.class.getSimpleName(), TestHelper.TEST_APPLICATION_ID, config,
            Optional.<Path>absent());
    this.gobblinClusterManager.getEventBus().register(this.gobblinClusterManager);
    this.gobblinClusterManager.connectHelixManager();
  }

  static class GetInstanceMessageNumFunc implements Function<Void, Integer> {

    private final CuratorFramework curatorFramework;
    private final String testName;

    public GetInstanceMessageNumFunc(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/INSTANCES/%s/MESSAGES", this.testName,
                TestHelper.TEST_HELIX_INSTANCE_NAME)).size();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
  }

  @Test
  public void testQuotaConfig() throws Exception {
    this.gobblinClusterManager.configureHelixQuotaBasedTaskScheduling();

    ClusterConfig clusterConfig =
        this.gobblinClusterManager.multiManager.getJobClusterHelixManager().getConfigAccessor()
        .getClusterConfig(GobblinClusterManagerTest.class.getSimpleName());

    Assert.assertEquals(clusterConfig.getTaskQuotaRatio("DEFAULT"), "1");
    Assert.assertEquals(clusterConfig.getTaskQuotaRatio("OTHER"), "10");
  }

  @Test
  public void testSendShutdownRequest() throws Exception {
    Logger log = LoggerFactory.getLogger("testSendShutdownRequest");
    Closer closer = Closer.create();
    try {
      CuratorFramework curatorFramework = TestHelper.createZkClient(this.testingZKServer, closer);
      final GetInstanceMessageNumFunc getMessageNumFunc =
          new GetInstanceMessageNumFunc(GobblinClusterManagerTest.class.getSimpleName(),
              curatorFramework);
      AssertWithBackoff assertWithBackoff =
          AssertWithBackoff.create().logger(log).timeoutMs(30000);

      this.gobblinClusterManager.sendShutdownRequest();

      Assert.assertEquals(curatorFramework.checkExists().forPath(String
          .format("/%s/INSTANCES/%s/MESSAGES", GobblinClusterManagerTest.class.getSimpleName(),
              TestHelper.TEST_HELIX_INSTANCE_NAME)).getVersion(), 0);

      assertWithBackoff.assertEquals(getMessageNumFunc, 1, "1 message queued");

      // Give Helix sometime to handle the message
      assertWithBackoff.assertEquals(getMessageNumFunc, 0, "all messages processed");
    } finally {
      closer.close();
    }
  }

  @Test(dependsOnMethods = "testSendShutdownRequest")
  public void testHandleClusterManagerShutdownRequest() throws Exception {
    Logger log = LoggerFactory.getLogger("testHandleClusterManagerShutdownRequest");
    this.gobblinClusterManager.getEventBus().post(new ClusterManagerShutdownRequest());
    AssertWithBackoff.create().logger(log).timeoutMs(20000)
        .assertTrue(new Predicate<Void>() {
          @Override public boolean apply(Void input) {
            return !GobblinClusterManagerTest.this.gobblinClusterManager.isHelixManagerConnected();
          }
        }, "Cluster Manager shutdown");
  }

  @Test
  public void testBuildFileSystemConfig() {
    FileSystem fileSystem = this.gobblinClusterManager.getFs();
    Assert.assertEquals(fileSystem.getConf().get(HADOOP_OVERRIDE_PROPERTY_NAME), "value");
  }

  @AfterClass
  public void tearDown() throws Exception {
    try {
      if (this.helixManager.isConnected()) {
        this.helixManager.disconnect();
      }
      this.gobblinClusterManager.disconnectHelixManager();
    } catch (Throwable t) {
      Assert.fail();
    } finally {
      this.testingZKServer.close();
    }
  }

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