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

import java.lang.reflect.Array;
import java.util.List;

import jakarta.persistence.Tuple;
import jakarta.persistence.criteria.CompoundSelection;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Root;

import org.apache.openjpa.persistence.criteria.CriteriaTest;
import org.apache.openjpa.persistence.criteria.Person;
import org.apache.openjpa.persistence.criteria.Person_;


/**
 * Test variations of {@link CriteriaQuery#multiselect(java.util.List)} arguments.
 *
 * @author Pinaki Poddar
 *
 */

public class TestMultiselect extends CriteriaTest {
    private static boolean initialized = false;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        if (!initialized) {
            clearData();
            createData();
            initialized = true;
        }
    }

    void clearData() {
        em.getTransaction().begin();
        em.createQuery("DELETE FROM Foo f").executeUpdate();
        em.createQuery("DELETE FROM Bar b").executeUpdate();
        em.createQuery("DELETE FROM Person p").executeUpdate();
        em.getTransaction().commit();
    }

    void createData() {
        em.getTransaction().begin();
        Person p = new Person("Test Result Shape");
        em.persist(p);

        Foo foo = new Foo(100L, "Test Foo");
        Bar bar = new Bar(200L, "Test Bar");
        foo.setBar(bar);
        em.persist(foo);
        em.persist(bar);
        em.getTransaction().commit();
    }

    /**
    * If the type of the criteria query is CriteriaQuery<Tuple>
    * (i.e., a criteria query object created by either the
    * createTupleQuery method or by passing a Tuple class argument
    * to the createQuery method), a Tuple object corresponding to
    * the elements of the list passed to the multiselect method
    * will be instantiated and returned for each row that results
    * from the query execution.
    * */

    public void testTupleQuery() {
        CriteriaQuery<Tuple> q = cb.createTupleQuery();
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name), p.get(Person_.id));

        assertResult(q, Tuple.class, String.class, Integer.class);
    }

    public void testTupleQueryExplicit() {
        CriteriaQuery<Tuple> q = cb.createQuery(Tuple.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name), p.get(Person_.id));

        assertResult(q, Tuple.class, String.class, Integer.class);
    }

    //=======================================================================

    /**
     * If the type of the criteria query is CriteriaQuery<X> for
     * some user-defined class X (i.e., a criteria query object
     * created by passing a X class argument to the createQuery
     * method), then the elements of the list passed to the
     * multiselect method will be passed to the X constructor and
     * an instance of type X will be returned for each row.
     */
    public void testUserResultQueryWithExplictProjectionOfConstructorArguments() {
        CriteriaQuery<Person> q = cb.createQuery(Person.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name));

        assertResult(q, Person.class);
    }

    public void testUserResultQueryWithImplicitProjection() {
        CriteriaQuery<Person> q = cb.createQuery(Person.class);
        q.from(Person.class);

        assertResult(q, Person.class);
    }

    public void testUserResultQueryWithMismatchProjectionOfConstructorArguments() {
        CriteriaQuery<Person> q = cb.createQuery(Person.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name), p.get(Person_.id));

        fail("Person has no constrcutor with (name,id)", q);
    }

    public void testMultipleConstructorWithAliasRepeatedAndInOrderingClause() {
        // SELECT NEW(p.name), p.id, NEW(p.name), p.name FROM Person p ORDER BY p.name
        CriteriaQuery<Tuple> q = cb.createTupleQuery();
        Root<Person> p = q.from(Person.class);
        q.multiselect(
            cb.construct(Person.class, p.get(Person_.name)),
            p.get(Person_.id),
            cb.construct(Person.class, p.get(Person_.name)),
            p.get(Person_.name))
            .orderBy(cb.asc(p.get(Person_.name)));

        List<Tuple> tuples = em.createQuery(q).getResultList();
        assertTrue(!tuples.isEmpty());
        for (Tuple row : tuples) {
            assertEquals(4, row.getElements().size());

            assertEquals(Person.class,  row.get(0).getClass());
            assertEquals(Integer.class, row.get(1).getClass());
            assertEquals(Person.class,  row.get(2).getClass());
            assertEquals(String.class,  row.get(3).getClass());

            assertEquals(((Person)row.get(0)).getName(), ((Person)row.get(2)).getName());
            assertEquals(((Person)row.get(0)).getName(), row.get(3));
        }
    }

    // ======================================================================
    /**
     * If the type of the criteria query is CriteriaQuery<X[]> for
     * some class X, an instance of type X[] will be returned for
     * each row.   The elements of the array will correspond to the
     * elements of the list passed to the multiselect method.
     */

    public void testUserClassArray() {
        CriteriaQuery<Person[]> q = cb.createQuery(Person[].class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p,p);

        assertResult(q, Person[].class, Person.class, Person.class);
    }

    public void testBasicClassArray() {
        CriteriaQuery<String[]> q = cb.createQuery(String[].class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name), p.get(Person_.name));

        assertResult(q, String[].class);
    }

    public void testTupleArray() {
        CriteriaQuery<Tuple[]> q = cb.createQuery(Tuple[].class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name), p.get(Person_.id), p.get(Person_.name));

        assertResult(q, Tuple[].class, String.class, Integer.class, String.class);
    }
// =================================================================
    /**
     * If the type of the criteria query is CriteriaQuery<Object>
     * or if the criteria query was created without specifying a
     * type, and the list passed to the multiselect method contains
     * only a single element, an instance of type Object will be
     * returned for each row.
     */
    public void testSingleObject() {
        CriteriaQuery<Object> q = cb.createQuery(Object.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p);

        assertResult(q, Object.class);
    }

    public void testSingleObjectViaConstructor() {
        CriteriaQuery<Object> q = cb.createQuery(Object.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(cb.construct(Person.class, p.get(Person_.name)));

        assertResult(q, Object.class);
    }

    public void testSingleObjectAsProperty() {
        CriteriaQuery<Object> q = cb.createQuery(Object.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name));

        assertResult(q, Object.class);
    }

    public void testSingleObjectImplicit() {
        CriteriaQuery<?> q = cb.createQuery();
        Root<Person> p = q.from(Person.class);
        q.multiselect(p);

        assertResult(q, Object.class);
    }

    public void testSingleObjectViaConstructorImplicit() {
        CriteriaQuery<?> q = cb.createQuery();
        Root<Person> p = q.from(Person.class);
        q.multiselect(cb.construct(Person.class, p.get(Person_.name)));

        assertResult(q, Object.class);
    }

    public void testSingleObjectAsPropertyImplicit() {
        CriteriaQuery<?> q = cb.createQuery();
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name));

        assertResult(q, Object.class);
    }

// ================================================================================
    /**
     * If the type of the criteria query is CriteriaQuery<Object>
     * or if the criteria query was created without specifying a
     * type, and the list passed to the multiselect method contains
     * more than one element, an instance of type Object[] will be
     * instantiated and returned for each row.  The elements of the
     * array will correspond to the elements of the list passed to
     * the multiselect method.
     */
    public void testSingleObjectMultipleProjections() {
        CriteriaQuery<Object> q = cb.createQuery(Object.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(p.get(Person_.name), p.get(Person_.id));

        assertResult(q, Object[].class, String.class, Integer.class);
    }

    public void testSingleObjectMultipleProjectionsAndConstructor() {
        CriteriaQuery<Object> q = cb.createQuery(Object.class);
        Root<Person> p = q.from(Person.class);
        q.multiselect(cb.construct(Person.class, p.get(Person_.name)), p.get(Person_.id), p.get(Person_.name));

        assertResult(q, Object[].class, Person.class, Integer.class, String.class);
    }

    /**
     * An element of the list passed to the multiselect method
     * must not be a tuple- or array-valued compound selection item.
     *
     */
    // This test is retired because we are now supporting arbitrary nesting
    public void xtestTupleCanNotBeNested() {
        CriteriaQuery<Tuple> q = cb.createTupleQuery();
        Root<Person> p = q.from(Person.class);

        CompoundSelection<Tuple> tuple1 = cb.tuple(p.get(Person_.name), p.get(Person_.id));
        CompoundSelection<Tuple> tuple2 = cb.tuple(p.get(Person_.id), p.get(Person_.name));

        try {
            cb.tuple(tuple1, tuple2);
            fail("Expected exception while nesting tuples");
        } catch (IllegalArgumentException e) {

        }
    }

    public void testMultiConstructor() {
        // SELECT NEW Foo(f.fid,f.fint), b, NEW FooBar(f.fid, b.bid) from Foo f JOIN f.bar b WHERE f.b=b
        CriteriaQuery<Object[]> q = cb.createQuery(Object[].class);
        Root<Foo> f = q.from(Foo.class);
        Join<Foo, Bar> b = f.join(Foo_.bar);
        q.multiselect(cb.construct(Foo.class, f.get(Foo_.fid), f.get(Foo_.fstring)),
                      b,
                      cb.construct(FooBar.class, f.get(Foo_.fid), b.get(Bar_.bid)));
        q.where(cb.equal(f.get(Foo_.fid), 100L));

        List<Object[]> result = em.createQuery(q).getResultList();
        assertFalse(result.isEmpty());
        for (Object[] row : result) {
            assertEquals(3, row.length);
            assertTrue("0-th element " + row[0].getClass() + " is not Foo", row[0] instanceof Foo);
            assertTrue("1-st element " + row[1].getClass() + " is not Bar", row[1] instanceof Bar);
            assertTrue("2-nd element " + row[2].getClass() + " is not FooBar", row[2] instanceof FooBar);
        }
    }

    public void testSelectSingleTermWithMultiselectObjectArray() {
        CriteriaQuery<Object[]> q = cb.createQuery(Object[].class);
        Root<Foo> f = q.from(Foo.class);
        q.multiselect(f);

        assertResult(q, Object[].class, Foo.class);
    }

    public void testSelectSingleTermWithMultiselectObject() {
        CriteriaQuery<Object> q = cb.createQuery(Object.class);
        Root<Foo> f = q.from(Foo.class);
        q.multiselect(f);

        assertResult(q, Foo.class);
    }

    public void testSelectSingleTermWithMultiselectTuple() {
        CriteriaQuery<Tuple> q = cb.createQuery(Tuple.class);
        Root<Foo> f = q.from(Foo.class);
        q.multiselect(f);

        assertResult(q, Tuple.class, Foo.class);
    }

    public void testSelectSingleTermWithMultiselectTupleArray() {
        CriteriaQuery<Tuple[]> q = cb.createQuery(Tuple[].class);
        Root<Foo> f = q.from(Foo.class);
        q.multiselect(f);

        assertResult(q, Tuple[].class, Foo.class);
    }

    public void testSanity() {
        CriteriaQuery<Foo> q = cb.createQuery(Foo.class);
        Root<Foo> f = q.from(Foo.class);

        assertResult(q, Foo.class);
    }

    public void testSanity2() {
        CriteriaQuery<Foo> q = cb.createQuery(Foo.class);
        Root<Foo> f = q.from(Foo.class);
        q.select(f);
        assertResult(q, Foo.class);
    }

    public void testDeeplyNestedShape() {
        CriteriaQuery<Tuple> q = cb.createQuery(Tuple.class);
        Root<Foo> foo = q.from(Foo.class);
        q.multiselect(cb.construct(Foo.class, foo.get(Foo_.flong), foo.get(Foo_.fstring)),
                 cb.tuple(foo, cb.array(foo.get(Foo_.fint), cb.tuple(foo.get(Foo_.fstring)))));
        List<Tuple> result = em.createQuery(q).getResultList();
        assertFalse(result.isEmpty());
        Tuple tuple = result.get(0);

        assertEquals(Foo.class,   tuple.get(0).getClass());
        assertTrue(Tuple.class.isAssignableFrom(tuple.get(1).getClass()));
        Tuple tuple2 = (Tuple)tuple.get(1);
        assertEquals(Foo.class,   tuple2.get(0).getClass());
        assertEquals(Object[].class, tuple2.get(1).getClass());
        Object[] level3 = (Object[])tuple2.get(1);
        assertEquals(Integer.class, level3[0].getClass());
        assertTrue(Tuple.class.isAssignableFrom(level3[1].getClass()));
        Tuple tuple4 = (Tuple)level3[1];
        assertEquals(String.class, tuple4.get(0).getClass());
    }

    public void testConstructorFailsFast() {
        CriteriaQuery<Tuple> q = cb.createQuery(Tuple.class);
        Root<Foo> foo = q.from(Foo.class);
        try {
            q.multiselect(cb.construct(Foo.class, foo.get(Foo_.flong)));
            fail("Expected IllegalArgumentException becuase Foo(long) is not a valid constructor");
        } catch (IllegalArgumentException e) {
            // good -- but print the error message to check it is informative enough
            System.err.println(e);
        }
        try {
            q.multiselect(cb.construct(Foo.class));
            fail("Expected IllegalArgumentException becuase Foo() is not a valid constructor");
        } catch (IllegalArgumentException e) {
            // good -- but print the error message to check it is informative enough
            System.err.println(e);
        }

    }

// =============== assertions by result types ========================

    void assertResult(CriteriaQuery<?> q, Class<?> resultClass) {
        assertResult(q, resultClass, (Class<?>[])null);
    }
    /**
     * Assert the query result elements by their types
     */
    void assertResult(CriteriaQuery<?> q, Class<?> resultClass, Class<?>... arrayElementClasses) {
        List<?> result = em.createQuery(q).getResultList();
        assertFalse(result.isEmpty());
        for (Object row : result) {
            assertTrue(toClass(row) + " does not match actual result " + toString(resultClass),
                resultClass.isInstance(row));
            if (resultClass.isArray() && arrayElementClasses != null) {
                for (int i = 0; i < arrayElementClasses.length; i++) {
                    Object element = Array.get(row, i);
                    if (Tuple.class.isInstance(element)) {
                        assertEquals(arrayElementClasses[i], Tuple.class.cast(element).get(0).getClass());
                    } else {
                    assertTrue(i + "-th array element " + toString(arrayElementClasses[i]) +
                       " does not match actual result " + toClass(element), arrayElementClasses[i].isInstance(element));
                    }
                }
            }
            if (resultClass == Tuple.class && arrayElementClasses != null) {
                Tuple tuple = (Tuple)row;
                for (int i = 0; i < arrayElementClasses.length; i++) {
                    Object element = tuple.get(i);
                    assertTrue(i + "-th tuple element " + toString(arrayElementClasses[i]) +
                       " does not match actual result " + toClass(element), arrayElementClasses[i].isInstance(element));
                }
            }
        }
    }


    void fail(String msg, CriteriaQuery<?> q) {
        try {
            em.createQuery(q).getResultList();
            fail("Expected to fail " + msg);
        } catch (Exception e) {
            // this is an expected exception
        }
    }

    String toClass(Object o) {
       return toString(o.getClass());
    }

    String toString(Class<?> cls) {
        return cls.isArray() ? toString(cls.getComponentType())+"[]" : cls.toString();
    }
}
