/**
 * 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.hive.hcatalog.api;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.apache.hive.hcatalog.messaging.AddPartitionMessage;
import org.apache.hive.hcatalog.messaging.CreateDatabaseMessage;
import org.apache.hive.hcatalog.messaging.CreateTableMessage;
import org.apache.hive.hcatalog.messaging.DropDatabaseMessage;
import org.apache.hive.hcatalog.messaging.DropPartitionMessage;
import org.apache.hive.hcatalog.messaging.DropTableMessage;
import org.apache.hive.hcatalog.messaging.MessageDeserializer;
import org.apache.hive.hcatalog.messaging.MessageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.IMetaStoreClient;
import org.apache.hadoop.hive.metastore.api.NotificationEvent;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory;
import org.apache.hive.hcatalog.common.HCatConstants;
import org.apache.hive.hcatalog.data.schema.HCatFieldSchema;
import org.apache.hive.hcatalog.listener.DbNotificationListener;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This can't use TestHCatClient because it has to have control over certain conf variables when
 * the metastore is started.  Plus, we don't need a metastore running in another thread.  The
 * local one is fine.
 */
public class TestHCatClientNotification {

  private static final Logger LOG = LoggerFactory.getLogger(TestHCatClientNotification.class.getName());
  private static HCatClient hCatClient;
  private static MessageDeserializer md = null;
  private int startTime;
  private long firstEventId;

  @BeforeClass
  public static void setupClient() throws Exception {
    HiveConf conf = new HiveConf(); conf.setVar(HiveConf.ConfVars.METASTORE_EVENT_LISTENERS,
        DbNotificationListener.class.getName());
    hCatClient = HCatClient.create(conf);
    md = MessageFactory.getInstance().getDeserializer();
  }

  @Before
  public void setup() throws Exception {
    long now = System.currentTimeMillis() / 1000;
    startTime = 0;
    if (now > Integer.MAX_VALUE) fail("Bummer, time has fallen over the edge");
    else startTime = (int)now;
    firstEventId = hCatClient.getCurrentNotificationEventId();
  }

  @Test
  public void createDatabase() throws Exception {
    hCatClient.createDatabase(HCatCreateDBDesc.create("myhcatdb").build());
    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, null);
    assertEquals(1, events.size());

    HCatNotificationEvent event = events.get(0);
    assertEquals(firstEventId + 1, event.getEventId());
    assertTrue(event.getEventTime() >= startTime);
    assertEquals(HCatConstants.HCAT_CREATE_DATABASE_EVENT, event.getEventType());
    assertEquals("myhcatdb", event.getDbName());
    assertNull(event.getTableName());
    CreateDatabaseMessage createDatabaseMessage = md.getCreateDatabaseMessage(event.getMessage());
    assertEquals("myhcatdb", createDatabaseMessage.getDB());
  }

  @Test
  public void dropDatabase() throws Exception {
    String dbname = "hcatdropdb";
    hCatClient.createDatabase(HCatCreateDBDesc.create(dbname).build());
    hCatClient.dropDatabase(dbname, false, HCatClient.DropDBMode.RESTRICT);

    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, null);
    assertEquals(2, events.size());

    HCatNotificationEvent event = events.get(1);
    assertEquals(firstEventId + 2, event.getEventId());
    assertTrue(event.getEventTime() >= startTime);
    assertEquals(HCatConstants.HCAT_DROP_DATABASE_EVENT, event.getEventType());
    assertEquals(dbname, event.getDbName());
    assertNull(event.getTableName());
    DropDatabaseMessage dropDatabaseMessage = md.getDropDatabaseMessage(event.getMessage());
    assertEquals(dbname, dropDatabaseMessage.getDB());
  }

  @Test
  public void createTable() throws Exception {
    String dbName = "default";
    String tableName = "hcatcreatetable";
    HCatTable table = new HCatTable(dbName, tableName);
    table.cols(Arrays.asList(new HCatFieldSchema("onecol", TypeInfoFactory.stringTypeInfo, "")));
    hCatClient.createTable(HCatCreateTableDesc.create(table).build());

    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, null);
    assertEquals(1, events.size());

    HCatNotificationEvent event = events.get(0);
    assertEquals(firstEventId + 1, event.getEventId());
    assertTrue(event.getEventTime() >= startTime);
    assertEquals(HCatConstants.HCAT_CREATE_TABLE_EVENT, event.getEventType());
    assertEquals(dbName, event.getDbName());
    assertEquals("hcatcreatetable", event.getTableName());

    // Parse the message field
    CreateTableMessage createTableMessage = md.getCreateTableMessage(event.getMessage());
    assertEquals(dbName, createTableMessage.getDB());
    assertEquals(tableName, createTableMessage.getTable());

    // fetch the table marked by the message and compare
    HCatTable createdTable = hCatClient.getTable(dbName,tableName);
    assertTrue(createdTable.diff(table).equals(HCatTable.NO_DIFF));
  }

  // TODO - Currently no way to test alter table, as this interface doesn't support alter table

  @Test
  public void dropTable() throws Exception {
    String dbName = "default";
    String tableName = "hcatdroptable";
    HCatTable table = new HCatTable(dbName, tableName);
    table.cols(Arrays.asList(new HCatFieldSchema("onecol", TypeInfoFactory.stringTypeInfo, "")));
    hCatClient.createTable(HCatCreateTableDesc.create(table).build());
    hCatClient.dropTable(dbName, tableName, false);

    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, null);
    assertEquals(2, events.size());

    HCatNotificationEvent event = events.get(1);
    assertEquals(firstEventId + 2, event.getEventId());
    assertTrue(event.getEventTime() >= startTime);
    assertEquals(HCatConstants.HCAT_DROP_TABLE_EVENT, event.getEventType());
    assertEquals(dbName, event.getDbName());
    assertEquals(tableName, event.getTableName());

    DropTableMessage dropTableMessage = md.getDropTableMessage(event.getMessage());
    assertEquals(dbName, dropTableMessage.getDB());
    assertEquals(tableName, dropTableMessage.getTable());
  }

  @Test
  public void addPartition() throws Exception {
    String dbName = "default";
    String tableName = "hcataddparttable";
    String partColName = "pc";
    HCatTable table = new HCatTable(dbName, tableName);
    table.partCol(new HCatFieldSchema(partColName, TypeInfoFactory.stringTypeInfo, ""));
    table.cols(Arrays.asList(new HCatFieldSchema("onecol", TypeInfoFactory.stringTypeInfo, "")));
    hCatClient.createTable(HCatCreateTableDesc.create(table).build());
    String partName = "testpart";
    Map<String, String> partSpec = new HashMap<String, String>(1);
    partSpec.put(partColName, partName);
    HCatPartition part = new HCatPartition(table, partSpec, null);
    hCatClient.addPartition(HCatAddPartitionDesc.create(part).build());

    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, null);
    assertEquals(2, events.size());

    HCatNotificationEvent event = events.get(1);
    assertEquals(firstEventId + 2, event.getEventId());
    assertTrue(event.getEventTime() >= startTime);
    assertEquals(HCatConstants.HCAT_ADD_PARTITION_EVENT, event.getEventType());
    assertEquals("default", event.getDbName());
    assertEquals(tableName, event.getTableName());

    // Parse the message field
    AddPartitionMessage addPartitionMessage = md.getAddPartitionMessage(event.getMessage());
    assertEquals(dbName, addPartitionMessage.getDB());
    assertEquals(tableName, addPartitionMessage.getTable());
    List<Map<String,String>> ptndescs = addPartitionMessage.getPartitions();

    // fetch the partition referred to by the message and compare
    HCatPartition addedPart = hCatClient.getPartition(dbName, tableName, ptndescs.get(0));

    assertEquals(part.getDatabaseName(), addedPart.getDatabaseName());
    assertEquals(part.getTableName(), addedPart.getTableName());
    assertEquals(part.getValues(), addedPart.getValues());
    assertEquals(part.getColumns(), addedPart.getColumns());
    assertEquals(part.getPartColumns(), addedPart.getPartColumns());
    assertEquals(part.getLocation(), addedPart.getLocation());
  }

  // TODO - currently no way to test alter partition, as HCatClient doesn't support it.
  @Test
  public void dropPartition() throws Exception {
    String dbName = "default";
    String tableName = "hcatdropparttable";
    String partColName = "pc";
    HCatTable table = new HCatTable(dbName, tableName);
    table.partCol(new HCatFieldSchema(partColName, TypeInfoFactory.stringTypeInfo, ""));
    table.cols(Arrays.asList(new HCatFieldSchema("onecol", TypeInfoFactory.stringTypeInfo, "")));
    hCatClient.createTable(HCatCreateTableDesc.create(table).build());
    String partName = "testpart";
    Map<String, String> partSpec = new HashMap<String, String>(1);
    partSpec.put(partColName, partName);
    hCatClient.addPartition(
        HCatAddPartitionDesc.create(
            new HCatPartition(table, partSpec, null)
        ).build()
    );
    hCatClient.dropPartitions(dbName, tableName, partSpec, false);

    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, null);
    assertEquals(3, events.size());

    HCatNotificationEvent event = events.get(2);
    assertEquals(firstEventId + 3, event.getEventId());
    assertTrue(event.getEventTime() >= startTime);
    assertEquals(HCatConstants.HCAT_DROP_PARTITION_EVENT, event.getEventType());
    assertEquals(dbName, event.getDbName());
    assertEquals(tableName, event.getTableName());

    // Parse the message field
    DropPartitionMessage dropPartitionMessage = md.getDropPartitionMessage(event.getMessage());
    assertEquals(dbName, dropPartitionMessage.getDB());
    assertEquals(tableName, dropPartitionMessage.getTable());
    List<Map<String, String>> droppedPartSpecs = dropPartitionMessage.getPartitions();
    assertNotNull(droppedPartSpecs);
    assertEquals(1,droppedPartSpecs.size());
    assertEquals(partSpec,droppedPartSpecs.get(0));
  }

  @Test
  public void getOnlyMaxEvents() throws Exception {
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatdb1").build());
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatdb2").build());
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatdb3").build());

    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 2, null);
    assertEquals(2, events.size());
    assertEquals(firstEventId + 1, events.get(0).getEventId());
    assertEquals(firstEventId + 2, events.get(1).getEventId());
  }

  @Test
  public void filter() throws Exception {
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatf1").build());
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatf2").build());
    hCatClient.dropDatabase("hcatf2", false, HCatClient.DropDBMode.RESTRICT);

    IMetaStoreClient.NotificationFilter filter = new IMetaStoreClient.NotificationFilter() {
      @Override
      public boolean accept(NotificationEvent event) {
        return event.getEventType().equals(HCatConstants.HCAT_DROP_DATABASE_EVENT);
      }
    };
    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 0, filter);
    assertEquals(1, events.size());
    assertEquals(firstEventId + 3, events.get(0).getEventId());
  }

  @Test
  public void filterWithMax() throws Exception {
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatm1").build());
    hCatClient.createDatabase(HCatCreateDBDesc.create("hcatm2").build());
    hCatClient.dropDatabase("hcatm2", false, HCatClient.DropDBMode.RESTRICT);

    IMetaStoreClient.NotificationFilter filter = new IMetaStoreClient.NotificationFilter() {
      @Override
      public boolean accept(NotificationEvent event) {
        return event.getEventType().equals(HCatConstants.HCAT_CREATE_DATABASE_EVENT);
      }
    };
    List<HCatNotificationEvent> events = hCatClient.getNextNotification(firstEventId, 1, filter);
    assertEquals(1, events.size());
    assertEquals(firstEventId + 1, events.get(0).getEventId());
  }

}
