/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.sharedSession;

import org.hibernate.FlushMode;
import org.hibernate.IrrelevantEntity;
import org.hibernate.Session;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.persister.entity.EntityPersister;

import org.hibernate.resource.jdbc.spi.JdbcSessionOwner;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;

import java.lang.reflect.Field;
import java.util.Collection;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

/**
 * @author Steve Ebersole
 */
public class SessionWithSharedConnectionTest extends BaseCoreFunctionalTestCase {
	@Test
	@TestForIssue( jiraKey = "HHH-7090" )
	public void testSharedTransactionContextSessionClosing() {
		Session session = sessionFactory().openSession();
		session.getTransaction().begin();

		Session secondSession = session.sessionWithOptions()
				.transactionContext()
				.openSession();
		secondSession.createCriteria( IrrelevantEntity.class ).list();

		//the list should have registered and then released a JDBC resource
		assertFalse(
				((SessionImplementor) secondSession)
						.getJdbcCoordinator()
						.getResourceRegistry()
						.hasRegisteredResources()
		);

		assertTrue( session.isOpen() );
		assertTrue( secondSession.isOpen() );

		assertSame( session.getTransaction(), secondSession.getTransaction() );

		session.getTransaction().commit();

		assertTrue( session.isOpen() );
		assertTrue( secondSession.isOpen() );

		secondSession.close();
		assertTrue( session.isOpen() );
		assertFalse( secondSession.isOpen() );

		session.close();
		assertFalse( session.isOpen() );
		assertFalse( secondSession.isOpen() );
	}

	@Test
	@TestForIssue( jiraKey = "HHH-7090" )
	public void testSharedTransactionContextAutoClosing() {
		Session session = sessionFactory().openSession();
		session.getTransaction().begin();

		// COMMIT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		Session secondSession = session.sessionWithOptions()
				.transactionContext()
				.autoClose( true )
				.openSession();

		// directly assert state of the second session
		assertTrue( ((SessionImplementor) secondSession).isAutoCloseSessionEnabled() );
		assertTrue( ((SessionImplementor) secondSession).shouldAutoClose() );

		// now commit the transaction and make sure that does not close the sessions
		session.getTransaction().commit();
		assertFalse( ((SessionImplementor) session).isClosed() );
		assertTrue( ((SessionImplementor) secondSession).isClosed() );

		session.close();
		assertTrue( ((SessionImplementor) session).isClosed() );
		assertTrue( ((SessionImplementor) secondSession).isClosed() );


		// ROLLBACK ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

		session = sessionFactory().openSession();
		session.getTransaction().begin();

		secondSession = session.sessionWithOptions()
				.transactionContext()
				.autoClose( true )
				.openSession();

		// directly assert state of the second session
		assertTrue( ((SessionImplementor) secondSession).isAutoCloseSessionEnabled() );
		assertTrue( ((SessionImplementor) secondSession).shouldAutoClose() );

		// now rollback the transaction and make sure that does not close the sessions
		session.getTransaction().rollback();
		assertFalse( ((SessionImplementor) session).isClosed() );
		assertTrue( ((SessionImplementor) secondSession).isClosed() );

		session.close();
		assertTrue( ((SessionImplementor) session).isClosed() );
		assertTrue( ((SessionImplementor) secondSession).isClosed() );

	}

//	@Test
//	@TestForIssue( jiraKey = "HHH-7090" )
//	public void testSharedTransactionContextAutoJoining() {
//		Session session = sessionFactory().openSession();
//		session.getTransaction().begin();
//
//		Session secondSession = session.sessionWithOptions()
//				.transactionContext()
//				.autoJoinTransactions( true )
//				.openSession();
//
//		// directly assert state of the second session
//		assertFalse( ((SessionImplementor) secondSession).shouldAutoJoinTransaction() );
//
//		secondSession.close();
//		session.close();
//	}

	@Test
	@TestForIssue( jiraKey = "HHH-7090" )
	public void testSharedTransactionContextFlushBeforeCompletion() {
		Session session = sessionFactory().openSession();
		session.getTransaction().begin();

		Session secondSession = session.sessionWithOptions()
				.transactionContext()
				.flushBeforeCompletion( true )
				.autoClose( true )
				.openSession();

		// directly assert state of the second session
//		assertTrue( ((SessionImplementor) secondSession).isFlushBeforeCompletionEnabled() );

		// now try it out
		Integer id = (Integer) secondSession.save( new IrrelevantEntity() );
		session.getTransaction().commit();
		assertFalse( ((SessionImplementor) session).isClosed() );
		assertTrue( ((SessionImplementor) secondSession).isClosed() );

		session.close();
		assertTrue( ((SessionImplementor) session).isClosed() );
		assertTrue( ((SessionImplementor) secondSession).isClosed() );

		session = sessionFactory().openSession();
		session.getTransaction().begin();
		IrrelevantEntity it = (IrrelevantEntity) session.byId( IrrelevantEntity.class ).load( id );
		assertNotNull( it );
		session.delete( it );
		session.getTransaction().commit();
		session.close();
	}
	
	@Test
	@TestForIssue( jiraKey = "HHH-7239" )
	public void testChildSessionCallsAfterTransactionAction() throws Exception {
		Session session = openSession();

		final String postCommitMessage = "post commit was called";
		
		EventListenerRegistry eventListenerRegistry = sessionFactory().getServiceRegistry().getService(EventListenerRegistry.class);
		//register a post commit listener
		eventListenerRegistry.appendListeners(
				EventType.POST_COMMIT_INSERT,
				new PostInsertEventListener() {
					@Override
					public void onPostInsert(PostInsertEvent event) {
						((IrrelevantEntity) event.getEntity()).setName( postCommitMessage );
					}

					@Override
					public boolean requiresPostCommitHanding(EntityPersister persister) {
						return true;
					}
				}
		);
		
		session.getTransaction().begin();
		
		IrrelevantEntity irrelevantEntityMainSession = new IrrelevantEntity();
		irrelevantEntityMainSession.setName( "main session" );
		session.save( irrelevantEntityMainSession );
		
		//open secondary session to also insert an entity
		Session secondSession = session.sessionWithOptions()
				.connection()
				.flushBeforeCompletion( true )
				.autoClose( true )
				.openSession();

		IrrelevantEntity irrelevantEntitySecondarySession = new IrrelevantEntity();
		irrelevantEntitySecondarySession.setName( "secondary session" );
		secondSession.save( irrelevantEntitySecondarySession );

		session.getTransaction().commit();
		
		//both entities should have their names updated to the postCommitMessage value
		assertEquals(postCommitMessage, irrelevantEntityMainSession.getName());
		assertEquals(postCommitMessage, irrelevantEntitySecondarySession.getName());
	}

	@Test
	@TestForIssue( jiraKey = "HHH-7239" )
	public void testChildSessionTwoTransactions() throws Exception {
		Session session = openSession();
		
		session.getTransaction().begin();
		
		//open secondary session with managed options
		Session secondarySession = session.sessionWithOptions()
				.connection()
				.flushBeforeCompletion( true )
				.autoClose( true )
				.openSession();
		
		//the secondary session should be automatically closed after the commit
		session.getTransaction().commit();
		
		assertFalse( secondarySession.isOpen() );

		//should be able to create a new transaction and carry on using the original session
		session.getTransaction().begin();
		session.getTransaction().commit();
	}

	@Test
	@TestForIssue(jiraKey = "HHH-11830")
	public void testSharedSessionTransactionObserver() throws Exception {
		Session session = openSession();

		session.getTransaction().begin();

		Field field = null;
		Class<?> clazz = ((JdbcSessionOwner) session).getTransactionCoordinator().getClass();
		while (clazz != null) {
			try {
				field = clazz.getDeclaredField("observers");
				field.setAccessible(true);
				break;
			} catch (NoSuchFieldException e) {
				clazz = clazz.getSuperclass();
			} catch (Exception e) {
				throw new IllegalStateException(e);
			}
		}
		assertNotNull("Observers field was not found", field);

		//Some of these collections could be lazily initialize: check for null before invoking size()
		final Collection collection = (Collection) field.get( ( (SessionImplementor) session ).getTransactionCoordinator() );
		assertTrue(collection == null || collection.size() == 0 );

		//open secondary sessions with managed options and immediately close
		Session secondarySession;
		for (int i = 0; i < 10; i++){
			secondarySession = session.sessionWithOptions()
					.connection()
					.flushMode( FlushMode.COMMIT )
					.autoClose( true )
					.openSession();

			//when the shared session is opened it should register an observer
			assertEquals(1, ((Collection) field.get(((JdbcSessionOwner) session).getTransactionCoordinator())).size());

			//observer should be released
			secondarySession.close();

			assertEquals(0, ((Collection) field.get(((JdbcSessionOwner) session).getTransactionCoordinator())).size());
		}
	}


	@Override
	protected Class<?>[] getAnnotatedClasses() {
		return new Class[] { IrrelevantEntity.class };
	}
}
