package com.atlassian.stash.internal;

import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.context.DelegatingTestExecutionListener;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.junit.Assert;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;
import java.util.function.Predicate;

/**
 * A base class for building DAO tests, and other tests which should be run against a database rather than using mocks.
 * <p>
 * For simplicity in testing, this base class extends JUnit's {@code Assert} class, exposing all testing assertions
 * directly to derived types. As a result, derived types should not need to import assertions.
 * <p>
 * The Spring application context created by this class for any tests derived from it is <i>cached</i>. That means the
 * same context will be used across all such tests. As a result, tests which irreparably change the context must be
 * marked with {@code DirtiesContext} to trigger the context to be reconstructed for the next test or they may trigger
 * cascading test failures.
 * <p>
 * All tests are run in a transaction, and that transaction is <i>rolled back</i> when the test ends (even if the test
 * ends successfully without throwing an exception). This reduces the amount of database cleanup that has to be done
 * to keep table contents in a consistent state.
 * <p>
 * <b>Warning</b>: It is <i>not safe</i> to assume that tests are being run against a throwaway in-memory HSQL database.
 * The {@code test-context.xml} allows for specifying a real database. As a result, tests <i>must</i> clean up after
 * themselves. The easiest way to accomplish this is to ensure that the test does not commit changes it makes. Failing
 * that, another approach must be used. The {@link DataSet DataSet} annotation includes
 * a {@code delete()} flag which may be useful in applying cleanup in some tests. These cleanup rules apply not only to
 * rows inserted, updated or deleted, but also to any schema changes such as creating or dropping tables.
 *
 * @since 1.0
 */
@ContextConfiguration(locations = "classpath:/spring/dao-test-context.xml")
@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners(DelegatingTestExecutionListener.class)
@Transactional
public abstract class AbstractDaoTest extends Assert {

    protected static final String DESCRIPTION_CHARS = "~`!@#$%^&*()_+-={}|[]\\:\";<>?,./' àáâãäåçèéêëìíîðñòôõöö 国際交流абвгдежзبْجَدِيَّة عَرَبِيَّة‎עִבְרִיתदिंदुसरोवरλληνικ \"';alert(666)//';alert(666)//\\\";alert(666)//\\\\\\\";alert(666)//--></SCRIPT>\\\">'><SCRIPT>alert(666)</SCRIPT>";

    protected static final Predicate<Object> ALWAYS_TRUE = o -> true;

    @Autowired
    protected SessionFactory sessionFactory;

    /**
     * Clears the {@code Session} and evicts <i>all</i> entities from both the first- and second-level caches.
     * <p>
     * <b>Warning</b>: Any statements pending {@link #flush() flush} in the {@code Session} <i>will never be
     * applied</i> after calling this method. If there may be pending statements in the session, they must be
     * explicitly flushed before calling {@code clear()}.
     */
    protected void clear() {
        //Clear the session, which clears both any operations pending flush and evicts all L1 cached entities
        getCurrentSession().clear();
        //Evict all entities from the L2 cache (Ehcache) as well
        sessionFactory.getCache().evictEntityRegions();
        //Also evict any collections that have been cached
        sessionFactory.getCache().evictCollectionRegions();
    }

    /**
     * Evicts the provided entity from both the first- and second-level caches in Hibernate, ensuring any attempt to
     * load it again must roundtrip to the database. This is useful when verifying that data is stored and retrieved
     * as expected in all DBMSs.
     * <p>
     * All entities which store user input should have DAO tests which store exotic Unicode characters in those fields
     * and then verify their roundtrip representation. Such tests will need to call this method to ensure the entity
     * is actually reloaded from the database rather than returned from Hibernate's caches.
     * <p>
     * <b>Note</b>: You must call {@link #flush()} before calling {@code evict} to ensure all statements in the
     * {@code Session} have been applied to the database. Otherwise, evicting the entity from the {@code Session}
     * <i>may</i> prevent the SQL from ever running. This may trigger random not found failures in the DAO tests.
     *
     * @param entity   the entity to evict from first- and second-level caches
     * @param entityId the identifier of the entity, required to evict it from the second-level cache
     * @since 1.3
     */
    protected void evict(Object entity, Serializable entityId) {
        getCurrentSession().evict(entity);

        Class<?> entityClass = Hibernate.getClass(entity);
        sessionFactory.getCache().evictEntity(entityClass, entityId);
    }

    /**
     * Flushes the {@code Session}, ensuring all statements have been applied to the database.
     * <p>
     * when operations are performed on the {@code Session}, it may store them internally for some period of time
     * rather than immediately executing SQL statements against the database. Typically the session is flushed at
     * the end of the transaction, but the transaction wrapping DAO tests is never committed. As a result, some
     * tests may need to explicitly flush the session if they rely on server-side behaviours, such as foreign keys
     * with cascading deletions, or to verify that Unicode characters are stored correctly in the database.
     */
    protected void flush() {
        getCurrentSession().flush();
    }

    /**
     * Retrieves the current Hibernate {@code Session} for the test.
     *
     * @return the current session
     */
    protected Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    /**
     * Re-reads the provided entity from the database, updating its fields with any changes that may have been
     * applied since it was loaded by Hibernate.
     *
     * @param entity the entity to refresh
     */
    protected void refresh(Object entity) {
        getCurrentSession().refresh(entity);
    }
}
