/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 * http://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.apache.openjpa.persistence.cache.jpa;

import java.util.List;

import jakarta.persistence.Cache;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
import jakarta.persistence.EntityManager;

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI;
import org.apache.openjpa.persistence.cache.jpa.model.CacheEntity;
import org.apache.openjpa.persistence.cache.jpa.model.CacheableEntity;
import org.apache.openjpa.persistence.cache.jpa.model.NegatedCachableEntity;
import org.apache.openjpa.persistence.cache.jpa.model.NegatedUncacheableEntity;
import org.apache.openjpa.persistence.cache.jpa.model.UncacheableEntity;
import org.apache.openjpa.persistence.cache.jpa.model.UnspecifiedEntity;
import org.apache.openjpa.persistence.cache.jpa.model.XmlCacheableEntity;
import org.apache.openjpa.persistence.cache.jpa.model.XmlUncacheableEntity;

public abstract class AbstractCacheModeTestCase extends AbstractCacheTestCase {
    @Override
    public abstract OpenJPAEntityManagerFactorySPI getEntityManagerFactory();

    public abstract List<String> getSql();

    protected abstract Class<?>[] getExpectedNotInCache();

    protected abstract Class<?>[] getExpectedInCache();

    // =======================================================================
    // Asserts
    // =======================================================================
    /**
     * Assert whether the cache contains the expected results.
     *
     * @param cache
     *            The JPA Cache to verify
     * @param expectCacheables
     *            Whether entities with @Cacheable(true) should be in the cache
     *            (almost always true)
     * @param expectUncacheables
     *            Whether entities with @Cacheable(false) should be in the cache
     *            (almost always false)
     * @param expectUnspecified
     *            Whether entities with no @Cacheable annotation should be in
     *            the cache (varies per config).
     */
    protected void assertCacheContents(Cache cache, boolean expectCacheables, boolean expectUncacheables,
        boolean expectUnspecified) {
        assertCacheables(cache, expectCacheables);
        assertUncacheables(cache, expectUncacheables);
        assertUnspecified(cache, expectUnspecified);
    }

    /**
     * Assert whether the cacheable types are in the cache. This method exits on
     * the first cache 'miss'.
     *
     * @param cache
     *            JPA Cache to verify
     * @param expected
     *            If true the cacheable types should be in the cache, if false
     *            they should not be.
     */
    protected void assertCacheables(Cache cache, boolean expected) {
        assertCached(cache, CacheableEntity.class, 1, expected);
        assertCached(cache, NegatedUncacheableEntity.class, 1, expected);
        assertCached(cache, XmlCacheableEntity.class, 1, expected);
    }

    /**
     * Assert whether the uncacheable types are in the cache. This method exits
     * on the first cache 'miss'.
     *
     * @param cache
     *            JPA Cache to verify
     * @param expected
     *            If true the uncacheable types should be in the cache, if false
     *            they should not be.
     */
    protected void assertUncacheables(Cache cache, boolean expected) {
        assertCached(cache, UncacheableEntity.class, 1, expected);
        assertCached(cache, XmlUncacheableEntity.class, 1, expected);
        assertCached(cache, NegatedCachableEntity.class, 1, expected);
    }

    /**
     * Assert whether the unspecified types are in the cache. This method exits
     * on the first cache 'miss'.
     *
     * @param cache
     *            JPA Cache to verify
     * @param expected
     *            If true the unspecified types should be in the cache, if false
     *            they should not be.
     */
    protected void assertUnspecified(Cache cache, boolean expected) {
        assertCached(cache, UnspecifiedEntity.class, 1, expected);
    }

    /**
     * Assert that no sql is executed when running the supplied Action.
     *
     * @param act
     *            Action to execute.
     */
    public void assertNoSql(Action act) {
        assertSqlInc(act, 0);
    }

    /**
     * Assert that <literal>expectedSqls</literal> SQL statements are executed
     * when running <literal>act</literal>
     *
     * @param act
     *            Action to run.
     * @param expectedSqls
     *            Number of SQL statements that should be executed.
     */
    public void assertSqlInc(Action act, int expectedSqls) {
        int before = getSql().size();
        act.run();
        assertEquals(before + expectedSqls, getSql().size());
    }

    // =======================================================================
    // Utility classes
    // =======================================================================
    /**
     * Basic 'runnable' interface used to run a set of commands, then analyze
     * the number of SQL statements that result.
     */
    public interface Action {
        void run();
    }

    // =======================================================================
    // Test utilities
    // =======================================================================
    public boolean getCacheEnabled() {
        return true;
    }

    // =======================================================================
    // Common test methods.
    // =======================================================================
    /**
     * Ensure that each call the em.find generates an SQL statement when
     * CacheRetrieveMode.BYPASS is used.
     */
    public void testReadModeByass() {
        assertSqlInc(new Action() {
            @Override
            public void run() {
                EntityManager em = getEntityManagerFactory().createEntityManager();
                em.setProperty(RETRIEVE_MODE_PROP, CacheRetrieveMode.BYPASS);
                for (Class<?> cls : persistentTypes) {
                    em.find(cls, 1);
                }
                em.close();
            }
        }, persistentTypes.length);
    }

    /**
     * <p>
     * Ensure that each entity in getExpectedInCache():
     * <ul>
     * <li>is in the cache</li>
     * <li>does not go to the database for a find operation</li>
     * <li>is not null</li>
     * </ul>
     * </p>
     * <p>
     * and
     * </p>
     * <p>
     * Ensure that each entity in getExpectedNotInCache() :
     * <ul>
     * <li>is not in the cache</li>
     * <li>results in a single SQL statement when em.find() is called</li>
     * <li>is not null</li>
     * </ul>
     * </p>
     *
     */
    public void testRetrieveModeUse() {
        if (getCacheEnabled()) {
            assertNoSql(new Action() {
                @Override
                public void run() {
                    EntityManager em = getEntityManagerFactory().createEntityManager();
                    em.setProperty(RETRIEVE_MODE_PROP, CacheRetrieveMode.USE);
                    for (Class<?> cls : getExpectedInCache()) {
                        assertCached(getEntityManagerFactory().getCache(), cls, 1, true);
                        assertNotNull(em.find(cls, 1));
                    }
                    em.close();
                }
            });
            assertSqlInc(new Action() {
                @Override
                public void run() {
                    EntityManager em = getEntityManagerFactory().createEntityManager();
                    em.setProperty(RETRIEVE_MODE_PROP, CacheRetrieveMode.USE);
                    for (Class<?> cls : getExpectedNotInCache()) {
                        assertCached(getEntityManagerFactory().getCache(), cls, 1, false);
                        assertNotNull(em.find(cls, 1));
                    }
                    em.close();
                }
            }, getExpectedNotInCache().length);
        }
    }

    public void updateAndFind(Class<? extends CacheEntity> classToUpdate, int idToUpdate,
            Class<? extends CacheEntity> classToFind, int idToFind,
            CacheStoreMode storeMode, CacheRetrieveMode retrieveMode) {
        EntityManager em = getEntityManagerFactory().createEntityManager();

        if (storeMode != null) {
            em.setProperty(STORE_MODE_PROP, storeMode);
        }
        if (retrieveMode != null) {
            em.setProperty(RETRIEVE_MODE_PROP, retrieveMode);
        }

        em.getTransaction().begin();
        CacheEntity ce1 = em.find(classToUpdate, idToUpdate);
        CacheEntity ce2 = em.find(classToFind, idToFind);
        assertNotNull(ce1);
        assertNotNull(ce2);
        ce1.setName(ce1.getName() + "UPD");
        em.getTransaction().commit();
        em.close();
    }

    /**
     * <p>
     * Test logic to validate different CacheStoreModes. It should behave
     * identically for all shared-cache-modes except NONE which never caches
     * anything.
     * </p>
     * <p>
     * This method only tests setting the store mode on the EntityManager
     * itself.
     * </p>
     * <p>
     * The first transaction updates CacheableEntity::1 with CacheStoreMode
     * tran1StoreMode, calls find for CacheableEntity::1 and
     * XmlCacheableEntity::1. This will never trigger a cache refresh since the
     * data is up to date - but it could trigger additional SQL
     * </p>
     * <p>
     * The second transaction updates XmlCacheableEntity::1 with CacheStoreMode
     * tran2StoreMode, calls find for CacheableEntity::1 and
     * XmlCacheableEntity::1. In this case if tran2StoreMode ==
     * CacheStoreMode.REFRESH we may update the cache with the state of
     * CacheableEntity::1.
     * </p>
     *
     * @param tran1StoreMode
     *            CacheStoreMode to use in transaction 1.
     * @param tran2StoreMode
     *            cacheStoreMode to use in transaction 2.
     * @param cacheUpdatedForTran1
     *            Whether the cache will contain an updated version of
     *            CacheableEntity::1
     * @param cacheUpdatedForTran2
     *            Whether the cache will contain an updated version of
     *            XmlCacheableEntity::1
     * @param version
     *            Expected starting version of for both entities
     */
    public void entityManagerStoreModeTest(CacheStoreMode tran1StoreMode, CacheStoreMode tran2StoreMode,
        boolean cacheUpdatedForTran1, boolean cacheUpdatedForTran2, int version) {
        updateAndFind(CacheableEntity.class, 1, XmlCacheableEntity.class, 1, tran1StoreMode, null);
        updateAndFind(XmlCacheableEntity.class, 1, CacheableEntity.class, 1, tran2StoreMode, null);

        // get entities from the cache and ensure their versions are as
        // expected.
        EntityManager em = getEntityManagerFactory().createEntityManager();
        em = getEntityManagerFactory().createEntityManager();
        CacheableEntity ceFromEM = em.find(CacheableEntity.class, 1);
        XmlCacheableEntity xceFromEM = em.find(XmlCacheableEntity.class, 1);
        em.close();
        assertEquals(cacheUpdatedForTran1 ? version + 1 : version, ceFromEM.getVersion());
        assertEquals(cacheUpdatedForTran2 ? version + 1 : version, xceFromEM.getVersion());

        // get the data from the database. Version should always have been
        // updated in this case.
        em = getEntityManagerFactory().createEntityManager();
        em.setProperty(RETRIEVE_MODE_PROP, CacheRetrieveMode.BYPASS);
        CacheableEntity ceFromDB =
            (CacheableEntity) em.createNativeQuery("Select * from CacheableEntity where ID = 1", CacheableEntity.class)
                .getSingleResult();

        XmlCacheableEntity xceFromDB =
            (XmlCacheableEntity) em.createNativeQuery("Select * from XmlCacheableEntity where ID = 1",
                XmlCacheableEntity.class).getSingleResult();

        assertEquals(version + 1, ceFromDB.getVersion());
        assertEquals(version + 1, xceFromDB.getVersion());
        em.close();
    }

    /**
     * Execute the defaultStoreModeTest with
     */
    public void testStoreModeUseBypass() throws Exception {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.USE, CacheStoreMode.BYPASS, true, false, 1);
        }
    }

    public void testStoreModeUseUse() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.USE, CacheStoreMode.USE, true, true, 1);
        }
    }

    public void testRefresh() {
        if (getCacheEnabled()) {
            OpenJPAEntityManagerSPI em = getEntityManagerFactory().createEntityManager();
            CacheableEntity e1 = em.find(CacheableEntity.class, 1);
            XmlCacheableEntity e2 = em.find(XmlCacheableEntity.class, 1);
            assertNotNull(e1);
            assertNotNull(e2);
            int e1Version = e1.getVersion();
            int e2Version = e2.getVersion();

            String e1Sql = "UPDATE CacheableEntity SET VERSN=?1 WHERE ID=?2";
            String e2Sql = "UPDATE XmlCacheableEntity SET VERSN=?1 WHERE ID=?2";
            em.getTransaction().begin();
            assertEquals(1, em.createNativeQuery(e1Sql).setParameter(1, e1Version + 1).setParameter(2, e1.getId())
                .executeUpdate());
            assertEquals(1, em.createNativeQuery(e2Sql).setParameter(1, e2Version + 1).setParameter(2, e2.getId())
                .executeUpdate());
            em.getTransaction().commit();
            em.refresh(e1);
            em.refresh(e2);
            assertEquals(e1Version + 1, e1.getVersion());
            assertEquals(e2Version + 1, e2.getVersion());
            em.close();
        }
    }

    public void testStoreModeUseRefresh() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.USE, CacheStoreMode.REFRESH, true, true, 1);
        }
    }

    public void entityManagerStoreModeTest() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.BYPASS, CacheStoreMode.BYPASS, false, false, 1);
        }
    }

    public void testStoreModeBypassUse() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.BYPASS, CacheStoreMode.USE, false, true, 1);
        }
    }

    public void testStoreModeBypassRefresh() {
        if (getCacheEnabled()) {
            // REFRESH picks up the changes from the database, even though the
            // first update was done with BYPASS
            entityManagerStoreModeTest(CacheStoreMode.BYPASS, CacheStoreMode.REFRESH, true, true, 1);
        }
    }

    public void testStoreModeRefreshUse() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.REFRESH, CacheStoreMode.USE, true, true, 1);
        }
    }

    public void testStoreModeRefreshBypass() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.REFRESH, CacheStoreMode.BYPASS, true, false, 1);
        }
    }

    public void testStoreModeRefreshRefresh() {
        if (getCacheEnabled()) {
            entityManagerStoreModeTest(CacheStoreMode.REFRESH, CacheStoreMode.REFRESH, true, true, 1);
        }
    }

    public void testResultsFromQueryAreInCache() {
        if (getCacheEnabled()) {
            // clear cache
            getEntityManagerFactory().getStoreCache().evictAll();
            getEntityManagerFactory().getQueryResultCache().evictAll();

            EntityManager em = getEntityManagerFactory().createEntityManager();
            String query;
            for(Class<?> cls : persistentTypes) {
                query = "Select e from " + getAlias(cls) + " e";
                List<?> res = em.createQuery(query).getResultList();
                assertNotNull(String.format("Expected to find some results when running query %s",query), res);
                assertTrue(String.format("Expected more than 0 results running query %s",query),res.size() != 0 ) ;
            }
            for(Class<?> cls : getExpectedInCache()) {
                assertCached(getEntityManagerFactory().getCache(), cls, 1, true);
            }

            for(Class<?> cls : getExpectedNotInCache()) {
                assertCached(getEntityManagerFactory().getCache(), cls, 1, false);
            }
            em.close();
        }
    }

    public void testResultsFromFindAreInCache() {
        if (getCacheEnabled()) {
            // clear cache
            getEntityManagerFactory().getStoreCache().evictAll();
            getEntityManagerFactory().getQueryResultCache().evictAll();

            EntityManager em = getEntityManagerFactory().createEntityManager();
            for(Class<?> cls : persistentTypes) {
                assertNotNull(String.format("Expected to find %s::%d from database or from cache", cls, 1),
                		em.find(cls, 1));
            }
            for(Class<?> cls : getExpectedInCache()) {
                assertCached(getEntityManagerFactory().getCache(), cls, 1, true);
            }

            for(Class<?> cls : getExpectedNotInCache()) {
                assertCached(getEntityManagerFactory().getCache(), cls, 1, false);
            }
            em.close();
        }
    }

    private String getAlias(Class<?> cls) {
        ClassMapping mapping =
            (ClassMapping) getEntityManagerFactory().getConfiguration().getMetaDataRepositoryInstance().getMetaData(
                cls, null, true);
        return mapping.getTypeAlias();
    }
}
