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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.testclassification.SmallTests;
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.HBaseProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Quotas;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceLimitRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuota;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Throttle;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.TimedQuota;

/**
 * Test class for {@link QuotaSettingsFactory}.
 */
@Tag(SmallTests.TAG)
public class TestQuotaSettingsFactory {

  @Test
  public void testAllQuotasAddedToList() {
    final SpaceQuota spaceQuota = SpaceQuota.newBuilder().setSoftLimit(1024L * 1024L * 1024L * 50L) // 50G
      .setViolationPolicy(QuotaProtos.SpaceViolationPolicy.DISABLE) // Disable the table
      .build();
    final long readLimit = 1000;
    final long writeLimit = 500;
    final Throttle throttle = Throttle.newBuilder()
      // 1000 read reqs/min
      .setReadNum(TimedQuota.newBuilder().setSoftLimit(readLimit)
        .setTimeUnit(HBaseProtos.TimeUnit.MINUTES).build())
      // 500 write reqs/min
      .setWriteNum(TimedQuota.newBuilder().setSoftLimit(writeLimit)
        .setTimeUnit(HBaseProtos.TimeUnit.MINUTES).build())
      .build();
    final Quotas quotas = Quotas.newBuilder().setSpace(spaceQuota) // Set the FS quotas
      .setThrottle(throttle) // Set some RPC limits
      .build();
    final TableName tn = TableName.valueOf("my_table");
    List<QuotaSettings> settings = QuotaSettingsFactory.fromTableQuotas(tn, quotas);
    assertEquals(3, settings.size());
    boolean seenRead = false;
    boolean seenWrite = false;
    boolean seenSpace = false;
    for (QuotaSettings setting : settings) {
      if (setting instanceof ThrottleSettings) {
        ThrottleSettings throttleSettings = (ThrottleSettings) setting;
        switch (throttleSettings.getThrottleType()) {
          case READ_NUMBER:
            assertFalse(seenRead, "Should not have multiple read quotas");
            assertEquals(readLimit, throttleSettings.getSoftLimit());
            assertEquals(TimeUnit.MINUTES, throttleSettings.getTimeUnit());
            assertEquals(tn, throttleSettings.getTableName());
            assertNull(throttleSettings.getUserName(), "Username should be null");
            assertNull(throttleSettings.getNamespace(), "Namespace should be null");
            assertNull(throttleSettings.getRegionServer(), "RegionServer should be null");
            seenRead = true;
            break;
          case WRITE_NUMBER:
            assertFalse(seenWrite, "Should not have multiple write quotas");
            assertEquals(writeLimit, throttleSettings.getSoftLimit());
            assertEquals(TimeUnit.MINUTES, throttleSettings.getTimeUnit());
            assertEquals(tn, throttleSettings.getTableName());
            assertNull(throttleSettings.getUserName(), "Username should be null");
            assertNull(throttleSettings.getNamespace(), "Namespace should be null");
            assertNull(throttleSettings.getRegionServer(), "RegionServer should be null");
            seenWrite = true;
            break;
          default:
            fail("Unexpected throttle type: " + throttleSettings.getThrottleType());
        }
      } else if (setting instanceof SpaceLimitSettings) {
        assertFalse(seenSpace, "Should not have multiple space quotas");
        SpaceLimitSettings spaceLimit = (SpaceLimitSettings) setting;
        assertEquals(tn, spaceLimit.getTableName());
        assertNull(spaceLimit.getUserName(), "Username should be null");
        assertNull(spaceLimit.getNamespace(), "Namespace should be null");
        assertNull(spaceLimit.getRegionServer(), "RegionServer should be null");
        assertTrue(spaceLimit.getProto().hasQuota(), "SpaceLimitSettings should have a SpaceQuota");
        assertEquals(spaceQuota, spaceLimit.getProto().getQuota());
        seenSpace = true;
      } else {
        fail("Unexpected QuotaSettings implementation: " + setting.getClass());
      }
    }
    assertTrue(seenRead, "Should have seen a read quota");
    assertTrue(seenWrite, "Should have seen a write quota");
    assertTrue(seenSpace, "Should have seen a space quota");
  }

  @Test
  public void testNeitherTableNorNamespace() {
    final SpaceQuota spaceQuota = SpaceQuota.newBuilder().setSoftLimit(1L)
      .setViolationPolicy(QuotaProtos.SpaceViolationPolicy.DISABLE).build();
    assertThrows(IllegalArgumentException.class,
      () -> QuotaSettingsFactory.fromSpace(null, null, spaceQuota));
  }

  @Test
  public void testBothTableAndNamespace() {
    final SpaceQuota spaceQuota = SpaceQuota.newBuilder().setSoftLimit(1L)
      .setViolationPolicy(QuotaProtos.SpaceViolationPolicy.DISABLE).build();
    assertThrows(IllegalArgumentException.class,
      () -> QuotaSettingsFactory.fromSpace(TableName.valueOf("foo"), "bar", spaceQuota));
  }

  @Test
  public void testSpaceLimitSettings() {
    final TableName tableName = TableName.valueOf("foo");
    final long sizeLimit = 1024L * 1024L * 1024L * 75; // 75GB
    final SpaceViolationPolicy violationPolicy = SpaceViolationPolicy.NO_INSERTS;
    QuotaSettings settings =
      QuotaSettingsFactory.limitTableSpace(tableName, sizeLimit, violationPolicy);
    assertNotNull(settings, "QuotaSettings should not be null");
    assertTrue(settings instanceof SpaceLimitSettings,
      "Should be an instance of SpaceLimitSettings");
    SpaceLimitSettings spaceLimitSettings = (SpaceLimitSettings) settings;
    SpaceLimitRequest protoRequest = spaceLimitSettings.getProto();
    assertTrue(protoRequest.hasQuota(), "Request should have a SpaceQuota");
    SpaceQuota quota = protoRequest.getQuota();
    assertEquals(sizeLimit, quota.getSoftLimit());
    assertEquals(violationPolicy, ProtobufUtil.toViolationPolicy(quota.getViolationPolicy()));
    assertFalse(quota.getRemove(), "The remove attribute should be false");
  }

  @Test
  public void testSpaceLimitSettingsForDeletes() {
    final String ns = "ns1";
    final TableName tn = TableName.valueOf("tn1");
    QuotaSettings nsSettings = QuotaSettingsFactory.removeNamespaceSpaceLimit(ns);
    assertNotNull(nsSettings, "QuotaSettings should not be null");
    assertTrue(nsSettings instanceof SpaceLimitSettings,
      "Should be an instance of SpaceLimitSettings");
    SpaceLimitRequest nsProto = ((SpaceLimitSettings) nsSettings).getProto();
    assertTrue(nsProto.hasQuota(), "Request should have a SpaceQuota");
    assertTrue(nsProto.getQuota().getRemove(), "The remove attribute should be true");

    QuotaSettings tableSettings = QuotaSettingsFactory.removeTableSpaceLimit(tn);
    assertNotNull(tableSettings, "QuotaSettings should not be null");
    assertTrue(tableSettings instanceof SpaceLimitSettings,
      "Should be an instance of SpaceLimitSettings");
    SpaceLimitRequest tableProto = ((SpaceLimitSettings) tableSettings).getProto();
    assertTrue(tableProto.hasQuota(), "Request should have a SpaceQuota");
    assertTrue(tableProto.getQuota().getRemove(), "The remove attribute should be true");
  }
}
