/**
 *  Copyright 2010 Wallace Wadge
 *
 *    Licensed 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 com.jolbox.bonecp;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.makeThreadSafe;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.fail;

import java.lang.Thread.State;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

import com.jolbox.bonecp.hooks.CoverageHook;
import com.jolbox.bonecp.proxy.TransactionRecoveryResult;

/**
 * @author wwadge
 *
 */
public class TestMemorizeTransactionProxy {
	/** Mock handle. */
	private Connection mockConnection;
	/** Mock handle. */
	private Connection mockConnection2;
	/** Mock handle. */
	private CallableStatement mockCallableStatement;
	/** Mock handle. */
	private Statement mockStatement;
	/** Mock handle. */
	private PreparedStatement mockPreparedStatement;
	/** Mock handle. */
	private Connection mockConnection3;
	/** Config handle. */
	private BoneCPConfig config;

	/**
	 * Test reset.
	 */
	@Before
	public void before(){

		this.config =  new BoneCPConfig();
		this.config.setDisableConnectionTracking(true);
		this.config.setMinConnectionsPerPartition(1);
		this.config.setMaxConnectionsPerPartition(5);

		mockConnection = EasyMock.createNiceMock(Connection.class);
		// make it return a new connection when asked for again
		mockConnection2 = EasyMock.createNiceMock(Connection.class);
		mockConnection3 = EasyMock.createNiceMock(Connection.class);
		mockCallableStatement = EasyMock.createNiceMock(CallableStatement.class);
		mockStatement = EasyMock.createNiceMock(Statement.class);
		mockPreparedStatement = EasyMock.createNiceMock(PreparedStatement.class);
		reset(mockConnection, mockConnection2, mockConnection3, mockCallableStatement, mockPreparedStatement, mockStatement);
		makeThreadSafe(mockConnection, true);
		makeThreadSafe(mockConnection2, true);
		makeThreadSafe(mockConnection3, true);
	}


	/** Tests that the statements and connections are proxified.
	 * @throws SQLException
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws NoSuchMethodException
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 */
	@SuppressWarnings("deprecation")
	@Test
	public void testProxies() throws SQLException, SecurityException, IllegalArgumentException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException{

		MockJDBCDriver mockDriver = new MockJDBCDriver(mockConnection);
		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver3");
		this.config.setReleaseHelperThreads(0);
		BoneCP pool = new BoneCP(this.config);
		this.config.setTransactionRecoveryEnabled(true);
		expect(mockConnection.prepareCall("")).andReturn(mockCallableStatement).anyTimes();
		replay(mockConnection);
		Connection con = pool.getConnection();
		PreparedStatement ps = con.prepareStatement("");
		CallableStatement cs = con.prepareCall("");

		Statement stmt = con.createStatement();
		InvocationHandler handler = java.lang.reflect.Proxy.getInvocationHandler( ((ConnectionHandle) con).getInternalConnection());
		assertEquals(MemorizeTransactionProxy.class, handler.getClass());

		Field field = PreparedStatementHandle.class.getDeclaredField("internalPreparedStatement");
		field.setAccessible(true);
		ps = (PreparedStatement) field.get(ps);

		handler = java.lang.reflect.Proxy.getInvocationHandler(ps);
		assertEquals(MemorizeTransactionProxy.class, handler.getClass());

		field = CallableStatementHandle.class.getDeclaredField("internalCallableStatement");
		field.setAccessible(true);
		cs = (CallableStatement) field.get(cs);

		handler = java.lang.reflect.Proxy.getInvocationHandler(cs);
		assertEquals(MemorizeTransactionProxy.class, handler.getClass());

		field = StatementHandle.class.getDeclaredField("internalStatement");
		field.setAccessible(true);
		stmt = (Statement) field.get(stmt);

		handler = java.lang.reflect.Proxy.getInvocationHandler(stmt);
		assertEquals(MemorizeTransactionProxy.class, handler.getClass());


		// fake stuff to test for clear
		((ConnectionHandle)con).getReplayLog().add(new ReplayLog(null, null, null));
		((ConnectionHandle)con).recoveryResult.getReplaceTarget().put("test", "test1");
		con.rollback(); // should clear out log
		assertTrue(((ConnectionHandle)con).getReplayLog().isEmpty());
//		assertTrue(((ConnectionHandle)con).recoveryResult.getReplaceTarget().isEmpty());

		// fake stuff to test for clear
		((ConnectionHandle)con).getReplayLog().add(new ReplayLog(null, null, null));
		((ConnectionHandle)con).recoveryResult.getReplaceTarget().put("test", "test1");
		con.commit(); // should clear out log
		assertTrue(((ConnectionHandle)con).getReplayLog().isEmpty());
//		assertTrue(((ConnectionHandle)con).recoveryResult.getReplaceTarget().isEmpty());

		assertNotNull(((ConnectionHandle)con).getProxyTarget());
		try{
			field = con.getClass().getDeclaredField("connection");
			field.setAccessible(true);
			field.set(con, null);
			((ConnectionHandle)con).getProxyTarget();
			fail("should have thrown an exception");
		} catch (Throwable t){
			// should throw an exception
		}
		mockDriver.disable();
		pool.close();
	}

	/** Temp. */
	static int count = 1;


	/** Test of normal replay functionality.
	 * @throws IllegalArgumentException
	 * @throws Throwable
	 */
	@Test
	public void testReplayTransaction() throws IllegalArgumentException, Throwable{

		count = 1;

		MockJDBCDriver mockDriver = new MockJDBCDriver(new MockJDBCAnswer() {

			// @Override
			public Connection answer() throws SQLException {
				if (count == 1){
					return mockConnection;
				}
				return mockConnection2;
			}
		});


		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver");
		this.config.setMinConnectionsPerPartition(2);
		this.config.setMaxConnectionsPerPartition(2);
		BoneCP pool = new BoneCP(this.config);
		reset(mockConnection);
		this.config.setTransactionRecoveryEnabled(false);

		makeThreadSafe(mockConnection, true);
		String prepCall = "whatever2";
		expect(mockConnection.prepareStatement("whatever")).andReturn(mockPreparedStatement).anyTimes();
		expect(mockConnection.prepareCall(prepCall)).andReturn(mockCallableStatement).anyTimes();
		expect(mockConnection.createStatement()).andReturn(mockStatement).anyTimes();

		// trigger a replay
		expect(mockPreparedStatement.execute()).andThrow(new SQLException("", "08S01")).once();

		// This connection should be closed off
		mockConnection.close(); // remember that this is the internal connection
		expectLastCall().once();
//		.andThrow(new SQLException("just a fake exception for code coverage")).once();


		// we should be getting a new connection and everything replayed on it
		PreparedStatement mockPreparedStatement2 = EasyMock.createStrictMock(PreparedStatement.class);
		expect(mockConnection2.prepareStatement("whatever")).andReturn(mockPreparedStatement2).anyTimes();
		mockPreparedStatement2.setInt(1, 1);
		expectLastCall().once();
		expect(mockPreparedStatement2.execute()).andReturn(true).once();

		CallableStatement mockCallableStatement2 = EasyMock.createStrictMock(CallableStatement.class);
		expect(mockConnection2.prepareCall(prepCall)).andReturn(mockCallableStatement2).anyTimes();
		mockCallableStatement2.clearWarnings();
		expectLastCall().once();

		Statement mockStatement2 = EasyMock.createStrictMock(Statement.class);
		expect(mockConnection2.createStatement()).andReturn(mockStatement2).anyTimes();
		mockStatement2.clearWarnings();
		expectLastCall().once();

		replay(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2, mockCallableStatement2, mockStatement, mockStatement2);

		Connection con = pool.getConnection();

		((ConnectionHandle)con).recoveryResult=new TransactionRecoveryResult(); 		// for code coverage
		((ConnectionHandle)con).recoveryResult.getReplaceTarget().put(mockConnection, mockConnection); 		// for code coverage
		((ConnectionHandle)con).recoveryResult.getReplaceTarget().put(con, con);		// for code coverage

		count=0;

		mockDriver.setConnection(mockConnection2);

		PreparedStatement ps = con.prepareStatement("whatever");
		ps.setInt(1, 1);
		CallableStatement cs = con.prepareCall(prepCall);
		cs.clearWarnings();
		Statement stmt = con.createStatement();
		stmt.clearWarnings();
		ps.execute();
		con.close();

		verify(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2, mockCallableStatement2, mockStatement, mockStatement2);

		mockDriver.disable();
		pool.close();
	}


	/** Make the proxy fail but via a normal (non-recoverable) application error eg bad parameters passed to a preparedStatement.
	 * @throws IllegalArgumentException
	 * @throws Throwable
	 */
	@Test
	public void testReplayTransactionWithUserError() throws IllegalArgumentException, Throwable{

		count = 1;

		MockJDBCDriver mockDriver = new MockJDBCDriver(new MockJDBCAnswer() {

			// @Override
			public Connection answer() throws SQLException {
					return mockConnection;
			}
		});


		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver");
		this.config.setMinConnectionsPerPartition(2);
		this.config.setMaxConnectionsPerPartition(2);
		BoneCP pool = new BoneCP(this.config);
		reset(mockConnection);
		this.config.setTransactionRecoveryEnabled(false);

		makeThreadSafe(mockConnection, true);
		expect(mockConnection.prepareStatement("whatever")).andReturn(mockPreparedStatement).anyTimes();
		expect(mockPreparedStatement.execute()).andThrow(new SQLException("Fake user-error", "123")).once();

		replay(mockConnection, mockPreparedStatement);

		Connection con = pool.getConnection();
		count=0;

		PreparedStatement ps = con.prepareStatement("whatever");
		ps.setInt(999, 1); // should throw an error in real-life
		try{
			ps.execute();
			fail("Should have thrown an error");
		} catch(SQLException e){
			// expected
		}


		verify(mockConnection, mockPreparedStatement);
		mockDriver.disable();
		pool.close();


	} 

	/** Fail, then fail again on replay to see that it recovers.
	 * @throws IllegalArgumentException
	 * @throws Throwable
	 */
	@Test
//	@Ignore
	public void testReplayTransactionWithFailuresOnReplay() throws IllegalArgumentException, Throwable{

		count = 1;
		MockJDBCDriver mockDriver = null;
try{
		 mockDriver = new MockJDBCDriver(new MockJDBCAnswer() {

			// @Override
			public Connection answer() throws SQLException {
				if (count == 1){
					return mockConnection;
				}  if (count == 0){
					count--;
					return mockConnection2;
				}
					return mockConnection3;
			}
		});


		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver");
		this.config.setMinConnectionsPerPartition(2);
		this.config.setMaxConnectionsPerPartition(2);
		this.config.setAcquireRetryDelayInMs(1);
		this.config.setAcquireRetryAttempts(2);
//		config.setConnectionHook(new CoverageHook());
		BoneCP pool = new BoneCP(this.config);
		reset(mockConnection);
		this.config.setTransactionRecoveryEnabled(false);

		makeThreadSafe(mockConnection, true);
		// we should be getting new connections and everything replayed on it
		PreparedStatement mockPreparedStatement2 = EasyMock.createStrictMock(PreparedStatement.class);
		PreparedStatement mockPreparedStatement3 = EasyMock.createStrictMock(PreparedStatement.class);

		expect(mockConnection.prepareStatement("whatever")).andReturn(mockPreparedStatement).once();
		expect(mockConnection2.prepareStatement("whatever")).andReturn(mockPreparedStatement2).once();
		expect(mockConnection3.prepareStatement("whatever")).andReturn(mockPreparedStatement3).once();

		// trigger a replay
		expect(mockPreparedStatement.execute()).andThrow(new SQLException("", "08S01")).once();

		// This connection should be closed off
		mockConnection.close(); // remember that this is the internal connection
		expectLastCall().once().andThrow(new SQLException("just a fake exception for code coverage")).anyTimes();
		mockConnection2.close(); // remember that this is the internal connection
		mockConnection3.close(); // remember that this is the internal connection




		mockPreparedStatement2.setInt(1, 1);
		expect(mockPreparedStatement2.execute()).andThrow(new SQLException("Fake errors to force replaying")).once();

		mockPreparedStatement3.setInt(1, 1);
		expect(mockPreparedStatement3.execute()).andReturn(true).once();

		replay(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2, mockPreparedStatement3, mockConnection3);

		Connection con = pool.getConnection();
		count=0;

//		mockDriver.setConnection(mockConnection2);

		PreparedStatement ps = con.prepareStatement("whatever");
		ps.setInt(1, 1);
		ps.execute();

//		verify(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2, mockPreparedStatement3);
		pool.close();

	}finally{
			mockDriver.unregister();
			mockDriver.disable();
		}

	}


	/** Test to see what happens when a replay keeps failing.
	 * @throws IllegalArgumentException
	 * @throws Throwable
	 */
	@Test
	public void testReplayTransactionWithFailuresOnReplayKeepFailing() throws IllegalArgumentException, Throwable{

		count = 1;

		MockJDBCDriver mockDriver = new MockJDBCDriver(new MockJDBCAnswer() {

			// @Override
			public Connection answer() throws SQLException {
				if (count == 1){
					return mockConnection;
				}
					return mockConnection2;
			}
		});


		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver");
		this.config.setMinConnectionsPerPartition(2);
		this.config.setMaxConnectionsPerPartition(2);
		this.config.setAcquireRetryDelayInMs(1L);
		this.config.setAcquireRetryAttempts(1);
		this.config.setDisableConnectionTracking(true);
//		config.setConnectionHook(new CoverageHook());
		BoneCP pool = new BoneCP(this.config);
		reset(mockConnection);
		
		this.config.setTransactionRecoveryEnabled(false);

		makeThreadSafe(mockConnection, true);
		// we should be getting new connections and everything replayed on it
		PreparedStatement mockPreparedStatement2 = EasyMock.createStrictMock(PreparedStatement.class);

		expect(mockConnection.prepareStatement("whatever")).andReturn(mockPreparedStatement).once();
		expect(mockConnection2.prepareStatement("whatever")).andReturn(mockPreparedStatement2).once();

		// trigger a replay
		expect(mockPreparedStatement.execute()).andThrow(new SQLException("", "08S01")).once();

		// This connection should be closed off
		mockConnection.close(); // remember that this is the internal connection
		expectLastCall().once().andThrow(new SQLException("just a fake exception for code coverage")).anyTimes();


		mockPreparedStatement2.setInt(1, 1);
		expect(mockPreparedStatement2.execute()).andThrow(new SQLException("Fake errors to force replaying")).once();

		replay(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2);

		Connection con = pool.getConnection();
		count=0;

//		mockDriver.setConnection(mockConnection2);

		PreparedStatement ps = con.prepareStatement("whatever");
		ps.setInt(1, 1);
		try{
			ps.execute();
			fail("Should have thrown exception");
		} catch (SQLException e){
//			e.printStackTrace();
			// expected exception
		}


		verify(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2);

		mockDriver.disable();
		pool.close();

	}

	/** Interrupted
	 * @throws IllegalArgumentException
	 * @throws Throwable
	 */
	@Test
	public void testReplayTransactionWithFailuresInterruptedException() throws IllegalArgumentException, Throwable{

		count = 1;

		MockJDBCDriver mockDriver = new MockJDBCDriver(new MockJDBCAnswer() {

			// @Override
			public Connection answer() throws SQLException {
				if (count == 1){
					return mockConnection;
				}
					return mockConnection2;
			}
		});


		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver");
		this.config.setMinConnectionsPerPartition(2);
		this.config.setMaxConnectionsPerPartition(2);
		this.config.setAcquireRetryDelayInMs(100L);
		this.config.setAcquireRetryAttempts(2);
//		config.setConnectionHook(new CoverageHook());
		BoneCP pool = new BoneCP(this.config);
		reset(mockConnection);
		this.config.setTransactionRecoveryEnabled(false);

		makeThreadSafe(mockConnection, true);
		// we should be getting new connections and everything replayed on it
		PreparedStatement mockPreparedStatement2 = EasyMock.createStrictMock(PreparedStatement.class);

		expect(mockConnection.prepareStatement("whatever")).andReturn(mockPreparedStatement).once();
		expect(mockConnection2.prepareStatement("whatever")).andReturn(mockPreparedStatement2).once();

		// trigger a replay
		expect(mockPreparedStatement.execute()).andThrow(new SQLException("", "08S01")).once();

		// This connection should be closed off
		mockConnection.close(); // remember that this is the internal connection
		expectLastCall().once().andThrow(new SQLException("just a fake exception for code coverage")).anyTimes();




		mockPreparedStatement2.setInt(1, 1);
		expect(mockPreparedStatement2.execute()).andThrow(new SQLException("Fake errors to force replaying")).once();

		replay(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2);

		Connection con = pool.getConnection();
		count=0;

		final Thread currentThread = Thread.currentThread();

			new Thread(new Runnable() {

				// @Override
				public void run() {
					while (!currentThread.getState().equals(State.TIMED_WAITING)){
						try {
							Thread.sleep(50);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					currentThread.interrupt();
				}
			}).start();


		PreparedStatement ps = con.prepareStatement("whatever");
		ps.setInt(1, 1);
		try{
			ps.execute();
			fail("Should have thrown exception");
		} catch (SQLException e){
			// expected exception
		}


		verify(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2);

		mockDriver.disable();
		pool.close();

	}

	/** Test hooks.
	 * @throws IllegalArgumentException
	 * @throws Throwable
	 */
	@Test
	public void testReplayTransactionWithFailuresCustomHook() throws IllegalArgumentException, Throwable{

		count = 1;

		MockJDBCDriver mockDriver = new MockJDBCDriver(new MockJDBCAnswer() {

			// @Override
			public Connection answer() throws SQLException {
				if (count == 1){
					return mockConnection;
				}
					return mockConnection2;
			}
		});


		this.config.setTransactionRecoveryEnabled(true);
		this.config.setJdbcUrl("jdbc:mock:driver");
		this.config.setMinConnectionsPerPartition(2);
		this.config.setMaxConnectionsPerPartition(2);
		this.config.setAcquireRetryDelayInMs(1);
		this.config.setAcquireRetryAttempts(2);
		this.config.setConnectionHook(new CoverageHook());
		BoneCP pool = new BoneCP(this.config);
		reset(mockConnection);
		this.config.setTransactionRecoveryEnabled(false);

		makeThreadSafe(mockConnection, true);
		// we should be getting new connections and everything replayed on it
		PreparedStatement mockPreparedStatement2 = EasyMock.createStrictMock(PreparedStatement.class);

		expect(mockConnection.prepareStatement("whatever")).andReturn(mockPreparedStatement).once();
		expect(mockConnection2.prepareStatement("whatever")).andReturn(mockPreparedStatement2).once();

		// trigger a replay
		expect(mockPreparedStatement.execute()).andThrow(new SQLException("", "08S01")).once();

		// This connection should be closed off
		mockConnection.close(); // remember that this is the internal connection
		expectLastCall().once().andThrow(new SQLException("just a fake exception for code coverage")).anyTimes();




		mockPreparedStatement2.setInt(1, 1);
		expect(mockPreparedStatement2.execute()).andThrow(new SQLException("Fake errors to force replaying")).once();

		replay(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2);

		Connection con = pool.getConnection();
		count=0;



		PreparedStatement ps = con.prepareStatement("whatever");
		ps.setInt(1, 1);
		try{
			ps.execute();
			fail("Should have thrown exception");
		} catch (SQLException e){
			// expected exception
		}


		verify(mockConnection, mockPreparedStatement,mockConnection2, mockPreparedStatement2);

		mockDriver.disable();
		pool.close();

	}
}