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

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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

import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.IMetaStoreClient;
import org.apache.hadoop.hive.metastore.api.StorageDescriptor;
import org.apache.hadoop.hive.metastore.api.Table;
import org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat;
import org.apache.hive.hcatalog.streaming.TransactionBatch.TxnState;
import org.apache.hive.hcatalog.streaming.mutate.client.lock.Lock;
import org.apache.hive.hcatalog.streaming.mutate.client.lock.LockFailureListener;
import org.apache.thrift.TException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class TestMutatorClient {

  private static final long TRANSACTION_ID = 42L;
  private static final long WRITE_ID1 = 78L;
  private static final long WRITE_ID2 = 33L;
  private static final String TABLE_NAME_1 = "TABLE_1";
  private static final String TABLE_NAME_2 = "TABLE_2";
  private static final String DB_NAME = "DB_1";
  private static final String USER = "user";
  private static final AcidTable TABLE_1 = new AcidTable(DB_NAME, TABLE_NAME_1, true, TableType.SINK);
  private static final AcidTable TABLE_2 = new AcidTable(DB_NAME, TABLE_NAME_2, true, TableType.SINK);

  @Mock
  private IMetaStoreClient mockMetaStoreClient;
  @Mock
  private Lock mockLock;
  @Mock
  private Table mockTable1, mockTable2;
  @Mock
  private StorageDescriptor mockSd;
  @Mock
  private Map<String, String> mockParameters;
  @Mock
  private HiveConf mockConfiguration;
  @Mock
  private LockFailureListener mockLockFailureListener;

  private MutatorClient client;

  @Before
  public void configureMocks() throws Exception {
    when(mockMetaStoreClient.getTable(DB_NAME, TABLE_NAME_1)).thenReturn(mockTable1);
    when(mockTable1.getDbName()).thenReturn(DB_NAME);
    when(mockTable1.getTableName()).thenReturn(TABLE_NAME_1);
    when(mockTable1.getSd()).thenReturn(mockSd);
    when(mockTable1.getParameters()).thenReturn(mockParameters);
    when(mockMetaStoreClient.getTable(DB_NAME, TABLE_NAME_2)).thenReturn(mockTable2);
    when(mockTable2.getDbName()).thenReturn(DB_NAME);
    when(mockTable2.getTableName()).thenReturn(TABLE_NAME_2);
    when(mockTable2.getSd()).thenReturn(mockSd);
    when(mockTable2.getParameters()).thenReturn(mockParameters);
    when(mockSd.getNumBuckets()).thenReturn(1, 2);
    when(mockSd.getOutputFormat()).thenReturn(OrcOutputFormat.class.getName());
    when(mockParameters.get("transactional")).thenReturn(Boolean.TRUE.toString());

    when(mockMetaStoreClient.openTxn(USER)).thenReturn(TRANSACTION_ID);
    when(mockMetaStoreClient.allocateTableWriteId(TRANSACTION_ID, DB_NAME, TABLE_NAME_1)).thenReturn(WRITE_ID1);
    when(mockMetaStoreClient.allocateTableWriteId(TRANSACTION_ID, DB_NAME, TABLE_NAME_2)).thenReturn(WRITE_ID2);

    client = new MutatorClient(mockMetaStoreClient, mockConfiguration, mockLockFailureListener, USER,
        Collections.singletonList(TABLE_1));
  }

  @Test
  public void testCheckValidTableConnect() throws Exception {
    List<AcidTable> inTables = new ArrayList<>();
    inTables.add(TABLE_1);
    inTables.add(TABLE_2);
    client = new MutatorClient(mockMetaStoreClient, mockConfiguration, mockLockFailureListener, USER, inTables);

    client.connect();
    List<AcidTable> outTables = client.getTables();

    assertThat(client.isConnected(), is(true));
    assertThat(outTables.size(), is(2));
    assertThat(outTables.get(0).getDatabaseName(), is(DB_NAME));
    assertThat(outTables.get(0).getTableName(), is(TABLE_NAME_1));
    assertThat(outTables.get(0).getTotalBuckets(), is(2));
    assertThat(outTables.get(0).getOutputFormatName(), is(OrcOutputFormat.class.getName()));
    assertThat(outTables.get(0).getWriteId(), is(0L));
    assertThat(outTables.get(0).getTable(), is(mockTable1));
    assertThat(outTables.get(1).getDatabaseName(), is(DB_NAME));
    assertThat(outTables.get(1).getTableName(), is(TABLE_NAME_2));
    assertThat(outTables.get(1).getTotalBuckets(), is(2));
    assertThat(outTables.get(1).getOutputFormatName(), is(OrcOutputFormat.class.getName()));
    assertThat(outTables.get(1).getWriteId(), is(0L));
    assertThat(outTables.get(1).getTable(), is(mockTable2));
  }

  @Test
  public void testCheckNonTransactionalTableConnect() throws Exception {
    when(mockParameters.get("transactional")).thenReturn(Boolean.FALSE.toString());

    try {
      client.connect();
      fail();
    } catch (ConnectionException e) {
    }

    assertThat(client.isConnected(), is(false));
  }

  @Test
  public void testCheckUnBucketedTableConnect() throws Exception {
    when(mockSd.getNumBuckets()).thenReturn(0);

    try {
      client.connect();
      fail();
    } catch (ConnectionException e) {
    }

    assertThat(client.isConnected(), is(false));
  }

  @Test
  public void testMetaStoreFailsOnConnect() throws Exception {
    when(mockMetaStoreClient.getTable(anyString(), anyString())).thenThrow(new TException());

    try {
      client.connect();
      fail();
    } catch (ConnectionException e) {
    }

    assertThat(client.isConnected(), is(false));
  }

  @Test(expected = ConnectionException.class)
  public void testGetDestinationsFailsIfNotConnected() throws Exception {
    client.getTables();
  }

  @Test
  public void testNewTransaction() throws Exception {
    List<AcidTable> inTables = new ArrayList<>();
    inTables.add(TABLE_1);
    inTables.add(TABLE_2);
    client = new MutatorClient(mockMetaStoreClient, mockConfiguration, mockLockFailureListener, USER, inTables);

    client.connect();
    Transaction transaction = client.newTransaction();
    List<AcidTable> outTables = client.getTables();

    assertThat(client.isConnected(), is(true));

    assertThat(transaction.getTransactionId(), is(TRANSACTION_ID));
    assertThat(transaction.getState(), is(TxnState.INACTIVE));
    assertThat(outTables.get(0).getWriteId(), is(WRITE_ID1));
    assertThat(outTables.get(1).getWriteId(), is(WRITE_ID2));
  }

  @Test
  public void testCloseClosesClient() throws Exception {
    client.close();
    assertThat(client.isConnected(), is(false));
    verify(mockMetaStoreClient).close();
  }

}
