/**
 * 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.ambari.server.orm.db;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;


/**
 * Test to check the sanity and conisistence of DDL scripts for different SQL dialects.
 * (e.g. no unnamed constraints, the same tables with the same columns and constraints must exist)
 */
public class DDLTests {
  private static final Logger LOG = LoggerFactory.getLogger(DDLTestUtils.class);

  private static final int EXPECTED_ALTER_TABLE_COUNT = 1;

  @Test
  public void testVerifyDerby() throws Exception {
    verifyDDL("Derby");
  }

  @Test
  public void testVerifyPostgres() throws Exception {
    verifyDDL("Postgres");
  }

  @Test
  public void testVerifyPostgresEmbedded() throws Exception {
    verifyDDL("Postgres-EMBEDDED");
  }

  @Test
  public void testVerifyMySQL() throws Exception {
    verifyDDL("MySQL");
  }

  @Test
  public void testVerifyOracle() throws Exception {
    verifyDDL("Oracle");
  }

  @Test
  public void testVerifySqlAnywhere() throws Exception {
    verifyDDL("SQLAnywhere");
  }

  @Test
  public void testVerifyMsSqlServer() throws Exception {
    verifyDDL("SQLServer");
  }

  /**
   * To verify if DDL have certain characteristics:
   * <ul>
   *   <li>There are no unnamed constraints</li>
   *   <li>Alter tables are only used in exceptional cases</li>
   *   <li>Table and constraint name lenghts doesn't exceed 30 (a restriction imposed by Oracle)</li>
   * </ul>
   *
   */
  private void verifyDDL(String dbType) throws Exception {
    LOG.info("Checking DDL for {}", dbType);
    DDL ddl = DDLTestUtils.getDdl(dbType);
    printDDLMetrics(ddl);

    // check for unwanted alter tables
    Assert.assertEquals("Expected count of alter tables mismatch. Please include all constraint definitions in " +
            "the create table statement, only use alter table in exceptional cases, such as to work around a circular " +
            "FK dependency. Would another such case occur, please document it in the DDL's and adjust the " +
            "EXPECTED_ALTER_TABLE_COUNT in this test.",
        EXPECTED_ALTER_TABLE_COUNT,
        ddl.alterTables.size());

    // check for too long table/constraint names
    for (String tableName: ddl.tableNames()) {
      Assert.assertTrue("Table name exceeds the 30 character limit: " + tableName, tableName.length() <= 30);
    }
    for (Table table: ddl.tables.values()) {
      Assert.assertTrue("PK name exceeds the 30 character limit: " + table.primaryKey,
          !table.primaryKey.isPresent() || table.primaryKey.get().name().length() <= 30);
      for (Constraint constr: Sets.union(table.foreignKeys, table.uniqueConstraints)) {
        Assert.assertTrue("Constraint name exceeds the 30 character limit: " + constr, constr.name().length() <= 30);
      }
    }
    // check for unnamed PK's (skip quartz tables)
    for (Table table: ddl.tables.values()) {
      Assert.assertFalse("Unnamed PK exists for table: " + table.name,
          !table.name.startsWith("qrtz") && table.primaryKey.isPresent() && table.primaryKey.get().name().equals("<default>"));
      for (Constraint constr: Sets.union(table.foreignKeys, table.uniqueConstraints)) {
        Assert.assertTrue("Constraint name exceeds the 30 character limit: " + constr, constr.name().length() <= 30);
      }
    }

  }

  @Test
  public void testComparePostgresEmbedded() throws Exception {
    compareAgainstPostgres("Postgres-EMBEDDED");
  }

  @Test
  public void testCompareDerby() throws Exception {
    compareAgainstPostgres("Derby");
  }

  @Test
  public void testCompareOracle() throws Exception {
    compareAgainstPostgres("Oracle");
  }

  @Test
  public void testCompareMySQL() throws Exception {
    compareAgainstPostgres("MySQL");
  }

  @Test
  public void testCompareSQLAnywhere() throws Exception {
    compareAgainstPostgres("SQLAnywhere");
  }

  @Test
  public void testCompareSQLServer() throws Exception {
    compareAgainstPostgres("SQLServer");
  }

  static void compareAgainstPostgres(String dbType) throws Exception {
    LOG.info("Comparing {} against Postgres", dbType);
    DDL postgres = DDLTestUtils.getDdl("Postgres");
    DDL other = DDLTestUtils.getDdl(dbType);
    List<String> diffs = compareDdls(postgres, other);
    if (diffs.isEmpty()) {
      LOG.info("Compare OK.");
    }
    else {
      LOG.info("{} differences found:", diffs.size());
      for (String diff: diffs) { LOG.info(diff); }
      Assert.fail("Found " + diffs.size() + " differences when comparing " + other + " against Postgres.");
    }
  }

  static void printDDLMetrics(DDL ddl) {
    LOG.info("DDL metrics for {}", ddl.dbType);
    int colCount = 0;
    int pkCount = 0;
    int fkCount = 0;
    int uqCount = 0;
    for (Table t: ddl.tables.values()) {
      colCount += t.columns.size();
      if (t.primaryKey.isPresent()) pkCount ++;
      fkCount += t.foreignKeys.size();
      uqCount += t.uniqueConstraints.size();
    }
    LOG.info("Found {} tables", ddl.tables.size());
    List<String> tableNames = new ArrayList<>();
    tableNames.addAll(ddl.tableNames());
    Collections.sort(tableNames);
    LOG.info("Table names: {}", Joiner.on(',').join(tableNames));
    LOG.info("Total number of Columns: {}", colCount);
    LOG.info("Total number of PK's: {}", pkCount);
    LOG.info("Total number of FK's: {}", fkCount);
    LOG.info("Total number of UQ's: {}", uqCount);
    LOG.info("Number of Alter table statements: {}", ddl.alterTables.size());

  }

  static List<String> compareDdls(DDL base, DDL other) {
    List<String> diffs = new ArrayList<>();

    if (!base.tableNames().equals(other.tableNames())) {
      Set<String> missingTables = Sets.difference(base.tableNames(), other.tableNames());
      if (!missingTables.isEmpty()) {
        diffs.add("Missing tables: " + Joiner.on(", ").join(missingTables));
      }

      Set<String> extraTables = Sets.difference(other.tableNames(), base.tableNames());
      if (!extraTables.isEmpty()) {
        diffs.add("Extra tables: " + Joiner.on(", ").join(extraTables));
      }
    }

    Set<String> commonTables = Sets.intersection(base.tableNames(), other.tableNames());
    for (String tableName: commonTables) {
      Table baseTable = base.tables.get(tableName);
      Table otherTable = other.tables.get(tableName);
      diffs.addAll(
          compareSets(String.format("Comparing columns of table %s.", tableName), baseTable.columns, otherTable.columns));
      diffs.addAll(
          DDLTests.<FKConstraintContent>compareConstraints(tableName, "FK", baseTable.foreignKeys, otherTable.foreignKeys, false));
      diffs.addAll(
          DDLTests.<Set<String>>compareConstraints(tableName, "UQ", baseTable.uniqueConstraints, otherTable.uniqueConstraints, false));
      boolean comparePKName = !tableName.contains("qrtz"); // we are more lenient with quartz tables
      diffs.addAll(
          DDLTests.<Set<String>>compareConstraints(tableName, "PK", toSet(baseTable.primaryKey), toSet(otherTable.primaryKey), comparePKName));
    }

    return diffs;
  }

  static <T> Set<T> toSet(Optional<T> arg) {
    return arg.isPresent() ? ImmutableSet.of(arg.get()) : ImmutableSet.<T>of();
  }

  static <ContentType> List<String> compareSets(String message, Set<ContentType> base, Set<ContentType> other) {
    List<String> diffs = new ArrayList<>(2);
    Set<ContentType> missingItems = Sets.difference(base, other);
    if (!missingItems.isEmpty()) {
      diffs.add(message + " Missing items: " + Joiner.on(", ").join(missingItems));
    }
    Set<ContentType> extraItems = Sets.difference(other, base);
    if (!extraItems.isEmpty()) {
      diffs.add(message + " Extra items: " + Joiner.on(", ").join(extraItems));
    }
    return diffs;
  }


  static <ContentType> List<String> compareConstraints(String tableName,
                                             String constraintType,
                                             Set<? extends Constraint<ContentType>> base,
                                             Set<? extends Constraint<ContentType>> other,
                                             boolean compareConstraintNames) {
    List<String> diffs = new ArrayList<>();
    Map<ContentType, Constraint<ContentType>> baseByContent = Maps.newHashMap();
    Map<ContentType, Constraint<ContentType>> otherByContent = Maps.newHashMap();
    for (Constraint<ContentType> c: base) {
      baseByContent.put(c.content(), c);
    }
    for (Constraint<ContentType> c: other) {
      otherByContent.put(c.content(), c);
    }
    diffs.addAll(compareSets(String.format("Comparing %ss of table %s.", constraintType, tableName),
        baseByContent.keySet(),
        otherByContent.keySet()));

    Set<ContentType> common = Sets.intersection(baseByContent.keySet(), otherByContent.keySet());
    for (ContentType constrContent : common) {
      Constraint b = baseByContent.get(constrContent);
      Constraint o = otherByContent.get(constrContent);
      if (!b.name().equals(o.name())) {
        if (compareConstraintNames) {
          diffs.add(String.format("Constraint name mismatch for table %s: %s vs. %s", tableName, b, o));
        }
        else {
          LOG.info("Ignoring constraint name mismatch for table {}: {} vs. {}", tableName, b, o);
        }
      }
    }

    return diffs;
  }

}
