package org.apache.helix.gateway.base.util;

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

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.apache.helix.BaseDataAccessor;
import org.apache.helix.HelixManager;
import org.apache.helix.PropertyKey.Builder;
import org.apache.helix.controller.rebalancer.strategy.CrushEdRebalanceStrategy;
import org.apache.helix.controller.rebalancer.strategy.RebalanceStrategy;
import org.apache.helix.manager.zk.ZKHelixAdmin;
import org.apache.helix.manager.zk.ZKHelixDataAccessor;
import org.apache.helix.manager.zk.ZNRecordSerializer;
import org.apache.helix.manager.zk.ZkBaseDataAccessor;
import org.apache.helix.model.CurrentState;
import org.apache.helix.model.ExternalView;
import org.apache.helix.model.IdealState.RebalanceMode;
import org.apache.helix.model.Message;
import org.apache.helix.model.Message.MessageType;
import org.apache.helix.model.StateModelDefinition;
import org.apache.helix.model.StateModelDefinition.StateModelDefinitionProperty;
import org.apache.helix.store.zk.ZNode;
import org.apache.helix.tools.ClusterSetup;
import org.apache.helix.util.ZKClientPool;
import org.apache.helix.zookeeper.api.client.HelixZkClient;
import org.apache.helix.zookeeper.api.client.RealmAwareZkClient;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.helix.zookeeper.impl.factory.SharedZkClientFactory;
import org.apache.helix.zookeeper.zkclient.IDefaultNameSpace;
import org.apache.helix.zookeeper.zkclient.ZkClient;
import org.apache.helix.zookeeper.zkclient.ZkServer;
import org.apache.helix.zookeeper.zkclient.exception.ZkNoNodeException;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;


public class TestHelper {
  private static final Logger LOG = LoggerFactory.getLogger(TestHelper.class);
  public static final long WAIT_DURATION = 60 * 1000L; // 60 seconds
  public static final int DEFAULT_REBALANCE_PROCESSING_WAIT_TIME = 1500;

  /**
   * Returns a unused random port.
   */
  public static int getRandomPort() throws IOException {
    ServerSocket sock = new ServerSocket();
    sock.bind(null);
    int port = sock.getLocalPort();
    sock.close();

    return port;
  }

  static public ZkServer startZkServer(final String zkAddress) throws Exception {
    List<String> empty = Collections.emptyList();
    return TestHelper.startZkServer(zkAddress, empty, true);
  }

  static public ZkServer startZkServer(final String zkAddress, final String rootNamespace) throws Exception {
    List<String> rootNamespaces = new ArrayList<String>();
    rootNamespaces.add(rootNamespace);
    return TestHelper.startZkServer(zkAddress, rootNamespaces, true);
  }

  static public ZkServer startZkServer(final String zkAddress, final List<String> rootNamespaces) throws Exception {
    return startZkServer(zkAddress, rootNamespaces, true);
  }

  static public ZkServer startZkServer(final String zkAddress, final List<String> rootNamespaces, boolean overwrite)
      throws Exception {
    System.out.println("Start zookeeper at " + zkAddress + " in thread " + Thread.currentThread().getName());

    String zkDir = zkAddress.replace(':', '_');
    final String logDir = "/tmp/" + zkDir + "/logs";
    final String dataDir = "/tmp/" + zkDir + "/dataDir";
    if (overwrite) {
      FileUtils.deleteDirectory(new File(dataDir));
      FileUtils.deleteDirectory(new File(logDir));
    }
    ZKClientPool.reset();

    IDefaultNameSpace defaultNameSpace = new IDefaultNameSpace() {
      @Override
      public void createDefaultNameSpace(ZkClient zkClient) {
        if (rootNamespaces == null) {
          return;
        }

        for (String rootNamespace : rootNamespaces) {
          try {
            zkClient.deleteRecursive(rootNamespace);
          } catch (Exception e) {
            LOG.error("fail to deleteRecursive path:" + rootNamespace, e);
          }
        }
      }
    };

    int port = Integer.parseInt(zkAddress.substring(zkAddress.lastIndexOf(':') + 1));
    ZkServer zkServer = new ZkServer(dataDir, logDir, defaultNameSpace, port);
    zkServer.start();

    return zkServer;
  }

  static public void stopZkServer(ZkServer zkServer) {
    if (zkServer != null) {
      zkServer.shutdown();
      System.out.println(
          "Shut down zookeeper at port " + zkServer.getPort() + " in thread " + Thread.currentThread().getName());
    }
  }

  public static void setupEmptyCluster(HelixZkClient zkClient, String clusterName) {
    ZKHelixAdmin admin = new ZKHelixAdmin(zkClient);
    admin.addCluster(clusterName, true);
  }

  /**
   * convert T[] to set<T>
   * @param s
   * @return
   */
  public static <T> Set<T> setOf(T... s) {
    Set<T> set = new HashSet<T>(Arrays.asList(s));
    return set;
  }

  /**
   * generic method for verification with a timeout
   * @param verifierName
   * @param args
   */
  public static void verifyWithTimeout(String verifierName, long timeout, Object... args) {
    final long sleepInterval = 1000; // in ms
    final int loop = (int) (timeout / sleepInterval) + 1;
    try {
      boolean result = false;
      int i = 0;
      for (; i < loop; i++) {
        Thread.sleep(sleepInterval);
        // verifier should be static method
        result = (Boolean) TestHelper.getMethod(verifierName).invoke(null, args);

        if (result == true) {
          break;
        }
      }

      // debug
      // LOG.info(verifierName + ": wait " + ((i + 1) * 1000) + "ms to verify ("
      // + result + ")");
      System.err.println(verifierName + ": wait " + ((i + 1) * 1000) + "ms to verify " + " (" + result + ")");
      LOG.debug("args:" + Arrays.toString(args));
      // System.err.println("args:" + Arrays.toString(args));

      if (result == false) {
        LOG.error(verifierName + " fails");
        LOG.error("args:" + Arrays.toString(args));
      }

      Assert.assertTrue(result);
    } catch (Exception e) {
      LOG.error("Exception in verify: " + verifierName, e);
    }
  }

  private static Method getMethod(String name) {
    Method[] methods = TestHelper.class.getMethods();
    for (Method method : methods) {
      if (name.equals(method.getName())) {
        return method;
      }
    }
    return null;
  }

  public static boolean verifyEmptyCurStateAndExtView(String clusterName, String resourceName,
      Set<String> instanceNames, String zkAddr) {
    HelixZkClient zkClient =
        SharedZkClientFactory.getInstance().buildZkClient(new HelixZkClient.ZkConnectionConfig(zkAddr));
    zkClient.setZkSerializer(new ZNRecordSerializer());

    try {
      ZKHelixDataAccessor accessor = new ZKHelixDataAccessor(clusterName, new ZkBaseDataAccessor<ZNRecord>(zkClient));
      Builder keyBuilder = accessor.keyBuilder();

      for (String instanceName : instanceNames) {
        List<String> sessionIds = accessor.getChildNames(keyBuilder.sessions(instanceName));

        for (String sessionId : sessionIds) {
          CurrentState curState = accessor.getProperty(keyBuilder.currentState(instanceName, sessionId, resourceName));

          if (curState != null && curState.getRecord().getMapFields().size() != 0) {
            return false;
          }

          CurrentState taskCurState =
              accessor.getProperty(keyBuilder.taskCurrentState(instanceName, sessionId, resourceName));

          if (taskCurState != null && taskCurState.getRecord().getMapFields().size() != 0) {
            return false;
          }
        }

        ExternalView extView = accessor.getProperty(keyBuilder.externalView(resourceName));

        if (extView != null && extView.getRecord().getMapFields().size() != 0) {
          return false;
        }
      }

      return true;
    } finally {
      zkClient.close();
    }
  }

  public static boolean verifyNotConnected(HelixManager manager) {
    return !manager.isConnected();
  }

  public static void setupCluster(String clusterName, String zkAddr, int startPort, String participantNamePrefix,
      String resourceNamePrefix, int resourceNb, int partitionNb, int nodesNb, int replica, String stateModelDef,
      boolean doRebalance) throws Exception {
    TestHelper.setupCluster(clusterName, zkAddr, startPort, participantNamePrefix, resourceNamePrefix, resourceNb,
        partitionNb, nodesNb, replica, stateModelDef, RebalanceMode.SEMI_AUTO, doRebalance);
  }

  public static void setupCluster(String clusterName, String zkAddr, int startPort, String participantNamePrefix,
      String resourceNamePrefix, int resourceNb, int partitionNb, int nodesNb, int replica, String stateModelDef,
      RebalanceMode mode, boolean doRebalance) {
    HelixZkClient zkClient =
        SharedZkClientFactory.getInstance().buildZkClient(new HelixZkClient.ZkConnectionConfig(zkAddr));
    try {
      if (zkClient.exists("/" + clusterName)) {
        LOG.warn("Cluster already exists:" + clusterName + ". Deleting it");
        zkClient.deleteRecursively("/" + clusterName);
      }

      ClusterSetup setupTool = new ClusterSetup(zkAddr);
      setupTool.addCluster(clusterName, true);

      for (int i = 0; i < nodesNb; i++) {
        int port = startPort + i;
        setupTool.addInstanceToCluster(clusterName, participantNamePrefix + "_" + port);
      }

      for (int i = 0; i < resourceNb; i++) {
        String resourceName = resourceNamePrefix + i;
        setupTool.addResourceToCluster(clusterName, resourceName, partitionNb, stateModelDef, mode.name(),
            mode == RebalanceMode.FULL_AUTO ? CrushEdRebalanceStrategy.class.getName()
                : RebalanceStrategy.DEFAULT_REBALANCE_STRATEGY);
        if (doRebalance) {
          setupTool.rebalanceStorageCluster(clusterName, resourceName, replica);
        }
      }
    } finally {
      zkClient.close();
    }
  }

  public static void dropCluster(String clusterName, RealmAwareZkClient zkClient) {
    ClusterSetup setupTool = new ClusterSetup(zkClient);
    dropCluster(clusterName, zkClient, setupTool);
  }

  public static void dropCluster(String clusterName, RealmAwareZkClient zkClient, ClusterSetup setup) {
    String namespace = "/" + clusterName;
    if (zkClient.exists(namespace)) {
      try {
        setup.deleteCluster(clusterName);
      } catch (Exception ex) {
        // Failed to delete, give some more time for connections to drop
        try {
          Thread.sleep(3000L);
          setup.deleteCluster(clusterName);
        } catch (Exception ignored) {
          // OK - just ignore
        }
      }
    }
  }

  /**
   * @param stateMap
   *          : "ResourceName/partitionKey" -> setOf(instances)
   * @param state
   *          : MASTER|SLAVE|ERROR...
   */
  public static void verifyState(String clusterName, String zkAddr, Map<String, Set<String>> stateMap, String state) {
    HelixZkClient zkClient =
        SharedZkClientFactory.getInstance().buildZkClient(new HelixZkClient.ZkConnectionConfig(zkAddr));
    zkClient.setZkSerializer(new ZNRecordSerializer());

    try {
      ZKHelixDataAccessor accessor = new ZKHelixDataAccessor(clusterName, new ZkBaseDataAccessor<ZNRecord>(zkClient));
      Builder keyBuilder = accessor.keyBuilder();

      for (String resGroupPartitionKey : stateMap.keySet()) {
        Map<String, String> retMap = getResourceAndPartitionKey(resGroupPartitionKey);
        String resGroup = retMap.get("RESOURCE");
        String partitionKey = retMap.get("PARTITION");

        ExternalView extView = accessor.getProperty(keyBuilder.externalView(resGroup));
        for (String instance : stateMap.get(resGroupPartitionKey)) {
          String actualState = extView.getStateMap(partitionKey).get(instance);
          Assert.assertNotNull(actualState,
              "externalView doesn't contain state for " + resGroup + "/" + partitionKey + " on " + instance
                  + " (expect " + state + ")");

          Assert.assertEquals(actualState, state,
              "externalView for " + resGroup + "/" + partitionKey + " on " + instance + " is " + actualState
                  + " (expect " + state + ")");
        }
      }
    } finally {
      zkClient.close();
    }
  }

  /**
   * @param resourcePartition
   *          : key is in form of "resource/partitionKey" or "resource_x"
   * @return
   */
  private static Map<String, String> getResourceAndPartitionKey(String resourcePartition) {
    String resourceName;
    String partitionName;
    int idx = resourcePartition.indexOf('/');
    if (idx > -1) {
      resourceName = resourcePartition.substring(0, idx);
      partitionName = resourcePartition.substring(idx + 1);
    } else {
      idx = resourcePartition.lastIndexOf('_');
      resourceName = resourcePartition.substring(0, idx);
      partitionName = resourcePartition;
    }

    Map<String, String> retMap = new HashMap<String, String>();
    retMap.put("RESOURCE", resourceName);
    retMap.put("PARTITION", partitionName);
    return retMap;
  }

  public static <T> Map<String, T> startThreadsConcurrently(final int nrThreads, final Callable<T> method,
      final long timeout) {
    final CountDownLatch startLatch = new CountDownLatch(1);
    final CountDownLatch finishCounter = new CountDownLatch(nrThreads);
    final Map<String, T> resultsMap = new ConcurrentHashMap<String, T>();
    final List<Thread> threadList = new ArrayList<Thread>();

    for (int i = 0; i < nrThreads; i++) {
      Thread thread = new Thread() {
        @Override
        public void run() {
          try {
            boolean isTimeout = !startLatch.await(timeout, TimeUnit.SECONDS);
            if (isTimeout) {
              LOG.error("Timeout while waiting for start latch");
            }
          } catch (InterruptedException ex) {
            LOG.error("Interrupted while waiting for start latch");
          }

          try {
            T result = method.call();
            if (result != null) {
              resultsMap.put("thread_" + this.getId(), result);
            }
            LOG.debug("result=" + result);
          } catch (Exception e) {
            LOG.error("Exeption in executing " + method.getClass().getName(), e);
          }

          finishCounter.countDown();
        }
      };
      threadList.add(thread);
      thread.start();
    }
    startLatch.countDown();

    // wait for all thread to complete
    try {
      boolean isTimeout = !finishCounter.await(timeout, TimeUnit.SECONDS);
      if (isTimeout) {
        LOG.error("Timeout while waiting for finish latch. Interrupt all threads");
        for (Thread thread : threadList) {
          thread.interrupt();
        }
      }
    } catch (InterruptedException e) {
      LOG.error("Interrupted while waiting for finish latch", e);
    }

    return resultsMap;
  }

  public static Message createMessage(String msgId, String fromState, String toState, String tgtName,
      String resourceName, String partitionName) {
    Message msg = new Message(MessageType.STATE_TRANSITION, msgId);
    msg.setFromState(fromState);
    msg.setToState(toState);
    msg.setTgtName(tgtName);
    msg.setResourceName(resourceName);
    msg.setPartitionName(partitionName);
    msg.setStateModelDef("MasterSlave");

    return msg;
  }

  public static String getTestMethodName() {
    StackTraceElement[] calls = Thread.currentThread().getStackTrace();
    return calls[2].getMethodName();
  }

  public static String getTestClassName() {
    StackTraceElement[] calls = Thread.currentThread().getStackTrace();
    String fullClassName = calls[2].getClassName();
    return fullClassName.substring(fullClassName.lastIndexOf('.') + 1);
  }

  public static <T> Map<String, T> startThreadsConcurrently(final List<Callable<T>> methods, final long timeout) {
    final int nrThreads = methods.size();
    final CountDownLatch startLatch = new CountDownLatch(1);
    final CountDownLatch finishCounter = new CountDownLatch(nrThreads);
    final Map<String, T> resultsMap = new ConcurrentHashMap<String, T>();
    final List<Thread> threadList = new ArrayList<Thread>();

    for (int i = 0; i < nrThreads; i++) {
      final Callable<T> method = methods.get(i);

      Thread thread = new Thread() {
        @Override
        public void run() {
          try {
            boolean isTimeout = !startLatch.await(timeout, TimeUnit.SECONDS);
            if (isTimeout) {
              LOG.error("Timeout while waiting for start latch");
            }
          } catch (InterruptedException ex) {
            LOG.error("Interrupted while waiting for start latch");
          }

          try {
            T result = method.call();
            if (result != null) {
              resultsMap.put("thread_" + this.getId(), result);
            }
            LOG.debug("result=" + result);
          } catch (Exception e) {
            LOG.error("Exeption in executing " + method.getClass().getName(), e);
          }

          finishCounter.countDown();
        }
      };
      threadList.add(thread);
      thread.start();
    }
    startLatch.countDown();

    // wait for all thread to complete
    try {
      boolean isTimeout = !finishCounter.await(timeout, TimeUnit.SECONDS);
      if (isTimeout) {
        LOG.error("Timeout while waiting for finish latch. Interrupt all threads");
        for (Thread thread : threadList) {
          thread.interrupt();
        }
      }
    } catch (InterruptedException e) {
      LOG.error("Interrupted while waiting for finish latch", e);
    }

    return resultsMap;
  }

  public static void printCache(Map<String, ZNode> cache) {
    System.out.println("START:Print cache");
    TreeMap<String, ZNode> map = new TreeMap<String, ZNode>();
    map.putAll(cache);

    for (String key : map.keySet()) {
      ZNode node = map.get(key);
      TreeSet<String> childSet = new TreeSet<String>();
      childSet.addAll(node.getChildSet());
      System.out.print(
          key + "=" + node.getData() + ", " + childSet + ", " + (node.getStat() == null ? "null\n" : node.getStat()));
    }
    System.out.println("END:Print cache");
  }

  public static void readZkRecursive(String path, Map<String, ZNode> map, HelixZkClient zkclient) {
    try {
      Stat stat = new Stat();
      ZNRecord record = zkclient.readData(path, stat);
      List<String> childNames = zkclient.getChildren(path);
      ZNode node = new ZNode(path, record, stat);
      node.addChildren(childNames);
      map.put(path, node);

      for (String childName : childNames) {
        String childPath = path + "/" + childName;
        readZkRecursive(childPath, map, zkclient);
      }
    } catch (ZkNoNodeException e) {
      // OK
    }
  }

  public static void readZkRecursive(String path, Map<String, ZNode> map, BaseDataAccessor<ZNRecord> zkAccessor) {
    try {
      Stat stat = new Stat();
      ZNRecord record = zkAccessor.get(path, stat, 0);
      List<String> childNames = zkAccessor.getChildNames(path, 0);
      // System.out.println("childNames: " + childNames);
      ZNode node = new ZNode(path, record, stat);
      node.addChildren(childNames);
      map.put(path, node);

      if (childNames != null && !childNames.isEmpty()) {
        for (String childName : childNames) {
          String childPath = path + "/" + childName;
          readZkRecursive(childPath, map, zkAccessor);
        }
      }
    } catch (ZkNoNodeException e) {
      // OK
    }
  }

  public static boolean verifyZkCache(List<String> paths, BaseDataAccessor<ZNRecord> zkAccessor, HelixZkClient zkclient,
      boolean needVerifyStat) {
    // read everything
    Map<String, ZNode> zkMap = new HashMap<String, ZNode>();
    Map<String, ZNode> cache = new HashMap<String, ZNode>();
    for (String path : paths) {
      readZkRecursive(path, zkMap, zkclient);
      readZkRecursive(path, cache, zkAccessor);
    }
    // printCache(map);

    return verifyZkCache(paths, null, cache, zkMap, needVerifyStat);
  }

  public static boolean verifyZkCache(List<String> paths, Map<String, ZNode> cache, HelixZkClient zkclient,
      boolean needVerifyStat) {
    return verifyZkCache(paths, null, cache, zkclient, needVerifyStat);
  }

  public static boolean verifyZkCache(List<String> paths, List<String> pathsExcludeForStat, Map<String, ZNode> cache,
      HelixZkClient zkclient, boolean needVerifyStat) {
    // read everything on zk under paths
    Map<String, ZNode> zkMap = new HashMap<String, ZNode>();
    for (String path : paths) {
      readZkRecursive(path, zkMap, zkclient);
    }
    // printCache(map);

    return verifyZkCache(paths, pathsExcludeForStat, cache, zkMap, needVerifyStat);
  }

  public static boolean verifyZkCache(List<String> paths, List<String> pathsExcludeForStat, Map<String, ZNode> cache,
      Map<String, ZNode> zkMap, boolean needVerifyStat) {
    // equal size
    if (zkMap.size() != cache.size()) {
      System.err.println("size mismatch: cacheSize: " + cache.size() + ", zkMapSize: " + zkMap.size());
      System.out.println("cache: (" + cache.size() + ")");
      TestHelper.printCache(cache);

      System.out.println("zkMap: (" + zkMap.size() + ")");
      TestHelper.printCache(zkMap);

      return false;
    }

    // everything in cache is also in map
    for (String path : cache.keySet()) {
      ZNode cacheNode = cache.get(path);
      ZNode zkNode = zkMap.get(path);

      if (zkNode == null) {
        // in cache but not on zk
        System.err.println("path: " + path + " in cache but not on zk: inCacheNode: " + cacheNode);
        return false;
      }

      if ((zkNode.getData() == null && cacheNode.getData() != null) || (zkNode.getData() != null
          && cacheNode.getData() == null) || (zkNode.getData() != null && cacheNode.getData() != null
          && !zkNode.getData().equals(cacheNode.getData()))) {
        // data not equal
        System.err.println(
            "data mismatch on path: " + path + ", inCache: " + cacheNode.getData() + ", onZk: " + zkNode.getData());
        return false;
      }

      if ((zkNode.getChildSet() == null && cacheNode.getChildSet() != null) || (zkNode.getChildSet() != null
          && cacheNode.getChildSet() == null) || (zkNode.getChildSet() != null && cacheNode.getChildSet() != null
          && !zkNode.getChildSet().equals(cacheNode.getChildSet()))) {
        // childSet not equal
        System.err.println("childSet mismatch on path: " + path + ", inCache: " + cacheNode.getChildSet() + ", onZk: "
            + zkNode.getChildSet());
        return false;
      }

      if (needVerifyStat && pathsExcludeForStat != null && !pathsExcludeForStat.contains(path)) {
        if (cacheNode.getStat() == null || !zkNode.getStat().equals(cacheNode.getStat())) {
          // stat not equal
          System.err.println(
              "Stat mismatch on path: " + path + ", inCache: " + cacheNode.getStat() + ", onZk: " + zkNode.getStat());
          return false;
        }
      }
    }

    return true;
  }

  public static StateModelDefinition generateStateModelDefForBootstrap() {
    ZNRecord record = new ZNRecord("Bootstrap");
    record.setSimpleField(StateModelDefinitionProperty.INITIAL_STATE.toString(), "IDLE");
    List<String> statePriorityList = new ArrayList<String>();
    statePriorityList.add("ONLINE");
    statePriorityList.add("BOOTSTRAP");
    statePriorityList.add("OFFLINE");
    statePriorityList.add("IDLE");
    statePriorityList.add("DROPPED");
    statePriorityList.add("ERROR");
    record.setListField(StateModelDefinitionProperty.STATE_PRIORITY_LIST.toString(), statePriorityList);
    for (String state : statePriorityList) {
      String key = state + ".meta";
      Map<String, String> metadata = new HashMap<String, String>();
      if (state.equals("ONLINE")) {
        metadata.put("count", StateModelDefinition.STATE_REPLICA_COUNT_ALL_REPLICAS);
        record.setMapField(key, metadata);
      } else if (state.equals("BOOTSTRAP")) {
        metadata.put("count", "-1");
        record.setMapField(key, metadata);
      } else if (state.equals("OFFLINE")) {
        metadata.put("count", "-1");
        record.setMapField(key, metadata);
      } else if (state.equals("IDLE")) {
        metadata.put("count", "-1");
        record.setMapField(key, metadata);
      } else if (state.equals("DROPPED")) {
        metadata.put("count", "-1");
        record.setMapField(key, metadata);
      } else if (state.equals("ERROR")) {
        metadata.put("count", "-1");
        record.setMapField(key, metadata);
      }
    }

    for (String state : statePriorityList) {
      String key = state + ".next";
      if (state.equals("ONLINE")) {
        Map<String, String> metadata = new HashMap<String, String>();
        metadata.put("BOOTSTRAP", "OFFLINE");
        metadata.put("OFFLINE", "OFFLINE");
        metadata.put("DROPPED", "OFFLINE");
        metadata.put("IDLE", "OFFLINE");
        record.setMapField(key, metadata);
      } else if (state.equals("BOOTSTRAP")) {
        Map<String, String> metadata = new HashMap<String, String>();
        metadata.put("ONLINE", "ONLINE");
        metadata.put("OFFLINE", "OFFLINE");
        metadata.put("DROPPED", "OFFLINE");
        metadata.put("IDLE", "OFFLINE");
        record.setMapField(key, metadata);
      } else if (state.equals("OFFLINE")) {
        Map<String, String> metadata = new HashMap<String, String>();
        metadata.put("ONLINE", "BOOTSTRAP");
        metadata.put("BOOTSTRAP", "BOOTSTRAP");
        metadata.put("DROPPED", "IDLE");
        metadata.put("IDLE", "IDLE");
        record.setMapField(key, metadata);
      } else if (state.equals("IDLE")) {
        Map<String, String> metadata = new HashMap<String, String>();
        metadata.put("ONLINE", "OFFLINE");
        metadata.put("BOOTSTRAP", "OFFLINE");
        metadata.put("OFFLINE", "OFFLINE");
        metadata.put("DROPPED", "DROPPED");
        record.setMapField(key, metadata);
      } else if (state.equals("ERROR")) {
        Map<String, String> metadata = new HashMap<String, String>();
        metadata.put("IDLE", "IDLE");
        record.setMapField(key, metadata);
      }
    }
    List<String> stateTransitionPriorityList = new ArrayList<String>();
    stateTransitionPriorityList.add("ONLINE-OFFLINE");
    stateTransitionPriorityList.add("BOOTSTRAP-ONLINE");
    stateTransitionPriorityList.add("OFFLINE-BOOTSTRAP");
    stateTransitionPriorityList.add("BOOTSTRAP-OFFLINE");
    stateTransitionPriorityList.add("OFFLINE-IDLE");
    stateTransitionPriorityList.add("IDLE-OFFLINE");
    stateTransitionPriorityList.add("IDLE-DROPPED");
    stateTransitionPriorityList.add("ERROR-IDLE");
    record.setListField(StateModelDefinitionProperty.STATE_TRANSITION_PRIORITYLIST.toString(),
        stateTransitionPriorityList);
    return new StateModelDefinition(record);
  }

  public static String znrecordToString(ZNRecord record) {
    StringBuffer sb = new StringBuffer();
    sb.append(record.getId() + "\n");
    Map<String, String> simpleFields = record.getSimpleFields();
    if (simpleFields != null) {
      sb.append("simpleFields\n");
      for (String key : simpleFields.keySet()) {
        sb.append("  " + key + "\t: " + simpleFields.get(key) + "\n");
      }
    }

    Map<String, List<String>> listFields = record.getListFields();
    sb.append("listFields\n");
    for (String key : listFields.keySet()) {
      List<String> list = listFields.get(key);
      sb.append("  " + key + "\t: ");
      for (String listValue : list) {
        sb.append(listValue + ", ");
      }
      sb.append("\n");
    }

    Map<String, Map<String, String>> mapFields = record.getMapFields();
    sb.append("mapFields\n");
    for (String key : mapFields.keySet()) {
      Map<String, String> map = mapFields.get(key);
      sb.append("  " + key + "\t: \n");
      for (String mapKey : map.keySet()) {
        sb.append("    " + mapKey + "\t: " + map.get(mapKey) + "\n");
      }
    }

    return sb.toString();
  }

  public interface Verifier {
    boolean verify() throws Exception;
  }

  public static boolean verify(Verifier verifier, long timeout) throws Exception {
    long start = System.currentTimeMillis();
    do {
      boolean result = verifier.verify();
      boolean isTimedout = (System.currentTimeMillis() - start) > timeout;
      if (result || isTimedout) {
        if (isTimedout && !result) {
          LOG.error("verifier time out, consider try longer timeout, stack trace{}",
              Arrays.asList(Thread.currentThread().getStackTrace()));
        }
        return result;
      }
      Thread.sleep(50);
    } while (true);
  }
}
