/*
 * 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.test.ops;

import javax.persistence.PersistenceException;
import java.util.List;

import org.junit.Test;

import org.hibernate.Hibernate;
import org.hibernate.PersistentObjectException;
import org.hibernate.Session;
import org.hibernate.StaleObjectStateException;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.testing.FailureExpected;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;

import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
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;
import static org.junit.Assert.fail;

/**
 * Tests merging multiple detached representations of the same entity when explicitly allowed.
 *
 * @author Gail Badner
 */
public class MergeMultipleEntityCopiesAllowedTest extends BaseCoreFunctionalTestCase {

	public String[] getMappings() {
		return new String[] {
				"ops/Hoarder.hbm.xml"
		};
	}

	public void configure(Configuration cfg) {
		super.configure( cfg );
		cfg.setProperty(
				"hibernate.event.merge.entity_copy_observer",
				"allow"
		);
	}

	@Test
	public void testNestedDiffBasicProperty() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );

		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		// change basic property of nested entity
		item1_1.setName( "item1_1 name" );

		// change the nested Item to be the copy with the new name
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// the name from the top level item will win.
		assertEquals( item1.getName(), item1Merged.getName() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		Item item1Get = (Item) s.get( Item.class, item1.getId() );
		assertEquals( item1.getName(), item1Get.getName() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testNestedManyToOneChangedToNull() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );

		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		// change many-to-one in nested entity to null.
		item1_1.setCategory( null );
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// the many-to-one from the top level item will win.
		assertEquals( category.getName(), item1Merged.getCategory().getName() );
		assertSame( item1Merged, item1Merged.getCategory().getExampleItem() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( category.getName(), item1.getCategory().getName() );
		assertSame( item1, item1.getCategory().getExampleItem() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testNestedManyToOneChangedToNewEntity() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );

		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		// change many-to-one in nested entity to a new (transient) value
		Category categoryNew = new Category();
		categoryNew.setName( "new category" );
		item1_1.setCategory( categoryNew );
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// the many-to-one from the top level item will win.
		assertEquals( category.getName(), item1Merged.getCategory().getName() );
		assertSame( item1Merged, item1Merged.getCategory().getExampleItem() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( category.getName(), item1.getCategory().getName() );
		assertSame( item1, item1.getCategory().getExampleItem() );
		// make sure new category got persisted
		Category categoryQueried = (Category) s.createQuery( "from Category c where c.name='new category'" ).uniqueResult();
		assertNotNull( categoryQueried );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testTopLevelManyToOneChangedToNewEntity() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );

		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		// change many-to-one in top level to be a new (transient)
		Category categoryNewer = new Category();
		categoryNewer.setName( "newer category" );
		item1.setCategory( categoryNewer );

		// put the other representation in categoryNewer
		categoryNewer.setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// the many-to-one from the top level item will win.
		assertEquals( categoryNewer.getName(), item1Merged.getCategory().getName() );
		assertSame( item1Merged, item1Merged.getCategory().getExampleItem() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( categoryNewer.getName(), item1.getCategory().getName() );
		assertSame( item1, item1.getCategory().getExampleItem() );
		// make sure original category is still there
		Category categoryQueried = (Category) s.createQuery( "from Category c where c.name='category'" ).uniqueResult();
		assertNotNull( categoryQueried );
		// make sure original category has the same item.
		assertSame( item1, categoryQueried.getExampleItem() );
		// set exampleItem to null to avoid constraint violation on cleanup.
		categoryQueried.setExampleItem( null );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testTopLevelManyToOneManagedNestedIsDetached() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );
		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );

		item1Merged.setCategory( category );
		category.setExampleItem( item1_1 );

		// now item1Merged is managed and it has a nested detached item
		s.merge( item1Merged );
		assertEquals( category.getName(), item1Merged.getCategory().getName() );
		assertSame( item1Merged, item1Merged.getCategory().getExampleItem() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( category.getName(), item1.getCategory().getName() );
		assertSame( item1, item1.getCategory().getExampleItem() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testNestedValueCollectionWithChangedElements() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );
		item1.getColors().add( "red" );

		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		Hibernate.initialize( item1_1.getColors() );
		tx.commit();
		s.close();

		// add an element to collection in nested entity
		item1_1.getColors().add( "blue" );
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// the collection from the top level item will win.
		assertEquals( 1, item1Merged.getColors().size() );
		assertEquals( "red", item1Merged.getColors().iterator().next() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 1, item1.getColors().size() );
		assertEquals( "red", item1.getColors().iterator().next() );
		Hibernate.initialize( item1.getCategory() );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		item1_1 = (Item) s.get( Item.class, item1.getId() );
		Hibernate.initialize( item1_1.getColors() );
		tx.commit();
		s.close();

		// remove the existing elements from collection in nested entity
		item1_1.getColors().clear();
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		item1Merged = (Item) s.merge( item1 );
		// the collection from the top level item will win.
		assertEquals( 1, item1Merged.getColors().size() );
		assertEquals( "red", item1Merged.getColors().iterator().next() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 1, item1.getColors().size() );
		assertEquals( "red", item1.getColors().iterator().next() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testTopValueCollectionWithChangedElements() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		Category category = new Category();
		category.setName( "category" );
		item1.getColors().add( "red" );

		item1.setCategory( category );
		category.setExampleItem( item1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		// add an element to collection in nested entity
		item1.getColors().add( "blue" );
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// the collection from the top level item will win.
		assertEquals( 2, item1Merged.getColors().size() );
		assertTrue( item1Merged.getColors().contains( "red" ) );
		assertTrue( item1Merged.getColors().contains( "blue" ) );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 2, item1.getColors().size() );
		assertTrue( item1.getColors().contains( "red" ) );
		assertTrue( item1.getColors().contains( "blue" ) );
		Hibernate.initialize( item1.getCategory() );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		// remove the existing elements from collection in nested entity
		item1.getColors().clear();
		item1.getCategory().setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		item1Merged = (Item) s.merge( item1 );
		// the collection from the top level item will win.
		assertTrue( item1Merged.getColors().isEmpty() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertTrue( item1.getColors().isEmpty() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testCascadeFromTransientToNonDirtyRepresentations() {

		Item item1 = new Item();
		item1.setName( "item1" );

		Session s = openSession();
		Transaction tx = session.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// Get another representation of the same Item from a different session.

		s = openSession();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		s.close();

		// item1_1 and item1_2 are unmodified representations of the same persistent entity.
		assertFalse( item1 == item1_1 );
		assertTrue( item1.equals( item1_1 ) );

		// Create a transient entity that references both representations.
		Hoarder hoarder = new Hoarder();
		hoarder.setName( "joe" );
		hoarder.getItems().add( item1 );
		hoarder.setFavoriteItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		hoarder = (Hoarder) s.merge( hoarder );
		assertEquals( 1, hoarder.getItems().size() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() );
		assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() );
		assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		hoarder = (Hoarder) s.merge( hoarder );
		assertEquals( 1, hoarder.getItems().size() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() );
		assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() );
		assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testCascadeFromDetachedToNonDirtyRepresentations() {
		Item item1 = new Item();
		item1.setName( "item1" );

		Hoarder hoarder = new Hoarder();
		hoarder.setName( "joe" );

		Session s = openSession();
		Transaction tx = session.beginTransaction();
		s.persist( item1 );
		s.persist( hoarder );
		tx.commit();
		s.close();

		// Get another representation of the same Item from a different session.

		s = openSession();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		s.close();

		// item1_1 and item1_2 are unmodified representations of the same persistent entity.
		assertFalse( item1 == item1_1 );
		assertTrue( item1.equals( item1_1 ) );

		// Update hoarder (detached) to references both representations.
		hoarder.getItems().add( item1 );
		hoarder.setFavoriteItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		hoarder = (Hoarder) s.merge( hoarder );
		assertEquals( 1, hoarder.getItems().size() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() );
		assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() );
		assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		hoarder = (Hoarder) s.merge( hoarder );
		assertEquals( 1, hoarder.getItems().size() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() );
		assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() );
		assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testCascadeFromDetachedToGT2DirtyRepresentations() {
		Item item1 = new Item();
		item1.setName( "item1" );
		Category category1 = new Category();
		category1.setName( "category1" );
		item1.setCategory( category1 );

		Hoarder hoarder = new Hoarder();
		hoarder.setName( "joe" );

		Session s = openSession();
		Transaction tx = session.beginTransaction();
		s.persist( item1 );
		s.persist( hoarder );
		tx.commit();
		s.close();

		// Get another representation of the same Item from a different session.

		s = openSession();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		s.close();

		// item1 and item1_1 are unmodified representations of the same persistent entity.
		assertFalse( item1 == item1_1 );
		assertTrue( item1.equals( item1_1 ) );

		// Get another representation of the same Item from a different session.

		s = openSession();
		Item item1_2 = (Item) s.get( Item.class, item1.getId() );
		s.close();

		// item1_1 and item1_2 are unmodified representations of the same persistent entity.
		assertFalse( item1 == item1_2 );
		assertTrue( item1.equals( item1_2) );

		item1_1.setName( "item1_1" );
		item1_2.setName( "item1_2" );

		// Update hoarder (detached) to references both representations.
		item1.getCategory().setExampleItem( item1_2 );
		hoarder.getItems().add( item1 );
		hoarder.setFavoriteItem( item1_1 );
		hoarder.getFavoriteItem().getCategory();

		s = openSession();
		tx = s.beginTransaction();
		hoarder = (Hoarder) s.merge( hoarder );
		assertEquals( 1, hoarder.getItems().size() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getFavoriteItem().getCategory().getExampleItem() );
		assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() );
		assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() );
		assertEquals( item1.getName(), hoarder.getFavoriteItem().getName() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		hoarder = (Hoarder) s.merge( hoarder );
		assertEquals( 1, hoarder.getItems().size() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() );
		assertSame( hoarder.getFavoriteItem(), hoarder.getFavoriteItem().getCategory().getExampleItem() );
		assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() );
		assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	public void testTopLevelEntityNewerThanNested() {
		Item item = new Item();
		item.setName( "item" );

		Category category = new Category();
		category.setName( "category" );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item );
		s.persist( category );
		tx.commit();
		s.close();

		// Get the Category from a different session.
		s = openSession();
		tx = s.beginTransaction();
		Category category1_2 = (Category) s.get( Category.class, category.getId() );
		tx.commit();
		s.close();

		// Get and update the same Category.
		s = openSession();
		tx = s.beginTransaction();
		Category category1_1 = (Category) s.get( Category.class, category.getId() );
		category1_1.setName( "new name" );
		tx.commit();
		s.close();

		assertTrue( category1_2.getVersion() < category1_1.getVersion() );

		category1_1.setExampleItem( item );
		item.setCategory( category1_2 );

		s = openSession();
		tx = s.beginTransaction();
		try {
			// representation merged at top level is newer than nested representation.
			category1_1 = (Category) s.merge( category1_1 );
			fail( "should have failed because one representation is an older version." );
		}
		catch (PersistenceException e){
			// expected
			assertTyping( StaleObjectStateException.class, e.getCause());
		}
		finally {
			tx.rollback();
			s.close();
		}

		cleanup();
	}

	@Test
	public void testNestedEntityNewerThanTopLevel() {
		Item item = new Item();
		item.setName( "item" );

		Category category = new Category();
		category.setName( "category" );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item );
		s.persist( category );
		tx.commit();
		s.close();

		// Get category1_1 from a different session.
		s = openSession();
		Category category1_1 = (Category) s.get( Category.class, category.getId() );
		s.close();

		// Get and update category1_2 to increment its version.
		s = openSession();
		tx = s.beginTransaction();
		Category category1_2 = (Category) s.get( Category.class, category.getId() );
		category1_2.setName( "new name" );
		tx.commit();
		s.close();

		assertTrue( category1_2.getVersion() > category1_1.getVersion() );

		category1_1.setExampleItem( item );
		item.setCategory( category1_2 );

		s = openSession();
		tx = s.beginTransaction();
		try {
			// nested representation is newer than top lever representation.
			category1_1 = (Category) s.merge( category1_1 );
			fail( "should have failed because one representation is an older version." );
		}
		catch (PersistenceException e){
			// expected
			assertTyping( StaleObjectStateException.class, e.getCause());
		}
		finally {
			tx.rollback();
			s.close();
		}

		cleanup();
	}

	@Test
	@FailureExpected( jiraKey = "HHH-9240" )
	public void testTopLevelUnidirOneToManyBackrefWithNewElement() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		SubItem subItem1 = new SubItem();
		subItem1.setName( "subItem1 name" );
		item1.getSubItemsBackref().add( subItem1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		assertFalse( Hibernate.isInitialized( item1_1.getSubItemsBackref() ) );

		Category category = new Category();
		category.setName( "category" );

		SubItem subItem2 = new SubItem();
		subItem2.setName( "subItem2 name" );
		item1.getSubItemsBackref().add( subItem2 );

		item1.setCategory( category );
		category.setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		// The following will fail due to PropertyValueException because item1 will
		// be removed from the inverted merge map when the operation cascades to item1_1.
		Item item1Merged = (Item) s.merge( item1 );
		// top-level collection should win
		assertEquals( 2, item1.getSubItemsBackref().size() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 2, item1.getSubItemsBackref().size() );
		tx.commit();
		s.close();


		cleanup();
	}

	@Test
	@FailureExpected( jiraKey = "HHH-9239" )
	public void testNestedUnidirOneToManyBackrefWithNewElement() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		SubItem subItem1 = new SubItem();
		subItem1.setName( "subItem1 name" );
		item1.getSubItemsBackref().add( subItem1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		Hibernate.initialize( item1_1.getSubItemsBackref() );
		tx.commit();
		s.close();

		Category category = new Category();
		category.setName( "category" );
		item1.setCategory( category );

		SubItem subItem2 = new SubItem();
		subItem2.setName( "subItem2 name" );
		item1_1.getSubItemsBackref().add( subItem2 );

		category.setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// The resulting collection should contain the added element
		assertEquals( 2, item1Merged.getSubItemsBackref().size() );
		assertEquals( "subItem1 name", item1Merged.getSubItemsBackref().get( 0 ).getName() );
		assertEquals( "subItem2 name", item1Merged.getSubItemsBackref().get( 1 ).getName() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 2, item1.getSubItemsBackref().size() );
		assertEquals( "subItem1 name", item1.getSubItemsBackref().get( 0 ).getName() );
		assertEquals( "subItem2 name", item1Merged.getSubItemsBackref().get( 1 ).getName() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	//@FailureExpected( jiraKey = "HHH-9106" )
	public void testTopLevelUnidirOneToManyBackrefWithRemovedElement() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		SubItem subItem1 = new SubItem();
		subItem1.setName( "subItem1 name" );
		item1.getSubItemsBackref().add( subItem1 );
		SubItem subItem2 = new SubItem();
		subItem2.setName( "subItem2 name" );
		item1.getSubItemsBackref().add( subItem2 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		tx.commit();
		s.close();

		assertFalse( Hibernate.isInitialized( item1_1.getSubItemsBackref() ) );

		Category category = new Category();
		category.setName( "category" );

		item1.setCategory( category );
		category.setExampleItem( item1_1 );

		// remove subItem1 from top-level Item
		item1.getSubItemsBackref().remove( subItem1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// entity should have been removed
		assertEquals( 1, item1Merged.getSubItemsBackref().size() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 1, item1.getSubItemsBackref().size() );
		subItem1 = (SubItem) s.get( SubItem.class, subItem1.getId() );
		// cascade does not include delete-orphan, so subItem1 should still be persistent.
		assertNotNull( subItem1 );
		tx.commit();


		cleanup();
	}

	@Test
	@FailureExpected( jiraKey = "HHH-9239" )
	public void testNestedUnidirOneToManyBackrefWithRemovedElement() {
		Item item1 = new Item();
		item1.setName( "item1 name" );
		SubItem subItem1 = new SubItem();
		subItem1.setName( "subItem1 name" );
		item1.getSubItemsBackref().add( subItem1 );
		SubItem subItem2 = new SubItem();
		subItem2.setName( "subItem2 name" );
		item1.getSubItemsBackref().add( subItem2 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( item1 );
		tx.commit();
		s.close();

		// get another representation of item1
		s = openSession();
		tx = s.beginTransaction();
		Item item1_1 = (Item) s.get( Item.class, item1.getId() );
		Hibernate.initialize( item1_1.getSubItemsBackref() );
		tx.commit();
		s.close();

		// remove subItem1 from the nested Item
		item1_1.getSubItemsBackref().remove( subItem1 );

		Category category = new Category();
		category.setName( "category" );
		item1.setCategory( category );
		category.setExampleItem( item1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Item item1Merged = (Item) s.merge( item1 );
		// entity should have been removed
		assertEquals( 1, item1Merged.getSubItemsBackref().size() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		item1 = (Item) s.get( Item.class, item1.getId() );
		assertEquals( 1, item1.getSubItemsBackref().size() );
		subItem1 = (SubItem) s.get( SubItem.class, subItem1.getId() );
		// cascade does not include delete-orphan, so subItem1 should still be persistent.
		assertNotNull( subItem1 );
		tx.commit();

		cleanup();
	}

	@Test
	//@FailureExpected( jiraKey = "HHH-9106" )
	public void testTopLevelUnidirOneToManyNoBackrefWithNewElement() {
		Category category1 = new Category();
		category1.setName( "category1 name" );
		SubCategory subCategory1 = new SubCategory();
		subCategory1.setName( "subCategory1 name" );
		category1.getSubCategories().add( subCategory1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( category1 );
		tx.commit();
		s.close();

		// get another representation of category1
		s = openSession();
		tx = s.beginTransaction();
		Category category1_1 = (Category) s.get( Category.class, category1.getId() );
		tx.commit();
		s.close();

		assertFalse( Hibernate.isInitialized( category1_1.getSubCategories() ) );

		SubCategory subCategory2 = new SubCategory();
		subCategory2.setName( "subCategory2 name" );
		category1.getSubCategories().add( subCategory2 );

		Item item = new Item();
		item.setName( "item" );
		category1.setExampleItem( item );
		item.setCategory( category1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Category category1Merged = (Category) s.merge( category1 );
		assertEquals( 2, category1Merged.getSubCategories().size() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		category1 = (Category) s.get( Category.class, category1.getId() );
		assertEquals( 2, category1.getSubCategories().size() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	@FailureExpected( jiraKey = "HHH-9239" )
	public void testNestedUnidirOneToManyNoBackrefWithNewElement() {
		Category category1 = new Category();
		category1.setName( "category1 name" );
		SubCategory subCategory1 = new SubCategory();
		subCategory1.setName( "subCategory1 name" );
		category1.getSubCategories().add( subCategory1 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( category1 );
		tx.commit();
		s.close();

		// get another representation of category1
		s = openSession();
		tx = s.beginTransaction();
		Category category1_1 = (Category) s.get( Category.class, category1.getId() );
		Hibernate.initialize( category1_1.getSubCategories() );
		tx.commit();
		s.close();

		SubCategory subCategory2 = new SubCategory();
		subCategory2.setName( "subCategory2 name" );
		category1_1.getSubCategories().add( subCategory2 );

		Item item = new Item();
		item.setName( "item" );
		category1.setExampleItem( item );
		item.setCategory( category1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Category category1Merged = (Category) s.merge( category1 );
		assertEquals( 2, category1Merged.getSubCategories().size() );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		category1 = (Category) s.get( Category.class, category1.getId() );
		assertEquals( 2, category1.getSubCategories().size() );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	//@FailureExpected( jiraKey = "HHH-9106" )
	public void testTopLevelUnidirOneToManyNoBackrefWithRemovedElement() {
		Category category1 = new Category();
		category1.setName( "category1 name" );
		SubCategory subCategory1 = new SubCategory();
		subCategory1.setName( "subCategory1 name" );
		category1.getSubCategories().add( subCategory1 );
		SubCategory subCategory2 = new SubCategory();
		subCategory2.setName( "subCategory2 name" );
		category1.getSubCategories().add( subCategory2 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( category1 );
		tx.commit();
		s.close();

		// get another representation of category1
		s = openSession();
		tx = s.beginTransaction();
		Category category1_1 = (Category) s.get( Category.class, category1.getId() );
		tx.commit();
		s.close();

		assertFalse( Hibernate.isInitialized( category1_1.getSubCategories() ) );

		Item item = new Item();
		item.setName( "item" );
		category1.setExampleItem( item );
		item.setCategory( category1_1 );

		category1.getSubCategories().remove( subCategory1 );

		s = openSession();
		tx = s.beginTransaction();
		Category category1Merged = (Category) s.merge( category1 );
		assertEquals( 1, category1Merged.getSubCategories().size() );
		assertTrue( category1Merged.getSubCategories().contains( subCategory2 ) );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		category1 = (Category) s.get( Category.class, category1.getId() );
		assertEquals( 1, category1.getSubCategories().size() );
		assertTrue( category1.getSubCategories().contains( subCategory2 ) );
		// cascade does not include delete-orphan, so subCategory1 should still be persistent.
		subCategory1 = (SubCategory) s.get( SubCategory.class, subCategory1.getId() );
		assertNotNull( subCategory1 );
		tx.commit();
		s.close();

		cleanup();
	}

	@Test
	@FailureExpected( jiraKey = "HHH-9239" )
	public void testNestedUnidirOneToManyNoBackrefWithRemovedElement() {
		Category category1 = new Category();
		category1.setName( "category1 name" );
		SubCategory subCategory1 = new SubCategory();
		subCategory1.setName( "subCategory1 name" );
		category1.getSubCategories().add( subCategory1 );
		SubCategory subCategory2 = new SubCategory();
		subCategory2.setName( "subCategory2 name" );
		category1.getSubCategories().add( subCategory2 );

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		s.persist( category1 );
		tx.commit();
		s.close();

		// get another representation of category1
		s = openSession();
		tx = s.beginTransaction();
		Category category1_1 = (Category) s.get( Category.class, category1.getId() );
		Hibernate.initialize( category1_1.getSubCategories() );
		tx.commit();
		s.close();

		category1_1.getSubCategories().remove( subCategory2 );

		Item item = new Item();
		item.setName( "item" );
		category1.setExampleItem( item );
		item.setCategory( category1_1 );

		s = openSession();
		tx = s.beginTransaction();
		Category category1Merged = (Category) s.merge( category1 );
		assertEquals( 1, category1Merged.getSubCategories().size() );
		assertTrue( category1Merged.getSubCategories().contains( subCategory2 ) );
		tx.commit();
		s.close();

		s = openSession();
		tx = s.beginTransaction();
		category1 = (Category) s.get( Category.class, category1.getId() );
		assertEquals( 1, category1.getSubCategories().size() );
		assertTrue( category1.getSubCategories().contains( subCategory2 ) );
		// cascade does not include delete-orphan, so subCategory1 should still be persistent.
		subCategory1 = (SubCategory) s.get( SubCategory.class, subCategory1.getId() );
		assertNotNull( subCategory1 );
		tx.commit();
		s.close();

		cleanup();
	}

	@SuppressWarnings( {"unchecked"})
	private void cleanup() {
		Session s = openSession();
		s.beginTransaction();

		s.createQuery( "delete from SubItem" ).executeUpdate();
		for ( Hoarder hoarder : (List<Hoarder>) s.createQuery( "from Hoarder" ).list() ) {
			hoarder.getItems().clear();
			s.delete( hoarder );
		}

		for ( Category category : (List<Category>) s.createQuery( "from Category" ).list() ) {
			Item exampleItem = category.getExampleItem();
			if ( exampleItem != null ) {
				category.setExampleItem( null );
				exampleItem.setCategory( null );
				s.delete( category );
				s.delete (exampleItem );
			}
		}

		for ( Item item : (List<Item>) s.createQuery( "from Item" ).list() ) {
			Category category = item.getCategory();
			item.setCategory( null );
			if ( category != null ) {
				category.setExampleItem( null );
			}
			s.delete( item );
		}

		s.createQuery( "delete from Item" ).executeUpdate();


		s.getTransaction().commit();
		s.close();
	}
}
