/**
 *  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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.AbstractMap.SimpleEntry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

import junit.framework.Assert;

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

import com.google.common.base.FinalizableReferenceQueue;
import com.google.common.base.FinalizableWeakReference;
import com.jolbox.bonecp.CachedConnectionStrategy.CachedConnectionStrategyThreadLocal;

/**
 * @author wwadge
 *
 */
public class TestCachedConnectionStrategy {

	private MockJDBCDriver driver;
	private BoneCP poolClass;

	BoneCPConfig config = new BoneCPConfig();

	@Before
	public void setup() throws SQLException{


		driver = new MockJDBCDriver(new MockJDBCAnswer() {

			public Connection answer() throws SQLException {
				return new MockConnection();
			}
		});

		config.setPoolStrategy("CACHED");
		config.setPartitionCount(1);
		config.setMaxConnectionsPerPartition(5);
		config.setMinConnectionsPerPartition(5);
		config.setDisableConnectionTracking(true);
		config.setJdbcUrl("jdbc:mock");


	}

	@After
	public void tearDown() throws SQLException{
		//	poolClass.close();
		driver.unregister();
	}

	@Test
	@Ignore
	public void testThreadDies() throws CloneNotSupportedException, SQLException, InterruptedException{
		BoneCPConfig config = this.config.clone();


		poolClass = new BoneCP(config);
		
		Thread t = new Thread(){

			public void run() {
				try {
					 poolClass.getConnection();
						System.gc();
						System.gc();
						System.gc();

				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		};
		
		t.start();
		t.join();
		t = null;
		System.gc(); System.gc(); System.gc(); System.gc(); 
		assertEquals(5, poolClass.partitions[0].getFreeConnections().size());
		
	}

	
	@Test
	public void testNormalCase() throws SQLException {
		poolClass = new BoneCP(config);
		ConnectionHandle c = (ConnectionHandle) poolClass.getConnection();
		Connection handle = c.getInternalConnection();
		c.close();

		// getting it again in this thread should give us the exact same object
		ConnectionHandle c2 = (ConnectionHandle) poolClass.getConnection();
		assertEquals(handle, c2.getInternalConnection());
		c2.close();

		poolClass.close();
	}

	@Test
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void testOneThreadTwoConnections() throws SQLException{	
		poolClass = new BoneCP(config);
		ConnectionHandle c1 = (ConnectionHandle) poolClass.getConnection();
		c1.setDebugHandle(123);
		assertFalse(c1.logicallyClosed.get());

		ConnectionHandle c2 = (ConnectionHandle) poolClass.getConnection();
		c2.setDebugHandle(456);
		assertFalse(c2.logicallyClosed.get());
		assertNotSame(c1.getDebugHandle(), c2.getDebugHandle());
		assertNotSame(c1.getInternalConnection(), c2.getInternalConnection());

		Connection c1Conn = c1.getInternalConnection();

		// closing off a connection should result in:
		// that connectionHandle having a blank internal
		// the old internal = in TL
		// the TL contains a new CH
		c1.close();
		SimpleEntry<ConnectionHandle, Boolean> handle = ((CachedConnectionStrategyThreadLocal)(((CachedConnectionStrategy)c1.getPool().connectionStrategy).tlConnections)).dumbGet();
//		assertNotSame(c1.getDebugHandle(), handle.getKey().getDebugHandle()); // still here?
		assertNotSame(c1.getInternalConnection(), handle.getKey().getInternalConnection());
		assertNull(c1.getInternalConnection());
		assertEquals(c1Conn,handle.getKey().getInternalConnection());

		assertEquals(3, poolClass.partitions[0].getFreeConnections().size());
		c2.close();
		assertEquals(4, poolClass.partitions[0].getFreeConnections().size());


		poolClass.close();

	}

	@Test 
	public void testMoreThreadsThanConnections() throws SQLException, InterruptedException, CloneNotSupportedException{
		BoneCPConfig config = this.config.clone();
		config.setNullOnConnectionTimeout(true);
		config.setConnectionTimeoutInMs(10);
		poolClass = new BoneCP(config);
		final CountDownLatch cdl = new CountDownLatch(5);
		final CountDownLatch cdlTerminate = new CountDownLatch(1);
		final CountDownLatch crudeJoin = new CountDownLatch(5);
		for (int i=0; i < 5; i++){
			new Thread(){


				public void run() {
					try {
						Connection c = poolClass.getConnection();
						cdl.countDown();
						cdlTerminate.await(); // everyone waits
						c.close();
						crudeJoin.countDown();
					} catch (SQLException e) {
						e.printStackTrace();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}.start();
		}
		cdl.await();

		// this next request should make the pool switch over (permanently) to the fallback strategy
		Connection c = poolClass.getConnection();
		assertNull(c); // we've run out
		cdlTerminate.countDown(); // release the threads holding the lock
		// try again
		c = poolClass.getConnection();
		assertNotNull(c); // we can get new connections again

		// by the time 1 thread times out, we'd already have flipped. Check to make sure
		assertFalse(poolClass.cachedPoolStrategy);
		c.close();
		crudeJoin.await(); // wait till everyone said close
		assertEquals(5, poolClass.partitions[0].getFreeConnections().size());

		poolClass.close();

	}

	/** Same test as above but this time the threads are done from using it. All connections should go back to the normal queue in the fallback
	 * strategy.
	 * @throws SQLException
	 * @throws InterruptedException
	 * @throws CloneNotSupportedException 
	 */
	@Test 
	@Ignore
	public void testMoreThreadsThanConnectionsSteal() throws SQLException, InterruptedException, CloneNotSupportedException{
		BoneCPConfig config = this.config.clone();

		config.setNullOnConnectionTimeout(false);
		config.setConnectionTimeoutInMs(Long.MAX_VALUE);

		poolClass = new BoneCP(config);
		final CountDownLatch cdl = new CountDownLatch(5);
		for (int i=0; i < 5; i++){
			new Thread(){

				public void run() {
					try {
						Connection c = poolClass.getConnection();
						c.close();
						cdl.countDown();
					} catch (SQLException e) {
						e.printStackTrace();
					}
				}
			}.start();
		}
		cdl.await();

		// this next request should make the pool switch over (permanently) to the fallback strategy
		poolClass.getConnection().close();
		// by the time 1 thread times out, we'd already have flipped. Check to make sure
		assertFalse(poolClass.cachedPoolStrategy);
		assertEquals(5, poolClass.partitions[0].getFreeConnections().size());
	}
	 
	
	@Test
	public void testCoverage() throws SQLException{
		poolClass = new BoneCP(config);
		try{
			poolClass.connectionStrategy.pollConnection();
			Assert.fail("Should throw an exception");
		} catch(Exception e){
			// nothing
		}
		
		BoneCP mockPool = EasyMock.createNiceMock(BoneCP.class);
		CachedConnectionStrategy ccs = new CachedConnectionStrategy(mockPool , new DefaultConnectionStrategy(mockPool));
		ConnectionHandle mockConnection = EasyMock.createNiceMock(ConnectionHandle.class);
		mockConnection.logicallyClosed = new AtomicBoolean(true);
		ccs.threadFinalizableRefs.put(mockConnection, new FinalizableWeakReference<Thread>(Thread.currentThread(), new FinalizableReferenceQueue()) {
			public void finalizeReferent() {
		}
	});
		
		mockPool.releaseConnection((Connection)EasyMock.anyObject());
		EasyMock.expectLastCall().andThrow(new SQLException("foo")).once();
		EasyMock.replay(mockPool);
		ccs.stealExistingAllocations();
		
	}
}
