package com.amazonaws.glue.catalog.metastore.integrationtest;

import com.amazonaws.glue.catalog.converters.CatalogToHiveConverter;
import com.amazonaws.glue.catalog.converters.GlueInputConverter;
import com.amazonaws.glue.catalog.metastore.AWSCatalogMetastoreClient;
import com.amazonaws.glue.catalog.metastore.GlueClientFactory;
import com.amazonaws.glue.catalog.util.GlueTestClientFactory;
import com.amazonaws.glue.catalog.util.TestObjects;
import com.amazonaws.services.glue.AWSGlue;
import com.amazonaws.services.glue.model.CreateDatabaseRequest;
import com.amazonaws.services.glue.model.CreateTableRequest;
import com.amazonaws.services.glue.model.DeleteDatabaseRequest;
import com.amazonaws.services.glue.model.DeleteTableRequest;
import com.amazonaws.services.glue.model.GetTableRequest;
import com.amazonaws.services.glue.model.GetTablesRequest;
import com.amazonaws.services.glue.model.GetTablesResult;
import com.amazonaws.services.glue.model.UpdateTableRequest;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.IMetaStoreClient;
import org.apache.hadoop.hive.metastore.Warehouse;
import org.apache.hadoop.hive.metastore.api.AlreadyExistsException;
import org.apache.hadoop.hive.metastore.api.Database;
import org.apache.hadoop.hive.metastore.api.Index;
import org.apache.hadoop.hive.metastore.api.MetaException;
import org.apache.hadoop.hive.metastore.api.NoSuchObjectException;
import org.apache.hadoop.hive.metastore.api.Table;
import org.apache.hadoop.hive.metastore.api.UnknownDBException;
import org.apache.thrift.TException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.amazonaws.glue.catalog.metastore.GlueMetastoreClientDelegate.INDEX_PREFIX;
import static com.amazonaws.glue.catalog.util.TestObjects.getTestDatabase;
import static com.amazonaws.glue.catalog.util.TestObjects.getTestTable;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MetastoreClientIndexIntegrationTest {

  private static AWSGlue glueClient;
  private static IMetaStoreClient metastoreClient;
  private static Warehouse wh;
  private static Database hiveDB;
  private static Table hiveTable;
  private static com.amazonaws.services.glue.model.Database catalogDB;
  private static com.amazonaws.services.glue.model.Table catalogTable;
  private static HiveConf conf;
  private static Path tmpPath;

  private static String invalidTable = "Invalid_Table";
  private static String invalidDatabase = "Invalid_Database";
  private static String invalidIndex = "Invalid_Index";

  private com.amazonaws.services.glue.model.Table catalogIndexTable;
  private Index hiveIndex;
  private Table hiveIndexTable;

  @BeforeClass
  public static void setup() throws MetaException {
    conf = mock(HiveConf.class);
    wh = mock(Warehouse.class);
    tmpPath = new Path("/db");
    when(wh.getDefaultDatabasePath(anyString())).thenReturn(tmpPath);
    when(wh.getDnsPath(any(Path.class))).thenReturn(tmpPath);
    when(wh.isDir(any(Path.class))).thenReturn(true);
    when(wh.getDatabasePath(any(Database.class))).thenReturn(tmpPath);
    when(conf.get(HiveConf.ConfVars.USERS_IN_ADMIN_ROLE.varname,"")).thenReturn("");

    glueClient = new GlueTestClientFactory().newClient();
    GlueClientFactory clientFactory = mock(GlueClientFactory.class);
    when(clientFactory.newClient()).thenReturn(glueClient);

    metastoreClient = new AWSCatalogMetastoreClient.Builder().withHiveConf(conf).withWarehouse(wh)
            .withClientFactory(clientFactory).build();
    catalogDB = getTestDatabase();
    catalogTable = getTestTable();
    hiveDB = CatalogToHiveConverter.convertDatabase(catalogDB);
    hiveTable = CatalogToHiveConverter.convertTable(catalogTable, catalogDB.getName());

    glueClient.createDatabase(new CreateDatabaseRequest()
      .withDatabaseInput(GlueInputConverter.convertToDatabaseInput(catalogDB)));
    glueClient.createTable(new CreateTableRequest()
      .withDatabaseName(catalogDB.getName())
      .withTableInput(GlueInputConverter.convertToTableInput(catalogTable)));
  }

  @Before
  public void setUpForTest() {
    catalogIndexTable = getTestTable();
    hiveIndexTable = CatalogToHiveConverter.convertTable(catalogIndexTable, hiveDB.getName());

    hiveIndex = TestObjects.getTestHiveIndex(catalogDB.getName());
    hiveIndex.setOrigTableName(hiveTable.getTableName());
    hiveIndex.setIndexTableName(hiveIndexTable.getTableName());
  }

  @After
  public void cleanUpForTest() {
    com.amazonaws.services.glue.model.Table catalogTable = glueClient.getTable(new GetTableRequest()
            .withDatabaseName(catalogDB.getName()).withName(hiveTable.getTableName())).getTable();
    Map<String, String> parameters = catalogTable.getParameters();
    List<String> to_delete = new ArrayList<>();
    for(String key : parameters.keySet()) {
      if(key.startsWith(INDEX_PREFIX)) {
        to_delete.add(key);
      }
    }

    for(String to_delete_key : to_delete) {
      parameters.remove(to_delete_key);
    }

    glueClient.updateTable(new UpdateTableRequest().withDatabaseName(hiveDB.getName())
            .withTableInput(GlueInputConverter.convertToTableInput(catalogTable)));
  }

  @AfterClass
  public static void cleanUp() {
    List<com.amazonaws.services.glue.model.Table> table_to_delete = new ArrayList<>();
    String token = null;
    do {
      GetTablesResult result = glueClient.getTables(new GetTablesRequest().withDatabaseName(
          catalogDB.getName()).withNextToken(token));
      table_to_delete.addAll(result.getTableList());
      token = result.getNextToken();
    } while(token != null);
    for(com.amazonaws.services.glue.model.Table table : table_to_delete) {
      glueClient.deleteTable(new DeleteTableRequest().withDatabaseName(catalogDB.getName()).withName(table.getName()));
    }
    glueClient.deleteDatabase(new DeleteDatabaseRequest().withName(catalogDB.getName()));
  }

  @Test
  public void createValidIndex() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    assertTrue(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));
  }

  @Test (expected = AlreadyExistsException.class)
  public void createDuplicateIndex() throws TException {
    assertFalse(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    assertTrue(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    hiveIndex.setIndexTableName(hiveIndexTable2.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable2);
  }

  @Test (expected = UnknownDBException.class)
  public void createIndexWithUnknownDatabase() throws TException {
    hiveIndex.setDbName(invalidDatabase);
    Table invalidHiveTable = CatalogToHiveConverter.convertTable(getTestTable(), invalidDatabase);
    metastoreClient.createIndex(hiveIndex, invalidHiveTable);
  }

  @Test (expected = NoSuchObjectException.class)
  public void createIndexWithUnknownTable() throws TException {
    hiveIndex.setOrigTableName(invalidTable);
    Table invalidHiveTable = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    metastoreClient.createIndex(hiveIndex, invalidHiveTable);
  }


  @Test
  public void alterValidIndex() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);

    Index newIndex = TestObjects.getTestHiveIndex(hiveDB.getName());
    newIndex.setIndexTableName(hiveIndexTable.getTableName());
    newIndex.setOrigTableName(hiveTable.getTableName());

    metastoreClient.alter_index(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName(), newIndex);

    assertFalse(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));
    assertTrue(indexExist(hiveDB.getName(), hiveTable.getTableName(), newIndex.getIndexName()));
  }

  @Test (expected = NoSuchObjectException.class)
  public void alterIndexInvalidWithUnknownDatabase() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);

    Index newIndex = TestObjects.getTestHiveIndex(hiveDB.getName());
    metastoreClient.alter_index(invalidDatabase, hiveTable.getTableName(), hiveIndex.getIndexName(), newIndex);
  }

  @Test (expected = NoSuchObjectException.class)
  public void alterIndexInvalidWithUnknownTable() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);

    Index newIndex = TestObjects.getTestHiveIndex(hiveDB.getName());
    newIndex.setOrigTableName(hiveTable.getTableName());
    newIndex.setIndexTableName(hiveIndexTable.getTableName());

    metastoreClient.alter_index(hiveDB.getName(), invalidTable, hiveIndex.getIndexName(), newIndex);
  }

  @Test
  public void dropIndexValid() throws TException{
    //TODO: update test after implementation of partition.
    // When cascade is turn on, it needs to drop index Table and its partitions.

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    assertTrue(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));

    metastoreClient.dropIndex(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName(), false);
    assertFalse(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexTableName()));
  }

  @Test (expected =  NoSuchObjectException.class)
  public void dropInvalidIndex() throws TException{
    assertFalse(indexExist(hiveDB.getName(), hiveTable.getTableName(), invalidIndex));
    metastoreClient.dropIndex(hiveDB.getName(), hiveTable.getTableName(), invalidIndex, false);
  }

  @Test (expected = NoSuchObjectException.class)
  public void dropIndexInvalidWithUnknownTable() throws TException{
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    assertTrue(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));

    metastoreClient.dropIndex(hiveDB.getName(),invalidTable, hiveIndex.getIndexName(), false);
  }

  @Test (expected = NoSuchObjectException.class)
  public void dropIndexInvalidWithUnknownDataBase() throws TException{
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    assertTrue(indexExist(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName()));

    metastoreClient.dropIndex(invalidDatabase,hiveTable.getTableName(), hiveIndex.getIndexName(), false);
  }

  @Test
  public void listIndexesValid() throws TException {
    Index index2 = TestObjects.getTestHiveIndex(hiveDB.getName());
    Index index3 = TestObjects.getTestHiveIndex(hiveDB.getName());

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    Table hiveIndexTable3 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());

    index2.setOrigTableName(hiveTable.getTableName());
    index2.setIndexTableName(hiveIndexTable2.getTableName());
    index3.setOrigTableName(hiveTable.getTableName());
    index3.setIndexTableName(hiveIndexTable3.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.createIndex(index2, hiveIndexTable2);
    metastoreClient.createIndex(index3, hiveIndexTable3);
    List<Index> indexList = new ArrayList<>();
    indexList.add(hiveIndex);
    indexList.add(index3);
    indexList.add(index2);

    List<Index> result = metastoreClient.listIndexes(hiveDB.getName(), hiveTable.getTableName(), (short) 2);
    assertTrue(result.containsAll(indexList));
  }

  @Test (expected = NoSuchObjectException.class)
  public void listIndexesInvalidWithUnknownTable() throws TException {
    Index index2 = TestObjects.getTestHiveIndex(hiveDB.getName());
    Index index3 = TestObjects.getTestHiveIndex(hiveDB.getName());

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    Table hiveIndexTable3 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());

    index2.setOrigTableName(hiveTable.getTableName());
    index2.setIndexTableName(hiveIndexTable2.getTableName());
    index3.setOrigTableName(hiveTable.getTableName());
    index3.setIndexTableName(hiveIndexTable3.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.createIndex(index2, hiveIndexTable2);
    metastoreClient.createIndex(index3, hiveIndexTable3);
    metastoreClient.listIndexes(hiveDB.getName(), invalidTable, (short) 2);
  }

  @Test (expected = NoSuchObjectException.class)
  public void listIndexesInvalidWithUnknownDb() throws TException {
    Index index2 = TestObjects.getTestHiveIndex(hiveDB.getName());
    Index index3 = TestObjects.getTestHiveIndex(hiveDB.getName());

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    Table hiveIndexTable3 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());

    index2.setOrigTableName(hiveTable.getTableName());
    index2.setIndexTableName(hiveIndexTable2.getTableName());
    index3.setOrigTableName(hiveTable.getTableName());
    index3.setIndexTableName(hiveIndexTable3.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.createIndex(index2, hiveIndexTable2);
    metastoreClient.createIndex(index3, hiveIndexTable3);
    metastoreClient.listIndexes(invalidDatabase, hiveTable.getTableName(), (short) 2);
  }

  @Test
  public void getIndexValid() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    Index result = metastoreClient.getIndex(hiveDB.getName(), hiveTable.getTableName(), hiveIndex.getIndexName());
    assertEquals(hiveIndex, result);
  }

  @Test (expected = NoSuchObjectException.class)
  public void getUnknownIndex() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.getIndex(hiveDB.getName(), hiveTable.getTableName(), invalidIndex);
  }

  @Test (expected = NoSuchObjectException.class)
  public void getIndexInvalidWithUnknownTable() throws TException{
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.getIndex(hiveDB.getName(), invalidTable, hiveIndex.getIndexName());
  }

  @Test (expected = NoSuchObjectException.class)
  public void getIndexInvalidWithUnknownDb() throws TException {
    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.getIndex(invalidDatabase, hiveTable.getTableName(), hiveIndex.getIndexName());
  }

  @Test
  public void listIndexNamesValid() throws TException {
    Index index2 = TestObjects.getTestHiveIndex(hiveDB.getName());
    Index index3 = TestObjects.getTestHiveIndex(hiveDB.getName());

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    Table hiveIndexTable3 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());

    index2.setOrigTableName(hiveTable.getTableName());
    index2.setIndexTableName(hiveIndexTable2.getTableName());
    index3.setOrigTableName(hiveTable.getTableName());
    index3.setIndexTableName(hiveIndexTable3.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.createIndex(index2, hiveIndexTable2);
    metastoreClient.createIndex(index3, hiveIndexTable3);
    List<String> indexNameList = new ArrayList<>();
    indexNameList.add(hiveIndex.getIndexName());
    indexNameList.add(index2.getIndexName());
    indexNameList.add(index3.getIndexName());

    List<String> result = metastoreClient.listIndexNames(hiveDB.getName(), hiveTable.getTableName(), (short) 2);

    assertTrue(result.containsAll(indexNameList));
  }

  @Test (expected = NoSuchObjectException.class)
  public void listIndexNameInvalidWithUnknownTable() throws TException {
    Index index2 = TestObjects.getTestHiveIndex(hiveDB.getName());
    Index index3 = TestObjects.getTestHiveIndex(hiveDB.getName());

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    Table hiveIndexTable3 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());

    index2.setOrigTableName(hiveTable.getTableName());
    index2.setIndexTableName(hiveIndexTable2.getTableName());
    index3.setOrigTableName(hiveTable.getTableName());
    index3.setIndexTableName(hiveIndexTable3.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.createIndex(index2, hiveIndexTable2);
    metastoreClient.createIndex(index3, hiveIndexTable3);

    metastoreClient.listIndexNames(hiveDB.getName(), invalidTable, (short) 2);
  }

  @Test (expected = NoSuchObjectException.class)
  public void listIndexNameInvalidWithUnknownDb() throws TException {
    Index index2 = TestObjects.getTestHiveIndex(hiveDB.getName());
    Index index3 = TestObjects.getTestHiveIndex(hiveDB.getName());

    Table hiveIndexTable2 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());
    Table hiveIndexTable3 = CatalogToHiveConverter.convertTable(getTestTable(), hiveDB.getName());

    index2.setOrigTableName(hiveTable.getTableName());
    index2.setIndexTableName(hiveIndexTable2.getTableName());
    index3.setOrigTableName(hiveTable.getTableName());
    index3.setIndexTableName(hiveIndexTable3.getTableName());

    metastoreClient.createIndex(hiveIndex, hiveIndexTable);
    metastoreClient.createIndex(index2, hiveIndexTable2);
    metastoreClient.createIndex(index3, hiveIndexTable3);

    metastoreClient.listIndexNames(invalidDatabase, hiveTable.getTableName(), (short) 2);
  }
  
  private boolean indexExist(final String dbName, final String tableName, final String indexName) throws TException{
    List<String> indexes = metastoreClient.listIndexNames(dbName, tableName, Short.MAX_VALUE);
    return indexes.contains(indexName);
  }

}