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

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

import java.util.List;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.LogEntry;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.namequeues.BalancerRejectionDetails;
import org.apache.hadoop.hbase.namequeues.request.NamedQueueGetRequest;
import org.apache.hadoop.hbase.namequeues.response.NamedQueueGetResponse;
import org.apache.hadoop.hbase.testclassification.MasterTests;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.RecentLogs;

/**
 * Test BalancerRejection ring buffer using namedQueue interface
 */
@Tag(MasterTests.TAG)
@Tag(MediumTests.TAG)
public class TestBalancerRejection extends BalancerTestBase {

  static class MockCostFunction extends CostFunction {
    public static double mockCost;

    public MockCostFunction(Configuration c) {
    }

    @Override
    protected double cost() {
      return mockCost;
    }

    @Override
    float getMultiplier() {
      return 1;
    }
  }

  @Test
  public void testBalancerRejections() throws Exception {
    try {
      // enabled balancer rejection recording
      conf.setBoolean(BaseLoadBalancer.BALANCER_REJECTION_BUFFER_ENABLED, true);
      conf.set(StochasticLoadBalancer.COST_FUNCTIONS_COST_FUNCTIONS_KEY,
        MockCostFunction.class.getName());
      loadBalancer.onConfigurationChange(conf);
      // Simulate 2 servers with 5 regions.
      Map<ServerName, List<RegionInfo>> servers = mockClusterServers(new int[] { 5, 5 });
      Map<TableName, Map<ServerName, List<RegionInfo>>> LoadOfAllTable =
        (Map) mockClusterServersWithTables(servers);

      // Reject case 1: Total cost < 0
      MockCostFunction.mockCost = -Double.MAX_VALUE;
      // Since the Balancer was rejected, there should not be any plans
      assertNull(loadBalancer.balanceCluster(LoadOfAllTable));

      // Reject case 2: Cost < minCostNeedBalance
      MockCostFunction.mockCost = 1;
      conf.setFloat("hbase.master.balancer.stochastic.minCostNeedBalance", Float.MAX_VALUE);
      loadBalancer.onConfigurationChange(conf);
      assertNull(loadBalancer.balanceCluster(LoadOfAllTable));

      // NamedQueue is an async Producer-consumer Pattern, waiting here until it completed
      int maxWaitingCount = 10;
      while (maxWaitingCount-- > 0 && getBalancerRejectionLogEntries().size() != 2) {
        Thread.sleep(1000);
      }
      // There are two cases, should be 2 logEntries
      List<LogEntry> logEntries = getBalancerRejectionLogEntries();
      assertEquals(2, logEntries.size());
      assertTrue(logEntries.get(0).toJsonPrettyPrint().contains("minCostNeedBalance"));
      assertTrue(logEntries.get(1).toJsonPrettyPrint().contains("cost1*multiplier1"));
    } finally {
      conf.unset(StochasticLoadBalancer.COST_FUNCTIONS_COST_FUNCTIONS_KEY);
      conf.unset(BaseLoadBalancer.BALANCER_REJECTION_BUFFER_ENABLED);
      loadBalancer.onConfigurationChange(conf);
    }
  }

  private List<LogEntry> getBalancerRejectionLogEntries() {
    NamedQueueGetRequest namedQueueGetRequest = new NamedQueueGetRequest();
    namedQueueGetRequest.setNamedQueueEvent(BalancerRejectionDetails.BALANCER_REJECTION_EVENT);
    namedQueueGetRequest
      .setBalancerRejectionsRequest(MasterProtos.BalancerRejectionsRequest.getDefaultInstance());
    NamedQueueGetResponse namedQueueGetResponse =
      loadBalancer.namedQueueRecorder.getNamedQueueRecords(namedQueueGetRequest);
    List<RecentLogs.BalancerRejection> balancerRejections =
      namedQueueGetResponse.getBalancerRejections();
    MasterProtos.BalancerRejectionsResponse response = MasterProtos.BalancerRejectionsResponse
      .newBuilder().addAllBalancerRejection(balancerRejections).build();
    List<LogEntry> balancerRejectionRecords = ProtobufUtil.getBalancerRejectionEntries(response);
    return balancerRejectionRecords;
  }
}
