/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2014, Red Hat Inc. or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.test.collection.multisession;

import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.jboss.logging.Logger;
import org.junit.Test;

import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.collection.internal.AbstractPersistentCollection;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

/**
 * @author Gail Badner
 */
public class MultipleSessionCollectionTest extends BaseCoreFunctionalTestCase {
	private static final Logger log = Logger.getLogger( MultipleSessionCollectionTest.class );

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testSaveOrUpdateOwnerWithCollectionInNewSessionBeforeFlush() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.saveOrUpdate( p );

		// try to save the same entity in a new session before flushing the first session

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( p );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}


	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testSaveOrUpdateOwnerWithCollectionInNewSessionAfterFlush() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.saveOrUpdate( p );
		s1.flush();

		// try to save the same entity in a new session after flushing the first session

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( p );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testSaveOrUpdateOwnerWithUninitializedCollectionInNewSession() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s = openSession();
		s.getTransaction().begin();
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		Session s1 = openSession();
		s1.getTransaction().begin();
		p = s1.get( Parent.class, p.id );
		assertFalse( Hibernate.isInitialized( p.children ) );

		// try to save the same entity (with an uninitialized collection) in a new session

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( p );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to initialize collection, modify and commit in first session
		assertFalse( Hibernate.isInitialized( p.children ) );
		Hibernate.initialize( p.children );
		p.children.add( new Child() );
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( 2, pGet.children.size());
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testSaveOrUpdateOwnerWithInitializedCollectionInNewSession() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s = openSession();
		s.getTransaction().begin();
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		Session s1 = openSession();
		s1.getTransaction().begin();
		p = s1.get( Parent.class, p.id );
		Hibernate.initialize( p.children );

		// try to save the same entity (with an initialized collection) in a new session

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( p );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	//

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testCopyPersistentCollectionReferenceBeforeFlush() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.persist( p );

		// Copy p.children into a different Parent before flush and try to save in new session.

		Parent pWithSameChildren = new Parent();
		pWithSameChildren.children = p.children;

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( pWithSameChildren );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testCopyPersistentCollectionReferenceAfterFlush() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.persist( p );
		s1.flush();

		// Copy p.children into a different Parent after flush and try to save in new session.

		Parent pWithSameChildren = new Parent();
		pWithSameChildren.children = p.children;

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( pWithSameChildren );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testCopyUninitializedCollectionReferenceAfterGet() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s = openSession();
		s.getTransaction().begin();
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		Session s1 = openSession();
		s1.getTransaction().begin();
		p = s1.get( Parent.class, p.id );
		assertFalse( Hibernate.isInitialized( p.children ) );

		// Copy p.children (uninitialized) into a different Parent and try to save in new session.

		Parent pWithSameChildren = new Parent();
		pWithSameChildren.children = p.children;

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( pWithSameChildren );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testCopyInitializedCollectionReferenceAfterGet() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s = openSession();
		s.getTransaction().begin();
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		Session s1 = openSession();
		s1.getTransaction().begin();
		p = s1.get( Parent.class, p.id );
		Hibernate.initialize( p.children );

		// Copy p.children (initialized) into a different Parent.children and try to save in new session.

		Parent pWithSameChildren = new Parent();
		pWithSameChildren.children = p.children;

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( pWithSameChildren );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testCopyInitializedCollectionReferenceToNewEntityCollectionRoleAfterGet() {
		Parent p = new Parent();
		Child c = new Child();
		p.children.add( c );

		Session s = openSession();
		s.getTransaction().begin();
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		Session s1 = openSession();
		s1.getTransaction().begin();
		p = s1.get( Parent.class, p.id );
		Hibernate.initialize( p.children );

		// Copy p.children (initialized) into a different Parent.oldChildren (note different collection role)
		// and try to save in new session.

		Parent pWithSameChildren = new Parent();
		pWithSameChildren.oldChildren = p.children;

		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( pWithSameChildren );
			s2.getTransaction().commit();
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit in first session
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		Parent pGet = s1.get( Parent.class, p.id );
		assertEquals( c.id, pGet.children.iterator().next().id );
		session.delete( pGet );
		s1.getTransaction().commit();
		session.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testDeleteCommitCopyToNewOwnerInNewSession() {
		Parent p1 = new Parent();
		p1.nickNames.add( "nick" );
		Parent p2 = new Parent();
		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.save( p1 );
		s1.save( p2 );
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		s1.delete( p1 );
		s1.flush();
		s1.getTransaction().commit();

		// need to commit after flushing; otherwise, will get lock failure when try to move the collection below

		assertNull( ( (SessionImplementor) s1 ).getPersistenceContext().getEntry( p1 ) );
		CollectionEntry ceChildren = ( (SessionImplementor) s1 ).getPersistenceContext().getCollectionEntry( (PersistentCollection) p1.children );
		CollectionEntry ceNickNames = ( (SessionImplementor) s1 ).getPersistenceContext().getCollectionEntry( (PersistentCollection) p1.nickNames );
		assertNull( ceChildren );
		assertNull( ceNickNames );
		assertNull( ( ( AbstractPersistentCollection) p1.children ).getSession() );
		assertNull( ( ( AbstractPersistentCollection) p1.oldChildren ).getSession() );
		assertNull( ( ( AbstractPersistentCollection) p1.nickNames ).getSession() );
		assertNull( ( (AbstractPersistentCollection) p1.oldNickNames ).getSession() );

		// Assign the deleted collection to a different entity with same collection role (p2.nickNames)

		p2.nickNames = p1.nickNames;
		Session s2 = openSession();
		s2.getTransaction().begin();
		s2.saveOrUpdate( p2 );
		s2.getTransaction().commit();
		s2.close();

		s1.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testDeleteCommitCopyToNewOwnerNewCollectionRoleInNewSession() {
		Parent p1 = new Parent();
		p1.nickNames.add( "nick" );
		Parent p2 = new Parent();
		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.save( p1 );
		s1.save( p2 );
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		s1.delete( p1 );
		s1.flush();
		s1.getTransaction().commit();

		// need to commit after flushing; otherwise, will get lock failure when try to move the collection below

		assertNull( ( (SessionImplementor) s1 ).getPersistenceContext().getEntry( p1 ) );
		CollectionEntry ceChildren = ( (SessionImplementor) s1 ).getPersistenceContext().getCollectionEntry( (PersistentCollection) p1.children );
		CollectionEntry ceNickNames = ( (SessionImplementor) s1 ).getPersistenceContext().getCollectionEntry( (PersistentCollection) p1.nickNames );
		assertNull( ceChildren );
		assertNull( ceNickNames );
		assertNull( ( ( AbstractPersistentCollection) p1.children ).getSession() );
		assertNull( ( ( AbstractPersistentCollection) p1.oldChildren ).getSession() );
		assertNull( ( ( AbstractPersistentCollection) p1.nickNames ).getSession() );
		assertNull( ( (AbstractPersistentCollection) p1.oldNickNames ).getSession() );

		// Assign the deleted collection to a different entity with different collection role (p2.oldNickNames)

		p2.oldNickNames = p1.nickNames;
		Session s2 = openSession();
		s2.getTransaction().begin();
		s2.saveOrUpdate( p2 );
		s2.getTransaction().commit();
		s2.close();

		s1.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testDeleteCopyToNewOwnerInNewSessionBeforeFlush() {
		Parent p1 = new Parent();
		p1.nickNames.add( "nick" );
		Parent p2 = new Parent();
		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.save( p1 );
		s1.save( p2 );
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		s1.delete( p1 );

		// Assign the deleted collection to a different entity with same collection role (p2.nickNames)
		// before committing delete.

		p2.nickNames = p1.nickNames;
		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( p2 );
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit the original delete
		s1.getTransaction().commit();
		s1.close();
	}

	@Test
	@TestForIssue( jiraKey = "HHH-9518" )
	public void testDeleteCopyToNewOwnerNewCollectionRoleInNewSessionBeforeFlush() {
		Parent p1 = new Parent();
		p1.nickNames.add( "nick" );
		Parent p2 = new Parent();
		Session s1 = openSession();
		s1.getTransaction().begin();
		s1.save( p1 );
		s1.save( p2 );
		s1.getTransaction().commit();
		s1.close();

		s1 = openSession();
		s1.getTransaction().begin();
		s1.delete( p1 );

		// Assign the deleted collection to a different entity with different collection role (p2.oldNickNames)
		// before committing delete.

		p2.oldNickNames = p1.nickNames;
		Session s2 = openSession();
		s2.getTransaction().begin();
		try {
			s2.saveOrUpdate( p2 );
			fail( "should have thrown HibernateException" );
		}
		catch (HibernateException ex) {
			log.error( ex );
			s2.getTransaction().rollback();
		}
		finally {
			s2.close();
		}

		// should still be able to commit the original delete
		s1.getTransaction().commit();
		s1.close();
	}

	@Override
	public Class<?>[] getAnnotatedClasses() {
		return new Class[] {
				Parent.class,
				Child.class
		};
	}

	@Entity
	@Table(name="Parent")
	public static class Parent {
		@Id
		@GeneratedValue
		private Long id;

		@ElementCollection
		private Set<String> nickNames = new HashSet<String>();

		@ElementCollection
		private Set<String> oldNickNames = new HashSet<String>();

		@OneToMany(cascade = CascadeType.ALL)
		@JoinColumn
		private Set<Child> children = new HashSet<Child>();

		@OneToMany(cascade = CascadeType.ALL)
		@JoinColumn
		private Set<Child> oldChildren = new HashSet<Child>();

	}

	@Entity
	@Table(name="Child")
	public static class Child {
		@Id
		@GeneratedValue
		private Long id;

	}
}
