/**
 *  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.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.easymock.EasyMock.anyInt;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.aryEq;
import static org.easymock.EasyMock.createNiceMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.fail;

import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

import junit.framework.Assert;

import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;

import com.google.common.base.FinalizableReferenceQueue;
import com.google.common.collect.MapMaker;

/**
 * Mock unit testing for Connection Handle class.
 * @author wwadge
 *
 */
public class TestConnectionHandle {
	/** Test class handle. */
	private ConnectionHandle testClass;
	/** Mock handle. */
	private ConnectionHandle mockConnection = createNiceMock(ConnectionHandle.class);
	/** Mock handle. */
	private IStatementCache mockPreparedStatementCache = createNiceMock(IStatementCache.class);
	/** Mock handle. */
	private IStatementCache mockCallableStatementCache = createNiceMock(IStatementCache.class);
	/** Mock handle. */
	private BoneCP mockPool = createNiceMock(BoneCP.class);
	/** Mock handle. */
	private Logger mockLogger;
	/** Mock handle. */
	private StatementCache testStatementCache = new StatementCache(100, false, new Statistics(this.mockPool));
	/** Config clone. */
	private BoneCPConfig config;

	/** Reset everything.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	@Before
	public void before() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException{
		reset(this.mockConnection, this.mockPreparedStatementCache, this.mockPool, this.mockCallableStatementCache);

		this.config = CommonTestUtils.getConfigClone();
		this.mockPool.connectionStrategy = new DefaultConnectionStrategy(this.mockPool);
		expect(this.mockPool.getConfig()).andReturn(this.config).anyTimes();

		expect(this.mockConnection.getPool()).andReturn(this.mockPool).anyTimes();
		expect(this.mockConnection.isLogStatementsEnabled()).andReturn(true).anyTimes();
		replay(this.mockConnection, this.mockPool);

		this.config.setCloseOpenStatements(true);
		this.config.setTransactionRecoveryEnabled(false);
		this.config.setStatementsCacheSize(1);
		this.config.setStatisticsEnabled(true);
		this.config.setDefaultAutoCommit(true);
		this.config.setDefaultCatalog("foo");

		this.testClass = ConnectionHandle.createTestConnectionHandle(this.mockConnection, this.mockPreparedStatementCache, this.mockCallableStatementCache, this.mockPool);

		this.mockPool.closeConnectionWatch=true;
		this.mockLogger = TestUtils.mockLogger(testClass.getClass());

		this.testClass.logicallyClosed.set(false);

		Field field = this.testClass.getClass().getDeclaredField("statisticsEnabled");
		field.setAccessible(true);
		field.set(this.testClass, true);

		field = this.testClass.getClass().getDeclaredField("statistics");
		field.setAccessible(true);
		field.set(this.testClass, new Statistics(this.mockPool));

		reset(this.mockConnection, this.mockPreparedStatementCache, this.mockPool, this.mockCallableStatementCache);
	}


	/** For test. */
	static int count=1;


	/** Test bounce of inner connection.
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	@Test
	public void testStandardMethods() throws IllegalArgumentException, SecurityException,  IllegalAccessException, InvocationTargetException{
		Set<String> skipTests = new HashSet<String>();
		skipTests.add("close");
		skipTests.add("getConnection");
		skipTests.add("markPossiblyBroken");
		skipTests.add("trackStatement");
		skipTests.add("checkClosed");
		skipTests.add("isClosed");
		skipTests.add("internalClose");
		skipTests.add("prepareCall");
		skipTests.add("prepareStatement");
		skipTests.add("setClientInfo");
		skipTests.add("getConnectionLastUsed");
		skipTests.add("setConnectionLastUsed");
		skipTests.add("getConnectionLastReset");	
		skipTests.add("setConnectionLastReset");
		skipTests.add("isPossiblyBroken");	
		skipTests.add("getOriginatingPartition");
		skipTests.add("setOriginatingPartition");
		skipTests.add("renewConnection");
		skipTests.add("clearStatementCaches");
		skipTests.add("obtainInternalConnection");
		skipTests.add("refreshConnection");
		skipTests.add("recreateConnectionHandle");
		skipTests.add("fillConnectionFields");
		skipTests.add("createConnectionHandle");
		if (CommonTestUtils.jvmMajorVersion < 7){
			skipTests.add("getNetworkTimeout");
			skipTests.add("setNetworkTimeout");
			skipTests.add("getSchema");
			skipTests.add("setSchema");
			skipTests.add("abort");

		}

		skipTests.add("sendInitSQL");
		skipTests.add("$VRi"); // this only comes into play when code coverage is started. Eclemma bug?
		expect(this.mockPool.getConfig()).andReturn(this.config).anyTimes();
		replay(this.mockPool);
		testClass.closeOpenStatements = true;
		CommonTestUtils.testStatementBounceMethod(this.mockConnection, this.testClass, skipTests, this.mockConnection);
	}
	

	/** Test marking of possibly broken status.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	@Test
	public void testMarkPossiblyBroken() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException{
		Field field = this.testClass.getClass().getDeclaredField("possiblyBroken");
		field.setAccessible(true);
		field.set(this.testClass, false);
		this.testClass.markPossiblyBroken(new SQLException());
		Assert.assertTrue(field.getBoolean(this.testClass));

		// Test that a db fatal error will lead to the pool being instructed to terminate all connections (+ log)
		expect(this.mockPool.getDbIsDown()).andReturn(new AtomicBoolean()).anyTimes();
		this.mockPool.connectionStrategy.terminateAllConnections();
		this.mockLogger.error((String)anyObject(), anyObject());
		replay(this.mockPool);
		this.testClass.markPossiblyBroken(new SQLException("test", "08001"));
		verify(this.mockPool);


	}

	/** Test. */
	boolean interrupted = false;
	/** Test. */
	boolean started = false;
	/** Closing a connection handle should release that connection back in the pool and mark it as closed.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 * @throws SQLException
	 * @throws InterruptedException 
	 */
	@Test 
	public void testClose() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, SQLException, InterruptedException{

		this.testClass.doubleCloseCheck = true;
		Connection mockInternalConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.setInternalConnection(mockInternalConnection);

		this.testClass.renewConnection();
		this.mockPool.releaseConnection((Connection)anyObject());
		expectLastCall().once().andThrow(new SQLException()).once();
		expect(this.mockPool.getFinalizableRefs()).andReturn(new HashMap<Connection, Reference<ConnectionHandle>>()).anyTimes();
		expect(this.mockPool.getConfig()).andReturn(this.config).anyTimes();
		expect(mockInternalConnection.isClosed()).andReturn(false).anyTimes();
		replay(this.mockPool, mockInternalConnection);

		// create a thread so that we can check that thread.interrupt was called during connection close.
		//		final Thread thisThread = Thread.currentThread();
		Thread testThread = new Thread(new Runnable() {

			public void run() {
				try {
					TestConnectionHandle.this.started = true;
					while(true){
						Thread.sleep(20);
					}
				} catch (Exception e) {
					TestConnectionHandle.this.interrupted = true;

				}
			}
		});
		testThread.start();
		while (!this.started){
			Thread.sleep(20);
		}
		this.testClass.setThreadWatch(testThread);
		this.testClass.close();
		testThread.join();
		assertTrue(this.interrupted); // thread should have been interrupted 

		// logically mark the connection as closed
		Assert.assertTrue(this.testClass.logicallyClosed.get());
		assertTrue(this.testClass.isClosed());




		this.testClass.renewConnection();
		try{
			this.testClass.close(); // 2nd time should throw an exception
			fail("Should have thrown an exception");
		} catch (Throwable t){
			// do nothing.
		}

	}

	@Test
	public void testCloseWithRollback() throws SQLException{
		Connection mockConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.logicallyClosed.set(false);
		this.testClass.resetConnectionOnClose = true;
		this.testClass.connectionTrackingDisabled = true;
		this.testClass.setInternalConnection(mockConnection);
		mockConnection.rollback();
		expect(mockConnection.getAutoCommit()).andReturn(false);
		mockConnection.setAutoCommit(true);
		replay(mockConnection);
		try{
			this.testClass.close();
		} catch(NullPointerException e){
			// we don't care about the rest of the method
		}
		verify(mockConnection);
	}

	
	@Test
	public void testCloseWithExceptionsOnRecreate() throws SQLException{
		Connection mockConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.logicallyClosed.set(false);
		this.testClass.resetConnectionOnClose = false;
		this.testClass.connectionTrackingDisabled = true;
		this.testClass.setInternalConnection(mockConnection);
		expect(mockConnection.getAutoCommit()).andReturn(false);
		expect(this.mockPool.getConfig()).andReturn(new BoneCPConfig()).anyTimes();
		this.mockPool.releaseConnection((Connection)anyObject());
		expectLastCall().andAnswer(new IAnswer<Object>() {

			public Object answer() throws Throwable {
				testClass.logicallyClosed.set(false);

				throw new SQLException();
			}
		});

		replay(mockConnection, mockPool);
		try{
			this.testClass.close();
		} catch(SQLException e){
			// expected
		}
		verify(mockConnection);
	}
	@Test
	public void testCloseWithRollbackThrowingException() throws SQLException{
		//coverage test
		Connection mockConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.logicallyClosed.set(false);
		this.testClass.resetConnectionOnClose = true;
		this.testClass.connectionTrackingDisabled = true;
		this.testClass.setInternalConnection(mockConnection);
		mockConnection.rollback();
		expect(mockConnection.getAutoCommit()).andThrow(new SQLException("FOO", "123"));
		replay(mockConnection);
		try{
			this.testClass.close();
			fail("exception was expected");
		} catch(SQLException e){
		}
		verify(mockConnection);
	}
	@Test
	public void testCloseWithRollbackNoResetAutoCommit() throws SQLException{
		// same test but let's set autocommit is set to true (the usual default)
		Connection mockConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.logicallyClosed.set(false);
		this.testClass.resetConnectionOnClose = true;
		this.testClass.connectionTrackingDisabled = true;
		this.testClass.setInternalConnection(mockConnection);

		mockConnection.rollback();
		expect(mockConnection.getAutoCommit()).andReturn(true);
		replay(mockConnection);
		try{
			this.testClass.close();
		} catch(NullPointerException e){
			// we don't care about the rest of the method
		}

		verify(mockConnection);
	}


	@Test
	public void testCloseWithOpenStatementsMakeSureTheyAreClosed() throws SQLException{
		Connection mockConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.logicallyClosed.set(false);
		this.testClass.closeOpenStatements = true;
		this.testClass.detectUnclosedStatements = true;

		this.testClass.setInternalConnection(mockConnection);

		this.testClass.trackedStatement = new MapMaker().makeMap();
		Statement mockStatement = EasyMock.createNiceMock(Statement.class);
		this.testClass.trackedStatement.put(mockStatement, "foo");
		mockStatement.close();

		Logger oldLogger = ConnectionHandle.logger;
		try{
			ConnectionHandle.logger = mockLogger;
			mockLogger.warn((String)anyObject());
			replay(mockStatement, mockConnection, mockLogger);
			try{
				this.testClass.close();
			} catch(NullPointerException e){
				// we don't care about the rest of the method
			}
		} finally {
			ConnectionHandle.logger = oldLogger;
		}

		verify(mockConnection, mockStatement, mockLogger);
	}
	
	@Test
	public void testCloseWithOpenStatementsMakeSureTheyAreClosedButNotLogger() throws SQLException{
		// Test mostly for coverage. 
		Connection mockConnection = EasyMock.createNiceMock(Connection.class);
		this.testClass.logicallyClosed.set(false);
		this.testClass.closeOpenStatements = true;
		this.testClass.detectUnclosedStatements = false;

		this.testClass.setInternalConnection(mockConnection);

		this.testClass.trackedStatement = new MapMaker().makeMap();
		Statement mockStatement = EasyMock.createNiceMock(Statement.class);
		this.testClass.trackedStatement.put(mockStatement, "foo");
		mockStatement.close();

			mockLogger.warn((String)anyObject());
			replay(mockStatement, mockConnection);
			try{
				this.testClass.close();
			} catch(NullPointerException e){
				// we don't care about the rest of the method
			}

		verify(mockConnection, mockStatement);
	}


	/** Tests sendInitialSQL method.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws SQLException
	 */
	@Test
	public void testSendInitialSQL() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, SQLException{

		BoneCPConfig mockConfig = createNiceMock(BoneCPConfig.class);
		expect(this.mockPool.getConfig()).andReturn(mockConfig).anyTimes();

		expect(mockConfig.getInitSQL()).andReturn("test").anyTimes();
		this.testClass.setInternalConnection(this.mockConnection);

		Statement mockStatement = createNiceMock(Statement.class);
		expect(this.mockConnection.createStatement()).andReturn(mockStatement).times(2).andReturn(null).once();
		expect(mockStatement.execute("test")).andReturn(true).once().andThrow(new SQLException()).once();
		mockStatement.close();
		expectLastCall().times(2);
		replay(mockConfig, this.mockPool, this.mockConnection, mockStatement);
		this.testClass.sendInitSQL();

		try{
			this.testClass.sendInitSQL(); // second time statement should fail and be closed
			fail("An exception should be thrown");
		} catch (SQLException e){
			// do nothing
		}


		try{
			this.testClass.sendInitSQL(); // third time, just for null check. Should never happen in real life unless
			// driver fails
			fail("Should throw NPE");
		} catch (NullPointerException e){
			// do nothing
		}

		verify(this.mockConnection,  mockStatement);

		ConnectionHandle.testSupport = true;
		reset(this.mockConnection, mockStatement);
		expect(this.mockConnection.createStatement()).andReturn(mockStatement).once();
		expect(mockStatement.execute("test")).andReturn(true).once();
		replay(this.mockConnection, mockStatement);
		this.testClass.sendInitSQL();
		ConnectionHandle.testSupport = false;


		verify(mockConfig, this.mockPool, this.mockConnection,  mockStatement);



	}

	@Test
	public void testCoverage(){
		this.testClass.url = "foo";
		assertEquals("foo", this.testClass.getUrl());
	}

	/**
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws SQLException
	 */
	@Test
	public void testDoubleClose() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, SQLException{
		this.testClass.doubleCloseCheck = true;
		this.testClass.logicallyClosed.set(true);


		this.testClass.doubleCloseException = "fakeexception";
		this.mockLogger.error((String)anyObject(), anyObject());
		expectLastCall().once();

		this.mockPool.releaseConnection((Connection)anyObject());
		expectLastCall().once().andThrow(new SQLException()).once();
		replay(this.mockLogger, this.mockPool);

		this.testClass.close();
	}

	@Test
	public void testDoubleCloseNoLogging() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, SQLException{
		// coverage
		this.testClass.doubleCloseCheck = true;
		this.testClass.logicallyClosed.set(true);


		this.testClass.doubleCloseException = null;

		this.mockPool.releaseConnection((Connection)anyObject());
		expectLastCall().once().andThrow(new SQLException()).once();
		replay( this.mockPool);

		this.testClass.close();
	}

	/** Closing a connection handle should release that connection back in the pool and mark it as closed.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 * @throws SQLException
	 */
	@SuppressWarnings("unchecked")
	@Test 
	public void testInternalClose() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, SQLException{
		ConcurrentLinkedQueue<Statement> mockStatementHandles = createNiceMock(ConcurrentLinkedQueue.class);
		StatementHandle mockStatement = createNiceMock(StatementHandle.class);

		this.mockConnection.close();
		expectLastCall().once().andThrow(new SQLException()).once();

		Map<Connection, Reference<ConnectionHandle>> refs = new HashMap<Connection, Reference<ConnectionHandle>>();
		expect(this.mockPool.getFinalizableRefs()).andReturn(refs).anyTimes();
		FinalizableReferenceQueue finalizableRefQueue = new FinalizableReferenceQueue();

		expect(this.mockPool.getFinalizableRefQueue()).andReturn(finalizableRefQueue).anyTimes();
		expect(this.mockConnection.getPool()).andReturn(this.mockPool).anyTimes();

		Field f = this.testClass.getClass().getDeclaredField("finalizableRefs");
		f.setAccessible(true);
		f.set(this.testClass, refs);

		replay(mockStatement, this.mockConnection, mockStatementHandles, this.mockPool);
		this.testClass.internalClose();
		try{
			this.testClass.internalClose(); //2nd time should throw exception
			fail("Should have thrown an exception");
		} catch (Throwable t){
			// do nothing.
		}

		verify(mockStatement, this.mockConnection, mockStatementHandles);
	}
	
	@SuppressWarnings("unchecked")
	@Test 
	public void testInternalCloseCoverage() throws SQLException {
		this.testClass.connectionTrackingDisabled = false;
		this.testClass.finalizableRefs = new MapMaker().makeMap();
		this.testClass.finalizableRefs.put(this.testClass.connection, createNiceMock(Reference.class));
		this.testClass.internalClose();
		assertTrue(this.testClass.finalizableRefs.isEmpty());
		
		this.testClass.connectionTrackingDisabled = false;
		this.testClass.finalizableRefs = null;  // should never happen
		this.testClass.internalClose();
		
		// should not fail if connection was somehow destroy. 
		this.testClass.connection = null;
		this.testClass.internalClose();

		
	}


	/** Test for check closed routine.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws NoSuchMethodException
	 */
	@Test 
	public void testCheckClosed() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException{

		this.testClass.renewConnection();

		// call the method (should not throw an exception)
		Method method = this.testClass.getClass().getDeclaredMethod("checkClosed");
		method.setAccessible(true);
		method.invoke(this.testClass);

		// logically mark the connection as closed
		this.testClass.logicallyClosed.set(true);

		try{
			method.invoke(this.testClass);
			fail("Should have thrown an exception");
		} catch (Throwable t){
			// do nothing.
		}
	}

	@Test 
	public void testToString(){
		expect(mockPool.getConfig()).andReturn(new BoneCPConfig()).anyTimes();
		replay(mockPool);
		assertNotNull(this.testClass.toString());
	}


	/** Test renewal of connection.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	@Test
	public void testRenewConnection() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException{
		Field field = this.testClass.getClass().getDeclaredField("doubleCloseCheck");
		field.setAccessible(true);
		field.set(this.testClass, true);

		this.testClass.logicallyClosed.set(true);


		this.testClass.renewConnection();
		assertFalse(	this.testClass.logicallyClosed.get());


	}
	/** Tests various getter/setters.
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 */
	@SuppressWarnings("deprecation")
	@Test
	public void testSettersGetters() throws IllegalArgumentException, IllegalAccessException, SecurityException, NoSuchFieldException {
		ConnectionPartition mockPartition = createNiceMock(ConnectionPartition.class);
		this.testClass.setOriginatingPartition(mockPartition);
		assertEquals(mockPartition, this.testClass.getOriginatingPartition());

		this.testClass.setConnectionLastResetInMs(123);
		assertEquals(this.testClass.getConnectionLastResetInMs(), 123);
		assertEquals(this.testClass.getConnectionLastReset(), 123);

		this.testClass.setConnectionLastUsedInMs(456);
		assertEquals(this.testClass.getConnectionLastUsedInMs(), 456);
		assertEquals(this.testClass.getConnectionLastUsed(), 456);

		Field field = this.testClass.getClass().getDeclaredField("possiblyBroken");
		field.setAccessible(true);
		field.setBoolean(this.testClass, true);
		assertTrue(this.testClass.isPossiblyBroken());

		field = this.testClass.getClass().getDeclaredField("connectionCreationTimeInMs");
		field.setAccessible(true);
		field.setLong(this.testClass, 1234L);
		assertEquals(1234L, this.testClass.getConnectionCreationTime());

		Object debugHandle = new Object();
		this.testClass.setDebugHandle(debugHandle);
		assertEquals(debugHandle, this.testClass.getDebugHandle());

		this.testClass.setInternalConnection(this.mockConnection);
		assertEquals(this.mockConnection, this.testClass.getInternalConnection());
		assertEquals(this.mockConnection, this.testClass.getRawConnection());

		this.testClass.logicallyClosed.set(true);

		assertTrue(this.testClass.isClosed());

		this.testClass.setLogStatementsEnabled(true);
		assertTrue(this.testClass.isLogStatementsEnabled());

		assertEquals(this.testClass.getPool(), this.mockPool);
		ArrayList<ReplayLog> testLog = new ArrayList<ReplayLog>();
		this.testClass.setReplayLog(testLog);
		assertEquals(this.testClass.getReplayLog(), testLog);
		this.testClass.setInReplayMode(true);
		assertTrue(this.testClass.isInReplayMode());
		this.testClass.setInReplayMode(false);

		this.testClass.threadUsingConnection = Thread.currentThread();
		assertEquals(Thread.currentThread(), this.testClass.getThreadUsingConnection());

		this.testClass.setThreadWatch(Thread.currentThread());
		assertEquals(Thread.currentThread(), this.testClass.getThreadWatch());

	}

	/**
	 *  Simple test.
	 */
	@Test
	public void testIsConnectionHandleAlive(){
		// just make sure this is bounced off to the right place
		reset(this.mockPool);
		expect(this.mockPool.isConnectionHandleAlive(this.testClass)).andReturn(true).once();
		replay(this.mockPool);
		this.testClass.isConnectionAlive();
		verify(this.mockPool);
	}

	/** Tests isExpired method.
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	@Test
	public void testIsExpired() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException{
		this.testClass.maxConnectionAgeInMs = 1234L;
		this.testClass.connectionCreationTimeInMs = System.currentTimeMillis(); 
		assertTrue(this.testClass.isExpired(System.currentTimeMillis() + 9999L));


		assertFalse(this.testClass.isExpired(System.currentTimeMillis()));
		this.testClass.maxConnectionAgeInMs = 0L;
		assertFalse(this.testClass.isExpired(System.currentTimeMillis()));


		this.testClass.maxConnectionAgeInMs = 0L;
		assertFalse(this.testClass.isExpired());
		this.testClass.maxConnectionAgeInMs = 1L;

		this.testClass.connectionCreationTimeInMs = System.currentTimeMillis() + 9999L;
		assertFalse(this.testClass.isExpired());
		this.testClass.connectionCreationTimeInMs = 0;
		assertTrue(this.testClass.isExpired());

	}

	@Test
	public void testMaybeCaptureStackTrace(){
		assertEquals("", this.testClass.maybeCaptureStackTrace());
		this.testClass.detectUnclosedStatements = true;
		assertNotSame("", this.testClass.maybeCaptureStackTrace());
		this.testClass.detectUnclosedStatements = false;
	}

	@Test
	public void testRefreshConnection() throws SQLException{
		Connection mockInternalConnection =  createNiceMock(Connection.class); 
		this.testClass.connection = mockInternalConnection;
		mockInternalConnection.close();
		expectLastCall().once();
		replay(mockInternalConnection);
		this.testClass.refreshConnection();

		assertNotSame(mockInternalConnection, this.testClass.getInternalConnection());
		verify(mockInternalConnection);

		this.testClass.connection = mockInternalConnection;
		reset(this.mockPool, mockInternalConnection);
		mockInternalConnection.close();
		expectLastCall().once();

		expect(this.mockPool.obtainRawInternalConnection()).andThrow(new SQLException("foo", "1234")).once();
		replay(this.mockPool, mockInternalConnection);
		try{
			this.testClass.refreshConnection();
			fail("Should throw exception");
		} catch(SQLException e){
			// do nothing
		}
	}

	@Test
	public void testUntrackStatement(){
		StatementHandle mockStatement = createNiceMock(StatementHandle.class);
		this.testClass.trackedStatement.put(mockStatement, "");
		this.testClass.closeOpenStatements = false;
		assertTrue(this.testClass.trackedStatement.containsKey(mockStatement)); // should be a NOP
		this.testClass.closeOpenStatements = true;
		this.testClass.untrackStatement(mockStatement);
		assertFalse(this.testClass.trackedStatement.containsKey(mockStatement));


	}

	/** Prepare statement tests.
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 */
	@Test
	public void testPrepareStatement() throws SecurityException, IllegalArgumentException,  NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException{
		expect(this.mockPool.captureStackTrace((String)anyObject())).andReturn("").anyTimes();
		expect(this.mockPool.getConfig()).andReturn(this.config).anyTimes();
		replay(this.mockPool);
		this.config.setStatementsCacheSize(1);
		prepareStatementTest(String.class);
		prepareStatementTest(String.class, int.class);
		prepareStatementTest(String.class, int[].class);
		prepareStatementTest(String.class, String[].class);
		prepareStatementTest(String.class, int.class, int.class);
		prepareStatementTest(String.class, int.class, int.class, int.class);
	}

	/** Callable statement tests.
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 */
	@Test
	public void testCallableStatement() throws SecurityException, IllegalArgumentException,  NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException{
		expect(this.mockPool.captureStackTrace((String)anyObject())).andReturn("").anyTimes();
		expect(this.mockPool.getConfig()).andReturn(this.config).anyTimes();
		this.config.setStatisticsEnabled(true);

		replay(this.mockPool);

		callableStatementTest(String.class);
		callableStatementTest(String.class, int.class, int.class);
		callableStatementTest(String.class, int.class, int.class, int.class);
	}


	/** Test routine.
	 * @param args
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 */
	@SuppressWarnings("rawtypes")
	private void prepareStatementTest(  Class... args) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException{
		Object[] params = new Object[args.length];
		for (int i=0; i < args.length; i++){
			params[i] = CommonTestUtils.instanceMap.get(args[i]);
		}

		Method prepStatementMethod = this.testClass.getClass().getMethod("prepareStatement", args);

		PreparedStatementHandle mockStatement = createNiceMock(PreparedStatementHandle.class);
		mockStatement.setOpenStackTrace((String)anyObject());
		expectLastCall().anyTimes();

		this.testClass.renewConnection(); // logically open the connection

		// fetching a statement that is found in cache. Statement should be returned and marked as being (logically) open
		doStatementMock(this.mockPreparedStatementCache, mockStatement, params, args);

		//		mockStatement.setLogicallyOpen();
		//		expectLastCall();
		replay(mockStatement, this.mockPreparedStatementCache);
		prepStatementMethod.invoke(this.testClass, params);
		verify(mockStatement, this.mockPreparedStatementCache);

		reset(mockStatement, this.mockPreparedStatementCache);


		// test for a cache miss
		doStatementMock(this.mockPreparedStatementCache, null, params, args);
		// we should be creating the preparedStatement because it's not in the cache
		expect(prepStatementMethod.invoke(this.mockConnection, params)).andReturn(mockStatement);

		replay(mockStatement, this.mockPreparedStatementCache, this.mockConnection);
		prepStatementMethod.invoke(this.testClass, params);
		verify(mockStatement, this.mockPreparedStatementCache, this.mockConnection);


		// test for cache miss + sql exception
		reset(mockStatement, this.mockPreparedStatementCache, this.mockConnection);

		Method mockConnectionPrepareStatementMethod = this.mockConnection.getClass().getMethod("prepareStatement", args);

		//		expect(this.mockPreparedStatementCache.get((String)anyObject())).andReturn(null).once();
		doStatementMock(this.mockPreparedStatementCache, null, params, args);

		// we should be trying to create the preparedStatement because it's not in the cache
		expect(mockConnectionPrepareStatementMethod.invoke(this.mockConnection, params)).andThrow(new SQLException("test", "Z"));

		replay(mockStatement, this.mockPreparedStatementCache, this.mockConnection);
		try{
			prepStatementMethod.invoke(this.testClass, params);
			fail("Should have thrown an exception");
		} catch (Throwable t) {
			// do nothing
		}

		verify(mockStatement, this.mockPreparedStatementCache, this.mockConnection);



		// test for no cache defined
		reset(mockStatement, this.mockPreparedStatementCache, this.mockConnection);
		boolean oldState = this.testClass.statementCachingEnabled;

		this.testClass.statementCachingEnabled = false;

		// we should be creating the preparedStatement because it's not in the cache
		expect(mockConnectionPrepareStatementMethod.invoke(this.mockConnection, params)).andReturn(mockStatement);

		replay(mockStatement, this.mockPreparedStatementCache, this.mockConnection);
		prepStatementMethod.invoke(this.testClass, params);
		verify(mockStatement, this.mockPreparedStatementCache, this.mockConnection);
		// restore sanity
		this.testClass.statementCachingEnabled = oldState;

		reset(mockStatement, this.mockPreparedStatementCache, this.mockConnection);

	}

	/** Mock setup.
	 * @param cache 
	 * @param returnVal 
	 * @param params
	 * @param args
	 */
	private void doStatementMock(IStatementCache cache, StatementHandle returnVal, Object[] params, Class<?>... args) {
		expect(cache.get((String)anyObject())).andReturn(returnVal).anyTimes();
		//		expect(cache.calculateCacheKey((String)anyObject())).andReturn(testStatementCache.calculateCacheKey((String)params[0])).anyTimes();

		if (args.length == 2) {
			if (params[1].getClass().equals(Integer.class)){

				expect(cache.calculateCacheKey((String)anyObject(), anyInt())).andReturn(this.testStatementCache.calculateCacheKey((String)params[0], (Integer)params[1])).anyTimes();
				expect(cache.get((String)anyObject(), anyInt())).andReturn(returnVal).anyTimes();
			}			
			expect(cache.calculateCacheKey((String)anyObject(), aryEq(new int[]{0,1}))).andReturn(this.testStatementCache.calculateCacheKey((String)params[0], new int[]{0,1})).anyTimes();
			expect(cache.get((String)anyObject(), aryEq(new int[]{0,1}))).andReturn(returnVal).anyTimes();

			expect(cache.calculateCacheKey((String)anyObject(), (String[])anyObject())).andReturn(this.testStatementCache.calculateCacheKey((String)params[0], new String[]{"test", "bar"})).anyTimes();
			expect(cache.get((String)anyObject(), (String[])anyObject())).andReturn(returnVal).anyTimes();
		}
		if (args.length == 3) {
			expect(cache.calculateCacheKey((String)anyObject(), anyInt(), anyInt())).andReturn(this.testStatementCache.calculateCacheKey((String)params[0], (Integer)params[1], (Integer)params[2])).anyTimes();
			expect(cache.get((String)anyObject(), anyInt(), anyInt())).andReturn(returnVal).anyTimes();
		}
		if (args.length == 4) {
			expect(cache.calculateCacheKey((String)anyObject(), anyInt(), anyInt(), anyInt())).andReturn(this.testStatementCache.calculateCacheKey((String)params[0], (Integer)params[1], (Integer)params[2], (Integer)params[3])).anyTimes();
			expect(cache.get((String)anyObject(), anyInt(), anyInt(), anyInt())).andReturn(returnVal).anyTimes();
		}

	}

	/** Test routine for callable statements.
	 * @param args
	 * @throws SecurityException
	 * @throws NoSuchFieldException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 */
	@SuppressWarnings("all")
	private void callableStatementTest(Class... args) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException{
		Object[] params = new Object[args.length];
		for (int i=0; i < args.length; i++){
			params[i] = CommonTestUtils.instanceMap.get(args[i]);
		}

		Method prepCallMethod = this.testClass.getClass().getMethod("prepareCall", args);

		CallableStatementHandle mockStatement = createNiceMock(CallableStatementHandle.class);
		ConcurrentLinkedQueue<Statement> mockStatementHandles = createNiceMock(ConcurrentLinkedQueue.class);


		this.testClass.renewConnection(); // logically open the connection

		// fetching a statement that is found in cache. Statement should be returned and marked as being (logically) open
		doStatementMock(this.mockCallableStatementCache, mockStatement, params, args);

		//		expect(this.mockCallableStatementCache.get((String)anyObject())).andReturn(mockStatement).anyTimes();

		//		((StatementHandle)mockStatement).setLogicallyOpen();
		//		expectLastCall();
		replay(mockStatement, this.mockCallableStatementCache);
		prepCallMethod.invoke(this.testClass, params);
		verify(mockStatement, this.mockCallableStatementCache);

		reset(mockStatement, this.mockCallableStatementCache, mockStatementHandles);


		// test for a cache miss
		doStatementMock(this.mockCallableStatementCache, null, params, args);

		// we should be creating the preparedStatement because it's not in the cache
		expect(prepCallMethod.invoke(this.mockConnection, params)).andReturn(mockStatement);
		// we should be tracking this statement
		expect(mockStatementHandles.add(mockStatement)).andReturn(true);

		replay(mockStatement, this.mockCallableStatementCache, this.mockConnection, mockStatementHandles);
		prepCallMethod.invoke(this.testClass, params);
		verify(mockStatement, this.mockCallableStatementCache, this.mockConnection);


		// test for cache miss + sql exception
		reset(mockStatement, this.mockCallableStatementCache, this.mockConnection);

		Method mockConnectionPrepareCallMethod = this.mockConnection.getClass().getMethod("prepareCall", args);

		//		expect(this.mockCallableStatementCache.get((String)anyObject())).andReturn(null).once();
		doStatementMock(this.mockCallableStatementCache, null, params, args);

		// we should be creating the preparedStatement because it's not in the cache
		expect(mockConnectionPrepareCallMethod.invoke(this.mockConnection, params)).andThrow(new SQLException("test", "Z"));

		replay(mockStatement, this.mockCallableStatementCache, this.mockConnection);
		try{
			prepCallMethod.invoke(this.testClass, params);
			fail("Should have thrown an exception");
		} catch (Throwable t) {
			// do nothing
		}

		verify(mockStatement, this.mockCallableStatementCache, this.mockConnection);



		// test for no cache defined
		reset(mockStatement, this.mockCallableStatementCache, this.mockConnection);
		boolean oldState = this.testClass.statementCachingEnabled;

		this.testClass.statementCachingEnabled = false;


		// we should be creating the preparedStatement because it's not in the cache
		expect(mockConnectionPrepareCallMethod.invoke(this.mockConnection, params)).andReturn(mockStatement);

		replay(mockStatement, this.mockCallableStatementCache, this.mockConnection);
		prepCallMethod.invoke(this.testClass, params);
		verify(mockStatement, this.mockCallableStatementCache, this.mockConnection);
		// restore sanity
		this.testClass.statementCachingEnabled = oldState;

		reset(mockStatement, this.mockCallableStatementCache, this.mockConnection);

	}

	/**
	 */
	@Test
	public void testSetClientInfo() {
		Properties prop = new Properties();
		try {
			this.mockConnection.setClientInfo(prop);
			replay(this.mockConnection);
			this.testClass.setClientInfo(prop);
			verify(this.mockConnection);

			reset(this.mockConnection);

			String name = "name";
			String value = "val";
			this.mockConnection.setClientInfo(name, value);
			replay(this.mockConnection);
			this.testClass.setClientInfo(name, value);
			verify(this.mockConnection);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

	}

	//	
	//	/** Tests that a thrown exception will call the onAcquireFail hook.
	//	 * @throws SQLException
	//	 */
	//	@Test
	//	public void testConstructorFail() throws SQLException{
	//		BoneCPConfig mockConfig = createNiceMock(BoneCPConfig.class);
	//		ConnectionHook mockConnectionHook = createNiceMock(CoverageHook.class);
	//		expect(this.mockPool.getConfig()).andReturn(mockConfig).anyTimes();
	////		expect(mockConfig.getReleaseHelperThreads()).andReturn(1).once();
	//		expect(mockConfig.getConnectionHook()).andReturn(mockConnectionHook).once();
	//		expect(mockConnectionHook.onAcquireFail((Throwable)anyObject(), (AcquireFailConfig)anyObject())).andReturn(false).once();
	//		replay(this.mockPool, mockConfig, mockConnectionHook);
	//		try{
	//			new ConnectionH("", "", "", this.mockPool);
	//			fail("Should throw an exception");
	//		} catch (Throwable t){
	//			// do nothing.
	//		}
	//		verify(this.mockPool, mockConfig, this.mockPool);
	//	}
	//

	/**
	 * Test for clear statement caches.
	 */
	@Test
	public void testClearStatementCaches(){

		this.testClass.statementCachingEnabled = true;
		this.mockPreparedStatementCache.clear();
		expectLastCall().once();
		this.mockCallableStatementCache.clear();
		expectLastCall().once();

		replay(this.mockPreparedStatementCache, this.mockCallableStatementCache);
		this.testClass.clearStatementCaches(true);
		verify(this.mockPreparedStatementCache, this.mockCallableStatementCache);
		reset(this.mockPreparedStatementCache, this.mockCallableStatementCache);


		this.mockPool.closeConnectionWatch = true;
		this.mockPreparedStatementCache.checkForProperClosure();
		expectLastCall().once();
		this.mockCallableStatementCache.checkForProperClosure();
		expectLastCall().once();

		replay(this.mockPreparedStatementCache, this.mockCallableStatementCache);
		this.testClass.clearStatementCaches(false);
		verify(this.mockPreparedStatementCache, this.mockCallableStatementCache);

	}

	/**
	 * 
	 */
	@Test
	public void testStackTraceAndTxResolve(){
		this.testClass.setAutoCommitStackTrace("foo");
		this.testClass.getAutoCommitStackTrace();
		assertEquals("foo", this.testClass.getAutoCommitStackTrace());
		this.testClass.txResolved = true;
		assertTrue(this.testClass.isTxResolved());
	}

	/**
	 * @throws SQLException 
	 * 
	 */
	@Test
	public void testAutoCommitSetToFalse() throws SQLException{
		this.testClass.detectUnresolvedTransactions = true;
		expect(this.mockPool.captureStackTrace((String)anyObject())).andReturn("foo").once();
		replay(this.mockPool);
		this.testClass.setAutoCommit(false);
		assertNotNull(this.testClass.getAutoCommitStackTrace());
	}

}