/*
 * 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.drill.exec.impersonation.hive;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;

import org.apache.calcite.schema.Schema.TableType;
import org.apache.drill.exec.hive.HiveTestUtilities;
import org.apache.drill.exec.impersonation.BaseTestImpersonation;
import org.apache.drill.exec.store.hive.HiveStoragePluginConfig;
import org.apache.drill.test.ClientFixture;
import org.apache.drill.test.TestBuilder;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
import org.apache.hadoop.hive.ql.Driver;
import org.junit.BeforeClass;

import static org.apache.drill.exec.hive.HiveTestUtilities.createDirWithPosixPermissions;
import static org.apache.drill.exec.hive.HiveTestUtilities.executeQuery;
import static org.apache.hadoop.fs.FileSystem.FS_DEFAULT_NAME_KEY;
import static org.apache.hadoop.hive.conf.HiveConf.ConfVars.METASTOREURIS;

public class BaseTestHiveImpersonation extends BaseTestImpersonation {
  protected static final String hivePluginName = "hive";

  protected static HiveConf hiveConf;
  protected static String whDir;

  protected static String studentData;
  protected static String voterData;

  protected static final String studentDef = "CREATE TABLE %s.%s" +
      "(rownum int, name string, age int, gpa float, studentnum bigint) " +
      "ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE";

  protected static final String voterDef = "CREATE TABLE %s.%s" +
      "(voter_id int,name varchar(30), age tinyint, registration string, " +
      "contributions double,voterzone smallint,create_time timestamp) " +
      "ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE";

  protected static final String partitionStudentDef = "CREATE TABLE %s.%s" +
      "(rownum INT, name STRING, gpa FLOAT, studentnum BIGINT) " +
      "partitioned by (age INT) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE";

  @BeforeClass
  public static void setUp() {
    HiveTestUtilities.assumeJavaVersion();
  }

  protected static void prepHiveConfAndData() throws Exception {
    hiveConf = new HiveConf();

    File scratchDir = createDirWithPosixPermissions(dirTestWatcher.getRootDir(), "scratch_dir");
    File localScratchDir = createDirWithPosixPermissions(dirTestWatcher.getRootDir(), "local_scratch_dir");
    File metaStoreDBDir = new File(dirTestWatcher.getRootDir(), "metastore_db");

    // Configure metastore persistence db location on local filesystem
    final String dbUrl = String.format("jdbc:derby:;databaseName=%s;create=true",  metaStoreDBDir.getAbsolutePath());
    hiveConf.set(ConfVars.METASTORECONNECTURLKEY.varname, dbUrl);

    hiveConf.set(ConfVars.SCRATCHDIR.varname, "file://" + scratchDir.getAbsolutePath());
    hiveConf.set(ConfVars.LOCALSCRATCHDIR.varname, localScratchDir.getAbsolutePath());
    hiveConf.set(ConfVars.METASTORE_SCHEMA_VERIFICATION.varname, "false");
    hiveConf.set(ConfVars.METASTORE_AUTO_CREATE_ALL.varname, "true");
    hiveConf.set(ConfVars.HIVE_CBO_ENABLED.varname, "false");
    hiveConf.set(ConfVars.HIVESTATSAUTOGATHER.varname, "false");
    hiveConf.set(ConfVars.HIVESTATSCOLAUTOGATHER.varname, "false");
    hiveConf.set(ConfVars.HIVESESSIONSILENT.varname, "true");

    // Set MiniDFS conf in HiveConf
    hiveConf.set(FS_DEFAULT_NAME_KEY, dfsConf.get(FS_DEFAULT_NAME_KEY));

    whDir = hiveConf.get(ConfVars.METASTOREWAREHOUSE.varname);
    FileSystem.mkdirs(fs, new Path(whDir), new FsPermission((short) 0777));

    dirTestWatcher.copyResourceToRoot(Paths.get("student.txt"));
    dirTestWatcher.copyResourceToRoot(Paths.get("voter.txt"));
    studentData = dirTestWatcher.getRootDir() + "/student.txt";
    voterData = dirTestWatcher.getRootDir() + "/voter.txt";
  }

  protected static void startHiveMetaStore() throws Exception {
    Class<?> metaStoreUtilsClass;
    Class<?> hadoopThriftAuthBridgeClass;
    Class<?> confClass;
    Object hadoopThriftAuthBridge;
    // TODO: remove reflection stuff when all supported profiles will be switched to Hive 3+ version
    try {
      metaStoreUtilsClass = Class.forName("org.apache.hadoop.hive.metastore.utils.MetaStoreUtils");
      hadoopThriftAuthBridgeClass = Class.forName("org.apache.hadoop.hive.metastore.security.HadoopThriftAuthBridge");
      hadoopThriftAuthBridge = hadoopThriftAuthBridgeClass.getDeclaredMethod("getBridge").invoke(null);
      confClass = Configuration.class;
    } catch (ClassNotFoundException e) {
      metaStoreUtilsClass = Class.forName("org.apache.hadoop.hive.metastore.MetaStoreUtils");
      hadoopThriftAuthBridgeClass = Class.forName("org.apache.hadoop.hive.thrift.HadoopThriftAuthBridge");
      hadoopThriftAuthBridge = Class.forName("org.apache.hadoop.hive.shims.ShimLoader")
          .getDeclaredMethod("getHadoopThriftAuthBridge").invoke(null);
      confClass = HiveConf.class;
    }
    final int port = (int) metaStoreUtilsClass.getDeclaredMethod("findFreePort").invoke(null);

    hiveConf.set(METASTOREURIS.varname, "thrift://localhost:" + port);

    metaStoreUtilsClass.getDeclaredMethod("startMetaStore", int.class, hadoopThriftAuthBridgeClass, confClass)
        .invoke(null, port, hadoopThriftAuthBridge, hiveConf);
  }

  protected static HiveStoragePluginConfig createHiveStoragePlugin(final Map<String, String> hiveConfig) throws Exception {
    HiveStoragePluginConfig pluginConfig = new HiveStoragePluginConfig(hiveConfig);
    pluginConfig.setEnabled(true);
    return pluginConfig;
  }

  protected static Path getWhPathForHiveObject(final String dbName, final String tableName) {
    if (dbName == null) {
      return new Path(whDir);
    }

    if (tableName == null) {
      return new Path(whDir, dbName + ".db");
    }

    return new Path(new Path(whDir, dbName + ".db"), tableName);
  }

  protected static void addHiveStoragePlugin(final Map<String, String> hiveConfig) throws Exception {
    cluster.storageRegistry().put(hivePluginName, createHiveStoragePlugin(hiveConfig));
  }

  protected void showTablesHelper(final String db, List<String> expectedTables, ClientFixture
client) throws Exception {
    final String dbQualified = hivePluginName + "." + db;
    final TestBuilder testBuilder = client.testBuilder()
        .sqlQuery("SHOW TABLES IN " + dbQualified)
        .unOrdered()
        .baselineColumns("TABLE_SCHEMA", "TABLE_NAME");

    if (expectedTables.size() == 0) {
      testBuilder.expectsEmptyResultSet();
    } else {
      for (String tbl : expectedTables) {
        testBuilder.baselineValues(dbQualified, tbl);
      }
    }

    testBuilder.go();
  }

  protected void fromInfoSchemaHelper(final String db, List<String> expectedTables, List<TableType>
expectedTableTypes, ClientFixture client) throws Exception {
    final String dbQualified = hivePluginName + "." + db;
    final TestBuilder testBuilder = client.testBuilder()
        .sqlQuery("SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE \n" +
            "FROM INFORMATION_SCHEMA.`TABLES` \n" +
            "WHERE TABLE_SCHEMA = '" + dbQualified + "'")
        .unOrdered()
        .baselineColumns("TABLE_SCHEMA", "TABLE_NAME", "TABLE_TYPE");

    if (expectedTables.size() == 0) {
      testBuilder.expectsEmptyResultSet();
    } else {
      for (int i = 0; i < expectedTables.size(); ++i) {
        testBuilder.baselineValues(dbQualified, expectedTables.get(i), expectedTableTypes.get(i).toString());
      }
    }

    testBuilder.go();
  }

  public static void stopHiveMetaStore() throws Exception {
    // Unfortunately Hive metastore doesn't provide an API to shut it down. It will be exited as part of the test JVM
    // exit. As each metastore server instance is using its own resources and not sharing it with other metastore
    // server instances this should be ok.
  }

  static void queryView(String viewName, ClientFixture client) throws Exception {
    String query = String.format("SELECT rownum FROM %s.tmp.%s ORDER BY rownum LIMIT 1", MINI_DFS_STORAGE_PLUGIN_NAME, viewName);
    client.testBuilder()
        .sqlQuery(query)
        .unOrdered()
        .baselineColumns("rownum")
        .baselineValues(1)
        .go();
  }

  static void queryViewNotAuthorized(String viewName, ClientFixture client) throws Exception {
    String query = String.format("SELECT rownum FROM %s.tmp.%s ORDER BY rownum LIMIT 1", MINI_DFS_STORAGE_PLUGIN_NAME, viewName);
    String expectedMsg = String.format(
      "Not authorized to read view [%s] in schema [%s.tmp]",
      viewName,
      MINI_DFS_STORAGE_PLUGIN_NAME
    );

    client.queryBuilder()
      .sql(query)
      .userExceptionMatcher()
      .include(expectedMsg)
      .match();
  }

  static void createTableWithStoragePermissions(final Driver hiveDriver, final String db, final String tbl, final String tblDef,
                                                final String tblData, final String user, final String group, final short permissions) throws Exception {
    createTable(hiveDriver, db, tbl, tblDef, tblData);
    setStoragePermissions(db, tbl, user, group, permissions);
  }

  static void setStoragePermissions(String db, String tbl, String user, String group, short permissions) throws IOException {
    final Path p = getWhPathForHiveObject(db, tbl);
    fs.setPermission(p, new FsPermission(permissions));
    fs.setOwner(p, user, group);
  }

  static void createTable(final Driver driver, final String db, final String tbl, final String tblDef,
                          final String data) throws Exception {
    executeQuery(driver, String.format(tblDef, db, tbl));
    executeQuery(driver, String.format("LOAD DATA LOCAL INPATH '%s' INTO TABLE %s.%s", data, db, tbl));
  }

}
