/*
 * Copyright 2012-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.test.autoconfigure.orm.jpa;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceUnitUtil;

import org.springframework.orm.jpa.EntityManagerFactoryUtils;
import org.springframework.util.Assert;

/**
 * Alternative to {@link EntityManager} for use in JPA tests. Provides a subset of
 * {@link EntityManager} methods that are useful for tests as well as helper methods for
 * common testing tasks such as {@link #persistFlushFind(Object) persist/flush/find}.
 *
 * @author Phillip Webb
 * @since 1.4.0
 */
public class TestEntityManager {

	private final EntityManagerFactory entityManagerFactory;

	/**
	 * Create a new {@link TestEntityManager} instance for the given
	 * {@link EntityManagerFactory}.
	 * @param entityManagerFactory the source entity manager factory
	 */
	public TestEntityManager(EntityManagerFactory entityManagerFactory) {
		Assert.notNull(entityManagerFactory, "EntityManagerFactory must not be null");
		this.entityManagerFactory = entityManagerFactory;
	}

	/**
	 * Make an instance managed and persistent then return its ID. Delegates to
	 * {@link EntityManager#persist(Object)} then {@link #getId(Object)}.
	 * <p>
	 * Helpful when setting up test data in a test: <pre class="code">
	 * Object entityId = this.testEntityManager.persist(new MyEntity("Spring"));
	 * </pre>
	 * @param entity the source entity
	 * @return the ID of the newly persisted entity
	 */
	public Object persistAndGetId(Object entity) {
		persist(entity);
		return getId(entity);
	}

	/**
	 * Make an instance managed and persistent then return its ID. Delegates to
	 * {@link EntityManager#persist(Object)} then {@link #getId(Object, Class)}.
	 * <p>
	 * Helpful when setting up test data in a test: <pre class="code">
	 * Long entityId = this.testEntityManager.persist(new MyEntity("Spring"), Long.class);
	 * </pre>
	 * @param <T> the ID type
	 * @param entity the source entity
	 * @param idType the ID type
	 * @return the ID of the newly persisted entity
	 */
	public <T> T persistAndGetId(Object entity, Class<T> idType) {
		persist(entity);
		return getId(entity, idType);
	}

	/**
	 * Make an instance managed and persistent. Delegates to
	 * {@link EntityManager#persist(Object)} then returns the original source entity.
	 * <p>
	 * Helpful when setting up test data in a test: <pre class="code">
	 * MyEntity entity = this.testEntityManager.persist(new MyEntity("Spring"));
	 * </pre>
	 * @param <E> the entity type
	 * @param entity the entity to persist
	 * @return the persisted entity
	 */
	public <E> E persist(E entity) {
		getEntityManager().persist(entity);
		return entity;
	}

	/**
	 * Make an instance managed and persistent, synchronize the persistence context to the
	 * underlying database and finally find the persisted entity by its ID. Delegates to
	 * {@link #persistAndFlush(Object)} then {@link #find(Class, Object)} with the
	 * {@link #getId(Object) entity ID}.
	 * <p>
	 * Helpful when ensuring that entity data is actually written and read from the
	 * underlying database correctly.
	 * @param <E> the entity type
	 * @param entity the entity to persist
	 * @return the entity found using the ID of the persisted entity
	 */
	@SuppressWarnings("unchecked")
	public <E> E persistFlushFind(E entity) {
		EntityManager entityManager = getEntityManager();
		persistAndFlush(entity);
		Object id = getId(entity);
		entityManager.detach(entity);
		return (E) entityManager.find(entity.getClass(), id);
	}

	/**
	 * Make an instance managed and persistent then synchronize the persistence context to
	 * the underlying database. Delegates to {@link EntityManager#persist(Object)} then
	 * {@link #flush()} and finally returns the original source entity.
	 * <p>
	 * Helpful when setting up test data in a test: <pre class="code">
	 * MyEntity entity = this.testEntityManager.persistAndFlush(new MyEntity("Spring"));
	 * </pre>
	 * @param <E> the entity type
	 * @param entity the entity to persist
	 * @return the persisted entity
	 */
	public <E> E persistAndFlush(E entity) {
		persist(entity);
		flush();
		return entity;
	}

	/**
	 * Merge the state of the given entity into the current persistence context. Delegates
	 * to {@link EntityManager#merge(Object)}
	 * @param <E> the entity type
	 * @param entity the entity to merge
	 * @return the merged entity
	 */
	public <E> E merge(E entity) {
		return getEntityManager().merge(entity);
	}

	/**
	 * Remove the entity instance. Delegates to {@link EntityManager#remove(Object)}
	 * @param entity the entity to remove
	 */
	public void remove(Object entity) {
		getEntityManager().remove(entity);
	}

	/**
	 * Find by primary key. Delegates to {@link EntityManager#find(Class, Object)}.
	 * @param <E> the entity type
	 * @param entityClass the entity class
	 * @param primaryKey the entity primary key
	 * @return the found entity or {@code null} if the entity does not exist
	 * @see #getId(Object)
	 */
	public <E> E find(Class<E> entityClass, Object primaryKey) {
		return getEntityManager().find(entityClass, primaryKey);
	}

	/**
	 * Synchronize the persistence context to the underlying database. Delegates to
	 * {@link EntityManager#flush()}.
	 */
	public void flush() {
		getEntityManager().flush();
	}

	/**
	 * Refresh the state of the instance from the database, overwriting changes made to
	 * the entity, if any. Delegates to {@link EntityManager#refresh(Object)}.
	 * @param <E> the entity type
	 * @param entity the entity to refresh
	 * @return the refreshed entity
	 */
	public <E> E refresh(E entity) {
		getEntityManager().refresh(entity);
		return entity;
	}

	/**
	 * Clear the persistence context, causing all managed entities to become detached.
	 * Delegates to {@link EntityManager#clear()}
	 */
	public void clear() {
		getEntityManager().clear();
	}

	/**
	 * Remove the given entity from the persistence context, causing a managed entity to
	 * become detached. Delegates to {@link EntityManager#detach(Object)}.
	 * @param entity the entity to detach.
	 */
	public void detach(Object entity) {
		getEntityManager().detach(entity);
	}

	/**
	 * Return the ID of the given entity. Delegates to
	 * {@link PersistenceUnitUtil#getIdentifier(Object)}.
	 * @param entity the source entity
	 * @return the ID of the entity or {@code null}
	 * @see #getId(Object, Class)
	 */
	public Object getId(Object entity) {
		return this.entityManagerFactory.getPersistenceUnitUtil().getIdentifier(entity);
	}

	/**
	 * Return the ID of the given entity cast to a specific type. Delegates to
	 * {@link PersistenceUnitUtil#getIdentifier(Object)}.
	 * @param <T> the ID type
	 * @param entity the source entity
	 * @param idType the expected ID type
	 * @return the ID of the entity or {@code null}
	 * @see #getId(Object)
	 */
	@SuppressWarnings("unchecked")
	public <T> T getId(Object entity, Class<T> idType) {
		Object id = getId(entity);
		Assert.isInstanceOf(idType, id, "ID mismatch:");
		return (T) id;
	}

	/**
	 * Return the underlying {@link EntityManager} that's actually used to perform all
	 * operations.
	 * @return the entity manager
	 */
	public final EntityManager getEntityManager() {
		EntityManager manager = EntityManagerFactoryUtils.getTransactionalEntityManager(this.entityManagerFactory);
		Assert.state(manager != null, "No transactional EntityManager found, is your test running in a transaction?");
		return manager;
	}

}
