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

import javax.persistence.AttributeConverter;
import javax.persistence.Cacheable;
import javax.persistence.Convert;
import javax.persistence.Converter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.Session;
import org.hibernate.annotations.Immutable;
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.internal.MetadataBuilderImpl;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.metamodel.spi.MetamodelImplementor;
import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan;
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry;
import org.hibernate.type.descriptor.java.MutabilityPlan;

import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

/**
 * @author Steve Ebersole
 */
public class ExplicitJavaTypeDescriptorTest extends BaseNonConfigCoreFunctionalTestCase {

	@Override
	protected void configureMetadataBuilder(MetadataBuilder metadataBuilder) {
		((MetadataBuilderImpl)metadataBuilder).contributeJavaTypeDescriptor(new JavaTypeDescriptorRegistry.FallbackJavaTypeDescriptor( MutableState2.class ) {
			@Override
			public MutabilityPlan getMutabilityPlan() {
				return ImmutableMutabilityPlan.INSTANCE;
			}
		});
	}

	@Override
	protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
		super.configureStandardServiceRegistryBuilder( ssrb );

		ssrb.applySetting( AvailableSettings.USE_SECOND_LEVEL_CACHE, "true" );
		ssrb.applySetting( AvailableSettings.GENERATE_STATISTICS, "true" );
	}

	@Override
	protected void applyMetadataSources(MetadataSources sources) {
		super.applyMetadataSources( sources );

		sources.addAnnotatedClass( TheEntity.class );
	}

	@Test
	@TestForIssue( jiraKey = "HHH-11098" )
	public void testIt() {
		// set up test data
		Session session = openSession();
		( (MetamodelImplementor) session.getMetamodel() ).getTypeConfiguration()
				.getJavaTypeDescriptorRegistry()
				.addDescriptor(
						new JavaTypeDescriptorRegistry.FallbackJavaTypeDescriptor( MutableState2.class ) {
							@Override
							public MutabilityPlan getMutabilityPlan() {
								return ImmutableMutabilityPlan.INSTANCE;
							}
						}
				);
		session.beginTransaction();
		session.persist( new TheEntity(1) );
		session.getTransaction().commit();
		session.close();

		// assertions based on the persist call
		assertThat( mutableToDomainCallCount, is(1) );  			// 1 instead of 0 because of the deep copy call
		assertThat( mutableToDatabaseCallCount, is(2) );  			// 2 instead of 1 because of the deep copy call

		assertThat( immutableToDomainCallCount, is(0) );			// logical
		assertThat( immutableToDatabaseCallCount, is(1) );			// logical

		assertThat( immutableMutableToDomainCallCount, is(0) );		// was 1 (like mutable) before the JavaTypeDescriptor registration
		assertThat( immutableMutableToDatabaseCallCount, is(1) );	// was 2 (like mutable) before the JavaTypeDescriptor registration

		// clean up test data
		session = openSession();
		session.beginTransaction();
		session.delete( session.byId( TheEntity.class ).getReference( 1 ) );
		session.getTransaction().commit();
		session.close();
	}


	@Entity( name = "TheEntity")
	@Table( name = "T_ENTITY" )
	@Cacheable
	public static class TheEntity {
		@Id
		private Integer id;

		@Convert( converter = MutableConverterImpl.class )
		private MutableState mutableState;

		@Convert( converter = ImmutableConverterImpl.class )
		private ImmutableState immutableState;

		@Convert( converter = ImmutableMutable2ConverterImpl.class )
		private MutableState2 immutableMutableState;

		public TheEntity() {
		}

		public TheEntity(Integer id) {
			this.id = id;

			this.mutableState = new MutableState( id.toString() );
			this.immutableState = new ImmutableState( id.toString() );
			this.immutableMutableState = new MutableState2( id.toString() );
		}
	}

	@Before
	public void clearCounts() {
		// in case we add additional tests
		sessionFactory().getStatistics().clear();

		mutableToDatabaseCallCount = 0;
		mutableToDomainCallCount = 0;

		immutableToDatabaseCallCount = 0;
		immutableToDomainCallCount = 0;

		immutableMutableToDatabaseCallCount = 0;
		immutableMutableToDomainCallCount = 0;
	}

	private static int mutableToDatabaseCallCount;
	private static int mutableToDomainCallCount;

	private static int immutableToDatabaseCallCount;
	private static int immutableToDomainCallCount;

	private static int immutableMutableToDatabaseCallCount;
	private static int immutableMutableToDomainCallCount;


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Purely mutable state

	public static class MutableState {
		private String state;

		public MutableState(String state) {
			this.state = state;
		}

		public String getState() {
			return state;
		}

		// mutable
		public void setState(String state) {
			this.state = state;
		}

		@Override
		public boolean equals(Object o) {
			if ( this == o ) {
				return true;
			}
			if ( o == null || getClass() != o.getClass() ) {
				return false;
			}

			MutableState that = (MutableState) o;

			return getState() != null ? getState().equals( that.getState() ) : that.getState() == null;

		}

		@Override
		public int hashCode() {
			return getState() != null ? getState().hashCode() : 0;
		}
	}

	@Converter
	public static class MutableConverterImpl implements AttributeConverter<MutableState,String> {
		@Override
		public String convertToDatabaseColumn(MutableState attribute) {
			mutableToDatabaseCallCount++;
			return attribute == null ? null : attribute.getState();
		}

		@Override
		public MutableState convertToEntityAttribute(String dbData) {
			mutableToDomainCallCount++;
			return new MutableState( dbData );
		}
	}


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Purely immutable state

	@Immutable
	public static class ImmutableState {
		private final String state;

		public ImmutableState(String state) {
			this.state = state;
		}

		public String getState() {
			return state;
		}

		@Override
		public boolean equals(Object o) {
			if ( this == o ) {
				return true;
			}
			if ( o == null || getClass() != o.getClass() ) {
				return false;
			}

			ImmutableState that = (ImmutableState) o;

			return getState().equals( that.getState() );

		}

		@Override
		public int hashCode() {
			return getState().hashCode();
		}
	}

	@Converter
	public static class ImmutableConverterImpl implements AttributeConverter<ImmutableState,String> {
		@Override
		public String convertToDatabaseColumn(ImmutableState attribute) {
			immutableToDatabaseCallCount++;
			return attribute == null ? null : attribute.getState();
		}

		@Override
		public ImmutableState convertToEntityAttribute(String dbData) {
			immutableToDomainCallCount++;
			return new ImmutableState( dbData );
		}
	}


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Mutable state we treat as immutable

	public static class MutableState2 {
		private String state;

		public MutableState2(String state) {
			this.state = state;
		}

		public String getState() {
			return state;
		}

		// mutable
		public void setState(String state) {
			// just a safety net - the idea is that the user is promising to not mutate the internal state
			throw new UnsupportedOperationException( "illegal attempt to mutate state" );
		}

		@Override
		public boolean equals(Object o) {
			if ( this == o ) {
				return true;
			}
			if ( o == null || getClass() != o.getClass() ) {
				return false;
			}

			MutableState2 that = (MutableState2) o;

			return getState() != null ? getState().equals( that.getState() ) : that.getState() == null;

		}

		@Override
		public int hashCode() {
			return getState() != null ? getState().hashCode() : 0;
		}
	}

	@Converter
	public static class ImmutableMutable2ConverterImpl implements AttributeConverter<MutableState2,String> {
		@Override
		public String convertToDatabaseColumn(MutableState2 attribute) {
			immutableMutableToDatabaseCallCount++;
			return attribute == null ? null : attribute.getState();
		}

		@Override
		public MutableState2 convertToEntityAttribute(String dbData) {
			immutableMutableToDomainCallCount++;
			return new MutableState2( dbData );
		}
	}



}
