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

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

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

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.Discriminator;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.inheritance.entity.AbstractClass;
import org.apache.openjpa.persistence.inheritance.entity.BaseClass;
import org.apache.openjpa.persistence.inheritance.entity.BaseClass2;
import org.apache.openjpa.persistence.inheritance.entity.BaseClass3;
import org.apache.openjpa.persistence.inheritance.entity.BaseClass4;
import org.apache.openjpa.persistence.inheritance.entity.BaseClass5;
import org.apache.openjpa.persistence.inheritance.entity.BaseClass6;
import org.apache.openjpa.persistence.inheritance.entity.ImplClassA;
import org.apache.openjpa.persistence.inheritance.entity.ManagedIface;
import org.apache.openjpa.persistence.inheritance.entity.ManagedIface2;
import org.apache.openjpa.persistence.inheritance.entity.MappedSuper;
import org.apache.openjpa.persistence.inheritance.entity.MidClass;
import org.apache.openjpa.persistence.inheritance.entity.MidClass2;
import org.apache.openjpa.persistence.inheritance.entity.MidClass3;
import org.apache.openjpa.persistence.inheritance.entity.SubclassA;
import org.apache.openjpa.persistence.inheritance.entity.SubclassB;
import org.apache.openjpa.persistence.inheritance.entity.SubclassC;
import org.apache.openjpa.persistence.inheritance.entity.SubclassD;
import org.apache.openjpa.persistence.inheritance.entity.SubclassE;
import org.apache.openjpa.persistence.inheritance.entity.SubclassF;
import org.apache.openjpa.persistence.inheritance.entity.SubclassG;
import org.apache.openjpa.persistence.inheritance.entity.SubclassH;
import org.apache.openjpa.persistence.inheritance.entity.SubclassI;
import org.apache.openjpa.persistence.inheritance.entity.SubclassJ;
import org.apache.openjpa.persistence.inheritance.entity.SubclassK;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/**
 * This test verifies that OpenJPA uses a single-table inheritance
 * strategy and default discriminator column if no inheritance strategy
 * is defined.
 *
 * OpenJPA JIRA: {@link http://issues.apache.org/jira/browse/OPENJPA-670}

 * @author Jeremy Bauer
 *
 */
public class TestDefaultInheritanceStrategy
    extends SingleEMFTestCase {

    @Override
    public void setUp() {
        setUp(BaseClass.class, SubclassA.class, SubclassB.class,
            SubclassC.class, MappedSuper.class, SubclassD.class,
            BaseClass2.class, MidClass.class, SubclassE.class,
            ManagedIface.class, ImplClassA.class,
            ManagedIface2.class, BaseClass3.class, SubclassF.class,
            BaseClass4.class, SubclassG.class,
            BaseClass5.class, MidClass2.class, SubclassH.class,
            AbstractClass.class, SubclassI.class, SubclassJ.class,
            BaseClass6.class, SubclassK.class,
            "openjpa.jdbc.FinderCache", "true",
            CLEAR_TABLES);
    }

    private Class[] classArray(Class... classes) {
        return classes;
    }

    /**
     * This variation tests a default simple class hierarchy with no inheritance
     * or discriminator column annotations defined.
     */
    public void testSimpleDefaultInheritance() {
        EntityManager em = emf.createEntityManager();

        // Create some entities
        SubclassA sca = new SubclassA();
        sca.setId(0);
        sca.setName("SubclassABaseClassName0");
        sca.setClassAName("SubclassAName0");

        SubclassA sca2 = new SubclassA();
        sca2.setId(1);
        sca2.setName("SubclassABaseClassName1");
        sca2.setClassAName("SubclassAName1");

        SubclassB scb = new SubclassB();
        scb.setId(2);
        scb.setName("SubclassBBaseClassName");
        scb.setClassBName("SubclassBName");

        BaseClass b = new BaseClass();
        b.setName("BaseClassName");
        b.setId(3);

        em.getTransaction().begin();
        em.persist(sca);
        em.persist(sca2);
        em.persist(scb);
        em.persist(b);
        em.getTransaction().commit();

        // test entity type discriminator queries for polymorphic results
        em.clear();
        String query = "select a from BaseClass a where TYPE(a) = BaseClass";
        List rs = em.createQuery(query).getResultList();
        assertTrue(rs.get(0) instanceof BaseClass);
        query = "select a from BaseClass a where TYPE(a) = SubclassA";
        rs = em.createQuery(query).getResultList();
        assertTrue(rs.get(0) instanceof SubclassA);

        em.clear();

        verifyDtypeColumnEntriesAndMapping(em, "BaseClass", 4, BaseClass.class);

        verifyInheritanceQueryResult(em, "SubclassA",
            classArray(SubclassA.class), 0, 1);

        verifyInheritanceQueryResult(em, "SubclassB",
            classArray(SubclassB.class), 2);

        verifyInheritanceQueryResult(em, "BaseClass",
            classArray(BaseClass.class), 0, 1, 2, 3);

        em.close();
    }

    /**
     * This variation ensures that a mapped superclass does not cause the
     * production of a discriminator column.
     */
    public void testMappedSuperclass() {
        EntityManager em = emf.createEntityManager();

        // Add two entities, each extending the same mapped interface
        em.getTransaction().begin();
        SubclassC sc = new SubclassC();
        sc.setId(1010);
        sc.setName("SubclassCMappedSuperName");
        sc.setClassCName("SubclassCName");

        SubclassD sd = new SubclassD();
        sd.setId(2020);
        sd.setName("SubclassDMappedSuperName");
        sd.setClassDName("SubclassDName");

        em.persist(sc);
        em.persist(sd);
        em.getTransaction().commit();

        em.clear();

        SubclassD sd2 =em.find(SubclassD.class, 2020);
        assertEquals(2020, sd2.getId());

        // The subclasses should not contain a discriminator column
        verifyNoDypeColumn(em, "SubclassC");
        verifyNoDypeColumn(em, "SubclassD");

        // Query the subclass entities.  Make sure the counts are correct and
        // the result is castable to the mapped sc.
        verifyInheritanceQueryResult(em, "SubclassC",
                classArray(SubclassC.class, MappedSuper.class), 1010);

        verifyInheritanceQueryResult(em, "SubclassD",
                classArray(SubclassD.class, MappedSuper.class), 2020);

        em.close();
    }

    /**
     * This variation ensures that a 3-level inheritance hierarchy uses
     * a discriminator column at the root class level.
     */
    public void testTwoLevelInheritance() {
        EntityManager em = emf.createEntityManager();

        // Add two entities, each extending the same mapped interface
        em.getTransaction().begin();
        SubclassE sc = new SubclassE();
        sc.setId(0);
        sc.setName("SubclassEBaseClassName");
        sc.setMidClassName("SubclassEMidClassName");
        sc.setClassEName("SubclassCName");

        MidClass mc = new MidClass();
        mc.setId(1);
        mc.setName("MidClassBaseClassName");
        mc.setMidClassName("MidClassName");

        BaseClass2 b2 = new BaseClass2();
        b2.setName("BaseClass2Name");
        b2.setId(2);

        em.persist(sc);
        em.persist(mc);
        em.persist(b2);
        em.getTransaction().commit();

        // test entity type discriminator queries for polymorphic results
        em.clear();
        String query = "select a from BaseClass2 a where TYPE(a) = MidClass";
        List rs = em.createQuery(query).getResultList();
        for (Object value : rs) assertTrue(value instanceof MidClass);

        query = "select a from BaseClass2 a where TYPE(a) = SubclassE";
        rs = em.createQuery(query).getResultList();
        for (Object o : rs) assertTrue(o instanceof SubclassE);

        query = "select a from BaseClass2 a where TYPE(a) = BaseClass2";
        rs = em.createQuery(query).getResultList();
        for (Object r : rs) assertTrue(r instanceof BaseClass2);

        em.clear();

        // Verify that baseclass2 contains a discriminator column
        verifyDtypeColumnEntriesAndMapping(em, "BaseClass2", 3,
                BaseClass2.class);

        // Verify that the subclass tables do not contain a discriminator column
        verifyNoDypeColumn(em, "MidClass");
        verifyNoDypeColumn(em, "SubclassE");

        // Query the subclass tables.  Make sure the counts are correct and
        // the result is castable to the mapped sc.
        verifyInheritanceQueryResult(em, "SubclassE",
                classArray(SubclassE.class, MidClass.class, BaseClass2.class),
                0);

        verifyInheritanceQueryResult(em, "MidClass",
                classArray(MidClass.class), 0, 1);

        verifyInheritanceQueryResult(em, "BaseClass2",
                classArray(BaseClass2.class), 0, 1, 2);

        em.close();
    }

    /**
     * This variation verifies that an entity with a managed interface
     * does not use a discriminator column.
     */
    public void testManagedInterface() {
        OpenJPAEntityManager em = emf.createEntityManager();

        // Add some entities
        em.getTransaction().begin();
        ManagedIface mif = em.createInstance(ManagedIface.class);
        mif.setIntFieldSup(10);

        ImplClassA ica = new ImplClassA();
        ica.setImplClassAName("ImplClassAName");
        ica.setIntFieldSup(11);

        em.persist(mif);
        em.persist(ica);
        em.getTransaction().commit();

        em.clear();

        // Verify that the iface table does not contain a discriminator column
        verifyNoDypeColumn(em, "ManagedIface");

        // Verify that the impl table does not contain a discriminator column
        verifyNoDypeColumn(em, "ImplClassA");

        // Query the subclass tables.  Make sure the counts are correct and
        // the result is castable to the entity and interface types.
        verifyInheritanceQueryResult(em, "ImplClassA",
                classArray(ImplClassA.class, ManagedIface.class), ica.getId());

        // Query the interface2 table.  Make sure the count is correct and
        // the result is castable to the interface type.
        verifyInheritanceQueryResult(em, "ManagedIface",
                classArray(ManagedIface.class), mif.getId(),
                ica.getId());

        em.close();
    }

    /**
     * This variation verifies that an entity with managed interface
     * and a superclass DOES use a discriminator column.
     */
    public void testManagedInterfaceAndBase() {
        OpenJPAEntityManager em = emf.createEntityManager();

        // Add some entities
        em.getTransaction().begin();
        ManagedIface2 mif2 = em.createInstance(ManagedIface2.class);
        mif2.setIntFieldSup(12);

        SubclassF scf = new SubclassF();
        scf.setClassFName("SubclassFName");
        scf.setIntFieldSup(13);

        BaseClass3 bc3 = new BaseClass3();
        bc3.setName("BaseClass3");

        em.persist(mif2);
        em.persist(scf);
        em.persist(bc3);
        em.getTransaction().commit();

        em.clear();

        // Verify that the iface table does not contain a discriminator column
        verifyNoDypeColumn(em, "ManagedIface2");

        // Verify that the subclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "SubclassF");

        // Verify that the base class does contain a discriminator column
        verifyDtypeColumnEntriesAndMapping(em, "BaseClass3", 2,
            BaseClass3.class);

        // Query the subclass table.  Make sure the counts are correct and
        // the result is castable to the entity and interface types.
        verifyInheritanceQueryResult(em, "SubclassF",
            classArray(SubclassF.class, ManagedIface2.class, BaseClass3.class),
            scf.getId());

        // Query the base class table.  Make sure the counts are correct and
        // the result is castable to the entity and interface types.
        verifyInheritanceQueryResult(em, "BaseClass3",
            classArray(BaseClass3.class),
            scf.getId(), bc3.getId());

        // Query the interface2 table.  Make sure the count is correct and
        // the result is castable to the interface type.
        verifyInheritanceQueryResult(em, "ManagedIface2",
                classArray(ManagedIface2.class),
                scf.getId(), mif2.getId());
        em.close();
    }

    /**
     * This variation tests a default simple class hierarchy with a inheritance
     * annotation defined on the subclass.
     */
    public void testSubclassSpecifiedInheritance() {
        EntityManager em = emf.createEntityManager();

        // Create some entities
        SubclassG scg = new SubclassG();
        scg.setId(0);
        scg.setName("SubclassGBaseClass4Name");
        scg.setClassGName("SubclassGName");

        BaseClass4 b = new BaseClass4();
        b.setName("BaseClass4Name");
        b.setId(1);

        em.getTransaction().begin();
        em.persist(scg);
        em.persist(b);
        em.getTransaction().commit();

        em.clear();

        verifyDtypeColumnEntriesAndMapping(em, "BaseClass4", 2,
            BaseClass4.class);

        // Verify that the subclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "SubclassG");

        // Run queries for each type. They should return only those values
        // which match their respective types.  This will not work for single
        // table inheritance unless a discriminator column is defined.
        verifyInheritanceQueryResult(em, "SubclassG",
            classArray(SubclassG.class, BaseClass4.class), 0);

        verifyInheritanceQueryResult(em, "BaseClass4",
                classArray(BaseClass4.class), 0, 1);

        em.close();
    }

    /**
     * This variation tests a default inheritance hierarchy with circular
     * relationships:
     *    BaseClass5 has rel to SubclassH
     *    MidClass2 extends BaseClass5 inherits rel to SubclassH
     *    SubClassH extends MidClass2 has rel to BaseClass5
     */
    public void testCircularInheritedRelationships() {
        EntityManager em = emf.createEntityManager();

        // Create and persist some related & inherited entities
        SubclassH sch = new SubclassH();
        sch.setId(1);
        sch.setClassHName("SubclassHName");
        sch.setName("SubclassHBaseClass5Name");
        sch.setMidClass2Name("SubclassHMidClass2Name");

        BaseClass5 bc5 = new BaseClass5();
        bc5.setId(2);
        bc5.setName("BaseClass5Name");
        bc5.setSubclassh(sch);

        sch.setBaseclass5(bc5);

        MidClass2 mc2 = new MidClass2();
        mc2.setId(3);
        mc2.setMidClass2Name("MidClass2Name");
        mc2.setName("MidClass2BaseClass5Name");
        mc2.setSubclassh(sch);

        em.getTransaction().begin();
        em.persist(sch);
        em.persist(bc5);
        em.persist(mc2);
        em.getTransaction().commit();

        em.clear();

        verifyDtypeColumnEntriesAndMapping(em, "BaseClass5", 3,
                BaseClass5.class);

        // Verify that the midclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "MidClass2");

        // Verify that the subclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "SubclassH");

        // Run queries for each type. They should return only those values
        // which match their respective types.  This will not work for single
        // table inheritance unless a discriminator column is defined.
        verifyInheritanceQueryResult(em, "SubclassH",
            classArray(SubclassH.class, MidClass2.class, BaseClass5.class),
            1);

        verifyInheritanceQueryResult(em, "MidClass2",
                classArray(MidClass2.class, BaseClass5.class),
                1, 3);

        verifyInheritanceQueryResult(em, "BaseClass5",
                classArray(BaseClass5.class),
                1, 2, 3);

        em.clear();

        // Validate entity relationships
        sch = em.find(SubclassH.class, 1);
        assertEquals(sch.getName(),"SubclassHBaseClass5Name");
        assertEquals(sch.getMidClass2Name(), "SubclassHMidClass2Name");
        // SubclassH has relationship to BaseClass5
        assertEquals(sch.getBaseclass5().getId(), 2);

        bc5 = em.find(BaseClass5.class, 3);
        assertEquals(bc5.getName(),"MidClass2BaseClass5Name");
        // BaseClass5 has relationship to SubclassH through MidClass2
        assertEquals(bc5.getSubclassh().getId(), 1);

        bc5 = em.find(BaseClass5.class, 2);
        assertEquals(bc5.getName(),"BaseClass5Name");
        // BaseClass5 has relationship to SubclassH
        assertEquals(bc5.getSubclassh().getId(), 1);

        mc2 = em.find(MidClass2.class, 3);
        assertEquals(mc2.getName(),"MidClass2BaseClass5Name");
        assertEquals(mc2.getMidClass2Name(), "MidClass2Name");
        // MidClass2 has relationship to SubclassH
        assertEquals(bc5.getSubclassh().getId(), 1);

        em.close();
    }

    /**
     * This variation verifies default inheritance with an abstract
     * entity.
     */
    public void testAbstractEntityInheritance() {
        EntityManager em = emf.createEntityManager();

        SubclassI sci = new SubclassI();
        sci.setId(1);
        sci.setClassIName("SubclassIName");
        sci.setName("SubclassIBaseClassName");

        SubclassJ scj = new SubclassJ();
        scj.setId(2);
        scj.setClassJName("SubclassJName");
        scj.setName("SubclassJBaseClassName");

        em.getTransaction().begin();
        em.persist(sci);
        em.persist(scj);
        em.getTransaction().commit();

        em.clear();

        verifyDtypeColumnEntriesAndMapping(em, "AbstractClass", 2,
                AbstractClass.class);

        // Verify that the midclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "SubclassI");

        // Verify that the subclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "SubclassJ");

        // Run queries for each type. They should return only those values
        // which match their respective types.  This will not work for single
        // table inheritance unless a discriminator column is defined.
        verifyInheritanceQueryResult(em, "AbstractClass",
            classArray(AbstractClass.class), 1, 2);

        verifyInheritanceQueryResult(em, "SubclassI",
                classArray(AbstractClass.class, SubclassI.class), 1);

        verifyInheritanceQueryResult(em, "SubclassJ",
                classArray(AbstractClass.class, SubclassJ.class), 2);

        em.close();
    }

    /**
     * This variation verifies that default inheritance is used when
     * there is a non-entity superclass in the mix:
     *   non-entity MidClass3 extends BaseClass6
     *   SubClassJ extends MidClass3
     */
    public void testMidNonEntityInheritance() {
        EntityManager em = emf.createEntityManager();

        SubclassK sck = new SubclassK();
        sck.setId(1);
        sck.setClassKName("SubclassKName");
        sck.setMidClass3Name("SubclassKMidClass3Name");
        sck.setName("SubclassKBaseClass6Name");

        BaseClass6 bk6 = new BaseClass6();
        bk6.setId(3);
        bk6.setName("BaseClass6Name");

        em.getTransaction().begin();
        em.persist(sck);
        em.persist(bk6);
        em.getTransaction().commit();

        em.clear();

        verifyDtypeColumnEntriesAndMapping(em, "BaseClass6", 2,
                BaseClass6.class);

        // Verify that the subclass table does not contain a discriminator
        // column
        verifyNoDypeColumn(em, "SubclassK");

        // Run queries for each type. They should return only those values
        // which match their respective types.  This will not work for single
        // table inheritance unless a discriminator column is defined.
        verifyInheritanceQueryResult(em, "BaseClass6",
            classArray(BaseClass6.class), 1, 3);

        verifyInheritanceQueryResult(em, "SubclassK",
                classArray(BaseClass6.class, MidClass3.class, SubclassK.class),
                1);

        em.close();
    }

    public void testFinder() {
        EntityManager em = emf.createEntityManager();
        SubclassK sck = new SubclassK();
        sck.setId(479);
        sck.setClassKName("SubclassKName");
        sck.setMidClass3Name("SubclassKMidClass3Name");
        sck.setName("SubclassKBaseClass6Name");

        BaseClass6 bk6 = new BaseClass6();
        bk6.setId(302);
        bk6.setName("BaseClass6Name");

        SubclassI sci = new SubclassI();
        sci.setId(109);
        sci.setClassIName("SubclassIName");
        sci.setName("SubclassIBaseClassName");

        SubclassJ scj = new SubclassJ();
        scj.setId(238);
        scj.setClassJName("SubclassJName");
        scj.setName("SubclassJBaseClassName");

        em.getTransaction().begin();
        em.persist(sck);
        em.persist(bk6);
        em.persist(sci);
        em.persist(scj);
        em.getTransaction().commit();

        em.clear();

        verifyInheritanceFinderResult(em, SubclassK.class, 479);
        verifyInheritanceFinderResult(em, BaseClass6.class, 479, 302);
        verifyInheritanceFinderResult(em, SubclassI.class, 109);
        verifyInheritanceFinderResult(em, SubclassJ.class, 238);
        em.close();
    }

    /**
     * Verifies that a table contains the specified number of entries
     * in its DTYPE (default discriminator) column.
     * @param em  Entity nanager
     * @param table  Name of the table to query
     * @param entries  Expected column entry count
     * @param baseClass Class mapping to verify
     */
    private void verifyDtypeColumnEntriesAndMapping(EntityManager em,
        String table, int entries, Class baseClass) {
        try {
            Query qry = em.createNativeQuery("SELECT DTYPE FROM " + table);
            List vals = qry.getResultList();
            assertTrue("Query should have returned " + entries + " values",
                vals.size() == entries);
        }
        catch (Exception e) {
            fail("Exception querying DTYPE column: " + e.getMessage());
        }

        // Check the discriminator column of the class mapping.
        ClassMapping cm = getMapping(baseClass);
        Discriminator d = cm.getDiscriminator();
        Column[] cols = d.getColumns();
        assertTrue("Discriminator should use DTYPE column",
            (cols != null && cols.length == 1 &&
            cols[0].getName().equals("DTYPE")));
    }

    /**
     * Verifies that a table does not contain a DTYPE column.
     * @param em  Entity manager
     * @param table Name of the table to query
     */
    private void verifyNoDypeColumn(EntityManager em, String table) {
        try {
            Query qry = em.createNativeQuery("SELECT DTYPE FROM " + table);
            qry.getResultList();
            fail("Expected exception.  DTYPE column should not exist on " +
                table);
        }
        catch (Exception e) {
            // Expected exception
        }
    }

    /**
     * Verifies the resulting entity count and expected entity ids from a
     * simple entity query.  This method requires a "getId" method on the
     * entity type in order to work properly.
     *
     * @param em entity manager
     * @param entity entity name
     * @param entityType entity class
     * @param expectedValues variable list of expected integral id values.
     */
    private void verifyInheritanceQueryResult(EntityManager em, String entity,
        Class[] types, int... expectedValues) {
    	String jpql = "SELECT e FROM " + entity + " e";
        Query qry = em.createQuery(jpql);
        List col = qry.getResultList();
        assertTrue("Query should return " + expectedValues.length + " entities",
            col.size() == expectedValues.length);
        int count = 0;
        for (Object ent : col) {
            // If a list of supers or interfaces is provided, make sure
            // the returned type is an instance of those types
            if (types != null) {
                for (Class type : types) assertTrue(type.isInstance(ent));
            }
            int id = -1;
            try {
                Method mth = ent.getClass().getMethod("getId", (Class[]) null);
                id = (Integer) mth.invoke(ent, (Object[]) null);
            }
            catch (Exception e) {
                fail("Caught unexepcted exception getting entity id: "
                        + e.getMessage());
            }
            for (int expectedValue : expectedValues)
                if (expectedValue == id)
                    count++;
        }
        assertTrue("Returned unexpected entities " + col + " for " + jpql,
                count == expectedValues.length);
    }

    private void verifyInheritanceFinderResult(EntityManager em,
        Class entityType, int... ids) {
        for (int j = 0; j < 2; j++) {
            em.clear();
            for (int id : ids) {
                Object pc = em.find(entityType, id);
                assertTrue(entityType.isAssignableFrom(pc.getClass()));
            }
        }
    }

}
