/*
 * 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.event.internal;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Id;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.hibernate.event.spi.EntityCopyObserver;
import org.hibernate.event.spi.EventSource;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;

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

/**
 * 2011/10/20 Unit test for code added in MergeContext for performance improvement.
 *
 * @author Wim Ockerman @ CISCO
 */
public class MergeContextTest extends BaseCoreFunctionalTestCase {
	private EventSource session = null;

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

	@Before
	public void setUp() {
		session = (EventSource) openSession();
	}

	@After
	public void tearDown() {
		session.close();
		session = null;
	}

	@Test
    public void testMergeToManagedEntityFillFollowedByInvertMapping() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

        Object mergeEntity = new Simple( 1 );
        Object managedEntity = new Simple( 2 );
        
        cache.put(mergeEntity, managedEntity);

		checkCacheConsistency( cache, 1 );

		assertTrue( cache.containsKey( mergeEntity ) );
        assertFalse( cache.containsKey( managedEntity ) );
		assertTrue( cache.containsValue( managedEntity ) );

		assertTrue( cache.invertMap().containsKey( managedEntity ) );
        assertFalse( cache.invertMap().containsKey( mergeEntity ) );
		assertTrue( cache.invertMap().containsValue( mergeEntity ) );

		cache.clear();

		checkCacheConsistency( cache, 0 );

		assertFalse(cache.containsKey(mergeEntity));
        assertFalse(cache.invertMap().containsKey(managedEntity));
	}

	@Test
    public void testMergeToManagedEntityFillFollowedByInvert() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

        Object mergeEntity = new Simple( 1 );
        Object managedEntity = new Simple( 2 );
        
        cache.put(mergeEntity, managedEntity);

		checkCacheConsistency( cache, 1 );

		assertTrue(cache.containsKey(mergeEntity));
        assertFalse( cache.containsKey( managedEntity ) );
        
        assertTrue( cache.invertMap().containsKey( managedEntity ) );
        assertFalse( cache.invertMap().containsKey( mergeEntity ) );
    }

	@Test
    public void testMergeToManagedEntityFillFollowedByInvertUsingPutAll() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

        Map<Object,Object> input = new HashMap<Object,Object>();
        Object mergeEntity1 = new Simple( 1 );
		//
        Object managedEntity1 = 1;
        input.put(mergeEntity1, managedEntity1);
        Object mergeEntity2 = new Simple( 3 );
        Object managedEntity2 = 2;
        input.put(mergeEntity2, managedEntity2);
        cache.putAll(input);

		checkCacheConsistency( cache, 2 );

		assertTrue(cache.containsKey(mergeEntity1));
        assertFalse(cache.containsKey(managedEntity1));
        assertTrue(cache.containsKey(mergeEntity2));
        assertFalse(cache.containsKey(managedEntity2));

        assertTrue(cache.invertMap().containsKey(managedEntity1));
        assertFalse(cache.invertMap().containsKey(mergeEntity1));

        assertTrue(cache.invertMap().containsKey(managedEntity2));
        assertFalse(cache.invertMap().containsKey(mergeEntity2));
    }

	@Test
    public void testMergeToManagedEntityFillFollowedByInvertUsingPutWithSetOperatedOnArg() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

        Object mergeEntity = new Simple( 1 );
        Object managedEntity = new Simple( 2 );
        
        cache.put(mergeEntity, managedEntity, true);

		checkCacheConsistency( cache, 1 );

		assertTrue(cache.containsKey(mergeEntity));
        assertFalse( cache.containsKey( managedEntity ) );

        assertTrue( cache.invertMap().containsKey( managedEntity ) );
        assertFalse( cache.invertMap().containsKey( mergeEntity ) );
        
        cache.clear();

		checkCacheConsistency( cache, 0 );

		cache.put(mergeEntity, managedEntity, false);
		assertFalse( cache.isOperatedOn( mergeEntity ) );

		checkCacheConsistency( cache, 1 );

		assertTrue(cache.containsKey(mergeEntity));
        assertFalse(cache.containsKey(managedEntity));
    }

	@Test
	public void testMergeToManagedEntityFillFollowedByIterateEntrySet() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Object mergeEntity = new Simple( 1 );
		Object managedEntity = new Simple( 2 );

		cache.put( mergeEntity, managedEntity, true );

		checkCacheConsistency( cache, 1 );

		Iterator it = cache.entrySet().iterator();
		assertTrue( it.hasNext() );
		Map.Entry entry = ( Map.Entry ) it.next();
		assertSame( mergeEntity, entry.getKey() );
		assertSame( managedEntity, entry.getValue() );
		assertFalse( it.hasNext() );

	}

	@Test
	public void testMergeToManagedEntityFillFollowedByModifyEntrySet() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Object mergeEntity = new Simple( 1 );
		Object managedEntity = new Simple( 2 );

		cache.put( mergeEntity, managedEntity, true );

		Iterator it = cache.entrySet().iterator();
		try {
			it.remove();
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

		Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
		try {
			cache.entrySet().remove( entry );
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

		Map.Entry anotherEntry = new Map.Entry() {
			private Object key = new Simple( 3 );
			private Object value = 4;
			@Override
			public Object getKey() {
				return key;
			}

			@Override
			public Object getValue() {
				return value;
			}

			@Override
			public Object setValue(Object value) {
				Object oldValue = this.value;
				this.value = value;
				return oldValue;
			}
		};
		try {
			cache.entrySet().add( anotherEntry );
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

	}

	@Test
	public void testMergeToManagedEntityFillFollowedByModifyKeys() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Object mergeEntity = new Simple( 1 );
		Object managedEntity = new Simple( 2 );

		cache.put( mergeEntity, managedEntity, true );

		Iterator it = cache.keySet().iterator();
		try {
			it.remove();
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

		try {
			cache.keySet().remove( mergeEntity );
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

		Object newmanagedEntity = new Simple( 3 );
		try {
			cache.keySet().add( newmanagedEntity );
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}
	}

	@Test
	public void testMergeToManagedEntityFillFollowedByModifyValues() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Object mergeEntity = new Simple( 1 );
		Object managedEntity = new Simple( 2 );

		cache.put( mergeEntity, managedEntity, true );

		Iterator it = cache.values().iterator();
		try {
			it.remove();
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

		try {
			cache.values().remove( managedEntity );
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}

		Object newmanagedEntity = new Simple( 3 );
		try {
			cache.values().add( newmanagedEntity );
			fail( "should have thrown UnsupportedOperationException" );
		}
		catch ( UnsupportedOperationException ex ) {
			// expected
		}
	}

	@Test
	public void testMergeToManagedEntityFillFollowedByModifyKeyOfEntrySetElement() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Simple mergeEntity = new Simple( 1 );
		Simple managedEntity = new Simple( 0 );
		cache.put(mergeEntity, managedEntity, true);

		Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
		( ( Simple ) entry.getKey() ).setValue( 2 );
		assertEquals( 2, mergeEntity.getValue() );

		checkCacheConsistency( cache, 1 );

		entry = (Map.Entry) cache.entrySet().iterator().next();
		assertSame( mergeEntity, entry.getKey() );
		assertSame( managedEntity, entry.getValue() );
	}

	@Test
	public void testMergeToManagedEntityFillFollowedByModifyValueOfEntrySetElement() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Simple mergeEntity = new Simple( 1 );
		Simple managedEntity = new Simple( 0 );
		cache.put(mergeEntity, managedEntity, true);

		Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
		( ( Simple ) entry.getValue() ).setValue( 2 );
		assertEquals( 2, managedEntity.getValue() );

		checkCacheConsistency( cache, 1 );

		entry = (Map.Entry) cache.entrySet().iterator().next();
		assertSame( mergeEntity, entry.getKey() );
		assertSame( managedEntity, entry.getValue() );
	}

	@Test
	public void testReplaceManagedEntity() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		Simple mergeEntity = new Simple( 1 );
		Simple managedEntity = new Simple( 0 );
		cache.put(mergeEntity, managedEntity);

		Simple managedEntityNew = new Simple( 0 );
		try {
			cache.put( mergeEntity, managedEntityNew );
		}
		catch( IllegalArgumentException ex) {
			// expected; cannot replace the managed entity result for a particular merge entity.
		}
	}

	@Test
	public void testManagedEntityAssociatedWithNewAndExistingMergeEntities() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		session.getTransaction().begin();
		Simple mergeEntity = new Simple( 1 );
		Simple managedEntity = new Simple( 0 );
		cache.put(mergeEntity, managedEntity);
		cache.put( new Simple( 1 ), managedEntity );
	}

	@Test
	public void testManagedAssociatedWith2ExistingMergeEntities() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );

		session.getTransaction().begin();
		Simple mergeEntity1 = new Simple( 1 );
		session.persist( mergeEntity1 );
		Simple managedEntity1 = new Simple( 1 );
		cache.put( mergeEntity1, managedEntity1 );
		Simple managedEntity2 = new Simple( 2 );

		try {
			cache.put( mergeEntity1, managedEntity2 );
			fail( "should have thrown IllegalArgumentException");
		}
		catch( IllegalArgumentException ex ) {
			// expected; cannot change managed entity associated with a merge entity
		}
		finally {
			session.getTransaction().rollback();
		}
	}

	@Test
	public void testRemoveNonExistingEntity() {
		MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() );
		try {
			cache.remove( new Simple( 1 ) );
		}
		catch (UnsupportedOperationException ex) {
			// expected; remove is not supported.
		}
	}

	private void checkCacheConsistency(MergeContext cache, int expectedSize) {
		Set entrySet = cache.entrySet();
		Set cacheKeys = cache.keySet();
		Collection cacheValues = cache.values();
		Map invertedMap = cache.invertMap();

		assertEquals( expectedSize, entrySet.size() );
		assertEquals( expectedSize, cache.size() );
		assertEquals( expectedSize, cacheKeys.size() );
		assertEquals( expectedSize, cacheValues.size() );
		assertEquals( expectedSize, invertedMap.size() );

		for ( Object entry : cache.entrySet() ) {
			Map.Entry mapEntry = ( Map.Entry ) entry;
			assertSame( cache.get( mapEntry.getKey() ), mapEntry.getValue() );
			assertTrue( cacheKeys.contains( mapEntry.getKey() ) );
			assertTrue( cacheValues.contains( mapEntry.getValue() ) );
			assertSame( mapEntry.getKey(), invertedMap.get( mapEntry.getValue() ) );
		}
	}

	@Entity
	private static class Simple {
		@Id
		private int value;

		public Simple(int value) {
			this.value = value;
		}

		public int getValue() {
			return value;
		}

		public void setValue(int value) {
			this.value = value;
		}

		@Override
		public String toString() {
			return "Simple{" +
					"value=" + value +
					'}';
		}
	}

	private class DoNothingEntityCopyObserver implements EntityCopyObserver {

		@Override
		public void entityCopyDetected(Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session) {

		}

		@Override
		public void topLevelMergeComplete(EventSource session) {

		}

		@Override
		public void clear() {

		}
	}

	private class ExceptionThrowingEntityCopyObserver implements EntityCopyObserver {

		@Override
		public void entityCopyDetected(Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session) {
			throw new IllegalStateException( "Entity copies not allowed." );
		}

		@Override
		public void topLevelMergeComplete(EventSource session) {
		}

		@Override
		public void clear() {

		}
	}
}
