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

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

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;

import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.persistence.JPAFacadeHelper;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAQuery;
import org.apache.openjpa.persistence.common.utils.AbstractTestCase;
import org.apache.openjpa.persistence.enhance.common.apps.
        BackingFieldNameMismatchInstance;
import org.apache.openjpa.persistence.enhance.common.apps.BaseEntity;
import org.apache.openjpa.persistence.enhance.common.apps.BasicSubclassInstance;
import org.apache.openjpa.persistence.enhance.common.apps.DerivedEntity;
import org.apache.openjpa.persistence.enhance.common.apps.Entity1;
import org.apache.openjpa.persistence.enhance.common.apps.
        ManagedInverseTestInstance;
import org.apache.openjpa.persistence.enhance.common.apps.
        ManagedInverseTestInstance2;
import org.apache.openjpa.persistence.enhance.common.apps.SubclassTestInstance;
import org.apache.openjpa.util.ExceptionInfo;
import org.apache.openjpa.util.ImplHelper;

import junit.framework.AssertionFailedError;

public class TestSubclassedBehavior extends AbstractTestCase {

    public TestSubclassedBehavior(String name) {
        super(name, "enhancecactusapp");
    }


    @Override
    public void setUp() {
        deleteAll(BasicSubclassInstance.class);
        deleteAll(BackingFieldNameMismatchInstance.class);
        deleteAll(BaseEntity.class);
        deleteAll(ManagedInverseTestInstance.class);
        deleteAll(ManagedInverseTestInstance2.class);
    }

    public void testInheritance() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        DerivedEntity de = (DerivedEntity) newInstance(pm, DerivedEntity.class);
        BasicSubclassInstance basic = (BasicSubclassInstance) newInstance(pm,
            BasicSubclassInstance.class);
        basic.setStringField("basic one-to-one");
        de.setOneToOne(basic);
        Object oid = persistenceOperations(pm, de, false);

        // ##### need a test case for JDOHelper.createEntityManager() for
        // subclass
        ClassMetaData meta = JPAFacadeHelper.getMetaData(pm, de.getClass());
        assertEquals(BaseEntity.class, meta.getPCSuperclass());

        pm = (OpenJPAEntityManager) currentEntityManager();

        Object o = pm.find(DerivedEntity.class, oid);
        assertTrue(o instanceof DerivedEntity);
        de = (DerivedEntity) o;
        Broker b = JPAFacadeHelper.toBroker(pm);
        OpenJPAStateManager sm = b.getStateManager(de);
        // we use getLoaded() here because isLoaded() always returns true.
        assertFalse(sm.getLoaded().get(
            sm.getMetaData().getField("oneToOne").getIndex()));
        assertEquals("basic one-to-one", de.getOneToOne().getStringField());
        assertTrue(sm.getLoaded().get(sm.getMetaData()
            .getField("oneToOne").getIndex()));

        startTx(pm);
        pm.remove(de);
        endTx(pm);
        endEm(pm);
    }

    public void testBasicSubclassPersistenceOperations()
        throws ClassNotFoundException {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BasicSubclassInstance o = (BasicSubclassInstance) newInstance(pm,
            BasicSubclassInstance.class);
        persistenceOperations(pm, o, true);
    }

    public void testBackingFieldNameMismatch() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BackingFieldNameMismatchInstance o = (BackingFieldNameMismatchInstance)
            newInstance(pm, BackingFieldNameMismatchInstance.class);
        persistenceOperations(pm, o, true);
    }

    private Object newInstance(OpenJPAEntityManager pm, Class cls) {
        return pm.createInstance(cls);
    }

    private Object createInstance(EntityManager em, Class cls) {
        return ((OpenJPAEntityManager) em).createInstance(cls);
    }

    private Object persistenceOperations(OpenJPAEntityManager pm,
        SubclassTestInstance o, boolean delete) {
        startTx(pm);
        pm.persist(o);
        o.setStringField("new persistent instance");
        endTx(pm);
        Object oid = pm.getObjectId(o);
        endEm(pm);

        pm = (OpenJPAEntityManager) currentEntityManager();
        o = (SubclassTestInstance) pm.find(SubclassTestInstance.class, oid);

        assertEquals("new persistent instance", o.getStringField());
        startTx(pm);
        o.setStringField("modified persistent instance");
        endTx(pm);
        endEm(pm);

        if (delete) {
            pm = (OpenJPAEntityManager) currentEntityManager();
            o = (SubclassTestInstance) pm.find(SubclassTestInstance.class, oid);
            assertEquals("modified persistent instance", o.getStringField());
            startTx(pm);
            pm.remove(o);
            endTx(pm);
            endEm(pm);
            return null;
        } else {
            return oid;
        }
    }

    public void testPolymorphicQueries() {
        deleteAll(BaseEntity.class);
        deleteAll(BasicSubclassInstance.class);

        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        startTx(pm);

        BaseEntity be = (BaseEntity) newInstance(pm, BaseEntity.class);
        be.setShortField((short) 0);
        pm.persist(be);

        be = (BaseEntity) newInstance(pm, BaseEntity.class);
        be.setShortField((short) 1);
        pm.persist(be);

        DerivedEntity de = (DerivedEntity) newInstance(pm, DerivedEntity.class);
        de.setShortField((short) 2);
        de.setOneToOne((BasicSubclassInstance) newInstance(pm,
            BasicSubclassInstance.class));
        pm.persist(de);

        de = (DerivedEntity) newInstance(pm, DerivedEntity.class);
        de.setShortField((short) 3);
        de.setOneToOne((BasicSubclassInstance) newInstance(pm,
            BasicSubclassInstance.class));
        pm.persist(de);

        endTx(pm);
        endEm(pm);

        pm = (OpenJPAEntityManager) currentEntityManager();
        OpenJPAQuery q =
            pm.createQuery("SELECT a FROM BaseEntity a "
                + "ORDER BY a.shortField ASC");
        List l = (List) q.getResultList();
        assertEquals(4, l.size());
        assertEquals(0, ((BaseEntity) l.get(0)).getShortField());
        assertEquals(1, ((BaseEntity) l.get(1)).getShortField());
        assertEquals(2, ((BaseEntity) l.get(2)).getShortField());
        assertEquals(3, ((BaseEntity) l.get(3)).getShortField());
        assertTrue(l.get(2) instanceof DerivedEntity);
        assertTrue(l.get(3) instanceof DerivedEntity);
        endEm(pm);
    }

    public void testEnhancedClassChangesOutsideTxWithoutNTW() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        startTx(pm);
        deleteAll(Entity1.class);
        endTx(pm);
        Entity1 o = new Entity1(8, "pk 8", 4);
        startTx(pm);
        pm.persist(o);
        endTx(pm);
        Object oid = pm.getObjectId(o);
        endEm(pm);

        pm = (OpenJPAEntityManager) currentEntityManager();
        o = (Entity1) pm.find(Entity1.class, oid);

        try {
            o.setStringField("hello");
            fail("non-transactional write should not be allowed");
        } catch (Exception e) {
            // expected
        } finally {
            endEm(pm);
        }
    }

    public void testSubclassChangesOutsideTxWithoutNTW() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BasicSubclassInstance basic = (BasicSubclassInstance) newInstance(pm,
            BasicSubclassInstance.class);
        basic.setStringField("foo");
        startTx(pm);
        pm.persist(basic);
        endTx(pm);
        Object oid = pm.getObjectId(basic);
        endEm(pm);

        pm = (OpenJPAEntityManager) currentEntityManager();
        basic =
            (BasicSubclassInstance) pm.find(BasicSubclassInstance.class, oid);

        try {
            basic.setStringField("hello");
            fail("non-transactional write should not be allowed");
        } catch (Exception e) {
            // expected
        } finally {
            endEm(pm);
        }
    }

    public void testBasicPMUses() {
        // retain so we don't reload in the reads after the tx commit
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        Broker broker = JPAFacadeHelper.toBroker(pm);
        startTx(pm);

        // register a new instance with the PM
        BasicSubclassInstance basic = (BasicSubclassInstance) newInstance
            (pm, BasicSubclassInstance.class);
        assertTrue(ImplHelper.isManageable(basic));
        basic.setStringField("foo");
        pm.persist(basic);
        assertTrue(broker.isNew(basic));
        assertTrue(broker.isPersistent(basic));

        // commit. this should cause the data to be written.
        // ### should check SQL count
        endTx(pm);

        assertFalse(broker.isNew(basic));

        OpenJPAStateManager sm = broker.getStateManager(basic);
        assertNotNull(sm);
        assertEquals(sm.getManagedInstance(), basic);

        FieldMetaData fmd = sm.getMetaData().getField("stringField");
        assertEquals("foo", sm.fetch(fmd.getIndex()));
        assertTrue(sm.getLoaded().get(fmd.getIndex()));

        pm.evict(basic);
        assertFalse(sm.getLoaded().get(fmd.getIndex()));
        // lazy loading
        assertNotNull(basic.getStringField());
        assertEquals("foo", sm.fetch(fmd.getIndex()));
        assertEquals("foo", basic.getStringField());
        assertTrue(sm.getLoaded().get(fmd.getIndex()));

        startTx(pm);
        basic.setStringField("bar");
        assertTrue(broker.isDirty(basic));
        endTx(pm);
        Object oid = broker.getObjectId(basic);
        assertNotNull(oid);
        endEm(pm);

        pm = (OpenJPAEntityManager) currentEntityManager();
        basic =
            (BasicSubclassInstance) pm.find(BasicSubclassInstance.class, oid);
        assertEquals("bar", basic.getStringField());

        startTx(pm);
        pm.remove(basic);
        assertTrue(JPAFacadeHelper.toBroker(pm).isDeleted(basic));
        endTx(pm);
        endEm(pm);
    }

    public void testGetObjectId() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BasicSubclassInstance basic = new BasicSubclassInstance();
        basic.setStringField("foo");
        startTx(pm);
        pm.persist(basic);
        endTx(pm);
        Object oid = null;
        try {
            assertNotNull(oid = pm.getObjectId(basic));
        } catch (Exception e) {
            fail("object id lookup failed: " + e.getMessage());
        }

        startTx(pm);
        pm.remove(basic);
        // before committing, id should exist still
        assertNotNull(pm.getObjectId(basic));

        endTx(pm);
        assertNull(pm.getObjectId(basic));
        endEm(pm);

        // looking up the instance by id in a new PM should fail.
        pm = (OpenJPAEntityManager) currentEntityManager();
        try {
            pm.find(BasicSubclassInstance.class, oid);
            fail("instance should have been deleted!");
        } catch (Exception e) {
            // expected
        }
        endEm(pm);
    }

    public void testChangesOutsideTxWithNTW() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BasicSubclassInstance basic = new BasicSubclassInstance();
        basic.setStringField("foo");
        startTx(pm);
        pm.persist(basic);
        endTx(pm);
        Object oid = pm.getObjectId(basic);
        endEm(pm);

        pm = getNTWPM();
        basic =
            (BasicSubclassInstance) pm.find(BasicSubclassInstance.class, oid);
        basic.setStringField("hello");
        startTx(pm);
        endTx(pm);
        endEm(pm);

        pm = (OpenJPAEntityManager) currentEntityManager();
        basic =
            (BasicSubclassInstance) pm.find(BasicSubclassInstance.class, oid);
        try {
            assertEquals("hello", basic.getStringField());
        } catch (AssertionFailedError afe) {
            bug(1205, afe, "JDO 2-style NTW not supported.");
        }
    }

    public void testChangesOutsideTxWithoutNTW() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BasicSubclassInstance basic = new BasicSubclassInstance();
        basic.setStringField("foo");
        startTx(pm);
        pm.persist(basic);
        endTx(pm);

        try {
            basic.setStringField("hello");
            fail("should not be able to write outside tx without NTW");
        } catch (RuntimeException re) {
            // expected case
            Object failed = ((ExceptionInfo) re).getFailedObject();
            assertNotNull(failed);
            assertSame(basic, failed);
        } finally {
            endEm(pm);
        }
    }

    private OpenJPAEntityManager getNTWPM() {
        EntityManagerFactory pmf = getEmf();
        OpenJPAEntityManager em =
            (OpenJPAEntityManager) pmf.createEntityManager();
        em.setNontransactionalWrite(true);
        return em;
    }

    /*
     public void testCallbacks ()
     {
         fail ("##### unimplemented test");
     }


     public void testTransactionListeners ()
     {
         fail ("#####");
     }


     public void testRemoteCommitListeners ()
     {
         fail ("#####");
     }


     public void testCaching ()
     {
         fail ("#####");
     }


     public void testRemote ()
     {
         fail ("#####");
     }
     */

    public void testVersionIncrementAndIdField() {
        // make sure that version increments happen correctly, and are
        // visible in the user-visible instance.
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        BasicSubclassInstance basic = new BasicSubclassInstance();
        basic.setStringField("foo");
        startTx(pm);
        pm.persist(basic);
        endTx(pm);

        assertEquals(1, basic.getVersion());
        long id = basic.getId();
        assertNotEquals(0, id);

        startTx(pm);
        basic.setStringField("bar");
        endTx(pm);
        assertEquals(2, basic.getVersion());
        endEm(pm);
    }

    /*
     public void testAutoAssignedFields ()
     {
         // make sure that auto-assigned field values get into the user-visible
         // instance.
         fail ("#####");
     }
     */

    public void testJPABasics() {
        EntityManager em = currentEntityManager();
        BasicSubclassInstance basic = (BasicSubclassInstance) createInstance(
            em, BasicSubclassInstance.class);
        basic.setStringField("hello");
        startTx(em);
        em.persist(basic);
        endTx(em);
        endEm(em);
    }

    /*
     public void testDetachmentAndAttachemnt ()
     {
         fail ("#####");
     }


     public void testEmbeddedNonEnhanced ()
     {
         fail ("#####");
     }


     public void testTransactionalNonEnhanced ()
     {
         fail ("#####");
     }


     public void testBulkTransactionalNonEnhanced ()
     {
         fail ("#####");
     }
     */

    public void testSingleValuedInverseManagement() {
        Map map = new HashMap();
        map.put("openjpa.InverseManager", "true");
        OpenJPAEntityManager pm = (OpenJPAEntityManager)
            getEmf(map).createEntityManager();
        ManagedInverseTestInstance managed = (ManagedInverseTestInstance)
            newInstance(pm, ManagedInverseTestInstance.class);
        ManagedInverseTestInstance2 managed2 = (ManagedInverseTestInstance2)
            newInstance(pm, ManagedInverseTestInstance2.class);
        managed.setStringField("managed");
        managed2.setStringField("managed2");
        managed.setManaged2(managed2);

        startTx(pm);
        pm.persist(managed);
        endTx(pm);

        assertSame(managed, managed2.getManaged());
    }

    public void testBackingFieldConfigurationWithTwoFactories() {
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) currentEntityManager();
        // this causes DerivedEntity.class to get loaded into PCRegistry
        newInstance(pm, DerivedEntity.class);
        Map map = new HashMap();
        map.put("openjpa.Log", "DiagnosticContext=subclass-two-factories-test");
        pm = (OpenJPAEntityManager) getEmf(map).createEntityManager();
        newInstance(pm, DerivedEntity.class);

        // this second new-instance creation will result in the metadata
        // defaults being loaded from the PCRegistry instead of via reflection.
        // Make sure that things still work as expected from the
        // registry-parsing code.
        ClassMetaData meta = getConfiguration()
            .getMetaDataRepositoryInstance().
            getMetaData(DerivedEntity.class, null, false);
        assertTrue("meta's access should be ACCESS_PROPERTY",
        		AccessCode.isProperty(meta.getAccessType()));
        FieldMetaData[] fmds = meta.getFields();
        for (FieldMetaData fmd : fmds) {
            assertEquals(Method.class, fmd.getBackingMember().getClass());

            // make sure that the fields are defined in the right part of the
            // hierarchy
            if (fmd.getName().equals("intField") ||
                    fmd.getName().equals("oneToOne")) {
                assertEquals(DerivedEntity.class,
                        fmd.getDefiningMetaData().getDescribedType());
            }
            else {
                assertEquals(BaseEntity.class,
                        fmd.getDefiningMetaData().getDescribedType());
            }
        }
    }
}
