package org.mariadb.jdbc;

import static org.junit.Assert.*;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mariadb.jdbc.internal.mysql.MySQLProtocol;


/**
 * @author Lennart Schedin
 */
public class ConnectionIsValidTest {
    
    /*
    To use Mysql JDBC and H2 in-memory database add the following to the pom.xml:
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
          <version>5.1.26</version>
          <scope>test</scope>
      </dependency>
      <dependency>
          <groupId>com.h2database</groupId>
          <artifactId>h2</artifactId>
          <version>1.4.178</version>
          <scope>test</scope>
      </dependency>

    To enable easy switch between MariaDB JDBC and Mysql JDBC:
    Change the class src/main/java/org/mariadb/jdbc/JDBCUrl.java in the method parse()
    At line 97-ish change to "return null" so the code looks like this: 
    
    if(url.startsWith("jdbc:mysql://")) {
        return null;
    }
    This will make MariaDB JDBC not to accept "jdbc:mysql://" and therefore Mysql will be used.
   */
    
    
    private Connection connection;
   
    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mariadb://localhost:3306/test?user=root");
//      return DriverManager.getConnection("jdbc:mysql://localhost:3306/test?user=root");
//      return DriverManager.getConnection("jdbc:h2:mem:myTestDatabase;DB_CLOSE_DELAY=-1");
    }
    
    @Before
    public void setUp() throws SQLException {
        connection = getConnection();
    }
    
    @After
    public void tearDown() throws SQLException {
        connection.close();
    }
    
    @Test
    public void isValid_shouldThrowExceptionWithNegativeTimeout() throws SQLException {
        try {
            connection.isValid(-1);
            
            //MariaDB Mysql and H2 JDBC will all fail here
            fail("The above row should have thrown an SQLException"); 
        } catch (SQLException e) {
            //Dummy assert, since I have no connector to actually test for the correct case
            assertTrue(e.getMessage().contains("negative"));
        }
    }
    
    @Test
    public void isValid_testWorkingConnection() throws SQLException {
        assertTrue(connection.isValid(0)); //This will pass with MariaDB, Mysql and H2
    }

    @Test
    public void isValid_closedConnection() throws SQLException {
        connection.close();
        
        boolean isValid = connection.isValid(0); //MariaDB JDBC will throw exception here 
        
        assertFalse(isValid); //This will pass with both Mysql JDBC and H2. 
    }
    
    @Test
    public void isValid_connectionThatTimesOutByServer() throws SQLException, InterruptedException {
        //Dynamically ignore the test case if H2 is used, because there is no wait_timeout feature in H2
        org.junit.Assume.assumeTrue(! connection.getClass().getName().equals("org.h2.jdbc.JdbcConnection"));
        
        Statement statement = connection.createStatement();
         
        //http://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_wait_timeout
        //The wait_timeout will make the server kill the connection after specified number of sec.
        statement.execute("set session wait_timeout=1");
        
        Thread.sleep(3000); //Wait for the server to kill the connection
        
        boolean isValid = connection.isValid(0); //MariaDB JDBC will throw exception here 
        assertFalse(isValid); 
        
        statement.close();
    }

    @Test
    public void isValid_connectionThatIsKilledExternally() throws Exception {
        //Dynamically ignore the test case if H2 is used, because there is no KILL CONNECTION feature in H2
        org.junit.Assume.assumeTrue(! connection.getClass().getName().equals("org.h2.jdbc.JdbcConnection"));
        
        long threadId = getServerThreadId();

        //KILL CONNECTION command http://dev.mysql.com/doc/refman/5.5/en/kill.html
        Connection killerConnection = getConnection();
        Statement killerStatement = killerConnection.createStatement();
        killerStatement.execute("KILL CONNECTION " + threadId);
        killerConnection.close();
        
        boolean isValid = connection.isValid(0); //MariaDB JDBC will throw exception here 
        assertFalse(isValid);
    }
    
    /** Reflection magic to extract the connection thread id assigned by the server */
    private long getServerThreadId() throws Exception {
        if (connection.isWrapperFor(org.mariadb.jdbc.MySQLConnection.class)) {
            Field protocolField = org.mariadb.jdbc.MySQLConnection.class.getDeclaredField("protocol");
            protocolField.setAccessible(true);
            MySQLProtocol protocol = (MySQLProtocol) protocolField.get(connection);

            Field serverThreadIdField = MySQLProtocol.class.getDeclaredField("serverThreadId");
            serverThreadIdField.setAccessible(true);
            long threadId = serverThreadIdField.getLong(protocol);
            return threadId;
        } else if (connection.isWrapperFor(com.mysql.jdbc.ConnectionImpl.class)) {
            Field ioField = com.mysql.jdbc.ConnectionImpl.class.getDeclaredField("io");
            ioField.setAccessible(true);
            com.mysql.jdbc.MysqlIO io = (com.mysql.jdbc.MysqlIO) ioField.get(connection);
            
            Field threadIdField = com.mysql.jdbc.MysqlIO.class.getDeclaredField("threadId");
            threadIdField.setAccessible(true);
            long threadId = threadIdField.getLong(io);
            return threadId;
        }
        throw new RuntimeException("Could not find the server thread id");
    }
}
