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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import jakarta.persistence.EntityManager;

import org.apache.openjpa.event.AbstractLifecycleListener;
import org.apache.openjpa.event.AbstractTransactionListener;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.event.TransactionEvent;
import org.apache.openjpa.persistence.InvalidStateException;
import org.apache.openjpa.persistence.JPAFacadeHelper;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.jdbc.JDBCFetchPlan;
import org.apache.openjpa.persistence.jdbc.JoinSyntax;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;

/*
 * To test:
 *  - managed transactions
 *  - converting non-enhanced classes to enhanced subclasses
 *    (maybe an ugly ThreadLocal, maybe through PCData?)
 */
public abstract class AbstractBrokerSerializationTest<T>
    extends SingleEMFTestCase {

    private static LifeListener deserializedLifeListener;
    private static int testGlobalRefreshCount = 0;

    private static TxListener deserializedTxListener;
    private static int testGlobalBeginCount = 0;


    private Object id;

    @Override
    public void setUp() {
        testGlobalRefreshCount = 0;
        deserializedLifeListener = null;
        testGlobalBeginCount = 0;
        deserializedTxListener = null;

        setUp(getManagedType(), getSecondaryType(), CLEAR_TABLES,
            "openjpa.EntityManagerFactoryPool", "true");

        T e = newManagedInstance();
        OpenJPAEntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        em.persist(e);
        em.getTransaction().commit();
        id = em.getObjectId(e);
        em.close();
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        testGlobalRefreshCount = 0;
        deserializedLifeListener = null;
        testGlobalBeginCount = 0;
        deserializedTxListener = null;
    }

    public void testEmptyBrokerSerialization() {
        OpenJPAEntityManager em = emf.createEntityManager();
        OpenJPAEntityManager em2 = deserializeEM(serialize(em));

        assertTrue(em != em2);
        assertTrue(
            JPAFacadeHelper.toBroker(em) != JPAFacadeHelper.toBroker(em2));
        assertSame(em.getEntityManagerFactory(), em2.getEntityManagerFactory());

        assertSame(em2, JPAFacadeHelper.toBroker(em2)
            .getUserObject(JPAFacadeHelper.EM_KEY));

        em.close();
        assertTrue(em2.isOpen());
        em2.close();
    }

    public void testNontransactionalBrokerSerialization() {
        OpenJPAEntityManager em = emf.createEntityManager();
        T e = em.find(getManagedType(), id);
        OpenJPAEntityManager em2 = deserializeEM(serialize(em));

        assertFalse(em2.getTransaction().isActive());

        assertFalse(em2.contains(e));
        assertEquals(1*graphSize(), em2.getManagedObjects().size());
        T e2 = em2.find(getManagedType(), id);
        assertEquals(em.getObjectId(e), em2.getObjectId(e2));

        em.close();
        em2.close();
    }

    public void testUnflushedOptimisticTxBrokerSerialization() {
        OpenJPAEntityManager em = emf.createEntityManager();
        T e = em.find(getManagedType(), id);
        OpenJPAEntityManager em2 = null;
        OpenJPAEntityManager em3 = null;
        try {
            em.getTransaction().begin();
            modifyInstance(e);
            T newe = newManagedInstance();
            em.persist(newe);
            em2 = deserializeEM(serialize(em));

            assertTrue(em2.getTransaction().isActive());

            assertFalse(em2.contains(e));
            T e2 = em2.find(getManagedType(), id);
            assertEquals(em.getObjectId(e), em2.getObjectId(e2));

            assertEquals("modified", getModifiedValue(e2));

            em.getTransaction().rollback();
            assertTrue(em2.getTransaction().isActive());
            em2.getTransaction().commit();

            em3 = emf.createEntityManager();
            T e3 = em3.find(getManagedType(), id);
            assertEquals(getModifiedValue(e2), getModifiedValue(e3));
            assertTrue(1 < ((Number) em3.createQuery("select count(o) from "
                + getManagedType().getName() + " o").getSingleResult())
                .intValue());
        } finally {
            close(em);
            close(em2);
            close(em3);
        }
    }

    public void testFlushedOptimisticTxBrokerSerialization() {
        OpenJPAEntityManager em = emf.createEntityManager();
        T e = em.find(getManagedType(), id);
        em.getTransaction().begin();
        modifyInstance(e);
        em.flush();
        try {
            serialize(em);
        } catch (InvalidStateException ise) {
            // expected
            assertTrue(ise.getMessage().contains("flushed"));
        } finally {
            em.getTransaction().rollback();
            em.close();
        }
    }

    public void testConnectedOptimisticTxBrokerSerialization() {
        Map m = new HashMap();
        m.put("openjpa.ConnectionRetainMode", "always");
        OpenJPAEntityManager em = emf.createEntityManager(m);
        try {
            serialize(em);
        } catch (InvalidStateException ise) {
            // expected
            assertTrue(ise.getMessage().contains("connected"));
        } finally {
            em.close();
        }
    }

    public void testEmptyPessimisticTxBrokerSerialization() {
        Map m = new HashMap();
        m.put("openjpa.Optimistic", "false");
        OpenJPAEntityManager em = emf.createEntityManager(m);
        em.getTransaction().begin();
        try {
            serialize(em);
            fail("should not be able to serialize");
        } catch (InvalidStateException ise) {
            // expected
            assertTrue(ise.getMessage().contains("datastore (pessimistic)"));
        } finally {
            em.getTransaction().rollback();
            em.close();
        }
    }

    public void testNonEmptyPessimisticTxBrokerSerialization() {
        Map m = new HashMap();
        m.put("openjpa.Optimistic", "false");
        OpenJPAEntityManager em = emf.createEntityManager(m);
        T e = em.find(getManagedType(), id);
        em.getTransaction().begin();
        try {
            serialize(em);
            fail("should not be able to serialize");
        } catch (InvalidStateException ise) {
            // expected
            assertTrue(ise.getMessage().contains("datastore (pessimistic)"));
        } finally {
            em.getTransaction().rollback();
            em.close();
        }
    }

    public void testFetchConfigurationMutations() {
        OpenJPAEntityManager em = emf.createEntityManager();
        JDBCFetchPlan plan = (JDBCFetchPlan) em.getFetchPlan();

        assertNotEquals(17, plan.getLockTimeout());
        assertNotEquals(JoinSyntax.TRADITIONAL, plan.getJoinSyntax());

        plan.setLockTimeout(17);
        plan.setJoinSyntax(JoinSyntax.TRADITIONAL);

        OpenJPAEntityManager em2 = deserializeEM(serialize(em));
        JDBCFetchPlan plan2 = (JDBCFetchPlan) em2.getFetchPlan();
        assertEquals(17, plan2.getLockTimeout());
        assertEquals(JoinSyntax.TRADITIONAL, plan2.getJoinSyntax());
    }

    public void testInMemorySavepointsWithNewInstances() {
        OpenJPAEntityManagerFactory emf1 = createEMF(
            getManagedType(), getSecondaryType(),
            "openjpa.EntityManagerFactoryPool", "true",
            "openjpa.SavepointManager", "in-mem");
        OpenJPAEntityManager em = emf1.createEntityManager();
        OpenJPAEntityManager em2 = null;
        try {
            em.getTransaction().begin();
            T t = newManagedInstance();
            Object orig = getModifiedValue(t);
            em.persist(t);
            Object id = em.getObjectId(t);
            em.setSavepoint("foo");
            modifyInstance(t);
            assertNotEquals(orig, getModifiedValue(t));

            em2 = deserializeEM(serialize(em));
            T t2 = em2.find(getManagedType(), id);
            assertNotEquals(orig, getModifiedValue(t2));

            em.rollbackToSavepoint("foo");
            assertEquals(orig, getModifiedValue(t));

            em2.rollbackToSavepoint("foo");
            assertEquals(orig, getModifiedValue(t2));
        } finally {
            close(em);
            close(em2);
            clear(emf1);
            closeEMF(emf1);
        }
    }

    public void testInMemorySavepointsWithModifiedInstances() {
        OpenJPAEntityManagerFactory emf1 = createEMF(
            getManagedType(), getSecondaryType(),
            "openjpa.EntityManagerFactoryPool", "true",
            "openjpa.SavepointManager", "in-mem");
        OpenJPAEntityManager em = emf1.createEntityManager();
        OpenJPAEntityManager em2 = null;
        try {
            em.getTransaction().begin();
            T t = em.find(getManagedType(), id);
            Object orig = getModifiedValue(t);
            em.setSavepoint("foo");
            modifyInstance(t);
            assertNotEquals(orig, getModifiedValue(t));

            em2 = deserializeEM(serialize(em));
            T t2 = em2.find(getManagedType(), id);
            assertNotEquals(orig, getModifiedValue(t2));

            em.rollbackToSavepoint("foo");
            assertEquals(orig, getModifiedValue(t));

            em2.rollbackToSavepoint("foo");
            assertEquals(orig, getModifiedValue(t2));
        } finally {
            close(em);
            close(em2);
            clear(emf1);
            closeEMF(emf1);
        }
    }

    public void testEventManagers() {
        TxListener txListener = new TxListener();
        emf.addTransactionListener(txListener);
        LifeListener lifeListener = new LifeListener();
        emf.addLifecycleListener(lifeListener, null);

        OpenJPAEntityManager em = emf.createEntityManager();
        T t = em.find(getManagedType(), id);
        assertEquals(0, lifeListener.refreshCount);
        em.refresh(t);
        assertEquals(1*graphSize(), lifeListener.refreshCount);
        em.getTransaction().begin();
        em.getTransaction().commit();
        em.getTransaction().begin();
        em.getTransaction().commit();
        assertEquals(2, txListener.beginCount);

        OpenJPAEntityManager em2 = deserializeEM(serialize(em));
        assertNotNull(deserializedLifeListener);
        assertEquals(1* graphSize(),
            deserializedLifeListener.refreshCount);
        assertNotSame(lifeListener, deserializedLifeListener);
        T t2 = em2.find(getManagedType(), id);
        em2.refresh(t2);
        assertEquals(2* graphSize(),
            deserializedLifeListener.refreshCount);

        // if this is 3*refreshMultiplier(), that means that there are
        // extra registered listeners
        assertEquals(2* graphSize(), testGlobalRefreshCount);


        assertNotNull(deserializedTxListener);
        assertEquals(2, deserializedTxListener.beginCount);
        assertNotSame(txListener, deserializedTxListener);
        em2.getTransaction().begin();
        em2.getTransaction().rollback();
        assertEquals(3, deserializedTxListener.beginCount);

        // if this is 4, that means that there are extra registered listeners
        assertEquals(3, testGlobalBeginCount);
    }

    byte[] serialize(Object o) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(o);
            oos.flush();
            return baos.toByteArray();
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }

    OpenJPAEntityManager deserializeEM(byte[] bytes) {
        return (OpenJPAEntityManager) deserialize(bytes);
    }

    private Object deserialize(byte[] bytes) {
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(bais);
            return ois.readObject();
        } catch (IOException | ClassNotFoundException ioe) {
            throw new RuntimeException(ioe);
        }
    }

    void close(EntityManager em) {
        if (em != null && em.isOpen() && em.getTransaction().isActive())
            em.getTransaction().rollback();
        if (em != null && em.isOpen())
            em.close();
    }

    protected abstract Class<T> getManagedType();

    protected abstract T newManagedInstance();

    protected abstract void modifyInstance(T t);

    protected abstract Object getModifiedValue(T t);

    /**
     * The number of instances in the graph created
     * by {@link #newManagedInstance()} of type T.
     */
    protected int graphSize() {
        return 1;
    }

    /**
     * An additional type that must be available in this PC. May be null.
     */
    protected Class<?> getSecondaryType() {
        return null;
    }

    private static class TxListener
        extends AbstractTransactionListener
        implements Serializable {

        
        private static final long serialVersionUID = 1L;
        private int beginCount = 0;

        public TxListener() {

        }

        @Override
        public void afterBegin(TransactionEvent event) {
            beginCount++;
            testGlobalBeginCount++;
        }

        private void readObject(ObjectInputStream in)
            throws ClassNotFoundException, IOException {
            in.defaultReadObject();
            deserializedTxListener = this;
        }
    }

    private static class LifeListener
        extends AbstractLifecycleListener
        implements Serializable {

        
        private static final long serialVersionUID = 1L;
        private int refreshCount = 0;

        @Override
        public void afterRefresh(LifecycleEvent event) {
            refreshCount++;
            testGlobalRefreshCount++;
        }

        private void readObject(ObjectInputStream in)
            throws ClassNotFoundException, IOException {
            in.defaultReadObject();
            deserializedLifeListener = this;
        }
    }
}
