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

import static org.apache.hadoop.hbase.TableName.META_TABLE_NAME;
import static org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory.TRACKER_IMPL;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.apache.hadoop.hbase.AsyncMetaTableAccessor;
import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.TableDescriptorBuilder.ModifyableTableDescriptor;
import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory;
import org.apache.hadoop.hbase.testclassification.ClientTests;
import org.apache.hadoop.hbase.testclassification.LargeTests;
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.TestTemplate;

/**
 * Class to test asynchronous table admin operations.
 * @see TestAsyncTableAdminApi2 This test and it used to be joined it was taking longer than our ten
 *      minute timeout so they were split.
 */
@Tag(LargeTests.TAG)
@Tag(ClientTests.TAG)
@HBaseParameterizedTestTemplate(name = "{index}: policy = {0}")
public class TestAsyncTableAdminApi3 extends TestAsyncAdminBase {

  public TestAsyncTableAdminApi3(Supplier<AsyncAdmin> admin) {
    super(admin);
  }

  @BeforeAll
  public static void setUpBeforeClass() throws Exception {
    TestAsyncAdminBase.setUpBeforeClass();
  }

  @AfterAll
  public static void tearDownAfterClass() throws Exception {
    TestAsyncAdminBase.tearDownAfterClass();
  }

  @TestTemplate
  public void testTableExist() throws Exception {
    boolean exist;
    exist = admin.tableExists(tableName).get();
    assertFalse(exist);
    TEST_UTIL.createTable(tableName, FAMILY);
    exist = admin.tableExists(tableName).get();
    assertTrue(exist);
    exist = admin.tableExists(TableName.META_TABLE_NAME).get();
    assertTrue(exist);
    // meta table already exists
    exist = admin.tableExists(TableName.META_TABLE_NAME).get();
    assertTrue(exist);
  }

  @TestTemplate
  public void testListTables() throws Exception {
    int numTables = admin.listTableDescriptors().get().size();
    final TableName tableName1 = TableName.valueOf(tableName.getNameAsString() + "1");
    final TableName tableName2 = TableName.valueOf(tableName.getNameAsString() + "2");
    final TableName tableName3 = TableName.valueOf(tableName.getNameAsString() + "3");
    TableName[] tables = new TableName[] { tableName1, tableName2, tableName3 };
    for (int i = 0; i < tables.length; i++) {
      createTableWithDefaultConf(tables[i]);
    }

    List<TableDescriptor> tableDescs = admin.listTableDescriptors().get();
    int size = tableDescs.size();
    assertTrue(size >= tables.length);
    for (int i = 0; i < tables.length && i < size; i++) {
      boolean found = false;
      for (int j = 0; j < size; j++) {
        if (tableDescs.get(j).getTableName().equals(tables[i])) {
          found = true;
          break;
        }
      }
      assertTrue(found, "Not found: " + tables[i]);
    }

    List<TableName> tableNames = admin.listTableNames().get();
    size = tableNames.size();
    assertTrue(size == (numTables + tables.length));
    for (int i = 0; i < tables.length && i < size; i++) {
      boolean found = false;
      for (int j = 0; j < size; j++) {
        if (tableNames.get(j).equals(tables[i])) {
          found = true;
          break;
        }
      }
      assertTrue(found, "Not found: " + tables[i]);
    }

    tableNames = new ArrayList<TableName>(tables.length + 1);
    tableDescs = admin.listTableDescriptors(tableNames).get();
    size = tableDescs.size();
    assertEquals(0, size);

    Collections.addAll(tableNames, tables);
    tableNames.add(TableName.META_TABLE_NAME);
    tableDescs = admin.listTableDescriptors(tableNames).get();
    size = tableDescs.size();
    assertEquals(tables.length + 1, size);
    for (int i = 0, j = 0; i < tables.length && j < size; i++, j++) {
      assertTrue(tableDescs.get(j).getTableName().equals(tables[i]),
        "tableName should be equal in order");
    }
    assertTrue(tableDescs.get(size - 1).getTableName().equals(TableName.META_TABLE_NAME));

    for (int i = 0; i < tables.length; i++) {
      admin.disableTable(tables[i]).join();
      admin.deleteTable(tables[i]).join();
    }

    tableDescs = admin.listTableDescriptors(true).get();
    assertTrue(tableDescs.size() > 0, "Not found system tables");
    tableNames = admin.listTableNames(true).get();
    assertTrue(tableNames.size() > 0, "Not found system tables");
  }

  @TestTemplate
  public void testGetTableDescriptor() throws Exception {
    byte[][] families = { FAMILY, FAMILY_0, FAMILY_1 };
    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName);
    for (byte[] family : families) {
      builder.setColumnFamily(ColumnFamilyDescriptorBuilder.of(family));
    }
    TableDescriptor desc = builder.build();
    admin.createTable(desc).join();
    ModifyableTableDescriptor modifyableDesc = ((ModifyableTableDescriptor) desc);
    TableDescriptor confirmedHtd = admin.getDescriptor(tableName).get();
    // HBASE-26246 introduced persist of store file tracker into table descriptor
    desc = TableDescriptorBuilder.newBuilder(desc).setValue(TRACKER_IMPL,
      StoreFileTrackerFactory.getStoreFileTrackerName(TEST_UTIL.getConfiguration())).build();
    assertEquals(0, TableDescriptor.COMPARATOR.compare(desc, confirmedHtd));
  }

  @TestTemplate
  public void testDisableAndEnableTable() throws Exception {
    createTableWithDefaultConf(tableName);
    AsyncTable<?> table = ASYNC_CONN.getTable(tableName);
    final byte[] row = Bytes.toBytes("row");
    final byte[] qualifier = Bytes.toBytes("qualifier");
    final byte[] value = Bytes.toBytes("value");
    Put put = new Put(row);
    put.addColumn(FAMILY, qualifier, value);
    table.put(put).join();
    Get get = new Get(row);
    get.addColumn(FAMILY, qualifier);
    table.get(get).get();

    this.admin.disableTable(tableName).join();
    assertTrue(TEST_UTIL.getHBaseCluster().getMaster().getTableStateManager()
      .isTableState(tableName, TableState.State.DISABLED), "Table must be disabled.");
    assertEquals(TableState.State.DISABLED, TestAsyncTableAdminApi.getStateFromMeta(tableName));

    // Test that table is disabled
    get = new Get(row);
    get.addColumn(FAMILY, qualifier);
    boolean ok = false;
    try {
      table.get(get).get();
    } catch (ExecutionException e) {
      ok = true;
    }
    ok = false;
    // verify that scan encounters correct exception
    try {
      table.scanAll(new Scan()).get();
    } catch (ExecutionException e) {
      ok = true;
    }
    assertTrue(ok);
    this.admin.enableTable(tableName).join();
    assertTrue(TEST_UTIL.getHBaseCluster().getMaster().getTableStateManager()
      .isTableState(tableName, TableState.State.ENABLED), "Table must be enabled.");
    assertEquals(TableState.State.ENABLED, TestAsyncTableAdminApi.getStateFromMeta(tableName));

    // Test that table is enabled
    try {
      table.get(get).get();
    } catch (Exception e) {
      ok = false;
    }
    assertTrue(ok);
    // meta table can not be disabled.
    try {
      admin.disableTable(TableName.META_TABLE_NAME).get();
      fail("meta table can not be disabled");
    } catch (ExecutionException e) {
      Throwable cause = e.getCause();
      assertThat(cause, instanceOf(DoNotRetryIOException.class));
    }
  }

  @TestTemplate
  public void testDisableAndEnableTables() throws Exception {
    final TableName tableName1 = TableName.valueOf(tableName.getNameAsString() + "1");
    final TableName tableName2 = TableName.valueOf(tableName.getNameAsString() + "2");
    createTableWithDefaultConf(tableName1);
    createTableWithDefaultConf(tableName2);
    AsyncTable<?> table1 = ASYNC_CONN.getTable(tableName1);
    AsyncTable<?> table2 = ASYNC_CONN.getTable(tableName1);

    final byte[] row = Bytes.toBytes("row");
    final byte[] qualifier = Bytes.toBytes("qualifier");
    final byte[] value = Bytes.toBytes("value");
    Put put = new Put(row);
    put.addColumn(FAMILY, qualifier, value);
    table1.put(put).join();
    table2.put(put).join();
    Get get = new Get(row);
    get.addColumn(FAMILY, qualifier);
    table1.get(get).get();
    table2.get(get).get();

    admin.listTableNames(Pattern.compile(tableName.getNameAsString() + ".*"), false).get()
      .forEach(t -> admin.disableTable(t).join());

    // Test that tables are disabled
    get = new Get(row);
    get.addColumn(FAMILY, qualifier);
    boolean ok = false;
    try {
      table1.get(get).get();
    } catch (ExecutionException e) {
      ok = true;
    }
    assertTrue(ok);

    ok = false;
    try {
      table2.get(get).get();
    } catch (ExecutionException e) {
      ok = true;
    }
    assertTrue(ok);
    assertEquals(TableState.State.DISABLED, TestAsyncTableAdminApi.getStateFromMeta(tableName1));
    assertEquals(TableState.State.DISABLED, TestAsyncTableAdminApi.getStateFromMeta(tableName2));

    admin.listTableNames(Pattern.compile(tableName.getNameAsString() + ".*"), false).get()
      .forEach(t -> admin.enableTable(t).join());

    // Test that tables are enabled
    try {
      table1.get(get).get();
    } catch (Exception e) {
      ok = false;
    }
    try {
      table2.get(get).get();
    } catch (Exception e) {
      ok = false;
    }
    assertTrue(ok);
    assertEquals(TableState.State.ENABLED, TestAsyncTableAdminApi.getStateFromMeta(tableName1));
    assertEquals(TableState.State.ENABLED, TestAsyncTableAdminApi.getStateFromMeta(tableName2));
  }

  @TestTemplate
  public void testEnableTableRetainAssignment() throws Exception {
    byte[][] splitKeys = { new byte[] { 1, 1, 1 }, new byte[] { 2, 2, 2 }, new byte[] { 3, 3, 3 },
      new byte[] { 4, 4, 4 }, new byte[] { 5, 5, 5 }, new byte[] { 6, 6, 6 },
      new byte[] { 7, 7, 7 }, new byte[] { 8, 8, 8 }, new byte[] { 9, 9, 9 } };
    int expectedRegions = splitKeys.length + 1;
    createTableWithDefaultConf(tableName, splitKeys);

    AsyncTable<AdvancedScanResultConsumer> metaTable = ASYNC_CONN.getTable(META_TABLE_NAME);
    List<HRegionLocation> regions =
      AsyncMetaTableAccessor.getTableHRegionLocations(metaTable, tableName).get();
    assertEquals(expectedRegions, regions.size(),
      "Tried to create " + expectedRegions + " regions " + "but only found " + regions.size());

    // Disable table.
    admin.disableTable(tableName).join();
    // Enable table, use retain assignment to assign regions.
    admin.enableTable(tableName).join();

    List<HRegionLocation> regions2 =
      AsyncMetaTableAccessor.getTableHRegionLocations(metaTable, tableName).get();
    // Check the assignment.
    assertEquals(regions.size(), regions2.size());
    assertTrue(regions2.containsAll(regions));
  }

  @TestTemplate
  public void testIsTableEnabledAndDisabled() throws Exception {
    createTableWithDefaultConf(tableName);
    assertTrue(admin.isTableEnabled(tableName).get());
    assertFalse(admin.isTableDisabled(tableName).get());
    admin.disableTable(tableName).join();
    assertFalse(admin.isTableEnabled(tableName).get());
    assertTrue(admin.isTableDisabled(tableName).get());

    // meta table is always enabled
    assertTrue(admin.isTableEnabled(TableName.META_TABLE_NAME).get());
    assertFalse(admin.isTableDisabled(TableName.META_TABLE_NAME).get());
  }

  @TestTemplate
  public void testIsTableAvailable() throws Exception {
    createTableWithDefaultConf(tableName);
    TEST_UTIL.waitTableAvailable(tableName);
    assertTrue(admin.isTableAvailable(tableName).get());
    assertTrue(admin.isTableAvailable(TableName.META_TABLE_NAME).get());
  }
}
