package org.postgresql.test.jdbc2.optional;

import org.postgresql.jdbc2.optional.ConnectionPool;
import org.postgresql.test.TestUtil;
import javax.sql.*;
import java.sql.*;

/**
 * Tests for the ConnectionPoolDataSource and PooledConnection
 * implementations.  They are tested together because the only client
 * interface to the PooledConnection is through the CPDS.
 *
 * @author Aaron Mulder (ammulder@chariotsolutions.com)
 * @version $Revision: 1.5 $
 */
public class ConnectionPoolTest extends BaseDataSourceTest
{
	/**
	 * Constructor required by JUnit
	 */
	public ConnectionPoolTest(String name)
	{
		super(name);
	}

	/**
	 * Creates and configures a ConnectionPool
	 */
	protected void initializeDataSource()
	{
		if (bds == null)
		{
			bds = new ConnectionPool();
			String db = TestUtil.getURL();
			if (db.indexOf('/') > -1)
			{
				db = db.substring(db.lastIndexOf('/') + 1);
			}
			else if (db.indexOf(':') > -1)
			{
				db = db.substring(db.lastIndexOf(':') + 1);
			}
			bds.setDatabaseName(db);
			bds.setUser(TestUtil.getUser());
			bds.setPassword(TestUtil.getPassword());
		}
	}

	/**
	 * Though the normal client interface is to grab a Connection, in
	 * order to test the middleware/server interface, we need to deal
	 * with PooledConnections.	Some tests use each.
	 */
	protected PooledConnection getPooledConnection() throws SQLException
	{
		initializeDataSource();
		return ((ConnectionPool)bds).getPooledConnection();
	}

	/**
	 * Instead of just fetching a Connection from the ConnectionPool,
	 * get a PooledConnection, add a listener to close it when the
	 * Connection is closed, and then get the Connection.  Without
	 * the listener the PooledConnection (and thus the physical connection)
	 * would never by closed.  Probably not a disaster during testing, but
	 * you never know.
	 */
	protected Connection getDataSourceConnection() throws SQLException
	{
		initializeDataSource();
		final PooledConnection pc = getPooledConnection();
		// Since the pooled connection won't be reused in these basic tests, close it when the connection is closed
		pc.addConnectionEventListener(new ConnectionEventListener()
									  {
										  public void connectionClosed(ConnectionEvent event)
										  {
											  try
											  {
												  pc.close();
											  }
											  catch (SQLException e)
											  {
												  fail("Unable to close PooledConnection: " + e);
											  }
										  }

										  public void connectionErrorOccurred(ConnectionEvent event)
										  {}
									  }
									 );
		return pc.getConnection();
	}

	/**
	 * Makes sure that if you get a connection from a PooledConnection,
	 * close it, and then get another one, you're really using the same
	 * physical connection.  Depends on the implementation of toString
	 * for the connection handle.
	 */
	public void testPoolReuse()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			con = pc.getConnection();
			String name = con.toString();
			con.close();
			con = pc.getConnection();
			String name2 = con.toString();
			con.close();
			pc.close();
			assertTrue("Physical connection doesn't appear to be reused across PooledConnection wrappers", name.equals(name2));
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Makes sure that when you request a connection from the
	 * PooledConnection, and previous connection it might have given
	 * out is closed.  See JDBC 2.0 Optional Package spec section
	 * 6.2.3
	 */
	public void testPoolCloseOldWrapper()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			con = pc.getConnection();
			Connection con2 = pc.getConnection();
			try
			{
				con.createStatement();
				fail("Original connection wrapper should be closed when new connection wrapper is generated");
			}
			catch (SQLException e)
			{}
			try
			{
				con.close();
				fail("Original connection wrapper should be closed when new connection wrapper is generated");
			}
			catch (SQLException e)
			{}
			con2.close();
			pc.close();
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Makes sure that if you get two connection wrappers from the same
	 * PooledConnection, they are different, even though the represent
	 * the same physical connection.  See JDBC 2.0 Optional Pacakge spec
	 * section 6.2.2
	 */
	public void testPoolNewWrapper()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			con = pc.getConnection();
			Connection con2 = pc.getConnection();
			con2.close();
			pc.close();
			assertTrue("Two calls to PooledConnection.getConnection should not return the same connection wrapper", con != con2);
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Makes sure that exactly one close event is fired for each time a
	 * connection handle is closed.  Also checks that events are not
	 * fired after a given handle has been closed once.
	 */
	public void testCloseEvent()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			CountClose cc = new CountClose();
			pc.addConnectionEventListener(cc);
			con = pc.getConnection();
			assertTrue(cc.getCount() == 0);
			assertTrue(cc.getErrorCount() == 0);
			con.close();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			con = pc.getConnection();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			con.close();
			assertTrue(cc.getCount() == 2);
			assertTrue(cc.getErrorCount() == 0);
			try
			{
				con.close();
				fail("Should not be able to close a connection wrapper twice");
			}
			catch (SQLException e)
			{}
			assertTrue(cc.getCount() == 2);
			assertTrue(cc.getErrorCount() == 0);
			pc.close();
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Makes sure that close events are not fired after a listener has
	 * been removed.
	 */
	public void testNoCloseEvent()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			CountClose cc = new CountClose();
			pc.addConnectionEventListener(cc);
			con = pc.getConnection();
			assertTrue(cc.getCount() == 0);
			assertTrue(cc.getErrorCount() == 0);
			con.close();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			pc.removeConnectionEventListener(cc);
			con = pc.getConnection();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			con.close();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Makes sure that a listener can be removed while dispatching
	 * events.	Sometimes this causes a ConcurrentModificationException
	 * or something.
	 */
	public void testInlineCloseEvent()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			RemoveClose rc1 = new RemoveClose();
			RemoveClose rc2 = new RemoveClose();
			RemoveClose rc3 = new RemoveClose();
			pc.addConnectionEventListener(rc1);
			pc.addConnectionEventListener(rc2);
			pc.addConnectionEventListener(rc3);
			con = pc.getConnection();
			con.close();
			con = pc.getConnection();
			con.close();
		}
		catch (Exception e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Tests that a close event is not generated when a connection
	 * handle is closed automatically due to a new connection handle
	 * being opened for the same PooledConnection.	See JDBC 2.0
	 * Optional Package spec section 6.3
	 */
	public void testAutomaticCloseEvent()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			CountClose cc = new CountClose();
			pc.addConnectionEventListener(cc);
			con = pc.getConnection();
			assertTrue(cc.getCount() == 0);
			assertTrue(cc.getErrorCount() == 0);
			con.close();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			con = pc.getConnection();
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			// Open a 2nd connection, causing the first to be closed.  No even should be generated.
			Connection con2 = pc.getConnection();
			assertTrue("Connection handle was not closed when new handle was opened", con.isClosed());
			assertTrue(cc.getCount() == 1);
			assertTrue(cc.getErrorCount() == 0);
			con2.close();
			assertTrue(cc.getCount() == 2);
			assertTrue(cc.getErrorCount() == 0);
			pc.close();
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

	/**
	 * Makes sure the isClosed method on a connection wrapper does what
	 * you'd expect.  Checks the usual case, as well as automatic
	 * closure when a new handle is opened on the same physical connection.
	 */
	public void testIsClosed()
	{
		try
		{
			PooledConnection pc = getPooledConnection();
			Connection con = pc.getConnection();
			assertTrue(!con.isClosed());
			con.close();
			assertTrue(con.isClosed());
			con = pc.getConnection();
			Connection con2 = pc.getConnection();
			assertTrue(con.isClosed());
			assertTrue(!con2.isClosed());
			con2.close();
			assertTrue(con.isClosed());
			pc.close();
		}
		catch (SQLException e)
		{
			fail(e.getMessage());
		}
	}

    /**
     * Ensures that a statement generated by a proxied connection returns the
     * proxied connection from getConnection() [not the physical connection].
     */
    public void testStatementConnection() {
        try {
            PooledConnection pc = getPooledConnection();
            Connection con = pc.getConnection();
            Statement s = con.createStatement();
            Connection conRetrieved = s.getConnection();

            assertTrue(con.getClass().equals(conRetrieved.getClass()));
            assertTrue(con.equals(conRetrieved));
        } catch (SQLException e) {
            fail(e.getMessage());
        }
    }

    /**
     * Ensures that the Statement proxy generated by the Connection handle
     * throws the correct kind of exception.
     */
    public void testStatementProxy() {
            Statement s = null;
            try 
            {
                    PooledConnection pc = getPooledConnection();
                    Connection con = pc.getConnection();
                    s = con.createStatement();
            } 
            catch (SQLException e) 
            {
                    fail(e.getMessage());
            }
            try 
            {
                    s.executeQuery("SELECT * FROM THIS_TABLE_SHOULD_NOT_EXIST");
                    fail("An SQL exception was not thrown that should have been");
            } 
            catch (SQLException e) 
            {
                    ; // This is the expected and correct path
            }
            catch (Exception e) 
            {
                    fail("bad exception; was expecting SQLException, not" +
                         e.getClass().getName());
            }
    }

    /**
     * Ensures that the Statement proxy generated by the Connection handle
     * throws the correct kind of exception.
     */
    public void testStatementProxy() {
            Statement s = null;
            try 
            {
                    PooledConnection pc = getPooledConnection();
                    Connection con = pc.getConnection();
                    s = con.createStatement();
            } 
            catch (SQLException e) 
            {
                    fail(e.getMessage());
            }
            try 
            {
                    s.executeQuery("SELECT * FROM THIS_TABLE_SHOULD_NOT_EXIST");
                    fail("An SQL exception was not thrown that should have been");
            } 
            catch (SQLException e) 
            {
                    ; // This is the expected and correct path
            }
            catch (Exception e) 
            {
                    fail("bad exception; was expecting SQLException, not" +
                         e.getClass().getName());
            }
    }

    /**
     * Ensures that a prepared statement generated by a proxied connection
     * returns the proxied connection from getConnection() [not the physical
     * connection].
     */
    public void testPreparedStatementConnection() {
        try {
            PooledConnection pc = getPooledConnection();
            Connection con = pc.getConnection();
            PreparedStatement s = con.prepareStatement("select 'x'");
            Connection conRetrieved = s.getConnection();

            assertTrue(con.getClass().equals(conRetrieved.getClass()));
            assertTrue(con.equals(conRetrieved));
        } catch (SQLException e) {
            fail(e.getMessage());
        }
    }

    /**
     * Ensures that a callable statement generated by a proxied connection
     * returns the proxied connection from getConnection() [not the physical
     * connection].
     */
    public void testCallableStatementConnection() {
        try {
            PooledConnection pc = getPooledConnection();
            Connection con = pc.getConnection();
            CallableStatement s = con.prepareCall("select 'x'");
            Connection conRetrieved = s.getConnection();

            assertTrue(con.getClass().equals(conRetrieved.getClass()));
            assertTrue(con.equals(conRetrieved));
        } catch (SQLException e) {
            fail(e.getMessage());
        }
    }

    /**
	 * Helper class to remove a listener during event dispatching.
	 */
	private class RemoveClose implements ConnectionEventListener
	{
		public void connectionClosed(ConnectionEvent event)
		{
			((PooledConnection)event.getSource()).removeConnectionEventListener(this);
		}

		public void connectionErrorOccurred(ConnectionEvent event)
		{
			((PooledConnection)event.getSource()).removeConnectionEventListener(this);
		}
	}

	/**
	 * Helper class that implements the event listener interface, and
	 * counts the number of events it sees.
	 */
	private class CountClose implements ConnectionEventListener
	{
		private int count = 0, errorCount = 0;
		public void connectionClosed(ConnectionEvent event)
		{
			count++;
		}

		public void connectionErrorOccurred(ConnectionEvent event)
		{
			errorCount++;
		}

		public int getCount()
		{
			return count;
		}

		public int getErrorCount()
		{
			return errorCount;
		}

		public void clear()
		{
			count = errorCount = 0;
		}
	}
}
