// Created May 26, 2005 8:17:10 AM

/********************************************************************
 * Copyright © 2005 VHA. All rights reserved
 ********************************************************************/

/**
 * This is a comment for new java file.
 */
package gov.va.med.esr.common.persistent.hibernate;

import java.util.BitSet;
import java.util.List;

import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import gov.va.med.fw.model.AbstractKeyedEntity;
import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.persistent.DAOException;

import gov.va.med.esr.common.util.AbstractCommonTestCase;

/**
 * Test framework for executing common DAO operations.
 * 
 * 
 * @author DNS   BOHMEG
 */
public abstract class AbstractKeyedEntityDAOTestCase extends
		AbstractCommonTestCase {

	protected static final DAOTestMode TEST_ALL = new DAOTestMode();

	protected static final DAOTestMode TEST_CREATE = new DAOTestMode();

	protected static final DAOTestMode TEST_RETRIEVE = new DAOTestMode();

	protected static final DAOTestMode TEST_UPDATE_COMMIT = new DAOTestMode();

	protected static final DAOTestMode TEST_DELETE = new DAOTestMode();

	protected static final DAOTestMode TEST_QUERY = new DAOTestMode();

	protected static final DAOTestMode TEST_NONE = new DAOTestMode();

	protected DAOTestMode testSuite = new DAOTestMode();

	static {
		TEST_ALL.set(0, 5); // 11111
		TEST_CREATE.set(0); // 00001
		TEST_RETRIEVE.set(1); // 00010
		TEST_UPDATE_COMMIT.set(2); // 00100
		TEST_DELETE.set(3); // 01000
		TEST_QUERY.set(4); // 10000
	}

	private AbstractKeyedEntity testObject;

	private SessionFactory sessionFactory;

	/**
	 * @return Returns the sessionFactoryBean.
	 */
	public SessionFactory getSessionFactory() {
		if (sessionFactory == null) {
			Object bean = applicationContext.getBean( "sessionFactory" );
			sessionFactory = bean instanceof SessionFactory ? (SessionFactory)bean : null; 
		}
		return sessionFactory;
	}

	/** must be implemented by subclasses */
	protected abstract AbstractKeyedEntity setUpTestObject() throws Exception;

	/** must be implemented by subclasses */
	protected abstract void modifyTestObject(AbstractKeyedEntity obj)
			throws Exception;

	/** subclasses can override for testing one at a time */
	protected DAOTestMode[] setUpTests() {
		return new DAOTestMode[] { TEST_ALL };
	}

	/** subclasses can override (currently not used though) */
	protected Object[] getPersistentObjects(Object tester) {
		return new Object[] { tester };
	}

	/** subclasses can override for testing specific named queries */
	protected QueryInfo[] getNamedQueries() {
		// default implementation returns nothing
		return null;
	}

	/** subclasses can override, note this only applies for Create test */
	protected boolean shouldCommitCreate() {
		return false;
	}

	/** subclasses can override if in that mode */
	protected EntityKey getTargetChildId() {
		return null;
	}

	protected final AbstractKeyedEntity getTestObject() {
		return testObject;
	}


	protected void customSetUp() throws Exception {
		super.customSetUp(); // forces login
		testObject = setUpTestObject();
		BitSet tests[] = setUpTests();
		for (int i = 0; i < tests.length; i++)
			testSuite.or(tests[i]);
	}

	private boolean isActive(DAOTestMode mode) {
		DAOTestMode tester = (DAOTestMode) testSuite.clone();
		tester.and(mode);
		boolean active = false;
		if(tester.equals(mode)) {
			active = true;
			// but if no testObject, then can't test
			if(!TEST_QUERY.equals(mode) && testObject == null) {
				active = false;
				log("Can not execute test case that writes to database with a null testObject");
			}
		}
		if(active) {
			StackTraceElement[] callers = new Throwable().getStackTrace();
			log("*************" + callers[1].getMethodName() + "*************");
		}
		return active;
	}

	public void testCreateObject() {
		if (!isActive(TEST_CREATE))
			return;

		try {
			saveObjectTest(getTestObject(), shouldCommitCreate());
		} catch (Exception e) {
			fail("Can not execute DAO", e);
		}
	}

	protected void saveObjectTest(AbstractKeyedEntity testObject, boolean shouldCommitCreate) throws Exception {
        getDAO().saveObject(testObject);
        log("Created persistent object: " + testObject);
        
        // let's make Hibernate go back to the database and exercise getByKey query
        getCurrentSession().flush();
        getCurrentSession().clear();
		
        if (shouldCommitCreate)
        	setComplete();
    }
	
	protected void saveObjectTest(AbstractKeyedEntity testObject) throws Exception {
		saveObjectTest(testObject, shouldCommitCreate());
    }

	protected AbstractKeyedEntity saveAndLoadInTransaction(AbstractKeyedEntity testObject) throws Exception {
		try {
			// create in TX1
			saveObjectTest(testObject, true);
			endTransaction();
			/*
			 * To ensure Hibernate update works with dirty checking, can do
			 * either: 1) have mapping select-before-update=true 2) manually
			 * lock all detached objects to session via attachObjectToSession()
			 * implementation 3) start Session and retrieve in Session Approach
			 * chosen here is 3)
			 */
			initTransactionAndSession(); // allows for lazy loading
			return (AbstractKeyedEntity) getDAO().getByKey(
					testObject.getEntityKey()); // forces objects to be loaded in
		} catch (Exception e) {
			fail("Can not execute test", e);
		}
		return null;
    }
	
    public void testRetrieveObject() {
		if (!isActive(TEST_RETRIEVE))
			return;

		try {
			AbstractKeyedEntity tester = getTestObject();
			saveObjectTest(tester, false);
			
			log("Retrieving: " + tester);
			Object retrieved = getDAO().getByKey(tester.getEntityKey());
			assertNotNull(retrieved);
			log("Retrieved persistent object: " + retrieved);
		} catch (Exception e) {
			fail("Can not execute DAO", e);
		}
	}

	public void testUpdateObject() {
		if (!isActive(TEST_UPDATE_COMMIT) || getTargetChildId() != null)
			return;

		try {
			AbstractKeyedEntity tester = getTestObject();
			// create in TX1
			saveObjectTest(tester, true);
			endTransaction();
			/*
			 * To ensure Hibernate update works with dirty checking, can do
			 * either: 1) have mapping select-before-update=true 2) manually
			 * lock all detached objects to session via attachObjectToSession()
			 * implementation 3) start Session and retrieve in Session Approach
			 * chosen here is 3)
			 */
			initTransactionAndSession(); // allows for lazy loading
			tester = (AbstractKeyedEntity) getDAO().getByKey(
					tester.getEntityKey()); // forces objects to be loaded in
			// Session
			// change
			modifyTestObject(tester);
			// update in TX2
			getDAO().saveObject(tester);
			log("Updated persistent object: " + tester);
			
			setComplete();
		} catch (Exception e) {
			fail("Can not execute test", e);
		}
	}

	protected Session initTransactionAndSession() throws Exception {
		/*
		 * Assumption here is that the injected transactionManager is the
		 * HibernateTransactionManager. That will ensure that transaction begin
		 * and commit will also clear and create Hibernate Session's for us
		 */
		transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
		return getCurrentSession(); // this is the new cleaned up session
	}
	
	protected Session getCurrentSession() throws Exception {
		return this.getSessionFactory().getCurrentSession();	
	}

	protected void attachObjectToSession(Object obj) throws Exception {
		Session session = initTransactionAndSession();
		Object[] persistentObjects = getPersistentObjects(obj);
		for (int i = 0; i < persistentObjects.length; i++) {
			session.lock(persistentObjects[i], LockMode.NONE);
		}
	}

	public void testChildUpdateWithoutParent() {
		if (!isActive(TEST_UPDATE_COMMIT))
			return;

		try {
			EntityKey identifier = getTargetChildId(); // needed since can not
			// set identifier on
			// object
			if (identifier == null)
				return;

			AbstractKeyedEntity child = (AbstractKeyedEntity) getDAO()
					.getByKey(identifier);
			modifyTestObject(child);
			getDAO().saveObject(child);
			log("Updated persistent object: " + child);
			setComplete();
		} catch (Exception e) {
			fail("unable to test child update", e);
		}
	}

	public void testNamedQueriesWithNamedParams() {
		if (!isActive(TEST_QUERY))
			return;

		try {
			QueryInfo[] queries = getNamedQueries();
			if (queries != null) {
				List result = null;
				for (int i = 0; i < queries.length; i++) {
					result = getDAO().findByNamedQueryAndNamedParam(queries[i].name, queries[i].paramNames, queries[i].params);
					log(queries[i].name + " returned " + result.size() + " records");
				}
			}
		} catch (DAOException e) {
			fail("Can not execute named queries with named params", e);
		}
	}

	protected void log(String message) {
		// for now, just use out
		System.out.println(message);
	}

	public static class DAOTestMode extends BitSet {
		/**
		 * An instance of serialVersionUID
		 */
		private static final long serialVersionUID = 6648317031696624742L;

		private DAOTestMode() {
		}
	}
	
	public static class QueryInfo {
		String name;
		String[] paramNames;
		Object[] params;
		public QueryInfo(String name) {
			this.name = name;
		}
		
		public QueryInfo(String name, String[] paramNames, Object[] params) {
			this(name);
			this.paramNames = paramNames;
			this.params = params;
		}		
	}
}
