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

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.test.util.DdlTransactionIsolatorTestingImpl;
import org.hibernate.tool.schema.internal.DefaultSchemaFilter;
import org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl;
import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool;
import org.hibernate.tool.schema.internal.SchemaCreatorImpl;
import org.hibernate.tool.schema.internal.SchemaDropperImpl;
import org.hibernate.tool.schema.internal.IndividuallySchemaValidatorImpl;
import org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase;
import org.hibernate.tool.schema.spi.ExceptionHandler;
import org.hibernate.tool.schema.spi.ExecutionOptions;
import org.hibernate.tool.schema.spi.SchemaManagementException;
import org.hibernate.tool.schema.spi.SchemaManagementTool;

import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.boot.JdbcConnectionAccessImpl;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.hibernate.testing.logger.LoggerInspectionRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import org.jboss.logging.Logger;

import static org.junit.Assert.assertEquals;

/**
 * @author Dominique Toupin
 */
@TestForIssue(jiraKey = "HHH-10332")
@RequiresDialect(H2Dialect.class)
public class IndividuallySchemaValidatorImplTest extends BaseUnitTestCase {

	@Rule
	public LoggerInspectionRule logInspection = new LoggerInspectionRule(
			Logger.getMessageLogger( CoreMessageLogger.class, IndividuallySchemaValidatorImplTest.class.getName() ) );

	private StandardServiceRegistry ssr;

	protected HibernateSchemaManagementTool tool;

	private Map configurationValues;

	protected ExecutionOptions executionOptions;

	@Before
	public void setUp() throws IOException {
		ssr = new StandardServiceRegistryBuilder().build();

		tool = (HibernateSchemaManagementTool) ssr.getService( SchemaManagementTool.class );

		configurationValues = ssr.getService( ConfigurationService.class ).getSettings();

		executionOptions = new ExecutionOptions() {
			@Override
			public boolean shouldManageNamespaces() {
				return true;
			}

			@Override
			public Map getConfigurationValues() {
				return configurationValues;
			}

			@Override
			public ExceptionHandler getExceptionHandler() {
				return ExceptionHandlerLoggedImpl.INSTANCE;
			}
		};
	}

	@After
	public void tearsDown() {
		StandardServiceRegistryBuilder.destroy( ssr );
	}

	@Test
	public void testMissingEntityContainsQualifiedEntityName() throws Exception {
		final MetadataSources metadataSources = new MetadataSources( ssr );
		metadataSources.addAnnotatedClass( MissingEntity.class );

		final MetadataImplementor metadata = (MetadataImplementor) metadataSources.buildMetadata();
		metadata.validate();

		try {
			getSchemaValidator( metadata );
			Assert.fail( "SchemaManagementException expected" );
		}
		catch (SchemaManagementException e) {
			assertEquals("Schema-validation: missing table [SomeCatalog.SomeSchema.MissingEntity]", e.getMessage());
		}
	}

	@Test
	public void testMissingEntityContainsUnqualifiedEntityName() throws Exception {
		final MetadataSources metadataSources = new MetadataSources( ssr );
		metadataSources.addAnnotatedClass( UnqualifiedMissingEntity.class );

		final MetadataImplementor metadata = (MetadataImplementor) metadataSources.buildMetadata();
		metadata.validate();

		try {
			getSchemaValidator( metadata );
			Assert.fail( "SchemaManagementException expected" );
		}
		catch (SchemaManagementException e) {
			assertEquals("Schema-validation: missing table [UnqualifiedMissingEntity]", e.getMessage());
		}
	}

	@Test
	public void testMissingColumn() throws Exception {
		MetadataSources metadataSources = new MetadataSources( ssr );
		metadataSources.addAnnotatedClass( NoNameColumn.class );

		MetadataImplementor metadata = (MetadataImplementor) metadataSources.buildMetadata();
		metadata.validate();

		Map<String, Object> settings = new HashMap<>(  );

		ServiceRegistryImplementor serviceRegistry = (ServiceRegistryImplementor) new StandardServiceRegistryBuilder()
				.applySettings( settings )
				.build();

		DriverManagerConnectionProviderImpl connectionProvider =
				new DriverManagerConnectionProviderImpl();
		connectionProvider.configure( properties() );

		final GenerationTargetToDatabase schemaGenerator =  new GenerationTargetToDatabase(
				new DdlTransactionIsolatorTestingImpl(
						serviceRegistry,
						new JdbcConnectionAccessImpl( connectionProvider )
				)
		);

		try {
			new SchemaCreatorImpl( ssr ).doCreation(
					metadata,
					serviceRegistry,
					settings,
					true,
					schemaGenerator
			);

			metadataSources = new MetadataSources( ssr );
			metadataSources.addAnnotatedClass( NameColumn.class );

			metadata = (MetadataImplementor) metadataSources.buildMetadata();
			metadata.validate();

			try {
				getSchemaValidator( metadata );
				Assert.fail( "SchemaManagementException expected" );
			}
			catch (SchemaManagementException e) {
				assertEquals("Schema-validation: missing column [name] in table [SomeSchema.ColumnEntity]", e.getMessage());
			}
		}
		finally {
			new SchemaDropperImpl( serviceRegistry ).doDrop( metadata, false, schemaGenerator );
		}
	}

	@Test
	public void testMismatchColumnType() throws Exception {
		MetadataSources metadataSources = new MetadataSources( ssr );
		metadataSources.addAnnotatedClass( NameColumn.class );

		MetadataImplementor metadata = (MetadataImplementor) metadataSources.buildMetadata();
		metadata.validate();

		Map<String, Object> settings = new HashMap<>(  );

		ServiceRegistryImplementor serviceRegistry = (ServiceRegistryImplementor) new StandardServiceRegistryBuilder()
				.applySettings( settings )
				.build();

		DriverManagerConnectionProviderImpl connectionProvider =
				new DriverManagerConnectionProviderImpl();
		connectionProvider.configure( properties() );

		final GenerationTargetToDatabase schemaGenerator =  new GenerationTargetToDatabase(
				new DdlTransactionIsolatorTestingImpl(
						serviceRegistry,
						new JdbcConnectionAccessImpl( connectionProvider )
				)
		);

		try {
			new SchemaCreatorImpl( ssr ).doCreation(
					metadata,
					serviceRegistry,
					settings,
					true,
					schemaGenerator
			);

			metadataSources = new MetadataSources( ssr );
			metadataSources.addAnnotatedClass( IntegerNameColumn.class );

			metadata = (MetadataImplementor) metadataSources.buildMetadata();
			metadata.validate();

			try {
				getSchemaValidator( metadata );
				Assert.fail( "SchemaManagementException expected" );
			}
			catch (SchemaManagementException e) {
				assertEquals("Schema-validation: wrong column type encountered in column [name] in table [SomeSchema.ColumnEntity]; found [varchar (Types#VARCHAR)], but expecting [integer (Types#INTEGER)]", e.getMessage());
			}
		}
		finally {
			new SchemaDropperImpl( serviceRegistry ).doDrop( metadata, false, schemaGenerator );
		}
	}

	protected void getSchemaValidator(MetadataImplementor metadata) {
		new IndividuallySchemaValidatorImpl( tool, DefaultSchemaFilter.INSTANCE )
				.doValidation( metadata, executionOptions );
	}

	protected Properties properties() {
		Properties properties = new Properties( );
		URL propertiesURL = Thread.currentThread().getContextClassLoader().getResource( "hibernate.properties" );
		try(FileInputStream inputStream = new FileInputStream( propertiesURL.getFile() )) {
			properties.load( inputStream );
		}
		catch (IOException e) {
			throw new IllegalArgumentException( e );
		}
		return properties;
	}

	@Entity
	@PrimaryKeyJoinColumn
	@Table(name = "UnqualifiedMissingEntity")
	public static class UnqualifiedMissingEntity {

		private String id;

		@Id
		public String getId() {
			return id;
		}

		public void setId(String id) {
			this.id = id;
		}
	}

	@Entity
	@PrimaryKeyJoinColumn
	@Table(name = "MissingEntity", catalog = "SomeCatalog", schema = "SomeSchema")
	public static class MissingEntity {

		private String id;

		@Id
		public String getId() {
			return id;
		}

		public void setId(String id) {
			this.id = id;
		}
	}

	@Entity
	@PrimaryKeyJoinColumn
	@Table(name = "ColumnEntity", schema = "SomeSchema")
	public static class NoNameColumn {

		private String id;

		@Id
		public String getId() {
			return id;
		}

		public void setId(String id) {
			this.id = id;
		}
	}

	@Entity
	@PrimaryKeyJoinColumn
	@Table(name = "ColumnEntity", schema = "SomeSchema")
	public static class NameColumn {

		private String id;

		private String name;

		@Id
		public String getId() {
			return id;
		}

		public void setId(String id) {
			this.id = id;
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}
	}

	@Entity
	@PrimaryKeyJoinColumn
	@Table(name = "ColumnEntity", schema = "SomeSchema")
	public static class IntegerNameColumn {

		private String id;

		private Integer name;

		@Id
		public String getId() {
			return id;
		}

		public void setId(String id) {
			this.id = id;
		}

		public Integer getName() {
			return name;
		}

		public void setName(Integer name) {
			this.name = name;
		}
	}
}
