//$Id: MergeTest.java 11037 2007-01-09 16:04:16Z steve.ebersole@jboss.com $
package org.hibernate.test.ops;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import junit.framework.Test;

import org.hibernate.Hibernate;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.Session;
import org.hibernate.StaleObjectStateException;
import org.hibernate.Transaction;
import org.hibernate.criterion.Projections;
import org.hibernate.junit.functional.FunctionalTestClassTestSuite;

/**
 * @author Gavin King
 */
public class MergeTest extends AbstractOperationTestCase {

	public MergeTest(String str) {
		super( str );
	}

	public static Test suite() {
		return new FunctionalTestClassTestSuite( MergeTest.class );
	}

	public void testMergeStaleVersionFails() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		VersionedEntity entity = new VersionedEntity( "entity", "entity" );
		s.persist( entity );
		s.getTransaction().commit();
		s.close();

		// make the detached 'entity' reference stale...
		s = openSession();
        s.beginTransaction();
		VersionedEntity entity2 = ( VersionedEntity ) s.get( VersionedEntity.class, entity.getId() );
		entity2.setName( "entity-name" );
		s.getTransaction().commit();
		s.close();

		// now try to reattch it
		s = openSession();
		s.beginTransaction();
		try {
			s.merge( entity );
			s.getTransaction().commit();
			fail( "was expecting staleness error" );
		}
		catch ( StaleObjectStateException expected ) {
			// expected outcome...
		}
		finally {
			s.getTransaction().rollback();
			s.close();
		}
	}

	public void testMergeBidiPrimayKeyOneToOne() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		Person p = new Person( "steve" );
		new PersonalDetails( "I have big feet", p );
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		p.getDetails().setSomePersonalDetail( p.getDetails().getSomePersonalDetail() + " and big hands too" );
		s = openSession();
        s.beginTransaction();
		p = ( Person ) s.merge( p );
		s.getTransaction().commit();
		s.close();

		assertInsertCount( 0 );
		assertUpdateCount( 1 );
		assertDeleteCount( 0 );

		s = openSession();
        s.beginTransaction();
		s.delete( p );
		s.getTransaction().commit();
		s.close();
	}

	public void testMergeBidiForeignKeyOneToOne() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		Person p = new Person( "steve" );
		Address a = new Address( "123 Main", "Austin", "US", p );
		s.persist( a );
		s.persist( p );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		p.getAddress().setStreetAddress( "321 Main" );
		s = openSession();
        s.beginTransaction();
		p = ( Person ) s.merge( p );
		s.getTransaction().commit();
		s.close();

		assertInsertCount( 0 );
		assertUpdateCount( 0 ); // no cascade
		assertDeleteCount( 0 );

		s = openSession();
        s.beginTransaction();
		s.delete( a );
		s.delete( p );
		s.getTransaction().commit();
		s.close();
	}

    public void testNoExtraUpdatesOnMerge() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		Node node = new Node( "test" );
		s.persist( node );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		// node is now detached, but we have made no changes.  so attempt to merge it
		// into this new session; this should cause no updates...
		s = openSession();
		s.beginTransaction();
		node = ( Node ) s.merge( node );
		s.getTransaction().commit();
		s.close();

		assertUpdateCount( 0 );
		assertInsertCount( 0 );

		///////////////////////////////////////////////////////////////////////
		// as a control measure, now update the node while it is detached and
		// make sure we get an update as a result...
		node.setDescription( "new description" );
		s = openSession();
		s.beginTransaction();
		node = ( Node ) s.merge( node );
		s.getTransaction().commit();
		s.close();
		assertUpdateCount( 1 );
		assertInsertCount( 0 );
		///////////////////////////////////////////////////////////////////////

		cleanup();
    }

	public void testNoExtraUpdatesOnMergeWithCollection() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		Node parent = new Node( "parent" );
		Node child = new Node( "child" );
		parent.getChildren().add( child );
		child.setParent( parent );
		s.persist( parent );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		// parent is now detached, but we have made no changes.  so attempt to merge it
		// into this new session; this should cause no updates...
		s = openSession();
		s.beginTransaction();
		parent = ( Node ) s.merge( parent );
		s.getTransaction().commit();
		s.close();

		assertUpdateCount( 0 );
		assertInsertCount( 0 );

		///////////////////////////////////////////////////////////////////////
		// as a control measure, now update the node while it is detached and
		// make sure we get an update as a result...
		( ( Node ) parent.getChildren().iterator().next() ).setDescription( "child's new description" );
		parent.addChild( new Node( "second child" ) );
		s = openSession();
		s.beginTransaction();
		parent = ( Node ) s.merge( parent );
		s.getTransaction().commit();
		s.close();
		assertUpdateCount( 1 );
		assertInsertCount( 1 );
		///////////////////////////////////////////////////////////////////////

		cleanup();
	}

    public void testNoExtraUpdatesOnMergeVersioned() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		VersionedEntity entity = new VersionedEntity( "entity", "entity" );
		s.persist( entity );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		// entity is now detached, but we have made no changes.  so attempt to merge it
		// into this new session; this should cause no updates...
		s = openSession();
		s.beginTransaction();
		VersionedEntity mergedEntity = ( VersionedEntity ) s.merge( entity );
		s.getTransaction().commit();
		s.close();

		assertUpdateCount( 0 );
		assertInsertCount( 0 );
        assertEquals( "unexpected version increment", entity.getVersion(), mergedEntity.getVersion() );


		///////////////////////////////////////////////////////////////////////
		// as a control measure, now update the node while it is detached and
		// make sure we get an update as a result...
		entity.setName( "new name" );
		s = openSession();
		s.beginTransaction();
		entity = ( VersionedEntity ) s.merge( entity );
		s.getTransaction().commit();
		s.close();
		assertUpdateCount( 1 );
		assertInsertCount( 0 );
		///////////////////////////////////////////////////////////////////////

		cleanup();
    }

    public void testNoExtraUpdatesOnMergeVersionedWithCollection() throws Exception {
		Session s = openSession();
        s.beginTransaction();
		VersionedEntity parent = new VersionedEntity( "parent", "parent" );
		VersionedEntity child = new VersionedEntity( "child", "child" );
		parent.getChildren().add( child );
		child.setParent( parent );
		s.persist( parent );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		// parent is now detached, but we have made no changes.  so attempt to merge it
		// into this new session; this should cause no updates...
		s = openSession();
		s.beginTransaction();
		VersionedEntity mergedParent = ( VersionedEntity ) s.merge( parent );
		s.getTransaction().commit();
		s.close();

		assertUpdateCount( 0 );
		assertInsertCount( 0 );
		assertEquals( "unexpected parent version increment", parent.getVersion(), mergedParent.getVersion() );
		VersionedEntity mergedChild = ( VersionedEntity ) mergedParent.getChildren().iterator().next();
		assertEquals( "unexpected child version increment", child.getVersion(), mergedChild.getVersion() );

		///////////////////////////////////////////////////////////////////////
		// as a control measure, now update the node while it is detached and
		// make sure we get an update as a result...
		mergedParent.setName( "new name" );
		mergedParent.getChildren().add( new VersionedEntity( "child2", "new child" ) );
		s = openSession();
		s.beginTransaction();
		parent = ( VersionedEntity ) s.merge( mergedParent );
		s.getTransaction().commit();
		s.close();
		assertUpdateCount( 1 );
		assertInsertCount( 1 );
		///////////////////////////////////////////////////////////////////////

		cleanup();
    }

	public void testNoExtraUpdatesOnPersistentMergeVersionedWithCollection() throws Exception {
		Session s = openSession();
		s.beginTransaction();
		VersionedEntity parent = new VersionedEntity( "parent", "parent" );
		VersionedEntity child = new VersionedEntity( "child", "child" );
		parent.getChildren().add( child );
		child.setParent( parent );
		s.persist( parent );
		s.getTransaction().commit();
		s.close();

		clearCounts();

		// parent is now detached, but we have made no changes. so attempt to merge it
		// into this new session; this should cause no updates...
		s = openSession();
		s.beginTransaction();
		// load parent so that merge will follow entityIsPersistent path
		VersionedEntity persistentParent = ( VersionedEntity ) s.get( VersionedEntity.class, parent.getId() );
		// load children
		VersionedEntity persistentChild = ( VersionedEntity ) persistentParent.getChildren().iterator().next();
		VersionedEntity mergedParent = ( VersionedEntity ) s.merge( persistentParent ); // <-- This merge leads to failure
		s.getTransaction().commit();
		s.close();

		assertUpdateCount( 0 );
		assertInsertCount( 0 );
		assertEquals( "unexpected parent version increment", parent.getVersion(), mergedParent.getVersion() );
		VersionedEntity mergedChild = ( VersionedEntity ) mergedParent.getChildren().iterator().next();
		assertEquals( "unexpected child version increment", child.getVersion(), mergedChild.getVersion() );

		///////////////////////////////////////////////////////////////////////
		// as a control measure, now update the node once it is loaded and
		// make sure we get an update as a result...
		s = openSession();
		s.beginTransaction();
		persistentParent = ( VersionedEntity ) s.get( VersionedEntity.class, parent.getId() );
		persistentParent.setName( "new name" );
		persistentParent.getChildren().add( new VersionedEntity( "child2", "new child" ) );
		persistentParent = ( VersionedEntity ) s.merge( persistentParent );
		s.getTransaction().commit();
		s.close();
		assertUpdateCount( 1 );
		assertInsertCount( 1 );
		///////////////////////////////////////////////////////////////////////

		// cleanup();
	}

	public void testPersistThenMergeInSameTxnWithVersion() {
		Session s = openSession();
		Transaction tx = s.beginTransaction();
		VersionedEntity entity = new VersionedEntity( "test", "test" );
		s.persist( entity );
		s.merge( new VersionedEntity( "test", "test-2" ) );

		try {
			// control operation...
			s.saveOrUpdate( new VersionedEntity( "test", "test-3" ) );
			fail( "saveOrUpdate() should fail here" );
		}
		catch( NonUniqueObjectException expected ) {
			// expected behavior
		}

		tx.commit();
		s.close();

		cleanup();
	}

	public void testPersistThenMergeInSameTxnWithTimestamp() {
		Session s = openSession();
		Transaction tx = s.beginTransaction();
		TimestampedEntity entity = new TimestampedEntity( "test", "test" );
		s.persist( entity );
		s.merge( new TimestampedEntity( "test", "test-2" ) );

		try {
			// control operation...
			s.saveOrUpdate( new TimestampedEntity( "test", "test-3" ) );
			fail( "saveOrUpdate() should fail here" );
		}
		catch( NonUniqueObjectException expected ) {
			// expected behavior
		}

		tx.commit();
		s.close();

		cleanup();
	}

	public void testMergeDeepTree() {

		clearCounts();

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		Node root = new Node("root");
		Node child = new Node("child");
		Node grandchild = new Node("grandchild");
		root.addChild(child);
		child.addChild(grandchild);
		s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(3);
		assertUpdateCount(0);
		clearCounts();

		grandchild.setDescription("the grand child");
		Node grandchild2 = new Node("grandchild2");
		child.addChild( grandchild2 );

		s = openSession();
		tx = s.beginTransaction();
		s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(1);
		assertUpdateCount(1);
		clearCounts();

		Node child2 = new Node("child2");
		Node grandchild3 = new Node("grandchild3");
		child2.addChild( grandchild3 );
		root.addChild(child2);

		s = openSession();
		tx = s.beginTransaction();
		s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(2);
		assertUpdateCount(0);
		clearCounts();

		s = openSession();
		tx = s.beginTransaction();
		s.delete(grandchild);
		s.delete(grandchild2);
		s.delete(grandchild3);
		s.delete(child);
		s.delete(child2);
		s.delete(root);
		tx.commit();
		s.close();

	}

	public void testMergeDeepTreeWithGeneratedId() {

		clearCounts();

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		NumberedNode root = new NumberedNode("root");
		NumberedNode child = new NumberedNode("child");
		NumberedNode grandchild = new NumberedNode("grandchild");
		root.addChild(child);
		child.addChild(grandchild);
		root = (NumberedNode) s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(3);
		assertUpdateCount(0);
		clearCounts();

		child = (NumberedNode) root.getChildren().iterator().next();
		grandchild = (NumberedNode) child.getChildren().iterator().next();
		grandchild.setDescription("the grand child");
		NumberedNode grandchild2 = new NumberedNode("grandchild2");
		child.addChild( grandchild2 );

		s = openSession();
		tx = s.beginTransaction();
		root = (NumberedNode) s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(1);
		assertUpdateCount(1);
		clearCounts();

		getSessions().evict(NumberedNode.class);

		NumberedNode child2 = new NumberedNode("child2");
		NumberedNode grandchild3 = new NumberedNode("grandchild3");
		child2.addChild( grandchild3 );
		root.addChild(child2);

		s = openSession();
		tx = s.beginTransaction();
		root = (NumberedNode) s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(2);
		assertUpdateCount(0);
		clearCounts();

		s = openSession();
		tx = s.beginTransaction();
		s.createQuery("delete from NumberedNode where name like 'grand%'").executeUpdate();
		s.createQuery("delete from NumberedNode where name like 'child%'").executeUpdate();
		s.createQuery("delete from NumberedNode").executeUpdate();
		tx.commit();
		s.close();

	}

	public void testMergeTree() {

		clearCounts();

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		Node root = new Node("root");
		Node child = new Node("child");
		root.addChild(child);
		s.persist(root);
		tx.commit();
		s.close();

		assertInsertCount(2);
		clearCounts();

		root.setDescription("The root node");
		child.setDescription("The child node");

		Node secondChild = new Node("second child");

		root.addChild(secondChild);

		s = openSession();
		tx = s.beginTransaction();
		s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(1);
		assertUpdateCount(2);

		cleanup();
	}

	public void testMergeTreeWithGeneratedId() {

		clearCounts();

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		NumberedNode root = new NumberedNode("root");
		NumberedNode child = new NumberedNode("child");
		root.addChild(child);
		s.persist(root);
		tx.commit();
		s.close();

		assertInsertCount(2);
		clearCounts();

		root.setDescription("The root node");
		child.setDescription("The child node");

		NumberedNode secondChild = new NumberedNode("second child");

		root.addChild(secondChild);

		s = openSession();
		tx = s.beginTransaction();
		s.merge(root);
		tx.commit();
		s.close();

		assertInsertCount(1);
		assertUpdateCount(2);

		cleanup();
	}

	public void testMergeManaged() {

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		NumberedNode root = new NumberedNode("root");
		s.persist(root);
		tx.commit();

		clearCounts();

		tx = s.beginTransaction();
		NumberedNode child = new NumberedNode("child");
		root.addChild(child);
		assertSame( root, s.merge(root) );
		Object mergedChild = root.getChildren().iterator().next();
		assertNotSame( mergedChild, child );
		assertTrue( s.contains(mergedChild) );
		assertFalse( s.contains(child) );
		assertEquals( root.getChildren().size(), 1 );
		assertTrue( root.getChildren().contains(mergedChild) );
		//assertNotSame( mergedChild, s.merge(child) ); //yucky :(
		tx.commit();

		assertInsertCount(1);
		assertUpdateCount(0);

		assertEquals( root.getChildren().size(), 1 );
		assertTrue( root.getChildren().contains(mergedChild) );

		tx = s.beginTransaction();
		assertEquals(
			s.createCriteria(NumberedNode.class)
				.setProjection( Projections.rowCount() )
				.uniqueResult(),
			new Integer(2)
		);

		s.close();

		cleanup();
	}

	public void testMergeManagedUninitializedCollection() {

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		NumberedNode root = new NumberedNode( "root" );
		root.addChild( new NumberedNode( "child" ) );
		s.persist(root);
		tx.commit();
		s.close();

		clearCounts();

		NumberedNode newRoot = new NumberedNode( "root" );
		newRoot.setId( root.getId() );

		s = openSession();
		tx = s.beginTransaction();
		root = ( NumberedNode ) s.get( NumberedNode.class, root.getId() );
		Set managedChildren = root.getChildren();
		assertFalse( Hibernate.isInitialized( managedChildren ) );
		newRoot.setChildren( managedChildren );
		assertSame( root, s.merge( newRoot ) );
		assertSame( managedChildren, root.getChildren() );
		assertFalse( Hibernate.isInitialized( managedChildren ) );
		tx.commit();

		assertInsertCount(0);
		assertUpdateCount(0);
		assertDeleteCount(0);

		tx = s.beginTransaction();
		assertEquals(
			s.createCriteria(NumberedNode.class)
				.setProjection( Projections.rowCount() )
				.uniqueResult(),
			new Integer(2)
		);
		tx.commit();

		s.close();

		cleanup();
	}

	public void testMergeManagedInitializedCollection() {

		Session s = openSession();
		Transaction tx = s.beginTransaction();
		NumberedNode root = new NumberedNode( "root" );
		root.addChild( new NumberedNode( "child" ) );
		s.persist(root);
		tx.commit();
		s.close();

		clearCounts();

		NumberedNode newRoot = new NumberedNode( "root" );
		newRoot.setId( root.getId() );

		s = openSession();
		tx = s.beginTransaction();
		root = ( NumberedNode ) s.get( NumberedNode.class, root.getId() );
		Set managedChildren = root.getChildren();
		Hibernate.initialize( managedChildren );
		assertTrue( Hibernate.isInitialized( managedChildren ) );
		newRoot.setChildren( managedChildren );
		assertSame( root, s.merge( newRoot ) );
		assertSame( managedChildren, root.getChildren() );
		assertTrue( Hibernate.isInitialized( managedChildren ) );
		tx.commit();

		assertInsertCount(0);
		assertUpdateCount(0);
		assertDeleteCount(0);

		tx = s.beginTransaction();
		assertEquals(
			s.createCriteria(NumberedNode.class)
				.setProjection( Projections.rowCount() )
				.uniqueResult(),
			new Integer(2)
		);
		tx.commit();

		s.close();

		cleanup();
	}

	public void testRecursiveMergeTransient() {
		Session s = openSession();
		Transaction tx = s.beginTransaction();
		Employer jboss = new Employer();
		Employee gavin = new Employee();
		jboss.setEmployees( new ArrayList() );
		jboss.getEmployees().add(gavin);
		s.merge(jboss);
		s.flush();
		jboss = (Employer) s.createQuery("from Employer e join fetch e.employees").uniqueResult();
		assertTrue( Hibernate.isInitialized( jboss.getEmployees() )  );
		assertEquals( 1, jboss.getEmployees().size() );
		s.clear();
		s.merge( jboss.getEmployees().iterator().next() );
		tx.commit();
		s.close();

		cleanup();
	}

	public void testDeleteAndMerge() throws Exception {
		Session s = openSession();
		s.getTransaction().begin();
		Employer jboss = new Employer();
		s.persist( jboss );
		s.getTransaction().commit();
		s.clear();

		s.getTransaction().begin();
		Employer otherJboss;
		otherJboss = (Employer) s.get( Employer.class, jboss.getId() );
		s.delete( otherJboss );
		s.getTransaction().commit();
		s.clear();
		jboss.setVers( new Integer(1) );
		s.getTransaction().begin();
		s.merge( jboss );
		s.getTransaction().commit();
		s.close();

		cleanup();
	}

	public void testMergeManyToManyWithCollectionDeference() throws Exception {
		// setup base data...
		Session s = openSession();
		Transaction tx = s.beginTransaction();
		Competition competition = new Competition();
		competition.getCompetitors().add( new Competitor( "Name" ) );
		competition.getCompetitors().add( new Competitor() );
		competition.getCompetitors().add( new Competitor() );
		s.persist( competition );
		tx.commit();
		s.close();

		// the competition graph is now detached:
		//   1) create a new List reference to represent the competitors
		s = openSession();
		tx = s.beginTransaction();
		List newComp = new ArrayList();
		Competitor originalCompetitor = ( Competitor ) competition.getCompetitors().get( 0 );
		originalCompetitor.setName( "Name2" );
		newComp.add( originalCompetitor );
		newComp.add( new Competitor() );
		//   2) set that new List reference unto the Competition reference
		competition.setCompetitors( newComp );
		//   3) attempt the merge
		Competition competition2 = ( Competition ) s.merge( competition );
		tx.commit();
		s.close();

		assertFalse( competition == competition2 );
		assertFalse( competition.getCompetitors() == competition2.getCompetitors() );
		assertEquals( 2, competition2.getCompetitors().size() );

		s = openSession();
		tx = s.beginTransaction();
		competition = ( Competition ) s.get( Competition.class, competition.getId() );
		assertEquals( 2, competition.getCompetitors().size() );
		s.delete( competition );
		tx.commit();
		s.close();

		cleanup();
	}

	private void cleanup() {
		Session s = openSession();
		s.beginTransaction();
		s.createQuery( "delete from NumberedNode where parent is not null" ).executeUpdate();
		s.createQuery( "delete from NumberedNode" ).executeUpdate();

		s.createQuery( "delete from Node where parent is not null" ).executeUpdate();
		s.createQuery( "delete from Node" ).executeUpdate();

		s.createQuery( "delete from VersionedEntity where parent is not null" ).executeUpdate();
		s.createQuery( "delete from VersionedEntity" ).executeUpdate();
		s.createQuery( "delete from TimestampedEntity" ).executeUpdate();

		s.createQuery( "delete from Competitor" ).executeUpdate();
		s.createQuery( "delete from Competition" ).executeUpdate();

		Iterator itr = s.createQuery( "from Employer" ).list().iterator();
		while ( itr.hasNext() ) {
			final Employer employer = ( Employer ) itr.next();
			s.delete( employer );
		}

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

