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

import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.jdbc.Work;
import org.hibernate.loader.JoinWalker;
import org.hibernate.loader.entity.EntityJoinWalker;
import org.hibernate.loader.plan.exec.process.spi.ResultSetProcessor;
import org.hibernate.loader.plan.exec.query.spi.NamedParameterContext;
import org.hibernate.loader.plan.exec.spi.LoadQueryDetails;
import org.hibernate.loader.plan.spi.LoadPlan;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.OuterJoinLoadable;

import org.junit.Test;

import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.junit4.ExtraAssertions;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;

/**
 * @author Gail Badner
 */
public class EncapsulatedCompositeAttributeResultSetProcessorTest extends BaseCoreFunctionalTestCase {

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

	@Test
	public void testSimpleNestedCompositeAttributeProcessing() throws Exception {
		// create some test data
		Session session = openSession();
		session.beginTransaction();
		Person person = new Person();
		person.id = 1;
		person.name = "Joe Blow";
		person.address = new Address();
		person.address.address1 = "1313 Mockingbird Lane";
		person.address.city = "Pleasantville";
		person.address.country = "USA";
		AddressType addressType = new AddressType();
		addressType.typeName = "snail mail";
		person.address.type = addressType;
		session.save( person );
		session.getTransaction().commit();
		session.close();

//		session = openSession();
//		session.beginTransaction();
//		Person personGotten = (Person) session.get( Person.class, person.id );
//		assertEquals( person.id, personGotten.id );
//		assertEquals( person.address.address1, personGotten.address.address1 );
//		assertEquals( person.address.city, personGotten.address.city );
//		assertEquals( person.address.country, personGotten.address.country );
//		assertEquals( person.address.type.typeName, personGotten.address.type.typeName );
//		session.getTransaction().commit();
//		session.close();

		List results = getResults( sessionFactory().getEntityPersister( Person.class.getName() ) );
		assertEquals( 1, results.size() );
		Object result = results.get( 0 );
		assertNotNull( result );

		Person personWork = ExtraAssertions.assertTyping( Person.class, result );
		assertEquals( person.id, personWork.id );
		assertEquals( person.address.address1, personWork.address.address1 );
		assertEquals( person.address.city, personWork.address.city );
		assertEquals( person.address.country, personWork.address.country );
		assertEquals( person.address.type.typeName, person.address.type.typeName );

		// clean up test data
		session = openSession();
		session.beginTransaction();
		session.createQuery( "delete Person" ).executeUpdate();
		session.getTransaction().commit();
		session.close();
	}

	@Test
	public void testNestedCompositeElementCollectionQueryBuilding() {
		doCompare(
				sessionFactory(),
				(OuterJoinLoadable) sessionFactory().getClassMetadata( Customer.class )
		);
	}

	private void doCompare(SessionFactoryImplementor sf, OuterJoinLoadable persister) {
		final LoadQueryInfluencers influencers = LoadQueryInfluencers.NONE;
		final LockMode lockMode = LockMode.NONE;
		final int batchSize = 1;

		final EntityJoinWalker walker = new EntityJoinWalker(
				persister,
				persister.getKeyColumnNames(),
				batchSize,
				lockMode,
				sf,
				influencers
		);

		final LoadQueryDetails details = Helper.INSTANCE.buildLoadQueryDetails( persister, sf );

		compare( walker, details );
	}

	private void compare(JoinWalker walker, LoadQueryDetails details) {
		System.out.println( "WALKER    : " + walker.getSQLString() );
		System.out.println( "LOAD-PLAN : " + details.getSqlStatement() );
		System.out.println();
	}

	@Test
	public void testNestedCompositeElementCollectionProcessing() throws Exception {
		// create some test data
		Session session = openSession();
		session.beginTransaction();
		Person person = new Person();
		person.id = 1;
		person.name = "Joe Blow";
		session.save( person );
		Customer customer = new Customer();
		customer.id = 1L;
		Investment investment1 = new Investment();
		investment1.description = "stock";
		investment1.date = new Date();
		investment1.monetaryAmount = new MonetaryAmount();
		investment1.monetaryAmount.currency = MonetaryAmount.CurrencyCode.USD;
		investment1.monetaryAmount.amount = BigDecimal.valueOf( 1234, 2 );
		investment1.performedBy = person;
		Investment investment2 = new Investment();
		investment2.description = "bond";
		investment2.date = new Date();
		investment2.monetaryAmount = new MonetaryAmount();
		investment2.monetaryAmount.currency = MonetaryAmount.CurrencyCode.EUR;
		investment2.monetaryAmount.amount = BigDecimal.valueOf( 98176, 1 );
		customer.investments.add( investment1 );
		customer.investments.add( investment2 );
		session.save( customer );
		session.getTransaction().commit();
		session.close();

//		session = openSession();
//		session.beginTransaction();
//		Customer customerGotten = (Customer) session.get( Customer.class, customer.id );
//		assertEquals( customer.id, customerGotten.id );
//		session.getTransaction().commit();
//		session.close();

		List results = getResults( sessionFactory().getEntityPersister( Customer.class.getName() ) );

		assertEquals( 2, results.size() );
		assertSame( results.get( 0 ), results.get( 1 ) );
		Object result = results.get( 0 );
		assertNotNull( result );

		Customer customerWork = ExtraAssertions.assertTyping( Customer.class, result );

		// clean up test data
		session = openSession();
		session.beginTransaction();
		session.delete( customerWork.investments.get( 0 ).performedBy );
		session.delete( customerWork );
		session.getTransaction().commit();
		session.close();
	}

	private List<?> getResults(EntityPersister entityPersister ) {
		final LoadPlan plan = Helper.INSTANCE.buildLoadPlan( sessionFactory(), entityPersister );

		final LoadQueryDetails queryDetails = Helper.INSTANCE.buildLoadQueryDetails( plan, sessionFactory() );
		final String sql = queryDetails.getSqlStatement();
		final ResultSetProcessor resultSetProcessor = queryDetails.getResultSetProcessor();

		final List results = new ArrayList();

		final Session workSession = openSession();
		workSession.beginTransaction();
		workSession.doWork(
				new Work() {
					@Override
					public void execute(Connection connection) throws SQLException {
						PreparedStatement ps = connection.prepareStatement( sql );
						ps.setInt( 1, 1 );
						ResultSet resultSet = ps.executeQuery();
						results.addAll(
								resultSetProcessor.extractResults(
										resultSet,
										(SessionImplementor) workSession,
										new QueryParameters(),
										Helper.parameterContext(),
										true,
										false,
										null,
										null
								)
						);
						resultSet.close();
						ps.close();
					}
				}
		);
		workSession.getTransaction().commit();
		workSession.close();
		return results;
	}

	@Entity( name = "Person" )
	public static class Person implements Serializable {
		@Id
		Integer id;
		String name;

		@Embedded
		Address address;
	}

	@Embeddable
	public static class Address implements Serializable {
		String address1;
		String city;
		String country;
		AddressType type;
	}

	@Embeddable
	public static class AddressType {
		String typeName;
	}

	@Entity( name = "Customer" )
	public static class Customer {
		private Long id;
		private List<Investment> investments = new ArrayList<Investment>();

		@Id
		public Long getId() {
			return id;
		}
		public void setId(Long id) {
			this.id = id;
		}

		@ElementCollection(fetch = FetchType.EAGER)
		@CollectionTable( name = "investments", joinColumns = @JoinColumn( name = "customer_id" ) )
		public List<Investment> getInvestments() {
			return investments;
		}
		public void setInvestments(List<Investment> investments) {
			this.investments = investments;
		}
	}

	@Embeddable
	public static class Investment {
		private MonetaryAmount monetaryAmount;
		private String description;
		private Date date;
		private Person performedBy;

		@Embedded
		public MonetaryAmount getMonetaryAmount() {
			return monetaryAmount;
		}
		public void setMonetaryAmount(MonetaryAmount monetaryAmount) {
			this.monetaryAmount = monetaryAmount;
		}
		public String getDescription() {
			return description;
		}
		public void setDescription(String description) {
			this.description = description;
		}
		@Column(name="`date`")
		public Date getDate() {
			return date;
		}
		public void setDate(Date date) {
			this.date = date;
		}
		@ManyToOne
		public Person getPerformedBy() {
			return performedBy;
		}
		public void setPerformedBy(Person performedBy) {
			this.performedBy = performedBy;
		}
	}

	@Embeddable
	public static class MonetaryAmount {
		public static enum CurrencyCode {
			USD,
			EUR
		}
		private BigDecimal amount;
		@Column(length = 3)
		@Enumerated(EnumType.STRING)
		private CurrencyCode currency;

		public BigDecimal getAmount() {
			return amount;
		}
		public void setAmount(BigDecimal amount) {
			this.amount = amount;
		}

		public CurrencyCode getCurrency() {
			return currency;
		}
		public void setCurrency(CurrencyCode currency) {
			this.currency = currency;
		}
	}
}
