/*
 * 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.jdbc.query.cache;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;

import org.apache.openjpa.datacache.ConcurrentQueryCache;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.QueryResultCacheImpl;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;
import org.apache.openjpa.util.CacheMap;

public abstract class AbstractQueryCacheTest extends SingleEMFTestCase {
    private Class[] entityClassTypes = { Part.class, PartBase.class,
            PartComposite.class, Supplier.class, Usage.class};

    protected boolean deleteData = false;
    protected boolean recreateData = true;

    @Override
    public void setUp(Object... props) {
        int arrLen = entityClassTypes.length + props.length;
        Object args[] = new Object[arrLen];

        // Add the entity class types supported by this testing
        int idx = 0;
        for (Class clazz : entityClassTypes) {
            args[idx++] = clazz;
        }

        // Add the property parameters passed in by the subclass
        for (Object obj : props) {
            args[idx++] = obj;
        }

        // Invoke superclass' implementation of setUp()...
        super.setUp(args);

        // Not all databases support GenerationType.IDENTITY column(s)
        if (checkSupportsIdentityGenerationType() && recreateData) {
            // deletes any data leftover data in the database due to the failed
            // last run of this testcase
            deleteAllData();
            reCreateData();
        }
    }

    @Override
    public void tearDown() throws Exception {
        if (deleteData) {
            deleteAllData();
        }
        super.tearDown();
    }

    /**
     * Populate the database with data that is consumable by the query cache
     * tests.
     *
     */
    protected void reCreateData() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        Supplier s1 = new Supplier(1, "S1");
        em.persist(s1);
        Supplier s2 = new Supplier(2, "S2");
        em.persist(s2);
        Supplier s3 = new Supplier(3, "S3");
        em.persist(s3);

        PartBase p1 = new PartBase(10, "Wheel", 150, 15.00);
        em.persist(p1);
        PartBase p2 = new PartBase(11, "Frame", 550.00, 25.00);
        em.persist(p2);
        PartBase p3 = new PartBase(12, "HandleBar", 125.00, 80.00);
        em.persist(p3);

        s1.addPart(p1).addPart(p2).addPart(p3);
        s2.addPart(p1).addPart(p3);

        PartComposite p4 = new PartComposite(20, "Bike", 180, 1.0);
        em.persist(p4);
        p4.addSubPart(em, 2, p1).addSubPart(em, 1, p2).addSubPart(em, 1, p3);

        em.getTransaction().commit();
        em.close();
    }

    /**
     * Remove all rows from the database.
     *
     */
    protected void deleteAllData() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        em.createNativeQuery("delete from Supplier_Part").executeUpdate();
        em.createQuery("delete from PartBase s").executeUpdate();
        em.createQuery("delete from Supplier s").executeUpdate();
        em.createQuery("delete from Usage u").executeUpdate();
        em.createQuery("delete from Part p").executeUpdate();

        em.getTransaction().commit();
        em.close();
    }

    /**
     * Populate the query cache with 35 entries.
     *
     */
    protected void loadQueryCache() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String qry = "select p from PartBase p where p.cost > ?1";
        for (int i=120; i<155; i++) {
            Query q = em.createQuery(qry);
            q.setParameter(1, (double) i);
            q.getResultList();
        }
        em.getTransaction().commit();
        em.close();
    }

    /**
     * Update an entity that is associated with the queries in the
     * query cache.
     *
     */
    protected void updateAnEntity() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        // Update entity
        PartBase p = em.find(PartBase.class,11);
        double oldcost = p.getCost();
        if (p != null) {
            p.setCost((oldcost + 10.0));
        }

        em.getTransaction().commit();
        em.close();
    }

    /**
     * Fetches a reference to the EntityManagerFactory's query cache
     * object.
     *
     */
    protected ConcurrentQueryCache getQueryCache() {
        OpenJPAEntityManagerFactory oemf = OpenJPAPersistence.cast(emf);
        QueryResultCacheImpl scache =
            (QueryResultCacheImpl) oemf.getQueryResultCache();

        return (ConcurrentQueryCache) scache.getDelegate();
    }

    /**
     * Returns the current size of the EntityManagerFactory's
     * query cache.
     *
     */
    protected int queryCacheGet() {
        ConcurrentQueryCache dcache = getQueryCache();
        CacheMap map = dcache.getCacheMap();
        return map.size();
    }

    /**
     * Returns true if the database supports GenerationType.IDENTITY column(s),
     * false if it does not.
     *
     */
    protected boolean checkSupportsIdentityGenerationType() {
        return (((JDBCConfiguration) emf.getConfiguration()).
                getDBDictionaryInstance().supportsAutoAssign);
    }

    /*
     * Common tests -- the following tests are common to any eviction policy
     * used by OpenJPA's query cache implementation.
     *
     */

    /**
     * Tests the query cache eviction function with the following test logic:
     *
     * 1) Populate the query cache with the entries supplied by loadQueryCache()
     * 2) Sleep 20ms (to avoid race conditions with the Timestamp eviction
     *                policy.)
     * 3) Update one of the entities associated with the cached queries, in
     *    order to dirty the query cache
     * 4) Insert a row into the database via native queries (this approach
     *    avoids dirtying the query cache, ensuring step #3 remains the
     *    control step.)
     * 5) With a common criteria of all part entities with a cost > 120,
     *    examine the query result list sizes from both a native query
     *    select invocation, and a JPA query invocation.  Because the
     *    query cache was dirtied by step #3, the JPA query should invoke
     *    a fresh SELECT operation on the database.
     *
     */
    public void testEviction() {
        // Not all databases support GenerationType.IDENTITY column(s)
        if (!checkSupportsIdentityGenerationType()) {
            return;
        }

        loadQueryCache();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        updateAnEntity();

        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        String insert1 =
            "insert into Part(partno,parttype,name,cost,mass)" +
            " values(13,'PartBase','breakes',1000.0,100.0)";
        em.createNativeQuery(insert1).executeUpdate();
        String insert2 =
            "insert into Supplier_Part(suppliers_sid,supplies_partno)" +
            " values(1,13)";
        em.createNativeQuery(insert2).executeUpdate();

        em.getTransaction().commit();
        em.close();

        em = emf.createEntityManager();
        em.getTransaction().begin();

        String sql = "select partno from Part where cost > 120 ";
        Query nativeq = em.createNativeQuery(sql);
        int nativelistSize = nativeq.getResultList().size();

        em.getTransaction().commit();
        em.close();

        em = emf.createEntityManager();
        em.getTransaction().begin();
        Query q = em.createQuery("select p from PartBase p where p.cost>?1");
        q.setParameter(1, 120.0);
        int jpalistSize = q.getResultList().size();

        em.getTransaction().commit();
        em.close();

        // The resultlist of nativelist and jpalist should be the same
        // in both eviction policies(dafault/timestamp)
        assertEquals(nativelistSize,jpalistSize);

        this.deleteData = true;
        this.recreateData = true;
    }
}
