/*
 *
 * MariaDB Client for Java
 *
 * Copyright (c) 2012-2014 Monty Program Ab.
 * Copyright (c) 2015-2020 MariaDB Corporation Ab.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to Monty Program Ab info@montyprogram.com.
 *
 * This particular MariaDB Client for Java file is work
 * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to
 * the following copyright and notice provisions:
 *
 * Copyright (c) 2009-2011, Marcus Eriksson
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this
 * list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 *
 * Neither the name of the driver nor the names of its contributors may not be
 * used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS  AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 *
 */

package org.mariadb.jdbc;

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

import com.sun.jna.Platform;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Thread.State;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;
import java.sql.Statement;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.mariadb.jdbc.failover.TcpProxy;
import org.mariadb.jdbc.internal.failover.AbstractMastersListener;
import org.mariadb.jdbc.internal.protocol.Protocol;
import org.mariadb.jdbc.internal.util.constant.HaMode;
import org.mariadb.jdbc.util.Options;

/**
 * Base util class. For testing mvn test -DdbUrl=jdbc:mariadb://localhost:3306/testj?user=root
 * -DlogLevel=FINEST
 */
@Ignore
@SuppressWarnings("Annotator")
public class BaseTest {
  protected static String mDefUrl;

  private static final NumberFormat numberFormat = DecimalFormat.getInstance(Locale.ROOT);
  protected static String connU;
  protected static String connUri;
  protected static String connDnsUri;
  protected static String hostname;
  protected static int port;
  protected static String database;
  protected static String username;
  protected static String password;
  protected static String defaultOther;
  protected static Options options;
  protected static String parameters;
  protected static boolean testSingleHost;
  protected static Connection sharedConnection;
  protected static boolean runLongTest = false;
  protected static boolean doPrecisionTest = true;
  protected static TcpProxy proxy = null;
  private static UrlParser urlParser;

  static {
    try (InputStream inputStream =
        BaseTest.class.getClassLoader().getResourceAsStream("conf.properties")) {
      Properties prop = new Properties();
      prop.load(inputStream);

      String val = System.getenv("TEST_REQUIRE_TLS");
      defaultOther = get("DB_OTHER", prop);
      if ("1".equals(val)) {
        String cert = System.getenv("TEST_DB_SERVER_CERT");
        defaultOther += "&useSsl&serverSslCert=" + cert;
      }
      hostname = get("DB_HOST", prop);
      username = get("DB_USER", prop);
      port = Integer.parseInt(get("DB_PORT", prop));
      password = get("DB_PASSWORD", prop);
      mDefUrl =
          String.format(
              "jdbc:mariadb://%s:%s/%s?user=%s&password=%s&%s",
              hostname, port, get("DB_DATABASE", prop), username, password, defaultOther);

      String version = System.getenv("v");
      double versionInt = 0;
      try {
        versionInt = Double.parseDouble(version);
      } catch (NumberFormatException | NullPointerException e) {
        // eat
      }
      try (Connection c = DriverManager.getConnection(mDefUrl + "&allowPublicKeyRetrieval=true")) {
        DatabaseMetaData meta = c.getMetaData();
        if ("MySQL".equals(meta.getDatabaseProductName())) {
          if (meta.getDatabaseProductVersion().substring(0, 3).compareTo("8.4") >= 0) {
            // choose to use allowPublicKeyRetrieval=true for testing
            Statement stmt = c.createStatement();
            ResultSet rs =
                stmt.executeQuery("SHOW STATUS LIKE 'Caching_sha2_password_rsa_public_key'");
            if (rs.next()) {
              mDefUrl += "&serverRsaPublicKeyFile=" + rs.getString(2);
            } else {
              mDefUrl += "&allowPublicKeyRetrieval=true";
            }
          } else {
            mDefUrl += "&allowPublicKeyRetrieval=true";
          }
        }
      } catch (SQLException e) {
        // eat
        e.printStackTrace();
      }
    } catch (IOException io) {
      io.printStackTrace();
    }
  }

  private static String get(String name, Properties prop) {
    String val = System.getenv("TEST_" + name);
    if (val == null) val = System.getProperty("TEST_" + name);
    if (val == null) val = prop.getProperty(name);
    return val;
  }

  public static String getHostSuffix() {
    if (hostname.equals("localhost")
        || "local".equals(System.getenv().getOrDefault("LOCAL_DB", "container"))) {
      return "@'localhost'";
    }
    return "@'%'";
  }

  @Rule
  public TestRule watcher =
      new TestWatcher() {
        private long ttime;

        protected void starting(Description description) {

          if (testSingleHost) {
            System.out.println(
                "start test : " + description.getClassName() + "." + description.getMethodName());
            ttime = System.nanoTime();
          }
        }

        // execute another query to ensure connection is stable
        protected void finished(Description description) {
          if (testSingleHost) {
            Random random = new Random();
            int randInt = random.nextInt();
            try (PreparedStatement preparedStatement =
                sharedConnection.prepareStatement("SELECT " + randInt)) {
              ResultSet rs = preparedStatement.executeQuery();
              assertTrue(rs.next());
              assertEquals(randInt, rs.getInt(1));
            } catch (SQLNonTransientConnectionException connFail) {
              connFail.printStackTrace();
              try {
                aBefore();
              } catch (SQLException e) {
                System.out.println("ERROR reconnecting");
                e.printStackTrace();
              }
              fail(
                  "Prepare after test fail for "
                      + description.getClassName()
                      + "."
                      + description.getMethodName());

            } catch (Exception e) {
              e.printStackTrace();
              fail(
                  "Prepare after test fail for "
                      + description.getClassName()
                      + "."
                      + description.getMethodName());
            }
          }
        }

        protected void succeeded(Description description) {
          if (testSingleHost) {
            System.out.println(
                "finished test success : "
                    + description.getClassName()
                    + "."
                    + description.getMethodName()
                    + " after "
                    + numberFormat.format(((double) System.nanoTime() - ttime) / 1000000)
                    + " ms");
          }
        }

        protected void failed(Throwable throwable, Description description) {
          if (testSingleHost) {
            System.out.println(
                "finished test failed : "
                    + description.getClassName()
                    + "."
                    + description.getMethodName()
                    + " after "
                    + numberFormat.format(((double) System.nanoTime() - ttime) / 1000000)
                    + " ms");
          }
        }
      };

  /**
   * Initialization.
   *
   * @throws SQLException exception
   */
  @BeforeClass()
  public static void aBefore() throws SQLException {
    String url = System.getProperty("dbUrl", mDefUrl);
    runLongTest = Boolean.parseBoolean(System.getProperty("runLongTest", "false"));
    testSingleHost = Boolean.parseBoolean(System.getProperty("testSingleHost", "true"));

    if (testSingleHost) {
      urlParser = UrlParser.parse(url); // + "&pool=true&maxPoolSize=2&minPoolSize=1");
      if (urlParser.getHostAddresses().size() > 0) {
        hostname = urlParser.getHostAddresses().get(0).host;
        port = urlParser.getHostAddresses().get(0).port;
      } else {
        hostname = null;
        port = 3306;
      }
      database = urlParser.getDatabase();
      username = urlParser.getUsername();
      password = urlParser.getPassword();
      options = urlParser.getOptions();
      int separator = url.indexOf("//");
      String urlSecondPart = url.substring(separator + 2);
      int dbIndex = urlSecondPart.indexOf("/");
      int paramIndex = urlSecondPart.indexOf("?");

      String additionalParameters;
      if ((dbIndex < paramIndex && dbIndex < 0) || (dbIndex > paramIndex && paramIndex > -1)) {
        additionalParameters = urlSecondPart.substring(paramIndex);
      } else if ((dbIndex < paramIndex && dbIndex > -1)
          || (dbIndex > paramIndex && paramIndex < 0)) {
        additionalParameters = urlSecondPart.substring(dbIndex);
      } else {
        additionalParameters = null;
      }
      if (additionalParameters != null) {
        parameters = "";
        if (additionalParameters.indexOf("?") >= 0) {
          parameters = additionalParameters.substring(additionalParameters.indexOf("?") + 1);
        }
      } else {
        parameters = null;
      }

      setUri();
      urlParser.auroraPipelineQuirks();

      try {
        sharedConnection = MariaDbConnection.newConnection(urlParser, null);
      } catch (SQLException sqle) {
        System.out.println("Connection from pool fail :" + sqle.getMessage());
        sharedConnection = DriverManager.getConnection(url);
      }

      String dbVersion = sharedConnection.getMetaData().getDatabaseProductVersion();
      doPrecisionTest =
          isMariadbServer() || !dbVersion.startsWith("5.5"); // MySQL 5.5 doesn't support precision
    }
  }

  private static void setUri() {
    connU =
        "jdbc:mariadb://"
            + ((hostname == null) ? "localhost" : hostname)
            + ":"
            + port
            + "/"
            + ((database == null) ? "" : database);
    connUri = connU + "?" + parameters;
    connDnsUri =
        "jdbc:mariadb://mariadb.example.com:"
            + port
            + "/"
            + database
            + "?"
            + parameters
            + (password != null && !"".equals(password) ? "&password=" + password : "");
  }

  /**
   * Destroy the test tables.
   *
   * @throws SQLException exception
   */
  @AfterClass
  public static void after() throws Throwable {
    if (testSingleHost && sharedConnection != null && !sharedConnection.isClosed()) {
      try {
        sharedConnection.close();
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
    if (!Platform.isWindows()) {

      Iterator<Thread> it = Thread.getAllStackTraces().keySet().iterator();
      boolean ok = true;
      Thread thread;
      while (it.hasNext()) {
        thread = it.next();
        if (thread.getName().contains("MariaDb-bulk-")) {
          if (thread.getState() != State.WAITING) {
            // print stack trace to console.
            for (StackTraceElement ste : thread.getStackTrace()) {
              System.out.println(ste);
            }
          }
          if (thread.getState() != State.WAITING) ok = false;
        }
      }
      if (ok) return;
      Thread.sleep(1000);
      it = Thread.getAllStackTraces().keySet().iterator();
      while (it.hasNext()) {
        thread = it.next();
        if (thread.getName().contains("MariaDb-bulk-")) {
          if (thread.getState() != State.WAITING) {
            // print stack trace to console.
            for (StackTraceElement ste : thread.getStackTrace()) {
              System.out.println(ste);
            }
          }
          assertEquals(State.WAITING, thread.getState());
        }
      }
    }
  }

  // common function for logging information
  static void logInfo(String message) {
    System.out.println(message);
  }

  /**
   * Check if current DB server is MariaDB.
   *
   * @return true if DB is mariadb
   * @throws SQLException exception
   */
  static boolean isMariadbServer() throws SQLException {
    if (testSingleHost) {
      DatabaseMetaData md = sharedConnection.getMetaData();
      return md.getDatabaseProductVersion().contains("MariaDB");
    }
    return false;
  }

  /**
   * List current connections to server.
   *
   * @return number of thread connected.
   * @throws SQLException if queries failed
   */
  public static int getCurrentConnections() {
    try {
      Statement stmt = sharedConnection.createStatement();
      ResultSet rs = stmt.executeQuery("show status where `variable_name` = 'Threads_connected'");
      assertTrue(rs.next());
      return rs.getInt(2);

    } catch (SQLException e) {
      return -1;
    }
  }

  /**
   * Check if version if at minimum the version asked.
   *
   * @param major database major version
   * @param minor database minor version
   * @param patch database patch version
   * @throws SQLException exception
   */
  public static boolean minVersion(int major, int minor, int patch) {
    return ((MariaDbConnection) sharedConnection).versionGreaterOrEqual(major, minor, patch);
  }

  public static boolean exactVersion(int major, int minor, int patch) {
    return ((MariaDbConnection) sharedConnection).getProtocol().getMajorServerVersion() == major
        && ((MariaDbConnection) sharedConnection).getProtocol().getMinorServerVersion() == minor
        && ((MariaDbConnection) sharedConnection).getProtocol().getPatchServerVersion() == patch;
  }

  private static String maxscaleVersion = null;

  /**
   * Indicate if the connection is established through MaxScale. Detected from the {@code maxscale}
   * session-tracked variable the proxy reports, falling back to the {@code maxscale-tag} CI
   * environment variable when the protocol value is unavailable.
   *
   * @return true if connected through MaxScale
   */
  public static boolean isMaxscale() {
    if (maxscaleVersion == null) {
      try {
        maxscaleVersion = ((MariaDbConnection) sharedConnection).getProtocol().getMaxscaleVersion();
      } catch (Exception e) {
        // shared connection not available (e.g. failover tests) : rely on the env fallback
      }
      if (maxscaleVersion == null) {
        String maxscaleTag = System.getenv("MAXSCALE_VERSION");
        return maxscaleTag != null && maxscaleTag.length() > 0;
      }
    }
    return true;
  }

  /**
   * Indicate if there is a anonymous user.
   *
   * @return true if anonymous user exist
   * @throws SQLException if any error occur
   */
  public boolean anonymousUser() throws SQLException {
    if (testSingleHost) {
      Statement stmt = sharedConnection.createStatement();
      ResultSet rs =
          stmt.executeQuery("SELECT * FROM mysql.user u where u.Host='localhost' and u.User=''");
      return rs.next();
    }
    return false;
  }

  /**
   * Create a connection with proxy.
   *
   * @param info additionnal properties
   * @return a proxyfied connection
   * @throws SQLException if any error occur
   */
  public Connection createProxyConnection(Properties info) throws SQLException {
    UrlParser tmpUrlParser = UrlParser.parse(connUri);
    username = tmpUrlParser.getUsername();
    hostname = tmpUrlParser.getHostAddresses().get(0).host;
    String sockethosts = "";
    HostAddress hostAddress;
    try {
      hostAddress = tmpUrlParser.getHostAddresses().get(0);
      proxy = new TcpProxy(hostAddress.host, hostAddress.port);
      sockethosts +=
          "address=(host=localhost)(port="
              + proxy.getLocalPort()
              + ")"
              + ((hostAddress.type != null) ? "(type=" + hostAddress.type + ")" : "");
    } catch (IOException e) {
      e.printStackTrace();
    }
    String[] splitValue = connUri.split("/");
    String[] subarray =
        Arrays.asList(splitValue).subList(3, splitValue.length).toArray(new String[0]);
    String dbAndParameters = String.join("/", subarray);

    return openConnection(
        "jdbc:mariadb://"
            + sockethosts
            + "/"
            + dbAndParameters
            + ((options.useSsl != null)
                ? "&useSsl" + options.useSsl + "&disableSslHostnameVerification"
                : "")
            + ((options.serverSslCert != null) ? "&serverSslCert=" + options.serverSslCert : ""),
        info);
  }

  /**
   * Stop proxy, and restart it after a certain amount of time.
   *
   * @param millissecond milliseconds
   */
  public void stopProxy(long millissecond) {
    proxy.restart(millissecond);
  }

  /** Stop proxy. */
  public void stopProxy() {
    proxy.stop();
  }

  public void delayProxy(int millissecond) {
    proxy.setDelay(millissecond);
  }

  public void forceCloseProxy() {
    proxy.forceClose();
  }

  public void removeDelayProxy() {
    proxy.removeDelay();
  }

  /** Restart proxy. */
  public void restartProxy() {
    proxy.restart();
  }

  /** Clean proxies. */
  public void closeProxy() {
    try {
      proxy.stop();
      proxy = null;
    } catch (Exception e) {
      // Eat exception
    }
  }

  @Before
  public void init() {
    Assume.assumeTrue(testSingleHost);
  }

  /**
   * Permit to assure that host are not in a blacklist after a test.
   *
   * @param connection connection
   */
  public void assureBlackList(Connection connection) {
    AbstractMastersListener.clearBlacklist();
  }

  protected Protocol getProtocolFromConnection(Connection conn) throws Throwable {

    Method getProtocol = MariaDbConnection.class.getDeclaredMethod("getProtocol");
    getProtocol.setAccessible(true);
    Object obj = getProtocol.invoke(conn);
    return (Protocol) obj;
  }

  protected void setHostname(String hostname) throws SQLException {
    BaseTest.hostname = hostname;
    setUri();
    setConnection();
  }

  protected void setPort(int port) throws SQLException {
    BaseTest.port = port;
    setUri();
    setConnection();
  }

  protected void setDatabase(String database) throws SQLException {
    BaseTest.database = database;
    BaseTest.setUri();
    setConnection();
  }

  protected void setUsername(String username) throws SQLException {
    BaseTest.username = username;
    setUri();
    setConnection();
  }

  protected void setPassword(String password) throws SQLException {
    BaseTest.password = password;
    setUri();
    setConnection();
  }

  protected Connection setBlankConnection(String parameters) throws SQLException {
    return openConnection(
        connU
            + "?user="
            + username
            + (password != null && !"".equals(password) ? "&password=" + password : "")
            + ((options.useSsl != null) ? "&useSsl=" + options.useSsl : "")
            + ((options.serverSslCert != null) ? "&serverSslCert=" + options.serverSslCert : "")
            + parameters,
        null);
  }

  protected Connection setConnection() throws SQLException {
    return openConnection(connUri, null);
  }

  protected Connection setConnection(Map<String, String> props) throws SQLException {
    Properties info = new Properties();
    for (String key : props.keySet()) {
      info.setProperty(key, props.get(key));
    }
    return openConnection(connU, info);
  }

  protected Connection setConnection(Properties info) throws SQLException {
    return openConnection(connUri, info);
  }

  protected Connection setConnection(String parameters) throws SQLException {
    return openConnection(connUri + parameters, null);
  }

  protected Connection setConnection(String additionalParameters, String database, int port)
      throws SQLException {
    StringBuilder sb = new StringBuilder();
    sb.append("jdbc:mariadb://");
    if (hostname == null) {
      sb.append("localhost");
    } else {
      sb.append(hostname);
    }
    sb.append(":").append(port).append("/");
    if (database != null) {
      sb.append(database);
    }
    sb.append("?user=").append(username);
    if (password != null && !password.isEmpty()) {
      sb.append("&password=").append(password);
    }
    if (parameters != null && !parameters.isEmpty()) {
      sb.append("&").append(parameters);
    }
    sb.append(additionalParameters);
    return openConnection(sb.toString(), null);
  }

  protected Connection setDnsConnection(String parameters) throws SQLException {
    String connU =
        "jdbc:mariadb://mariadb.example.com:" + port + "/" + ((database == null) ? "" : database);
    String connUri =
        connU
            + "?user="
            + username
            + (password != null && !"".equals(password) ? "&password=" + password : "")
            + (parameters != null ? "&" + parameters : "");
    return openConnection(connUri + parameters, null);
  }

  /**
   * Permit to reconstruct a connection.
   *
   * @param uri base uri
   * @param info additionnal properties
   * @return A connection
   * @throws SQLException is any error occur
   */
  public Connection openConnection(String uri, Properties info) throws SQLException {
    if (info == null) {
      return DriverManager.getConnection(uri);
    } else {
      return DriverManager.getConnection(uri, info);
    }
  }

  protected Connection openNewConnection(String url) throws SQLException {
    return DriverManager.getConnection(url);
  }

  protected Connection openNewConnection(String url, Properties info) throws SQLException {
    return DriverManager.getConnection(url, info);
  }

  protected boolean isGalera() {
    try {
      Statement st = sharedConnection.createStatement();
      ResultSet rs = st.executeQuery("show status like 'wsrep_cluster_size'");
      if (rs.next()) {
        return rs.getInt(2) > 0;
      }
    } catch (SQLException sqle) {
      // skip
    }
    return false;
  }

  /**
   * Check if max_allowed_packet value is equal or greater then 8m.
   *
   * @param testName test method name
   * @return true if max_allowed_packet value is equal or greater then 8m.
   * @throws SQLException if connection fail
   */
  public boolean checkMaxAllowedPacketMore8m(String testName) throws SQLException {
    Statement st = sharedConnection.createStatement();
    ResultSet rs = st.executeQuery("select @@max_allowed_packet");
    assertTrue(rs.next());
    long maxAllowedPacket = rs.getLong(1);

    if (maxAllowedPacket < 8 * 1024 * 1024L) {

      System.out.println(
          "test '" + testName + "' skipped  due to server variable max_allowed_packet < 8M");
      return false;
    }

    return true;
  }

  /**
   * Check if max_allowed_packet value is equal or greater then 20m.
   *
   * @param testName test method name
   * @return true if max_allowed_packet value is equal or greater then 20m.
   * @throws SQLException if connection fail
   */
  public boolean checkMaxAllowedPacketMore20m(String testName) throws SQLException {
    return checkMaxAllowedPacketMore20m(testName, true);
  }

  /**
   * Check if max_allowed_packet value is equal or greater then 20m.
   *
   * @param testName test method name
   * @param displayMessage message to display in case of error.
   * @return true if max_allowed_packet value is equal or greater then 20m.
   * @throws SQLException if connection fail
   */
  public boolean checkMaxAllowedPacketMore20m(String testName, boolean displayMessage)
      throws SQLException {
    Statement st = sharedConnection.createStatement();
    ResultSet rs = st.executeQuery("select @@max_allowed_packet");
    assertTrue(rs.next());
    long maxAllowedPacket = rs.getLong(1);

    if (maxAllowedPacket < 20 * 1024 * 1024L) {

      if (displayMessage) {
        System.out.println(
            "test '" + testName + "' skipped  due to server variable max_allowed_packet < 20M");
      }
      return false;
    }

    return true;
  }

  /**
   * Check if max_allowed_packet value is equal or greater then 40m.
   *
   * @param testName test method name
   * @return true if max_allowed_packet value is equal or greater then 40m.
   * @throws SQLException if connection fail
   */
  public boolean checkMaxAllowedPacketMore40m(String testName) throws SQLException {
    return checkMaxAllowedPacketMore40m(testName, true);
  }

  /**
   * Check if max_allowed_packet value is equal or greater then 40m.
   *
   * @param testName test method name
   * @param displayMsg message to display in case of error.
   * @return true if max_allowed_packet value is equal or greater then 40m.
   * @throws SQLException if connection fail
   */
  public boolean checkMaxAllowedPacketMore40m(String testName, boolean displayMsg)
      throws SQLException {
    Statement st = sharedConnection.createStatement();
    ResultSet rs = st.executeQuery("select @@max_allowed_packet");
    assertTrue(rs.next());
    long maxAllowedPacket = rs.getLong(1);

    if (maxAllowedPacket < 40 * 1024 * 1024L) {
      if (displayMsg) {
        System.out.println(
            "test '" + testName + "' skipped  due to server variable max_allowed_packet < 40M");
      }
      return false;
    }

    return true;
  }

  /**
   * Does the user have super privileges.
   *
   * @param testName test name
   * @return true if super user
   * @throws SQLException in any connection occur
   */
  public boolean hasSuperPrivilege(String testName) throws SQLException {
    boolean superPrivilege = false;
    try (Statement st = sharedConnection.createStatement()) {
      // first test for specific user and host combination
      try (ResultSet rs =
          st.executeQuery(
              "SELECT Super_Priv FROM mysql.user WHERE user = '"
                  + username
                  + "' AND host = '"
                  + hostname
                  + "'")) {
        if (rs.next()) {
          superPrivilege = (rs.getString(1).equals("Y"));
        } else {
          // then check for user on whatever (%) host
          try (ResultSet rs2 =
              st.executeQuery(
                  "SELECT Super_Priv FROM mysql.user WHERE user = '"
                      + username
                      + "' AND host = "
                      + getHostSuffix().substring(1))) {
            if (rs2.next()) {
              superPrivilege = (rs2.getString(1).equals("Y"));
            }
          }
        }
      }
    }

    if (!superPrivilege) {
      System.out.println(
          "test '"
              + testName
              + "' skipped because user '"
              + username
              + "' doesn't have SUPER privileges");
    }

    return superPrivilege;
  }

  /**
   * Is the connection local.
   *
   * @param testName test method name
   * @return true if local
   */
  public boolean isLocalConnection(String testName) {
    boolean isLocal = false;

    try {
      if (InetAddress.getByName(hostname).isAnyLocalAddress()
          || InetAddress.getByName(hostname).isLoopbackAddress()) {
        isLocal = true;
        String localDb = System.getenv().get("LOCAL_DB");
        if (localDb != null) isLocal = "local".equals(localDb);
      }
    } catch (UnknownHostException e) {
      // for some reason it wasn't possible to parse the hostname
      // do nothing
    }

    if (!isLocal) {
      System.out.println("test '" + testName + "' skipped because connection is not local");
    }

    return isLocal;
  }

  /**
   * Indicate if server has ssl configured.
   *
   * @param connection connection to check
   * @return true if SSL is enabled
   */
  public static boolean haveSsl(Connection connection) {
    try {
      ResultSet rs = connection.createStatement().executeQuery("select @@have_ssl");
      assertTrue(rs.next());
      return "YES".equals(rs.getString(1));
    } catch (Exception e) {
      return false; /* maybe 4.x ? */
    }
  }

  /**
   * Check if version if at minimum the version asked.
   *
   * @param major database major version
   * @param minor database minor version
   * @throws SQLException exception
   */
  public static boolean minVersion(int major, int minor) throws SQLException {
    DatabaseMetaData md = sharedConnection.getMetaData();
    int dbMajor = md.getDatabaseMajorVersion();
    int dbMinor = md.getDatabaseMinorVersion();
    return (dbMajor > major || (dbMajor == major && dbMinor >= minor));
  }

  /**
   * Check if version if before the version asked.
   *
   * @param major database major version
   * @param minor database minor version
   * @throws SQLException exception
   */
  public static boolean strictBeforeVersion(int major, int minor) throws SQLException {
    DatabaseMetaData md = sharedConnection.getMetaData();
    int dbMajor = md.getDatabaseMajorVersion();
    int dbMinor = md.getDatabaseMinorVersion();
    return (dbMajor < major || (dbMajor == major && dbMinor < minor));
  }

  /**
   * Cancel if database version match.
   *
   * @param major db major version
   * @param minor db minor version
   * @throws SQLException exception
   */
  public void cancelForVersion(int major, int minor) throws SQLException {

    String dbVersion = sharedConnection.getMetaData().getDatabaseProductVersion();
    Assume.assumeFalse(dbVersion.startsWith(major + "." + minor));
  }

  /**
   * Cancel if database version match.
   *
   * @param major db major version
   * @param minor db minor version
   * @param patch db patch version
   * @throws SQLException exception
   */
  public void cancelForVersion(int major, int minor, int patch) throws SQLException {
    String dbVersion = sharedConnection.getMetaData().getDatabaseProductVersion();
    Assume.assumeFalse(dbVersion.startsWith(major + "." + minor + "." + patch));
  }

  public void requireMinimumVersion(int major, int minor) throws SQLException {
    Assume.assumeTrue(minVersion(major, minor));
  }

  /**
   * Change session time zone.
   *
   * @param connection connection
   * @param timeZone timezone to set
   * @throws SQLException exception
   */
  public void setSessionTimeZone(Connection connection, String timeZone) throws SQLException {
    try (Statement statement = connection.createStatement()) {
      statement.execute("set @@session.time_zone = '" + timeZone + "'");
    }
  }

  /**
   * Get row number.
   *
   * @param tableName table name
   * @return resultset number in this table
   * @throws SQLException if error occur
   */
  public int getRowCount(String tableName) throws SQLException {
    ResultSet rs =
        sharedConnection.createStatement().executeQuery("SELECT COUNT(*) FROM " + tableName);
    if (rs.next()) {
      return rs.getInt(1);
    }
    throw new SQLException("No table " + tableName + " found");
  }

  /**
   * Permit to know if sharedConnection will use Prepare. (in case dbUrl modify default options)
   *
   * @return true if PreparedStatement will use Prepare.
   */
  public boolean sharedUsePrepare() {
    return urlParser.getOptions().useServerPrepStmts
        && !urlParser.getOptions().rewriteBatchedStatements;
  }

  /**
   * Permit access to current sharedConnection options.
   *
   * @return Options
   */
  public Options sharedOptions() {
    return urlParser.getOptions();
  }

  /**
   * Permit to know if sharedConnection use rewriteBatchedStatements.
   *
   * @return true if option rewriteBatchedStatements is set to true
   */
  public boolean sharedIsRewrite() {
    return urlParser.getOptions().rewriteBatchedStatements;
  }

  /**
   * Has server bulk capacity.
   *
   * @return true if server has bulk capacity and option not disabled
   */
  public boolean sharedBulkCapacity() {
    return urlParser.getOptions().useBatchMultiSend;
  }

  /**
   * Permit to know if sharedConnection use compression.
   *
   * @return true if option compression is set to true
   */
  public boolean sharedUseCompression() {
    return urlParser.getOptions().useCompression;
  }

  public boolean sharedIsAurora() {
    return urlParser.getHaMode() == HaMode.AURORA;
  }

  /**
   * Check if server and client are on same host (not using containers).
   *
   * @return true if server and client are really on same host
   */
  public boolean hasSameHost() {
    try {
      Statement st = sharedConnection.createStatement();
      ResultSet rs = st.executeQuery("select @@version_compile_os");
      if (rs.next()) {
        if ((rs.getString(1).contains("linux") && Platform.isWindows())
            || (rs.getString(1).contains("win") && Platform.isLinux())) {
          return false;
        }
      }
    } catch (SQLException sqle) {
      // eat
    }
    return true;
  }

  /**
   * Get current autoincrement value, since Galera values are automatically set.
   *
   * @throws SQLException if any error occur.
   */
  public int[] setAutoInc() throws SQLException {
    return setAutoInc(1, 0);
  }

  /**
   * Get current autoincrement value, since Galera values are automatically set.
   *
   * @param autoIncInit default increment
   * @param autoIncOffsetInit default increment offset
   * @throws SQLException if any error occur
   * @see <a
   *     href="https://mariadb.org/auto-increments-in-galera/">https://mariadb.org/auto-increments-in-galera/</a>
   */
  public int[] setAutoInc(int autoIncInit, int autoIncOffsetInit) throws SQLException {

    int autoInc = autoIncInit;
    int autoIncOffset = autoIncOffsetInit;
    if (isGalera()) {
      ResultSet rs =
          sharedConnection.createStatement().executeQuery("show variables like '%auto_increment%'");
      while (rs.next()) {
        if ("auto_increment_increment".equals(rs.getString(1))) {
          autoInc = rs.getInt(2);
        }
        if ("auto_increment_offset".equals(rs.getString(1))) {
          autoIncOffset = rs.getInt(2);
        }
      }
      if (autoInc == 1) {
        // galera with one node only, then offset is not used
        autoIncOffset = 0;
      }
    }
    return new int[] {autoInc, autoIncOffset};
  }
}
