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

import java.util.Iterator;
import java.util.List;

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

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.DerbyDictionary;
import org.apache.openjpa.jdbc.sql.SolidDBDictionary;
import org.apache.openjpa.persistence.simple.AllFieldTypes;
import org.apache.openjpa.persistence.test.SingleEMTestCase;

/**
 * <p>Tests grouping and having capabilities.</p>
 *
 * @author Abe White
 */
public abstract class GroupingTestCase
    extends SingleEMTestCase {

    protected abstract void prepareQuery(Query q);

    @Override
    public void setUp() {
        super.setUp(AllFieldTypes.class, CLEAR_TABLES,
            "openjpa.Compatibility", "JPQL=warn");

        AllFieldTypes pc1 = new AllFieldTypes();
        AllFieldTypes pc2 = new AllFieldTypes();
        AllFieldTypes pc3 = new AllFieldTypes();
        AllFieldTypes pc4 = new AllFieldTypes();

        // pc1 and pc2, pc3 and pc4 grouped on intField, shortField
        pc1.setIntField(1);
        pc1.setShortField((short) -1);
        pc2.setIntField(1);
        pc2.setShortField((short) -1);
        pc3.setIntField(2);
        pc3.setShortField((short) -2);
        pc4.setIntField(2);
        pc4.setShortField((short) -2);

        // pc1 and pc2 grouped on stringField
        pc1.setStringField("abc");
        pc2.setStringField("acd");
        pc3.setStringField("def");
        pc4.setStringField("efg");

        // pc2 and pc3 grouped on byteField
        pc2.setByteField((byte) 1);
        pc3.setByteField((byte) 1);
        pc1.setByteField((byte) 0);
        pc4.setByteField((byte) 2);

        // longField is unique id
        pc1.setLongField(1L);
        pc2.setLongField(2L);
        pc3.setLongField(3L);
        pc4.setLongField(4L);

        // set up some relations
        pc1.setSelfOneOne(pc4);
        pc2.setSelfOneOne(pc3);
        pc3.setSelfOneOne(pc2);
        pc4.setSelfOneOne(pc1);

        // if variable testing, set up some 1-Ms instead of the 1-1s above
        if (getName().startsWith("testVariable")) {
            pc1.setSelfOneOne(pc1);
            pc2.setSelfOneOne(pc1);
            pc1.getSelfOneMany().add(pc1);
            pc1.getSelfOneMany().add(pc2);

            pc3.setSelfOneOne(pc3);
            pc4.setSelfOneOne(pc3);
            pc3.getSelfOneMany().add(pc3);
            pc3.getSelfOneMany().add(pc4);
        }

        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        em.persist(pc1);
        em.persist(pc2);
        em.persist(pc3);
        em.persist(pc4);
        em.getTransaction().commit();
        em.close();
    }

    public void testSimpleGroup() {
        Query q = em.createQuery("select o.intField from AllFieldTypes o " +
            "group by o.intField order by o.intField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        assertEquals(1, itr.next());
        assertEquals(2, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testOrderByAggregate() {
        // this is an extension of JPQL
        Query q = em.createQuery("select sum(o.shortField) " +
            "from AllFieldTypes o"
            + " group by o.intField order by sum(o.shortField) asc");
        prepareQuery(q);
        // this might fail in MySQL/MariaDB
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        assertEquals((long) -4, itr.next());
        assertEquals((long) -2, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testCompoundGroupSame() {
        Query q = em.createQuery("select o.intField from AllFieldTypes o " +
            "group by o.intField, o.shortField order by o.shortField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        assertEquals(2, itr.next());
        assertEquals(1, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testCompoundGroupDifferent() {
        Query q = em.createQuery("select o.intField from AllFieldTypes o " +
            "group by o.intField, o.byteField order by o.intField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(4, res.size());
        Iterator itr = res.iterator();
        assertEquals(1, itr.next());
        assertEquals(1, itr.next());
        assertEquals(2, itr.next());
        assertEquals(2, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testDifferentGroupLengths() {
        Query q = em.createQuery("select o.byteField from AllFieldTypes o"
            + " group by o.byteField order by o.byteField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(3, res.size());
        Iterator itr = res.iterator();
        assertEquals((byte) 0, itr.next());
        assertEquals((byte) 1, itr.next());
        assertEquals((byte) 2, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testGroupRelationField() {
        Query q = em.createQuery("select o.selfOneOne.intField " +
            "from AllFieldTypes o group by o.selfOneOne.intField " +
            "order by o.selfOneOne.intField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        assertEquals(1, itr.next());
        assertEquals(2, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testSubstringInGroupBy() {
        DBDictionary dict = ((JDBCConfiguration)emf.getConfiguration()).getDBDictionaryInstance();
        if (dict instanceof SolidDBDictionary)
            return;

        // this is an extension of JPQL
        Query q = em.createQuery("select substring(o.stringField, 1, 1), " +
            "count(o) from AllFieldTypes o " +
            "group by substring(o.stringField, 1, 1)");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(3, res.size());

        q = em.createQuery("select substring(o.stringField, 1, 2), count(o) " +
            "from AllFieldTypes o group by substring(o.stringField, 1, 2)");
        prepareQuery(q);
        res = q.getResultList();
        assertEquals(4, res.size());
    }

    public void testGroupedAggregate() {
        Query q = em.createQuery("select count(o) from AllFieldTypes o " +
            "group by o.byteField order by o.byteField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(3, res.size());
        Iterator itr = res.iterator();
        assertEquals(1L, itr.next());
        assertEquals(2L, itr.next());
        assertEquals(1L, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testGroupedRelationAggregate() {
        Query q = em.createQuery("select count(o), max(o.selfOneOne.longField)"
            + " from AllFieldTypes o group by o.intField"
            + " order by o.intField asc");
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        Object[] o = (Object[]) itr.next();
        assertEquals(2L, o[0]);
        assertEquals(4L, o[1]);
        o = (Object[]) itr.next();
        assertEquals(2L, o[0]);
        assertEquals(2L, o[1]);
        assertTrue(!itr.hasNext());
    }

    public void testGroupedMixedProjection() {
        Query q = em.createQuery("select count(o), o.shortField " +
            "from AllFieldTypes o group by o.intField, o.shortField " +
            "order by o.intField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        Object[] o = (Object[]) itr.next();
        assertEquals(2L, o[0]);
        assertEquals((short) -1, o[1]);
        o = (Object[]) itr.next();
        assertEquals(2L, o[0]);
        assertEquals((short) -2, o[1]);
        assertTrue(!itr.hasNext());
    }

    public void testSimpleHaving() {
        Query q = em.createQuery("select o.intField from AllFieldTypes o " +
            "group by o.intField having o.intField < 2");
        prepareQuery(q);
        assertEquals(1, q.getSingleResult());
    }

    public void testAggregateHaving() {
        Query q = em.createQuery("select o.byteField from AllFieldTypes o " +
            "group by o.byteField having count(o) > 1");
        prepareQuery(q);
        assertEquals((byte) 1, q.getSingleResult());
    }

    public void testMixedHaving() {
        Query q = em.createQuery("select o.byteField from AllFieldTypes o " +
            "group by o.byteField having count(o) > 1 or o.byteField = 0 " +
            "order by o.byteField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        assertEquals((byte) 0, itr.next());
        assertEquals((byte) 1, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testVariableGroup() {
        Query q = em.createQuery("select max(other.longField) " +
            "from AllFieldTypes o, AllFieldTypes other " +
            "where other member of o.selfOneMany " +
            "group by other.intField order by other.intField asc");
        prepareQuery(q);
        List res = q.getResultList();
        assertEquals(2, res.size());
        Iterator itr = res.iterator();
        assertEquals(2L, itr.next());
        assertEquals(4L, itr.next());
        assertTrue(!itr.hasNext());
    }

    public void testVariableHaving() {
        JDBCConfiguration conf = (JDBCConfiguration) em.getConfiguration();
        DBDictionary dict = conf.getDBDictionaryInstance();
        if (dict instanceof DerbyDictionary) {
            // This test fails on Derby 10.5.3.0, so just skip it...
            return;
        }

        Query q = em.createQuery("select max(o.longField), other.byteField " +
            "from AllFieldTypes o, AllFieldTypes other " +
            "where other member of o.selfOneMany " +
            "group by other.byteField having sum(other.intField) = 2");
        prepareQuery(q);
        assertEquals(3L, ((Object[])q.getSingleResult())[0]);
    }
}
