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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.hadoop.hbase.ClusterMetrics.Option;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.apache.hadoop.hbase.testclassification.MiscTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.common.collect.Maps;

@Tag(MiscTests.TAG)
@Tag(MediumTests.TAG)
public class TestRegionMetrics {

  private static final Logger LOG = LoggerFactory.getLogger(TestRegionMetrics.class);
  private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
  private static Admin admin;

  private static final TableName TABLE_1 = TableName.valueOf("table_1");
  private static final TableName TABLE_2 = TableName.valueOf("table_2");
  private static final TableName TABLE_3 = TableName.valueOf("table_3");
  private static final TableName[] tables = new TableName[] { TABLE_1, TABLE_2, TABLE_3 };
  private static final int MSG_INTERVAL = 500; // ms

  @BeforeAll
  public static void beforeClass() throws Exception {
    // Make servers report eagerly. This test is about looking at the cluster status reported.
    // Make it so we don't have to wait around too long to see change.
    UTIL.getConfiguration().setInt("hbase.regionserver.msginterval", MSG_INTERVAL);
    UTIL.startMiniCluster(4);
    admin = UTIL.getAdmin();
    admin.balancerSwitch(false, true);
    byte[] FAMILY = Bytes.toBytes("f");
    for (TableName tableName : tables) {
      Table table = UTIL.createMultiRegionTable(tableName, FAMILY, 16);
      UTIL.waitTableAvailable(tableName);
      UTIL.loadTable(table, FAMILY);
    }
  }

  @AfterAll
  public static void afterClass() throws Exception {
    for (TableName table : tables) {
      UTIL.deleteTableIfAny(table);
    }
    UTIL.shutdownMiniCluster();
  }

  @Test
  public void testRegionMetrics() throws Exception {

    // Check if regions match with the RegionMetrics from the server
    for (ServerName serverName : admin.getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS))
      .getLiveServerMetrics().keySet()) {
      List<RegionInfo> regions = admin.getRegions(serverName);
      Collection<RegionMetrics> regionMetricsList = admin.getRegionMetrics(serverName);
      checkRegionsAndRegionMetrics(regions, regionMetricsList);
    }

    // Check if regionMetrics matches the table's regions and nothing is missed
    for (TableName table : new TableName[] { TABLE_1, TABLE_2, TABLE_3 }) {
      List<RegionInfo> tableRegions = admin.getRegions(table);

      List<RegionMetrics> regionMetrics = new ArrayList<>();
      for (ServerName serverName : admin.getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS))
        .getLiveServerMetrics().keySet()) {
        regionMetrics.addAll(admin.getRegionMetrics(serverName, table));
      }
      checkRegionsAndRegionMetrics(tableRegions, regionMetrics);
    }

    // Just wait here. If this fixes the test, come back and do a better job.
    // Would have to redo the below so can wait on cluster status changing.
    // Admin#getClusterMetrics retrieves data from HMaster. Admin#getRegionMetrics, by contrast,
    // get the data from RS. Hence, it will fail if we do the assert check before RS has done
    // the report.
    TimeUnit.MILLISECONDS.sleep(3 * MSG_INTERVAL);

    // Check RegionMetrics matches the RegionMetrics from ClusterMetrics
    for (Map.Entry<ServerName, ServerMetrics> entry : admin
      .getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS)).getLiveServerMetrics().entrySet()) {
      ServerName serverName = entry.getKey();
      ServerMetrics serverMetrics = entry.getValue();
      List<RegionMetrics> regionMetrics = admin.getRegionMetrics(serverName);
      LOG.debug("serverName=" + serverName + ", getRegionLoads=" + serverMetrics.getRegionMetrics()
        .keySet().stream().map(r -> Bytes.toString(r)).collect(Collectors.toList()));
      LOG.debug("serverName=" + serverName + ", regionLoads=" + regionMetrics.stream()
        .map(r -> Bytes.toString(r.getRegionName())).collect(Collectors.toList()));
      assertEquals(serverMetrics.getRegionMetrics().size(), regionMetrics.size());
      checkMetricsValue(regionMetrics, serverMetrics);
    }
  }

  private void checkMetricsValue(List<RegionMetrics> regionMetrics, ServerMetrics serverMetrics)
    throws InvocationTargetException, IllegalAccessException {
    for (RegionMetrics fromRM : regionMetrics) {
      RegionMetrics fromSM = serverMetrics.getRegionMetrics().get(fromRM.getRegionName());
      Class clazz = RegionMetrics.class;
      for (Method method : clazz.getMethods()) {
        // check numeric values only
        if (
          method.getReturnType().equals(Size.class) || method.getReturnType().equals(int.class)
            || method.getReturnType().equals(long.class)
            || method.getReturnType().equals(float.class)
        ) {
          Object valueRm = method.invoke(fromRM);
          Object valueSM = method.invoke(fromSM);
          assertEquals(valueRm.toString(), valueSM.toString(),
            "Return values of method " + method.getName() + " are different");
        }
      }
    }
  }

  private void checkRegionsAndRegionMetrics(Collection<RegionInfo> regions,
    Collection<RegionMetrics> regionMetrics) {

    assertEquals(regions.size(), regionMetrics.size(),
      "No of regions and regionMetrics doesn't match");

    Map<byte[], RegionMetrics> regionMetricsMap = Maps.newTreeMap(Bytes.BYTES_COMPARATOR);
    for (RegionMetrics r : regionMetrics) {
      regionMetricsMap.put(r.getRegionName(), r);
    }
    for (RegionInfo info : regions) {
      assertTrue(regionMetricsMap.containsKey(info.getRegionName()),
        "Region not in RegionMetricsMap region:" + info.getRegionNameAsString() + " regionMap: "
          + regionMetricsMap);
    }
  }
}
