/*******************************************************************************
 * Copyright (c) 2005, 2009 SAP. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     SAP - initial API and implementation
 ******************************************************************************/

package org.eclipse.persistence.testing.tests.wdf.jpa1.lock;

import java.sql.Connection;
import java.sql.Date;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;

import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.testing.framework.wdf.Bugzilla;
import org.eclipse.persistence.testing.framework.wdf.JPAEnvironment;
import org.eclipse.persistence.testing.framework.wdf.Skip;
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.Department;
import org.eclipse.persistence.testing.models.wdf.jpa1.employee.Review;
import org.eclipse.persistence.testing.models.wdf.jpa1.node.Node;
import org.eclipse.persistence.testing.tests.wdf.jpa1.JPA1Base;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

// TODO remove restrictions
public class TestOptimistic extends JPA1Base {

    @Test
    public void testOptimisticLockExceptionUpdateUpdate() {
        JPAEnvironment env = getEnvironment();
        EntityManager em1 = env.getEntityManagerFactory().createEntityManager();
        int id = 10;
        Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "one");
        try {
            env.beginTransaction(em1);
            em1.persist(rev1);
            env.commitTransactionAndClear(em1);

            env.beginTransaction(em1);
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            rev1.getVersion();

            EntityManager em2 = env.getEntityManagerFactory().createEntityManager();
            try {
                env.beginTransaction(em2);
                Review rev2 = em2.find(Review.class, new Integer(id));
                rev2.setReviewText("two"); // 1 update
                env.commitTransactionAndClear(em2);
            } finally {
                closeEntityManager(em2);
            }

            rev1.setReviewText("1"); // 2 update
            env.commitTransactionAndClear(em1);
            flop("OptimisticLockException not thrown");
        } catch (RollbackException rbe) {
            assertExceptionWrapsOLE(rev1, rbe);
        } catch (OptimisticLockException ole) {
            assertFailingEntity(rev1, ole);
        } finally {
            closeEntityManager(em1);
        }
        // bug 309681: version is incremented in spite of roll back -> not an
        // issue
        // verify(version == rev1.getVersion(), "wrong version:" + version +
        // " != " + rev1.getVersion());
    }

    private static void assertFailingEntity(Object entity, OptimisticLockException ole) {
        // $JL-EXC$ expected behavior
        Object failingEntity = ole.getEntity();
        if (failingEntity != null) {
            verify(entity.equals(failingEntity), "wrong entity");
        }
    }

    @Test
    public void testOptimisticLockExceptionDeleteUpdate() {
        JPAEnvironment env = getEnvironment();
        EntityManager em1 = env.getEntityManagerFactory().createEntityManager();
        EntityManager em2 = env.getEntityManagerFactory().createEntityManager();
        int id = 20;
        Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "one");
        try {
            env.beginTransaction(em1);
            em1.persist(rev1);
            env.commitTransactionAndClear(em1);

            env.beginTransaction(em1);
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            env.beginTransaction(em2);
            Review rev2 = em2.find(Review.class, new Integer(id));
            em2.remove(rev2); // 1 delete
            env.commitTransactionAndClear(em2);
            rev1.setReviewText("1"); // 2 update
            env.commitTransactionAndClear(em1);
            flop("OptimisticLockException not thrown");
        } catch (RollbackException rbe) {
            assertExceptionWrapsOLE(rev1, rbe);
        } catch (OptimisticLockException ole) {
            assertFailingEntity(rev1, ole);
        } finally {
            closeEntityManager(em1);
            closeEntityManager(em2);
        }
    }

    private static void assertExceptionWrapsOLE(Object entity, PersistenceException rbe) {
        Throwable cause = rbe.getCause();
        if (cause instanceof OptimisticLockException) {
            assertFailingEntity(entity, (OptimisticLockException) cause);
        } else {
            Assert.fail("Rollback not caused by OLE");
        }
    }

    @Test
    public void testOptimisticLockExceptionUpdateDelete() {
        JPAEnvironment env = getEnvironment();
        EntityManager em1 = env.getEntityManagerFactory().createEntityManager();
        EntityManager em2 = env.getEntityManagerFactory().createEntityManager();
        int id = 30;
        Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "one");
        try {
            env.beginTransaction(em1);
            em1.persist(rev1);
            env.commitTransactionAndClear(em1);
            env.beginTransaction(em1);
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            env.beginTransaction(em2);
            Review rev2 = em2.find(Review.class, new Integer(id));
            rev2.setReviewDate(Date.valueOf("2005-12-23")); // 1 update
            env.commitTransactionAndClear(em2);
            em1.remove(rev1); // 2 delete
            env.commitTransactionAndClear(em1);
            flop("OptimisticLockException not thrown");
        } catch (RollbackException rbe) {
            assertExceptionWrapsOLE(rev1, rbe);
        } catch (OptimisticLockException ole) {
            assertFailingEntity(rev1, ole);
            try {
                env.rollbackTransactionAndClear(em1);
                flop("no rollback after OptimisticLockException");
            } catch (IllegalStateException ise) {
                // $JL-EXC$ expected behavior
            }
        } finally {
            closeEntityManager(em1);
            closeEntityManager(em2);
        }
    }

    @Test
    public void testOptimisticLockExceptionDeleteDelete() {
        JPAEnvironment env = getEnvironment();
        EntityManager em1 = env.getEntityManagerFactory().createEntityManager();
        EntityManager em2 = env.getEntityManagerFactory().createEntityManager();
        int id = 40;
        Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "one");
        try {
            env.beginTransaction(em1);
            em1.persist(rev1);
            env.commitTransactionAndClear(em1);
            env.beginTransaction(em1);
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            env.beginTransaction(em2);
            Review rev2 = em2.find(Review.class, new Integer(id));
            em2.remove(rev2); // 1 delete
            env.commitTransactionAndClear(em2);
            em1.remove(rev1); // 2 delete
            env.commitTransactionAndClear(em1);
            flop("OptimisticLockException not thrown");
        } catch (RollbackException rbe) {
            assertExceptionWrapsOLE(rev1, rbe);
        } catch (OptimisticLockException ole) {
            assertFailingEntity(rev1, ole);
        } finally {
            closeEntityManager(em1);
            closeEntityManager(em2);
        }
    }

    @Test
    public void testNoChange() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        try {
            int id = 50;
            int version = 0;
            Review rev = new Review(id, Date.valueOf("2005-12-07"), "1");
            env.beginTransaction(em);
            em.persist(rev);
            env.commitTransactionAndClear(em);
            version = rev.getVersion();
            env.beginTransaction(em);
            rev = em.find(Review.class, new Integer(id));
            verify(rev != null, "Review is null");
            rev.setReviewText(rev.getReviewText()); // no change
            env.commitTransactionAndClear(em);
            verify(version == rev.getVersion(), "wrong version");
        } finally {
            closeEntityManager(em);
        }
    }

    @Test
    @Bugzilla(bugid = 309681)
    public void testIllegalVersionAccessNew() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        boolean caughtException = false;
        try {
            int id = 60;
            Review rev = new Review(id, Date.valueOf("2005-12-07"), "1");
            env.beginTransaction(em);
            em.persist(rev);
            rev.setVersion(5);
            env.commitTransactionAndClear(em);
        } catch (PersistenceException e) {
            // $JL-EXC$ expected behavior
            caughtException = true;
        } finally {
            closeEntityManager(em);
            verify(caughtException, "PersistenceException not thrown for versionModification");
        }
    }

    @Test
    public void testIllegalVersionAccessManaged() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        boolean caughtException = false;
        try {
            int id = 70;
            Review rev = new Review(id, Date.valueOf("2005-12-07"), "1");
            env.beginTransaction(em);
            em.persist(rev);
            env.commitTransactionAndClear(em);
            env.beginTransaction(em);
            rev = em.merge(rev);
            rev.setVersion(7);
            env.commitTransactionAndClear(em);
        } catch (PersistenceException e) {
            // $JL-EXC$ expected behavior
            caughtException = true;
        } finally {
            closeEntityManager(em);
        }
        verify(caughtException, "PersistenceException not thrown for versionModification");
    }

    @Test
    public void testFlushWithVersion() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        try {
            int id = 80;
            Review rev = new Review(id, Date.valueOf("2005-12-07"), "1");
            env.beginTransaction(em);
            em.persist(rev);
            em.flush(); // 1st version
            int startVersion = rev.getVersion();
            rev.setReviewDate(Date.valueOf("2005-12-23"));
            env.commitTransactionAndClear(em); // 2nd version
            env.beginTransaction(em);
            rev = em.merge(rev);
            rev.setReviewText("AAA");
            em.flush(); // 3rd version
            rev.setReviewText("BBB");
            env.commitTransactionAndClear(em); // 4th version
            env.beginTransaction(em);
            rev = em.merge(rev);
            em.flush();
            env.commitTransactionAndClear(em); // still 4th version
            rev = em.find(Review.class, new Integer(id));
            verify(rev.getReviewText().equals("BBB"), "wrong reviewText");
            verify(rev.getReviewDate().equals(Date.valueOf("2005-12-23")), "wrong reviewDate");
            verify(rev.getVersion() == startVersion + 3, "wrong version");
        } finally {
            closeEntityManager(em);
        }
    }

    @Test
    public void testMergeWithVersion() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        int id = 90;
        Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "a");
        Review rev2 = new Review(id, Date.valueOf("2005-12-07"), "b");
        try {
            env.beginTransaction(em);
            em.persist(rev1);
            env.commitTransactionAndClear(em);
            env.beginTransaction(em);
            rev1 = em.merge(rev2);
            flop("OptimisticLockException not thrown for merge with old version");
        } catch (OptimisticLockException ole) {
            assertFailingEntity(rev2, ole);
            verify(env.isTransactionMarkedForRollback(em), "transaction not marked for rollback on OptimisticLockException");
        } finally {
            closeEntityManager(em);
        }
    }

    @Test
    public void testRefreshWithVersion() {
        JPAEnvironment env = getEnvironment();
        EntityManager em1 = env.getEntityManagerFactory().createEntityManager();
        EntityManager em2 = env.getEntityManagerFactory().createEntityManager();
        try {
            int id = 100;
            Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "x");
            env.beginTransaction(em1);
            em1.persist(rev1);
            env.commitTransactionAndClear(em1);
            env.beginTransaction(em1);
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            env.beginTransaction(em2);
            Review rev2 = em2.find(Review.class, new Integer(id));
            rev2.setReviewDate(Date.valueOf("2005-12-23"));
            env.commitTransactionAndClear(em2);
            em1.refresh(rev1);
            rev1.setReviewText("y");
            env.commitTransactionAndClear(em1);
        } finally {
            closeEntityManager(em1);
            closeEntityManager(em2);
        }
    }

    @Test
    public void testVersion() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        try {
            int id = 110;
            int version = 0;
            Review rev = new Review(id, Date.valueOf("2005-12-07"), "1");
            env.beginTransaction(em);
            em.persist(rev);
            env.commitTransactionAndClear(em);
            for (int i = 0; i < 3; i++) {
                env.beginTransaction(em);
                rev = em.find(Review.class, new Integer(id));
                verify(rev != null, "Review is null");
                rev.setReviewText(new Integer(i).toString());
                env.commitTransactionAndClear(em);
                verify(version < rev.getVersion(), "new version to small");
                version = rev.getVersion();
            }
        } finally {
            closeEntityManager(em);
        }
    }

    @Test
    public void testPersistAgain() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        try {
            int id = 120;
            Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "x");
            env.beginTransaction(em);
            em.persist(rev1);
            env.commitTransactionAndClear(em);
            env.beginTransaction(em);
            rev1 = em.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            em.persist(rev1);
            verify(rev1.getVersion() > 0, "Version reset");
            env.commitTransactionAndClear(em);
        } finally {
            closeEntityManager(em);
        }
    }

    @Ignore
    // this only works on oracle
    public void testIsoLevel() throws SQLException {
        JPAEnvironment env = getEnvironment();
        EntityManager em1 = env.getEntityManagerFactory().createEntityManager();
        EntityManager em2 = env.getEntityManagerFactory().createEntityManager();
        boolean caughtException = false;
        try {
            Connection con = env.getDataSource().getConnection();
            verify(con.getTransactionIsolation() == Connection.TRANSACTION_READ_COMMITTED, "wrong isoLevel");
            int id = 130;
            Review rev1 = new Review(id, Date.valueOf("2005-12-07"), "1");
            env.beginTransaction(em1); // create entity
            em1.persist(rev1);
            env.commitTransactionAndClear(em1);
            env.beginTransaction(em1); // Tx1 read and flush change
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            rev1.setReviewText("2");
            em1.flush();
            env.beginTransaction(em2); // Tx2 read flushed entity ?
            Review rev2 = em2.find(Review.class, new Integer(id));
            verify(rev2 != null, "Review is null");
            env.rollbackTransactionAndClear(em1); // Tx1 rollback first change
                                                  // and commit second
            env.beginTransaction(em1);
            rev1 = em1.find(Review.class, new Integer(id));
            verify(rev1 != null, "Review is null");
            rev1.setReviewText("3");
            env.commitTransactionAndClear(em1);
            rev2.setReviewText("4"); // Tx2 commit based on rollback version
            env.commitTransactionAndClear(em2);
        } catch (OptimisticLockException ole) {
            // $JL-EXC$ expected behavior
            caughtException = true;
        } finally {
            closeEntityManager(em1);
            closeEntityManager(em2);
            verify(caughtException, "OptimisticLockException not thrown");
        }
    }

    @Test
    @Bugzilla(bugid = 309681)
    public void testNode() {
        JPAEnvironment env = getEnvironment();
        EntityManager em = env.getEntityManagerFactory().createEntityManager();
        int NUMBER_OF_NODES = 7;
        int NUMBER_OF_UPDATES = 3;
        try {
            Node first = new Node(0, null);
            first.setName("!0");
            Node parent = first;
            env.beginTransaction(em);
            em.persist(first);
            for (int i = 1; i < NUMBER_OF_NODES; i++) {
                Node tmp = new Node(i, parent);
                tmp.setName(new Integer(i).toString());
                em.persist(tmp);
                parent = tmp;
            }
            env.commitTransactionAndClear(em);
            for (int i = 0; i < NUMBER_OF_UPDATES; i++) {
                updateNodes(env, em, Integer.toString(i));
            }
        } finally {
            closeEntityManager(em);
        }
    }

    private void updateNodes(JPAEnvironment env, EntityManager em, String newName) {
        env.beginTransaction(em);
        Node tmp = em.find(Node.class, new Integer(0));
        verify(tmp.getVersion() != 0, "wrong version");
        while (tmp != null) {
            tmp.setName(newName);
            Set<Node> children = tmp.getChildren();
            if (!children.isEmpty()) {
                tmp = children.iterator().next(); // linkedList
                verify(tmp.getVersion() != 0, "wrong version");
            } else {
                tmp = null;
            }
        }
        env.commitTransactionAndClear(em);
    }

    private abstract class BatchTester {
        private final boolean throwsRollbackException;

        BatchTester(boolean rollback) {
            throwsRollbackException = rollback;
        }

        abstract void trigger(EntityManager em);
    }

    @Test
    @Skip(server = true)
    // RESOURCE_LOCAL only
    public void testBatchOLECommit() throws SQLException, NamingException {
        BatchTester tester = new BatchTester(true) {

            @Override
            void trigger(EntityManager em) {
                em.getTransaction().commit();
            }
        };
        verifyBatchOLE(tester, true);
        clearAllTables();
        verifyBatchOLE(tester, false);
    }

    @Test
    @Skip(server = true)
    // RESOURCE_LOCAL only
    public void testBatchOLEFlush() throws SQLException, NamingException {
        BatchTester tester = new BatchTester(true) {

            @Override
            void trigger(EntityManager em) {
                em.flush();
            }
        };
        verifyBatchOLE(tester, true);
        clearAllTables();
        verifyBatchOLE(tester, false);
    }

    @Test
    @Skip(server = true)
    // RESOURCE_LOCAL only
    public void testBatchOLEQuery() throws SQLException, NamingException {
        BatchTester tester = new BatchTester(true) {

            @Override
            void trigger(EntityManager em) {
                em.createQuery("select d from Department d").setFlushMode(FlushModeType.AUTO).getResultList();
                em.flush();
            }
        };
        verifyBatchOLE(tester, true);
        clearAllTables();
        verifyBatchOLE(tester, false);
    }

    private void verifyBatchOLE(BatchTester tester, boolean batch) throws NamingException {
        JPAEnvironment env = getEnvironment();
        final Map map = new HashMap();
        map.put(PersistenceUnitProperties.DDL_GENERATION, "none");
        map.put(PersistenceUnitProperties.BATCH_WRITING, batch ? "JDBC" : "None");
        EntityManagerFactory emf = env.createNewEntityManagerFactory(map);
        try {

            EntityManager em1 = emf.createEntityManager(map);

            try {
                em1.getTransaction().begin();
                em1.persist(new Department(1, "1"));
                em1.persist(new Department(2, "2"));
                em1.persist(new Department(3, "3"));
                em1.persist(new Department(4, "4"));
                em1.persist(new Department(5, "5"));
                em1.getTransaction().commit();
                em1.clear();

                em1.getTransaction().begin();
                Department i13 = em1.find(Department.class, 3);
                Department i14 = em1.find(Department.class, 4);
                Department i15 = em1.find(Department.class, 5);
                i13.setName("x");
                i14.setName("y");
                i15.setName("z");

                EntityManager em2 = emf.createEntityManager(map);
                try {
                    em2.getTransaction().begin();
                    Department i21 = em2.find(Department.class, 1);
                    Department i22 = em2.find(Department.class, 2);
                    Department i23 = em2.find(Department.class, 3);

                    i21.setName("a");
                    i22.setName("b");
                    i23.setName("c");

                    em2.getTransaction().commit();

                } finally {
                    em2.close();
                }

                try {
                    tester.trigger(em1);
                    flop("OptimisticLockException not thrown");
                } catch (RollbackException rbe) {
                    if (tester.throwsRollbackException) {
                        assertExceptionWrapsOLE(i13, rbe);
                    } else {
                        // no RollbackException expected
                        throw rbe;
                    }
                } catch (OptimisticLockException ole) {
                    assertFailingEntity(i13, ole);
                } catch (PersistenceException pe) {
                    if (!tester.throwsRollbackException) {
                        assertExceptionWrapsOLE(i13, pe);
                    } else {
                        // RollbackException expected
                        throw pe;
                    }
                } finally {
                    if (em1.getTransaction().isActive()) {
                        em1.getTransaction().rollback();
                    }
                }

            } finally {
                em1.close();
            }
        } finally {
            emf.close();
        }
    }
}
