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

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.enhance.PCEnhancer;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.kernel.DetachedStateManager;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.persistence.DetachStateType;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.kernel.common.apps.AttachA;
import org.apache.openjpa.persistence.kernel.common.apps.AttachB;
import org.apache.openjpa.persistence.kernel.common.apps.AttachD;
import org.apache.openjpa.persistence.kernel.common.apps.AttachE;
import org.apache.openjpa.persistence.kernel.common.apps.DetachSMPC;

public class TestDetachedStateManager extends BaseKernelTest {

    private static boolean enhanced = false;

    private int oid;
    private int doid;

    /**
     * Creates a new instance of TestDetachedStateManager
     */
    public TestDetachedStateManager(String name) {
        super(name);
    }

    private void deleteAll() {
        deleteAll(AttachA.class);
        deleteAll(AttachD.class);
    }

    @Override
    public OpenJPAEntityManager getPM() {
        OpenJPAEntityManager pm = super.getPM();
        //FIXME jthomas
        //pm.currentTransaction().setRestoreValues(false);
        return pm;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();

        deleteAll();

        OpenJPAEntityManager pm = getPM();
        startTx(pm);
        AttachB b = new AttachB();
        pm.persist(b);
        b.setAint(5);
        b.setBstr("5");
        b.getStringIntMap().put("5", 5);

        AttachE e = new AttachE();
        e.setEstr("E");
        e.setEint(5);

        AttachD d = new AttachD();
        d.setDint(5);
        d.setEmbeddedE(e);
        b.getDs().add(d);

        pm.persist(d);

        oid = b.getId();
        doid = d.getId();
        endTx(pm);
        endEm(pm);
    }

    public void testDetach() {
        OpenJPAEntityManager pm = getPM();
        AttachB b = pm.find(AttachB.class, oid);

        assertNotNull("b is null in testDetach", b);

        b = (AttachB) pm.detachCopy(b);
        endEm(pm);

        assertTrue(pm.isDetached(b));
        assertEquals(5, b.getAint());
        assertEquals("5", b.getBstr());
        assertNull(b.getStringIntMap());

        b.setAint(12);
        b.setBstr("12");
        TreeMap map = new TreeMap();
        map.put("12", 12);
        b.setStringIntMap(map);

        pm = getPM();
        startTx(pm);
        AttachB attached = (AttachB) pm.merge(b);
        assertEquals(12, attached.getAint());
        assertEquals("12", attached.getBstr());
        assertNull(attached.getStringIntMap().get("12"));
        assertEquals(5, attached.getStringIntMap().get("5"));
        endTx(pm);
        endEm(pm);

        pm = getPM();
        b = pm.find(AttachB.class, oid);
        assertEquals(12, b.getAint());
        assertEquals("12", b.getBstr());
        assertNull(b.getStringIntMap().get("12"));
        assertEquals(5, b.getStringIntMap().get("5"));
        endEm(pm);
    }

    public void testDetachWithGroups() {
        OpenJPAEntityManager pm = getPM();
        //FIXME jthomas
//      pm.getFetchPlan().setDetachmentOptions(
//      FetchPlanImpl.DETACH_LOAD_FIELDS | FetchPlanImpl.DETACH_UNLOAD_FIELDS);
        pm.setDetachState(DetachStateType.FETCH_GROUPS);
        pm.getFetchPlan().addFetchGroup("all");
        AttachB b = pm.find(AttachB.class, oid);

        assertNotNull("b is null in testDetachWithGroups", b);

        b = (AttachB) pm.detachCopy(b);
        endEm(pm);

        assertTrue(pm.isDetached(b));
        assertEquals("b.getAint() not 5", 5, b.getAint());
        assertEquals("b.getAint() not 5str", "5", b.getBstr());
        assertEquals("b.getStringIntMap().size() not equal to 1", 1,
            b.getStringIntMap().size());

        b.setAint(12);
        b.setBstr("12");
        b.getStringIntMap().put("12", 12);

        pm = getPM();
        startTx(pm);
        AttachB attached = (AttachB) pm.merge(b);
        assertEquals("not 12", 12, attached.getAint());
        assertEquals("not 12str", "12", attached.getBstr());
        assertEquals("not newInteger(12)", 12,
            attached.getStringIntMap().get("12"));
        assertEquals("not newInteger(5)", 5,
            attached.getStringIntMap().get("5"));
        endTx(pm);
        endEm(pm);

        pm = getPM();
        b = (AttachB) pm.find(AttachB.class, oid);
        assertEquals("not equal 12", 12, b.getAint());
        assertEquals("not equal 12str", "12", b.getBstr());
        assertEquals("not equal newinteger(12)", 12,
            b.getStringIntMap().get("12"));
        assertEquals("not equal newInteger(5)", 5,
            b.getStringIntMap().get("5"));
        endEm(pm);
    }

    public void testDetachNoOverwrite() {
        OpenJPAEntityManager pm = getPM();
        AttachB b = (AttachB) pm.find(AttachB.class, oid);
        b = (AttachB) pm.detachCopy(b);
        endEm(pm);

        b.setBstr("12");

        pm = getPM();
        startTx(pm);
        AttachB orig = pm.find(AttachB.class, oid);
        orig.setAint(50);

        AttachB attached = (AttachB) pm.merge(b);
        assertEquals(attached, orig);
        assertEquals(50, attached.getAint());
        assertEquals("12", attached.getBstr());
        endTx(pm);
        endEm(pm);

        pm = getPM();
        b = (AttachB) pm.find(AttachB.class, oid);
        assertEquals(50, b.getAint());
        assertEquals("12", b.getBstr());
        endEm(pm);
    }

    public void testOptimisticLock() {
        OpenJPAEntityManager pm = getPM();
        AttachB b = (AttachB) pm.find(AttachB.class, oid);

        assertNotNull("b is null in testOptimisticLock", b);

        b = (AttachB) pm.detachCopy(b);
        endEm(pm);

        b.setAint(12);
        b.setBstr("12");
        TreeMap map = new TreeMap();
        map.put("12", 12);
        b.setStringIntMap(map);

        pm = getPM();
        startTx(pm);
        AttachB b2 = (AttachB) pm.find(AttachB.class, oid);
        b2.setAint(15);
        endTx(pm);
        endEm(pm);

        pm = getPM();
        startTx(pm);
        try {
            pm.merge(b);
            endTx(pm);
            fail("OL expected.");
        } catch (Exception jove) {
            rollbackTx(pm);
        }
        endEm(pm);
    }

    public void testEmbedded() {
        OpenJPAEntityManager pm = getPM();
        AttachD d = pm.find(AttachD.class, doid);

        assertNotNull("d is null in testEmbedded", d);

        d.getEmbeddedE().getEstr();
        d = (AttachD) pm.detachCopy(d);
        endEm(pm);

        d.getEmbeddedE().setEstr("E12");
        pm = getPM();
        startTx(pm);
        AttachD d2 = (AttachD) pm.merge(d);
        assertNotEquals(d.getEmbeddedE(), d2.getEmbeddedE());
        assertEquals("E12", d2.getEmbeddedE().getEstr());
        assertEquals(5, d2.getEmbeddedE().getEint());
        endTx(pm);
        endEm(pm);

        pm = getPM();
        d2 = (AttachD) pm.find(AttachD.class, doid);

        assertNotNull("d2 is null in testEmbedded", d2);

        assertEquals("E12", d2.getEmbeddedE().getEstr());
        assertEquals(5, d2.getEmbeddedE().getEint());
        endEm(pm);
    }

    public void testNullEmbedded() {
        OpenJPAEntityManager pm = getPM();
        AttachD d = (AttachD) pm.find(AttachD.class, doid);

        assertNotNull("d is null in testNullEmbedded", d);
        d.getEmbeddedE().getEstr();
        d = (AttachD) pm.detachCopy(d);
        endEm(pm);

        d.setEmbeddedE(null);
        pm = getPM();
        startTx(pm);
        AttachD d2 = (AttachD) pm.merge(d);
        assertNull(d2.getEmbeddedE());
        endTx(pm);
        endEm(pm);

        pm = getPM();
        d2 = (AttachD) pm.find(AttachD.class, doid);

        assertNotNull("d2 is null in testNullEmbedded", d2);
        // no null ind
        if (d2.getEmbeddedE() != null) {
            assertNull(d2.getEmbeddedE().getEstr());
            assertEquals(0, d2.getEmbeddedE().getEint());
        }
        endEm(pm);
    }

    public void testNullEmbeddedRelated() {
        OpenJPAEntityManager pm = getPM();
        AttachD d = (AttachD) pm.find(AttachD.class, doid);

        assertNotNull("d is null in testNullEmbeddedRelated", d);

        d.getEmbeddedE().getEstr();
        d = (AttachD) pm.detachCopy(d);
        endEm(pm);

        d.getEmbeddedE().setEstr(null);
        pm = getPM();
        startTx(pm);
        AttachD d2 = (AttachD) pm.merge(d);
        assertNull("d2.getEmbeddedE().getEstr() is not null",
            d2.getEmbeddedE().getEstr());
        assertEquals("d2.getEmbeddedE().getEint() is not equal to 5", 5,
            d2.getEmbeddedE().getEint());
        endTx(pm);
        endEm(pm);

        pm = getPM();
        d2 = (AttachD) pm.find(AttachD.class, doid);
        assertNull("d2.getEmbeddedE().getEstr() is not null",
            d2.getEmbeddedE().getEstr());
        assertEquals("d2.getEmbeddedE().getEint() is not 5", 5,
            d2.getEmbeddedE().getEint());
        endEm(pm);
    }

    public void testNullCollection() {
        OpenJPAEntityManager pm = getPM();
        AttachB b = (AttachB) pm.find(AttachB.class, oid);
        b.getDs();
        b = (AttachB) pm.detachCopy(b);
        endEm(pm);

        assertEquals(1, b.getDs().size());
        b.setDs(null);

        pm = getPM();
        startTx(pm);
        b = (AttachB) pm.merge(b);
        assertNull(b.getDs());
        endTx(pm);
        endEm(pm);

        pm = getPM();
        b = (AttachB) pm.find(AttachB.class, oid);
        assertTrue(b.getDs() == null || b.getDs().size() == 0);
        endEm(pm);
    }

    public void testCollectionAdd() {
        doCollectionTest(false);
    }

    public void testCollectionChanges() {
        doCollectionTest(true);
    }

    private void doCollectionTest(boolean remove) {
        OpenJPAEntityManager pm = getPM();
        AttachB b = (AttachB) pm.find(AttachB.class, oid);

        assertNotNull("b is null in doCollectionTest", b);
        b.getDs();
        b = (AttachB) pm.detachCopy(b);
        endEm(pm);

        assertEquals("b is null in doCollectionTest", 1, b.getDs().size());
        if (remove) {
            for (Iterator it = b.getDs().iterator(); it.hasNext();) {
                it.next();
                it.remove();
            }
        }
        AttachD d = new AttachD();
        d.setDint(12);
        b.getDs().add(d);

        pm = getPM();
        startTx(pm);
        b = (AttachB) pm.merge(b);
        assertSize(remove ? 1 : 2, b.getDs());
        endTx(pm);
        endEm(pm);

        pm = getPM();
        b = (AttachB) pm.find(AttachB.class, oid);
        assertSize(remove ? 1 : 2, b.getDs());
        boolean found1 = false;
        boolean found2 = false;
        for (Object o : b.getDs()) {
            d = (AttachD) o;
            switch (d.getDint()) {
                case 5:
                    if (found1)
                        fail("Refound.");
                    found1 = true;
                    break;
                case 12:
                    if (found2)
                        fail("Refound.");
                    found2 = true;
                    break;
                default:
                    fail("Unknown d:" + d.getDint());
            }
        }

        if (remove)
            assertFalse(found1);

        endEm(pm);
    }

    /*
   //###
   // No time to get these working right now.  Have to figure out how to
   // enhance certain classes with different DetachState settings in autobuild.
   public void testSerialization ()
       throws Exception
   {
       doSerializationTest (false);
   }


   public void testSerializationAuto ()
       throws Exception
   {
       doSerializationTest (true);
   }
    */

    private void doSerializationTest(boolean auto) throws Exception {
        enhance();
        Map props = new HashMap();
        props.put("openjpa.DetachState", "DetachedStateField=true");

        OpenJPAEntityManagerFactory factory =
            (OpenJPAEntityManagerFactory) getEmf(props);
        OpenJPAEntityManager pm =
            (OpenJPAEntityManager) factory.createEntityManager();

        startTx(pm);
        DetachSMPC pc = new DetachSMPC();
        pc.setIntField(1);
        DetachSMPC rel = new DetachSMPC();
        rel.setIntField(2);
        pc.getRelSet().add(rel);
        pc.getStringIntMap().put("a", 99);
        pm.persist(pc);
        endTx(pm);
        Object pcoid = pm.getObjectId(pc);
        endEm(pm);

        pm = (OpenJPAEntityManager) factory.createEntityManager();
        pc = (DetachSMPC) pm.find(DetachSMPC.class, pcoid);
        pc.getRelSet();
        pc.getStringIntMap();
        if (!auto) {
            pc = (DetachSMPC) pm.detachCopy(pc);
            assertDetachedSM(pc);
        }
        pc = (DetachSMPC) roundtrip(pc, false);
        assertDetachedSM(pc);
        endEm(pm);

        assertDetachedSM(pc);
        assertSize(1, pc.getRelSet());
        assertEquals(1, pc.getStringIntMap().size());

        pc.setIntField(3);
        ((DetachSMPC) pc.getRelSet().iterator().next()).setIntField(4);
        pc.getStringIntMap().put("b", 100);

        pc = (DetachSMPC) roundtrip(pc, false);

        assertDetachedSM(pc);
        assertEquals(3, pc.getIntField());
        assertSize(1, pc.getRelSet());
        //assertDetachedSM (b.getDs ().iterator ().next ());
        assertEquals(4, ((DetachSMPC) pc.getRelSet().iterator().next())
            .getIntField());
        assertEquals(100, pc.getStringIntMap().get("b"));

        pm = (OpenJPAEntityManager) factory.createEntityManager();
        startTx(pm);
        pc = (DetachSMPC) pm.merge(pc);
        assertEquals(3, pc.getIntField());
        assertSize(1, pc.getRelSet());
        assertEquals(4, ((DetachSMPC) pc.getRelSet().iterator().next())
            .getIntField());
        assertEquals(2, pc.getStringIntMap().size());
        assertEquals(100, pc.getStringIntMap().get("b"));
        endTx(pm);
        endEm(pm);

        pm = (OpenJPAEntityManager) factory.createEntityManager();
        pc = (DetachSMPC) pm.find(DetachSMPC.class, pcoid);
        assertEquals(3, pc.getIntField());
        assertSize(1, pc.getRelSet());
        assertEquals(4, ((DetachSMPC) pc.getRelSet().iterator().next())
            .getIntField());
        assertEquals(2, pc.getStringIntMap().size());
        assertEquals(100, pc.getStringIntMap().get("b"));

        startTx(pm);
        deleteAll(DetachSMPC.class, pm);
        endTx(pm);
        endEm(pm);
        factory.close();
    }

    private void enhance() throws Exception {
        Properties props = getProperties(new String[]{
            "openjpa.DetachState", "DetachedStateField=true",
        });
        OpenJPAConfiguration conf = new OpenJPAConfigurationImpl(true, false);
        conf.fromProperties(props);

        Options opts = new Options();
        opts.put("jdo", "true");
        PCEnhancer.run(conf, new String[]{
            "org.apache.openjpa.persistence.kernel.noenhance.DetachSMPC" },
            opts);
    }

    private void assertDetachedSM(Object obj) {
        OpenJPAEntityManager pm = getPM();
        assertTrue(pm.isDetached(obj));
        PersistenceCapable pc = (PersistenceCapable) obj;
        assertEquals(DetachedStateManager.class,
            pc.pcGetStateManager().getClass());
        endEm(pm);
    }
}
