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

import java.util.ArrayList;
import java.util.List;

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

/**
 * Tests query ordering.
 *
 * @author Pinaki Poddar
 *
 */
public class TestQuery extends SliceTestCase {

    private int POBJECT_COUNT = 25;
    private int VALUE_MIN = 100;
    private int VALUE_MAX = VALUE_MIN + POBJECT_COUNT - 1;

    @Override
    protected String getPersistenceUnitName() {
        return "ordering";
    }

    @Override
    public void setUp() throws Exception {
        super.setUp(PObject.class, Person.class, Address.class, Country.class,
                Car.class, Manufacturer.class,
        		CLEAR_TABLES);
        int count = count(PObject.class);
        if (count == 0) {
            create(POBJECT_COUNT);
        }
    }

    void create(int N) {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        for (int i=0;i<POBJECT_COUNT;i++) {
            PObject pc = new PObject();
            pc.setValue(VALUE_MIN + i);
            em.persist(pc);
            String slice = SlicePersistence.getSlice(pc);
            String expected = (pc.getValue()%2 == 0) ? "Even" : "Odd";
            assertEquals(expected, slice);
        }
        Person p1 = new Person();
        Person p2 = new Person();
        Address a1 = new Address();
        Address a2 = new Address();
        p1.setName("Even");
        p2.setName("Odd");
        a1.setCity("San Francisco");
        a2.setCity("Rome");
        p1.setAddress(a1);
        p2.setAddress(a2);
        em.persist(p1);
        em.persist(p2);
        assertEquals("Even", SlicePersistence.getSlice(p1));
        assertEquals("Odd", SlicePersistence.getSlice(p2));

        em.getTransaction().commit();
    }

    public void testOrderedQueryResultWhenOrderableItemSelected() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query = em.createQuery(
                "SELECT p.value,p FROM PObject p ORDER BY p.value ASC");
        List result = query.getResultList();
        assertValidResult(result);
        Integer old = Integer.MIN_VALUE;
        for (Object row : result) {
            Object[] line = (Object[])row;
            int value = (Integer) line[0];
            PObject pc = (PObject)line[1];
            assertTrue(value >= old);
            old = value;
            assertEquals(value, pc.getValue());
        }
        em.getTransaction().rollback();
    }

    public void testOrderedQueryResultWhenOrderableItemNotSelected() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query =
            em.createQuery("SELECT p FROM PObject p ORDER BY p.value ASC");
        List<PObject> result = query.getResultList();
        assertValidResult(result);
        Integer old = Integer.MIN_VALUE;
        for (PObject pc : result) {
            int value = pc.getValue();
            assertTrue(value >= old);
            old = value;
        }
        em.getTransaction().rollback();
    }

    public void testOrderedQueryResultWhenNavigatedOrderableItemNotSelected() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query = em.createQuery(
     "SELECT p FROM Person p JOIN p.address a ORDER BY a.zip ASC, a.city DESC");
        List<Person> result = query.getResultList();
        assertValidResult(result);
        Integer oldZip = Integer.MIN_VALUE;
        String oldCity = null;
        for (Person pc : result) {
            int zip = pc.getAddress().getZip();
            String city = pc.getAddress().getCity();
            assertTrue(zip >= oldZip);
            assertTrue(oldCity == null || oldCity.compareTo(city) >= 0);
            oldZip = zip;
            oldCity = city;
        }
        em.getTransaction().rollback();
    }

    public void testAggregateQuery() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Object count = em.createQuery("SELECT COUNT(p) FROM PObject p")
                .getSingleResult();
        Object max = em.createQuery("SELECT MAX(p.value) FROM PObject p")
                .getSingleResult();
        Object min = em.createQuery("SELECT MIN(p.value) FROM PObject p")
                .getSingleResult();
        Object sum = em.createQuery("SELECT SUM(p.value) FROM PObject p")
                .getSingleResult();
        Object minmax = em.createQuery(
                "SELECT MIN(p.value),MAX(p.value) FROM PObject p")
                .getSingleResult();
        Object min1 = ((Object[])minmax)[0];
        Object max1 = ((Object[])minmax)[1];
        em.getTransaction().rollback();

        assertEquals(POBJECT_COUNT, ((Number)count).intValue());
        assertEquals(VALUE_MAX, ((Number)max).intValue());
        assertEquals(VALUE_MIN, ((Number)min).intValue());
        assertEquals((VALUE_MIN + VALUE_MAX) * POBJECT_COUNT,
                2 * ((Number)sum).intValue());
        assertEquals(min, min1);
        assertEquals(max, max1);
    }

    public void testAggregateQueryWithMissingValueFromSlice() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Object max = em.createQuery(
                "SELECT MAX(p.value) FROM PObject p WHERE MOD(p.value,2)=0")
                .getSingleResult();
        em.getTransaction().rollback();

        assertEquals(VALUE_MAX, ((Number)max).intValue());
    }

    public void testSetMaxResult() {
        EntityManager em = emf.createEntityManager();
        int limit = 3;
        em.getTransaction().begin();
        List<PObject> result =
            em.createQuery("SELECT p FROM PObject p ORDER BY p.value ASC")
                .setMaxResults(limit).getResultList();
        assertValidResult(result);
        Integer old = Integer.MIN_VALUE;
        for (PObject pc : result) {
            int value = pc.getValue();
            assertTrue(value >= old);
            old = value;
        }
        assertEquals(limit, result.size());
        em.getTransaction().rollback();
    }

    public void testHint() {
        List<String> targets = new ArrayList<>();
        targets.add("Even");
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query = em.createQuery("SELECT p FROM PObject p");
        query.setHint(SlicePersistence.HINT_TARGET, "Even");
        List result = query.getResultList();
        for (Object pc : result) {
            String slice = SlicePersistence.getSlice(pc);
            assertTrue("Expected original slice " + slice + " in " + targets, targets.contains(slice));
        }
        em.getTransaction().rollback();
    }

    public void testQueryTargetPolicy() {
        List<String> targets = new ArrayList<>();
        targets.add("Even");
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query = em.createQuery("SELECT p FROM PObject p");
        query.setHint(SlicePersistence.HINT_TARGET, "Even");
        List result = query.getResultList();
        for (Object pc : result) {
            String slice = SlicePersistence.getSlice(pc);
            assertTrue(targets.contains(slice));
        }
        em.getTransaction().rollback();
    }

    public void testInMemoryOrderBy() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query =
            em.createQuery("SELECT p FROM PObject p ORDER BY p.value");
        List result = query.getResultList();
        em.getTransaction().rollback();
    }

    public void testQueryParameter() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Query query = em.createQuery(
                "SELECT p FROM PObject p WHERE p.value > :v")
        	    .setParameter("v", 200);
        List result = query.getResultList();
        em.getTransaction().rollback();
    }

    public void testQueryParameterEntity() {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Address a = (Address)em.createQuery(
                "select a from Address a where a.city = :city")
        	    .setParameter("city", "Rome").getSingleResult();
        assertNotNull(a);
        assertEquals("Odd", SlicePersistence.getSlice(a));
        Query query =
            em.createQuery("SELECT p FROM Person p WHERE p.address = :a")
            .setParameter("a", a);
        List<Person> result = query.getResultList();
        assertEquals(1, result.size());
        Person p = result.get(0);
        assertEquals("Odd", SlicePersistence.getSlice(p));
        assertEquals("Rome", p.getAddress().getCity());
        em.getTransaction().rollback();
    }

    /**
     * Verifies that a lazy relation can be stored across different slices i.e.
     * collocation constraint can be violated under some restrictions.
     *
     * Car refers to Manufacturer. The relationship is uni-directional, no
     * cascade and most importantly lazy.
     * The distribution policy is designed to store Car and Manufacturer
     * *always* in different slices.
     *
     */
    public void testCollocationConstraintViolation() {
        // This transaction will store Manufacturer only
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Manufacturer bmw = new Manufacturer();
        bmw.setName("BMW");
        em.persist(bmw);
        em.getTransaction().commit();

        // This transaction will store a Car in a slice but the Car is related
        // to a Manufacturer that *always* reside in a different slice.
        em.getTransaction().begin();
        Car z4 = new Car();
        z4.setVin("1234V56789");
        z4.setMaker(bmw);
        z4.setModel("Z4");
        em.persist(z4);
        em.getTransaction().commit();
        em.clear();

        // Verify that all cars are stored in "Even" slice
        List cars = em.createQuery("select c from Car c").getResultList();
        assertFalse(cars.isEmpty());
        for (Object c : cars)
            assertEquals("Even", SlicePersistence.getSlice(c));

        // While all Manufacturers are stored in "Odd" slice.
        List makers =
            em.createQuery("select m from Manufacturer m").getResultList();
        assertFalse(makers.isEmpty());
        for (Object m : makers)
            assertEquals("Odd", SlicePersistence.getSlice(m));
        em.clear();

        // Now query for cars. The related manufacturer will be fetched
        // correctly, though it resides in a different slice because the
        // relationship is lazy and hence two separate SQLs are issued for
        // Car and Manufacturer rather than a single SQL with JOIN.
        cars = em.createQuery("select c from Car c").getResultList();
        assertFalse(cars.isEmpty());
        for (Object c : cars)
            assertNotNull(((Car)c).getMaker());
    }

    void assertValidResult(List result) {
        assertNotNull(result);
        assertFalse(result.isEmpty());
        assertTrue(result.size() > 1);
    }
}
