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

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import org.apache.openjpa.persistence.EntityManagerImpl;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.relations.TblChild;
import org.apache.openjpa.persistence.relations.TblGrandChild;
import org.apache.openjpa.persistence.relations.TblParent;
import org.apache.openjpa.persistence.simple.Person;
import org.apache.openjpa.persistence.test.AllowFailure;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

import junit.framework.TestCase;

/**
 * <b>TestQuerySQLCache</b> is used to verify multiple permutations of openjpa.jdbc.QuerySQLCache settings that were
 * valid in JPA 1.2 but may not be valid in JPA 2.0
 */
public class TestQuerySQLCache extends SingleEMFTestCase {

    final int nThreads = 5;
    final int nPeople = 100;
    final int nIterations = 10;

    @Override
    public void setUp() {
        // need this to cleanup existing tables as some entity names are reused
        setUp(DROP_TABLES, Person.class, TblChild.class, TblGrandChild.class, TblParent.class);
    }

    /*
     * Verify an exception is thrown if a bad cache implementation class is specified
     */
    public void testBadCustomCacheSetting() {
        Map props = new HashMap(System.getProperties());
        props.put("openjpa.MetaDataFactory", "jpa(Types=" + Person.class.getName() + ")");
        props.put("openjpa.jdbc.QuerySQLCache",
                  "org.apache.openjpa.persistence.compatible.TestQuerySQLCache.BadCacheMap");

        OpenJPAEntityManagerFactorySPI emf1 = null;
        try {
            emf1 = (OpenJPAEntityManagerFactorySPI)OpenJPAPersistence.
                                                 cast(Persistence.createEntityManagerFactory("test", props));
            //
            // EMF creation must throw an exception because the cache implementation class will not be found
            //
            fail("EMF creation must throw an exception because the cache implementation class will not be found");
        }
        catch (Exception e) {
            assertTrue(true);
        } finally {
            closeEMF(emf1);
        }
    }


    /*
     * Verify multi-threaded multi-entity manager finder works with the QuerySQLCache set to "all"
     */
    @AllowFailure(message="OPENJPA-1179 2.0 doesn't allow 'all' as in previous releases")
    public void testMultiEMCachingAll() {
        Map props = new HashMap(System.getProperties());
        props.put("openjpa.MetaDataFactory", "jpa(Types=" + Person.class.getName() + ")");
        props.put("openjpa.jdbc.QuerySQLCache", "all");
        runMultiEMCaching(props);
    }


    /*
     * Verify multi-threaded multi-entity manager finder works with the QuerySQLCache set to "true"
     */
    public void testMultiEMCachingTrue() {
        Map props = new HashMap(System.getProperties());
        props.put("openjpa.MetaDataFactory", "jpa(Types=" + Person.class.getName() + ")");
        props.put("openjpa.jdbc.QuerySQLCache", "true");
        runMultiEMCaching(props);
    }


    /*
     * Verify QuerySQLCacheValue setting "true" uses the expected cache implementation and is caching
     */
    @AllowFailure(message="Fails after first run with duplicate key value in a unique PK constraint or index")
    public void testEagerFetch() {
        Map props = new HashMap(System.getProperties());
        props.put("openjpa.MetaDataFactory", "jpa(Types=" + TblChild.class.getName() + ";"
                                                          + TblGrandChild.class.getName() + ";"
                                                          + TblParent.class.getName() + ")");
        props.put("openjpa.jdbc.QuerySQLCache", "true");

        OpenJPAEntityManagerFactorySPI emf1 = (OpenJPAEntityManagerFactorySPI) OpenJPAPersistence.
                                             cast(Persistence.createEntityManagerFactory("test", props));
        try {
            EntityManagerImpl em = (EntityManagerImpl)emf1.createEntityManager();

            em.getTransaction().begin();

            for (int i = 1; i < 3; i++) {
                TblParent p = new TblParent();
                p.setParentId(i);
                TblChild c = new TblChild();
                c.setChildId(i);
                c.setTblParent(p);
                p.addTblChild(c);
                em.persist(p);
                em.persist(c);

                TblGrandChild gc = new TblGrandChild();
                gc.setGrandChildId(i);
                gc.setTblChild(c);
                c.addTblGrandChild(gc);

                em.persist(p);
                em.persist(c);
                em.persist(gc);
            }
            em.flush();
            em.getTransaction().commit();
            em.clear();

            for (int i = 1; i < 3; i++) {
                TblParent p = em.find(TblParent.class, i);
                int pid = p.getParentId();
                assertEquals(pid, i);
                Collection<TblChild> children = p.getTblChildren();
                boolean hasChild = false;
                for (TblChild c : children) {
                    hasChild = true;
                    Collection<TblGrandChild> gchildren = c.getTblGrandChildren();
                    int cid = c.getChildId();
                    assertEquals(cid, i);
                    boolean hasGrandChild = false;
                    for (TblGrandChild gc : gchildren) {
                        hasGrandChild = true;
                        int gcId = gc.getGrandChildId();
                        assertEquals(gcId, i);
                    }
                    assertTrue(hasGrandChild);
                }
                assertTrue(hasChild);
                em.close();
            }
        } finally {
            closeEMF(emf1);
        }
    }


    private void runMultiEMCaching(Map props) {
        EntityManagerFactory emfac = Persistence.createEntityManagerFactory("test", props);
        try {
            EntityManager em = emfac.createEntityManager();

            //
            // Create some entities
            //
            em.getTransaction().begin();
            for (int i = 0; i < nPeople; i++) {
                Person p = new Person();
                p.setId(i);
                em.persist(p);
            }
            em.flush();
            em.getTransaction().commit();
            em.close();

            Thread[] newThreads = new Thread[nThreads];
            FindPeople[] customer = new FindPeople[nThreads];
            for (int i=0; i < nThreads; i++) {
                customer[i] = new FindPeople(emfac, 0, nPeople, nIterations, i);
                newThreads[i] = new Thread(customer[i]);
                newThreads[i].start();
            }

            //
            // Wait for the worker threads to complete
            //
            for (int i = 0; i < nThreads; i++) {
                try {
                    newThreads[i].join();
                }
                catch (InterruptedException e) {
                    TestCase.fail("Caught Interrupted Exception: " + e);
                }
            }

            //
            // Run through the state of all runnables to assert if any of them failed.
            //
            for (int i = 0; i < nThreads; i++) {
                assertFalse(customer[i].hadFailures());
            }

            //
            // Clean up the entities used in this test
            //
            em = emfac.createEntityManager();
            em.getTransaction().begin();
            for (int i = 0; i < nPeople; i++) {
                Person p = em.find(Person.class, i);
                em.remove(p);
            }
            em.flush();
            em.getTransaction().commit();
            em.close();
        } finally {
            closeEMF(emfac);
        }
    }


    /*
     * Simple runnable to test finder in a tight loop.  Multiple instances of this runnable will run simultaneously
     */
    private class FindPeople implements Runnable {
        private int startId;
        private int endId;
        private int thread;
        private int iterations;
        private EntityManagerFactory emf;
        private boolean failures = false;

        public FindPeople(EntityManagerFactory emf, int startId, int endId, int iterations, int thread) {
            super();
            this.startId = startId;
            this.endId = endId;
            this.thread = thread;
            this.iterations = iterations;
            this.emf = emf;
        }

        public boolean hadFailures() {
            return failures;
        }

        @Override
        public void run() {
            try {
                EntityManager em = emf.createEntityManager();
                for (int j = 0; j < iterations; j++) {

                    for (int i = startId; i < endId; i++) {
                        Person p1 = em.find(Person.class, i);
                        if (p1.getId() != i) {
                            System.out.println("Finder failed: " + i);
                            failures = true;
                            break;
                        }
                    }
                    em.clear();
                }
                em.close();
            }
            catch (Exception e) {
                failures = true;
                System.out.println("Thread " + thread + " exception :" + e );
            }
        }
    }
}
