/*
 *
 * MariaDB Client for Java
 *
 * Copyright (c) 2012-2014 Monty Program Ab.
 * Copyright (c) 2015-2017 MariaDB 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.failover;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.List;
import org.junit.After;
import org.junit.AfterClass;
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.HostAddress;
import org.mariadb.jdbc.MariaDbConnection;
import org.mariadb.jdbc.MariaDbPreparedStatementServer;
import org.mariadb.jdbc.UrlParser;
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.internal.util.dao.ServerPrepareResult;

/**
 * Base util class. For testing example mvn test -DdbUrl=jdbc:mariadb://localhost:3306,localhost:3307/test?user=root
 * -DlogLevel=FINEST specific parameters : defaultMultiHostUrl : If testing Aurora, set the region.
 * Default is US_EAST_1.
 */
@Ignore
public class BaseMultiHostTest {

  //hosts
  private static final HashMap<HaMode, TcpProxy[]> proxySet = new HashMap<>();
  protected static String initialGaleraUrl;
  protected static String initialAuroraUrl;
  protected static String initialReplicationUrl;
  protected static String initialSequentialUrl;
  protected static String initialLoadbalanceUrl;
  protected static String initialUrl;
  protected static String proxyGaleraUrl;
  protected static String proxySequentialUrl;
  protected static String proxyAuroraUrl;
  protected static String proxyReplicationUrl;
  protected static String proxyLoadbalanceUrl;
  protected static String proxyUrl;
  protected static String jobId;
  protected static String username;
  private static String hostname;
  public HaMode currentType;
  @Rule
  public TestRule watcher = new TestWatcher() {
    protected void starting(Description description) {
      System.out.println(
          "start test : " + description.getClassName() + "." + description.getMethodName());
    }

    protected void succeeded(Description description) {
      System.out.println("finished test success : " + description.getClassName() + "." + description
          .getMethodName());
    }

    protected void failed(Throwable throwable, Description description) {
      System.out.println("finished test failed : " + description.getClassName() + "." + description
          .getMethodName());
    }
  };
  protected String defaultUrl;

  /**
   * Initialize parameters.
   *
   * @throws SQLException exception
   */
  @BeforeClass
  public static void beforeClass() throws SQLException {

    initialUrl = System.getProperty("dbFailoverUrl");
    initialGaleraUrl = System.getProperty("defaultGaleraUrl");
    initialReplicationUrl = System.getProperty("defaultReplicationUrl");
    initialLoadbalanceUrl = System.getProperty("defaultLoadbalanceUrl");
    initialAuroraUrl = System.getProperty("defaultAuroraUrl");
    jobId = System.getProperty("jobId", "_0");

    if (initialUrl != null) {
      proxyUrl = createProxies(initialUrl, HaMode.NONE);
    }
    if (initialReplicationUrl != null) {
      proxyReplicationUrl = createProxies(initialReplicationUrl, HaMode.REPLICATION);
    }
    if (initialLoadbalanceUrl != null) {
      proxyLoadbalanceUrl = createProxies(initialLoadbalanceUrl, HaMode.LOADBALANCE);
    }
    if (initialGaleraUrl != null) {
      proxyGaleraUrl = createProxies(initialGaleraUrl, HaMode.FAILOVER);
    }
    if (initialGaleraUrl != null) {
      proxySequentialUrl = createProxies(initialGaleraUrl, HaMode.SEQUENTIAL);
    }
    if (initialAuroraUrl != null) {
      proxyAuroraUrl = createProxies(initialAuroraUrl, HaMode.AURORA);
    }
  }

  /**
   * Check server minimum version.
   *
   * @param connection connection to use
   * @param major      major minimal number
   * @param minor      minor minimal number
   * @return is server compatible
   * @throws SQLException exception
   */
  public static boolean requireMinimumVersion(Connection connection, int major, int minor)
      throws SQLException {
    DatabaseMetaData md = connection.getMetaData();
    int dbMajor = md.getDatabaseMajorVersion();
    int dbMinor = md.getDatabaseMinorVersion();
    return (dbMajor > major
        || (dbMajor == major && dbMinor >= minor));
  }

  private static String createProxies(String tmpUrl, HaMode proxyType) throws SQLException {
    UrlParser tmpUrlParser;
    if (proxyType == HaMode.AURORA) {
      //if using cluster end-point, permit to retrieve current master and replica instances
      tmpUrlParser = retrieveEndpointsForProxies(tmpUrl);
    } else {
      tmpUrlParser = UrlParser.parse(tmpUrl);
    }

    TcpProxy[] tcpProxies = new TcpProxy[tmpUrlParser.getHostAddresses().size()];
    username = tmpUrlParser.getUsername();
    hostname = tmpUrlParser.getHostAddresses().get(0).host;
    StringBuilder sockethosts = new StringBuilder();
    HostAddress hostAddress;
    for (int i = 0; i < tmpUrlParser.getHostAddresses().size(); i++) {
      try {
        hostAddress = tmpUrlParser.getHostAddresses().get(i);
        tcpProxies[i] = new TcpProxy(hostAddress.host, hostAddress.port);
        sockethosts.append(",address=(host=localhost)(port=").append(tcpProxies[i].getLocalPort())
            .append(")")
            .append((hostAddress.type != null) ? "(type=" + hostAddress.type + ")" : "");
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    proxySet.put(proxyType, tcpProxies);
    if (tmpUrlParser.getHaMode().equals(HaMode.NONE)) {
      return "jdbc:mariadb://" + sockethosts.substring(1) + "/" + tmpUrl.split("/")[3];
    } else {
      return "jdbc:mariadb:" + tmpUrlParser.getHaMode().toString().toLowerCase() + "://"
          + sockethosts.substring(1)
          + "/" + tmpUrl.split("/")[3];
    }

  }

  private static UrlParser retrieveEndpointsForProxies(String tmpUrl) throws SQLException {
    try {
      Connection connection = DriverManager.getConnection(tmpUrl);
      connection.setReadOnly(true);
      try {
        Protocol protocol = (new BaseMultiHostTest().getProtocolFromConnection(connection));
        return protocol.getUrlParser();
      } catch (Throwable throwable) {
        connection.close();
        return UrlParser.parse(tmpUrl);
      }
    } catch (SQLException se) {
      return UrlParser.parse(tmpUrl);
    }
  }

  /**
   * Clean proxies.
   */
  @AfterClass
  public static void afterClass() {
    for (TcpProxy[] tcpProxies : proxySet.values()) {
      for (TcpProxy tcpProxy : tcpProxies) {
        try {
          tcpProxy.stop();
        } catch (Exception e) {
          //Eat exception
        }
      }
    }
  }

  /**
   * Delete table and procedure if created. Close connection if needed
   */
  @After
  public void afterBaseTest() {
    assureProxy();
    assureBlackList();
  }

  protected Connection getNewConnection() throws SQLException {
    return getNewConnection(null, false);
  }

  protected Connection getNewConnection(boolean proxy) throws SQLException {
    return getNewConnection(null, proxy);
  }

  protected Connection getNewConnection(String additionnalConnectionData, boolean proxy)
      throws SQLException {
    return getNewConnection(additionnalConnectionData, proxy, false);
  }

  protected Connection getNewConnection(String additionnalConnectionData, boolean proxy,
      boolean forceNewProxy)
      throws SQLException {
    if (proxy) {
      String tmpProxyUrl = proxyUrl;
      if (forceNewProxy) {
        tmpProxyUrl = createProxies(defaultUrl, currentType);
      }
      tmpProxyUrl += (additionnalConnectionData == null) ? "" : additionnalConnectionData;
      return DriverManager.getConnection(tmpProxyUrl);

    } else {
      if (additionnalConnectionData == null) {
        return DriverManager.getConnection(defaultUrl);
      } else {
        return DriverManager.getConnection(defaultUrl + additionnalConnectionData);
      }
    }
  }

  /**
   * Stop proxy, and restart it after a certain amount of time.
   *
   * @param hostNumber   hostnumber (first is one)
   * @param millissecond milliseconds
   */
  public void stopProxy(int hostNumber, long millissecond) {
    proxySet.get(currentType)[hostNumber - 1].restart(millissecond);
  }

  /**
   * Stop proxy.
   *
   * @param hostNumber host number (first is 1)
   */
  public void stopProxy(int hostNumber) {
    proxySet.get(currentType)[hostNumber - 1].stop();
  }

  /**
   * Stop all proxy but the one in parameter.
   *
   * @param hostNumber the proxy to not close
   */
  public void stopProxyButParameter(int hostNumber) {
    TcpProxy[] proxies = proxySet.get(currentType);
    for (int i = 0; i < proxies.length; i++) {
      if (i != hostNumber - 1) {
        proxies[i].stop();
      }
    }
  }

  /**
   * Restart proxy.
   *
   * @param hostNumber host number (first is  1)
   */
  public void restartProxy(int hostNumber) {
    if (hostNumber != -1) {
      proxySet.get(currentType)[hostNumber - 1].restart();
    }
  }

  /**
   * Assure that proxies are reset after each test.
   */
  public void assureProxy() {
    for (TcpProxy[] tcpProxies : proxySet.values()) {
      for (TcpProxy tcpProxy : tcpProxies) {
        tcpProxy.assureProxyOk();
      }
    }
  }

  /**
   * Assure that blacklist is reset after each test.
   */
  public void assureBlackList() {
    AbstractMastersListener.clearBlacklist();
  }

  /**
   * Does the user have super privileges or not.
   */
  public boolean hasSuperPrivilege(Connection connection, String testName) throws SQLException {
    boolean superPrivilege = false;
    Statement st = connection.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 = '%'")) {
          if (rs2.next()) {
            superPrivilege = (rs2.getString(1).equals("Y"));
          }
        }
      }
    }

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

    return superPrivilege;
  }

  protected Protocol getProtocolFromConnection(Connection conn) throws Throwable {

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

  /**
   * Retrieve server Id.
   *
   * @param connection connection
   * @return server index
   * @throws Throwable exception
   */
  public int getServerId(Connection connection) throws Throwable {
    Protocol protocol = getProtocolFromConnection(connection);
    HostAddress hostAddress = protocol.getHostAddress();
    List<HostAddress> hostAddressList = protocol.getUrlParser().getHostAddresses();
    return hostAddressList.indexOf(hostAddress) + 1;
  }

  /**
   * Retrieve current HostAddress.
   *
   * @param connection connection
   * @return Current Host address
   * @throws Throwable if any exception occur
   */
  public HostAddress getServerHostAddress(Connection connection) throws Throwable {
    Protocol protocol = getProtocolFromConnection(connection);
    return protocol.getHostAddress();
  }

  public boolean inTransaction(Connection connection) throws Throwable {
    Protocol protocol = getProtocolFromConnection(connection);
    return protocol.inTransaction();
  }

  protected boolean isMariaDbServer(Connection connection) throws SQLException {
    DatabaseMetaData md = connection.getMetaData();
    return md.getDatabaseProductVersion().contains("MariaDB");
  }

  protected ServerPrepareResult getPrepareResult(MariaDbPreparedStatementServer preparedStatement)
      throws IllegalAccessException, NoSuchFieldException {
    Field prepareResultField = MariaDbPreparedStatementServer.class
        .getDeclaredField("serverPrepareResult");
    prepareResultField.setAccessible(true);
    return (ServerPrepareResult) prepareResultField.get(preparedStatement); //IllegalAccessException
  }
}
