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

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.persistence.jpql.entities.IColumnEntity;
import org.apache.openjpa.persistence.jpql.entities.INameEntity;
import org.apache.openjpa.persistence.jpql.entities.IOrderedElements;
import org.apache.openjpa.persistence.jpql.entities.IOrderedEntity;
import org.apache.openjpa.persistence.jpql.entities.OrderedElementEntity;
import org.apache.openjpa.persistence.jpql.entities.OrderedManyToManyEntity;
import org.apache.openjpa.persistence.jpql.entities.OrderedNameEntity;
import org.apache.openjpa.persistence.jpql.entities.OrderedOneToManyEntity;
import org.apache.openjpa.persistence.jpql.entities.UnorderedNameEntity;
import org.apache.openjpa.persistence.jpql.entities.XMLOrderedElementEntity;
import org.apache.openjpa.persistence.jpql.entities.XMLOrderedManyToManyEntity;
import org.apache.openjpa.persistence.jpql.entities.XMLOrderedNameEntity;
import org.apache.openjpa.persistence.jpql.entities.XMLOrderedOneToManyEntity;
import org.apache.openjpa.persistence.jpql.entities.XMLUnorderedNameEntity;
import org.apache.openjpa.persistence.proxy.TreeNode;
import org.apache.openjpa.persistence.test.JPAEntityClassEnum;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/**
 * Test JPQL Index function on O2M, M2M and Element collections using annotations and XML.
 *
 * @author Catalina Wei, Albert Lee, Donald Woods
 */
public class TestIndex extends SingleEMFTestCase {

    private Log log = null;

    private enum JPQLIndexEntityClasses implements JPAEntityClassEnum {
        OrderedElementEntity(OrderedElementEntity.class),
        OrderedOneToManyEntity(OrderedOneToManyEntity.class),
        OrderedManyToManyEntity(OrderedManyToManyEntity.class),
        XMLOrderedElementEntity(XMLOrderedElementEntity.class),
        XMLOrderedOneToManyEntity(XMLOrderedOneToManyEntity.class),
        XMLOrderedManyToManyEntity(XMLOrderedManyToManyEntity.class),
        UnorderedNameEntity(UnorderedNameEntity.class),
        XMLUnorderedNameEntity(XMLUnorderedNameEntity.class),
        OrderedNameEntity(OrderedNameEntity.class);

        private Class<?> clazz;
        private String fullEntityName;
        private String entityName;

        JPQLIndexEntityClasses(Class<?> clazz) {
            this.clazz = clazz;
            fullEntityName = clazz.getName();
            entityName = fullEntityName.substring(getEntityClassName()
                .lastIndexOf('.') + 1);
        }

        public Class<?> getEntityClass() {
            return clazz;
        }

        @Override
        public String getEntityClassName() {
            return fullEntityName;
        }

        @Override
        public String getEntityName() {
            return entityName;
        }
    }

    private static final String[] Element_Names = { "A_Element", "B_Element",
        "C_Element", "D_Element", "E_Element", "F_Element", };

    @Override
    protected String getPersistenceUnitName() {
        // this sets up the testcase code so our EMF is created and cleaned up for us
        return "JPQLIndex";
    }

    @Override
    public void setUp() {
        super.setUp(CLEAR_TABLES, TreeNode.class,
            OrderedElementEntity.class, UnorderedNameEntity.class,
            OrderedOneToManyEntity.class, OrderedManyToManyEntity.class,
            OrderedNameEntity.class);
            // XMLOrderedOneToManyEntity.class, XMLOrderedManyToManyEntity.class,
            // XMLOrderedElementEntity.class, XMLUnorderedNameEntity.class,
            // XMLOrderedNameEntity.class);

        log =  emf.getConfiguration().getLog("test");
    }

    // original testcase by Catalina
    public void testO2MTreeQueryIndex() {
        int[] fanOuts = {2,3,4};
        createTreeNodeEntities(fanOuts);
        EntityManager em = emf.createEntityManager();
        String query = "SELECT index(c) from TreeNode t, in (t.childern) c" +
            " WHERE index(c) = 2";

        List<Object> rs = em.createQuery(query).getResultList();
        for (Object t: rs)
            assertEquals(2, Integer.parseInt(t.toString()));

        em.close();
    }

    // Testcases added by Donald with code reused from annonxml tests by Albert
    public void testO2MQueryIndex() {
        createEntities(JPQLIndexEntityClasses.OrderedOneToManyEntity, UnorderedNameEntity.class);
        verifyEntities(JPQLIndexEntityClasses.OrderedOneToManyEntity, UnorderedNameEntity.class);
    }

    public void testO2MXMLQueryIndex() {
        createEntities(JPQLIndexEntityClasses.XMLOrderedOneToManyEntity, XMLUnorderedNameEntity.class);
        verifyEntities(JPQLIndexEntityClasses.XMLOrderedOneToManyEntity, XMLUnorderedNameEntity.class);
    }

    public void testM2MQueryIndex() {
        createEntities(JPQLIndexEntityClasses.OrderedManyToManyEntity, OrderedNameEntity.class);
        verifyEntities(JPQLIndexEntityClasses.OrderedManyToManyEntity, OrderedNameEntity.class);
    }

    public void testM2MXMLQueryIndex() {
        createEntities(JPQLIndexEntityClasses.OrderedManyToManyEntity, XMLOrderedNameEntity.class);
        verifyEntities(JPQLIndexEntityClasses.OrderedManyToManyEntity, XMLOrderedNameEntity.class);
    }

    public void testElementQueryIndex() {
        createEntities(JPQLIndexEntityClasses.OrderedElementEntity, String.class);
        verifyEntities(JPQLIndexEntityClasses.OrderedElementEntity, String.class);
    }

    public void testElementXMLQueryIndex() {
        createEntities(JPQLIndexEntityClasses.XMLOrderedElementEntity, String.class);
        verifyEntities(JPQLIndexEntityClasses.XMLOrderedElementEntity, String.class);
    }

    /**
     * Create and persist a uniform OneToMany tree with given fan out.
     */
    private TreeNode createTreeNodeEntities(int[] original) {
        TreeNode root = new TreeNode();
        root.createTree(original);
        assertArrayEquals(original, root.getFanOuts());

        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        em.persist(root);
        em.getTransaction().commit();
        em.clear();

        return root;
    }

    /**
     *  Asserts the given arrays have exactly same elements at the same index.
     */
    private void assertArrayEquals(int[] a, int[] b) {
        assertEquals(a.length, b.length);
        for (int i = 0; i<a.length; i++)
            assertEquals(a[i], b[i]);
    }

    private <C,E> void createEntities(JPQLIndexEntityClasses entityType, Class<E> elementClass)
    {
        if (IOrderedEntity.class.isAssignableFrom(entityType.getEntityClass())) {
            if (INameEntity.class.isAssignableFrom(elementClass)) {
                log.trace("** Test INameEntity modifications on IOrderedEntity.");
                createO2MEntities(entityType, (Class<INameEntity>)elementClass);
            } else if (IColumnEntity.class.isAssignableFrom(elementClass)) {
                log.trace("** Test IColumnEntity modifications on IOrderedEntity.");
                createM2MEntities(entityType, (Class<IColumnEntity>)elementClass);
            } else {
                fail("createEntities(IOrderedEntity) - Unexpected elementClass=" + elementClass.getSimpleName());
            }
        } else if (IOrderedElements.class.isAssignableFrom(entityType.getEntityClass())) {
            if (String.class.isAssignableFrom(elementClass)) {
                log.trace("** Test String modifications on IOrderedElements.");
                createOrderedElements(entityType);
            } else {
                fail("createEntities(IOrderedElements) - Unexpected elementClass=" + elementClass.getSimpleName());
            }
        } else {
            fail("createEntities() - Unexpected entityType=" + entityType.getEntityName());
        }
    }

    private void createO2MEntities(JPQLIndexEntityClasses entityType, Class<INameEntity> elementClass)
    {
        EntityManager em = null;

        try {
            Class<IOrderedEntity> entityClass =
                (Class<IOrderedEntity>)Class.forName(entityType.getEntityClassName());
            String entityClassName = entityType.getEntityName();
            String elementClassName = elementClass.getName().substring(
                elementClass.getName().lastIndexOf('.') + 1);
            Integer entityId = 1;

            // create the entity
            IOrderedEntity newEntity = (IOrderedEntity)constructNewEntityObject(entityType);
            newEntity.setId(entityId);

            // create the elements to add
            Constructor<INameEntity> elementConstrctor = elementClass.getConstructor(String.class);
            List<INameEntity> newElements = new ArrayList<>();
            for (String element_name : Element_Names) {
                newElements.add(elementConstrctor.newInstance(element_name));
            }

            // add the entities
            em = emf.createEntityManager();
            em.getTransaction().begin();
            for (INameEntity newElement : newElements)
            {
                em.persist(newElement);
                newEntity.addEntity((INameEntity)newElement);
            }
            em.persist(newEntity);
            em.getTransaction().commit();
            em.clear();

            // verify the entity was stored
            IOrderedEntity findEntity = em.find(entityClass, entityId);
            assertNotNull("Found entity just created", findEntity);
            assertEquals("Verify entity id = " + entityId, entityId.intValue(), findEntity.getId());
            assertEquals("Verify entity name = " + entityClass.getName(), entityClass.getName(),
                findEntity.getClass().getName());

        } catch (Throwable t) {
            log.error(t);
            throw new RuntimeException(t);
        } finally {
            if (em != null) {
                if (em.getTransaction().isActive()) {
                    em.getTransaction().rollback();
                }
                em.close();
                em = null;
            }
        }
    }

    private void createM2MEntities(JPQLIndexEntityClasses entityType, Class<IColumnEntity> elementClass)
    {
        EntityManager em = null;

        try {
            Class<IOrderedEntity> entityClass =
                (Class<IOrderedEntity>)Class.forName(entityType.getEntityClassName());
            String entityClassName = entityType.getEntityName();
            String elementClassName = elementClass.getName().substring(
                elementClass.getName().lastIndexOf('.') + 1);

            // create the EM and transaction
            em = emf.createEntityManager();
            em.getTransaction().begin();

            // create and persist the entities
            List<IOrderedEntity> newEntities = new ArrayList<>();
            for (int i=0; i<3; i++) {
                IOrderedEntity newEntity = (IOrderedEntity)constructNewEntityObject(entityType);
                newEntity.setId(i);
                em.persist(newEntity);
                newEntities.add(newEntity);
            }

            // create and persist the elements
            Constructor<IColumnEntity> elementConstrctor = elementClass.getConstructor(String.class);
            List<INameEntity> newElements = new ArrayList<>();
            IColumnEntity newElement;
            for (String element_name : Element_Names) {
                newElement = elementConstrctor.newInstance(element_name);
                // add parent relationships
                newElement.setEntities(newEntities);
                em.persist(newElement);
                newElements.add(newElement);
            }

            // update entities with elements
            for (IOrderedEntity newEntity : newEntities) {
                newEntity.setEntities(newElements);
                em.persist(newEntity);
            }
            em.getTransaction().commit();
            em.clear();

            // verify the entities were stored
            for (int i=0; i<Element_Names.length; i++) {
                IOrderedEntity findEntity = em.find(entityClass, i);
                assertNotNull("Found entity just created", findEntity);
                assertEquals("Verify entity id = " + i, i, findEntity.getId());
                assertEquals("Verify entity name = " + entityClass.getName(), entityClass.getName(),
                    findEntity.getClass().getName());
            }
        } catch (Throwable t) {
            log.error(t);
            throw new RuntimeException(t);
        } finally {
            if (em != null) {
                if (em.getTransaction().isActive()) {
                    em.getTransaction().rollback();
                }
                em.close();
                em = null;
            }
        }
    }

    private void createOrderedElements(JPQLIndexEntityClasses entityType)
    {
        EntityManager em = null;

        try {
            Class<IOrderedElements> entityClass =
                (Class<IOrderedElements>)Class.forName(entityType.getEntityClassName());
            String entityClassName = entityType.getEntityName();
            Integer entityId = 1;

            IOrderedElements newEntity = (IOrderedElements)constructNewEntityObject(entityType);
            newEntity.setId(entityId);
            List<String> namesList = new ArrayList<>();
            for (String element_name : Element_Names) {
                namesList.add(element_name);
            }
            newEntity.setListElements(namesList);
            // add the entity
            em = emf.createEntityManager();
            em.getTransaction().begin();
            em.persist(newEntity);
            em.getTransaction().commit();
            em.clear();

            // verify the entity was stored
            log.trace("Verifing the entity was stored");
            IOrderedElements findEntity = em.find(entityClass, entityId);
            assertNotNull("Found entity just created", findEntity);
            assertEquals("Verify entity id = " + entityId, entityId.intValue(), findEntity.getId());
            assertEquals("Verify entity name = " + entityClass.getName(), entityClass.getName(),
                findEntity.getClass().getName());

        } catch (Throwable t) {
            log.error(t);
            throw new RuntimeException(t);
        } finally {
            if (em != null) {
                if (em.getTransaction().isActive()) {
                    em.getTransaction().rollback();
                }
                em.close();
                em = null;
            }
        }
    }

    private <C,E> void verifyEntities(JPQLIndexEntityClasses entityType, Class<E> elementClass)
    {
        if (IOrderedEntity.class.isAssignableFrom(entityType.getEntityClass())) {
            if (INameEntity.class.isAssignableFrom(elementClass)) {
                log.trace("** Verify INameEntity modifications on IOrderedEntity.");
                verifyO2MEntities(entityType, (Class<INameEntity>)elementClass);
            } else if (IColumnEntity.class.isAssignableFrom(elementClass)) {
                log.trace("** Verify IColumnEntity modifications on IOrderedEntity.");
                verifyM2MEntities(entityType, (Class<IColumnEntity>)elementClass);
            } else {
                fail("verifyEntities(IOrderedEntity) - Unexpected elementClass=" + elementClass.getSimpleName());
            }
        } else if (IOrderedElements.class.isAssignableFrom(entityType.getEntityClass())) {
            if (String.class.isAssignableFrom(elementClass)) {
                log.trace("** Test String modifications on IOrderedElements.");
                verifyOrderedElements(entityType);
            } else {
                fail("verifyEntities(IOrderedElements) - Unexpected elementClass=" + elementClass.getSimpleName());
            }
        } else {
            fail("verifyEntities() - Unexpected entityType=" + entityType.getEntityName());
        }
    }

    private <E> void verifyO2MEntities(JPQLIndexEntityClasses entityType, Class<INameEntity> elementClass)
    {
        EntityManager em = null;

        try {
            Class<IOrderedEntity> entityClass = (Class<IOrderedEntity>)Class.forName(entityType.getEntityClassName());
            String entityClassName = entityType.getEntityName();
            entityClassName = entityClassName.substring(entityClassName.lastIndexOf('.') + 1);

            if (log.isTraceEnabled()) {
                log.trace("Query " + entityClassName + " and verify 'entities' collection has "
                    + Element_Names.length + " elements in this order: "
                    + Arrays.toString(Element_Names));
            }

            em = emf.createEntityManager();
            em.clear();
            int idx = 0;
            for (String expectedEntityName : Element_Names) {
                Query q = em.createQuery("select w from " + entityClassName
                    + " o join o.entities w where index(w) = " + idx);
                List<E> res = (List<E>)q.getResultList();
                assertEquals("  Verify query returns 1 element for index " + idx, 1, res.size());
                if (res.size() == 1) {
                    Object oo = res.get(0);
                    assertEquals("  Verify element type is " + elementClass.getName(), elementClass.getName(),
                        oo.getClass().getName());
                    try {
                        String name = (String) elementClass.getMethod("getName").invoke(oo);
                        assertEquals("  Verify element value is '"
                            + expectedEntityName + "'", expectedEntityName, name);
                    } catch (Exception e) {
                        log.error("  Caught unexpected exception:" + e.getMessage());
                        throw new RuntimeException(e);
                    }
                }
                ++idx;
            }
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        } finally {
            if (em != null) {
                em.close();
                em = null;
            }
        }
    }

    private <E> void verifyM2MEntities(JPQLIndexEntityClasses entityType, Class<IColumnEntity> elementClass)
    {
        EntityManager em = null;

        try {
            Class<IOrderedEntity> entityClass = (Class<IOrderedEntity>)Class.forName(entityType.getEntityClassName());
            String entityClassName = entityType.getEntityName();
            entityClassName = entityClassName.substring(entityClassName.lastIndexOf('.') + 1);
            String elementClassName = entityType.getEntityName();
            elementClassName = elementClassName.substring(elementClassName.lastIndexOf('.') + 1);

            if (log.isTraceEnabled()) {
                log.trace("Query " + entityClassName + " and verify 'entities' collection has "
                    + Element_Names.length + " elements in this order: "
                    + Arrays.toString(Element_Names));
            }

            em = emf.createEntityManager();
            em.clear();
            int idx = 0, idx2 = 0;
            for (String expectedEntityName : Element_Names) {
                Query q = em.createQuery("select w from " + entityClassName
                    + " o join o.entities w where index(w) = " + idx);
                List<E> res = (List<E>)q.getResultList();
                assertEquals("  Verify query returns 1 element for index " + idx, 1, res.size());
                if (res.size() == 1) {
                    Object oo = res.get(0);
                    assertEquals("  Verify element type is " + elementClass.getName(), elementClass.getName(),
                        oo.getClass().getName());
                    try {
                        String name = (String) elementClass.getMethod("getName").invoke(oo);
                        assertEquals("  Verify element value is '"
                            + expectedEntityName + "'", expectedEntityName, name);

                        if (log.isTraceEnabled()) {
                            log.trace("Query " + elementClassName + " and verify 'entities' collection content");
                        }
                        Query q2 = em.createQuery("select w from " + elementClassName
                            + " o join o.entities w where index(w) = " + idx2);
                        List<E> res2 = (List<E>)q.getResultList();
                        assertEquals("  Verify query returns 1 entity for index " + idx2, 1, res2.size());
                        if (res2.size() == 1) {
                            Object oo2 = res2.get(0);
                            assertEquals("  Verify entity type is " + entityClass.getName(), entityClass.getName(),
                                oo2.getClass().getName());
                            try {
                                String name2 = (String) entityClass.getMethod("getName").invoke(oo2);
                                assertEquals("  Verify entity value is '"
                                    + expectedEntityName + "'", expectedEntityName, name);
                            } catch (Exception e) {
                                log.error("  Caught unexpected exception:" + e.getMessage());
                                throw new RuntimeException(e);
                            }
                        }
                    } catch (Exception e) {
                        log.error("  Caught unexpected exception:" + e.getMessage());
                        throw new RuntimeException(e);
                    }
                }
                ++idx;
            }
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        } finally {
            if (em != null) {
                em.close();
                em = null;
            }
        }
    }

    private <E> void verifyOrderedElements(JPQLIndexEntityClasses entityType)
    {
        EntityManager em = null;

        try {
            Class<IOrderedEntity> entityClass = (Class<IOrderedEntity>)Class.forName(entityType.getEntityClassName());
            String entityClassName = entityType.getEntityName();
            entityClassName = entityClassName.substring(entityClassName.lastIndexOf('.') + 1);

            if (log.isTraceEnabled()) {
                log.trace("Query " + entityClassName + " and verify 'elements' collection has "
                    + Element_Names.length + " elements in this order: "
                    + Arrays.toString(Element_Names));
            }

            em = emf.createEntityManager();
            em.clear();
            int idx = 0;
            for (String expectedEntityName : Element_Names) {
                Query q = em.createQuery("select w from " + entityClassName
                    + " o join o.elements w where index(w) = " + idx);
                List<E> res = (List<E>)q.getResultList();
                assertEquals("  Verify query returns 1 element for index " + idx, 1, res.size());
                if (res.size() == 1) {
                    Object oo = res.get(0);
                    assertEquals("  Verify element type is String", String.class.getName(),
                        oo.getClass().getName());
                    String name;
                    try {
                        name = (String) oo.toString();
                        assertEquals("  Verify element value is '"
                            + expectedEntityName + "'", expectedEntityName, name);
                    } catch (Exception e) {
                        log.error("  Caught unexpected exception:" + e.getMessage());
                        throw new RuntimeException(e);
                    }
                }
                ++idx;
            }
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        } finally {
            if (em != null) {
                em.close();
                em = null;
            }
        }
    }

}
