/*
 * Copyright 2004-2014 the Seasar Foundation and the Others.
 *
 * Licensed 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.seasar.extension.jta;

import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.seasar.extension.jta.xa.DefaultXAResource;
import org.seasar.extension.jta.xa.XAResourceStatus;

/**
 * @author higa
 *
 */
public class TransactionImplTest {

    /** target */
    private TransactionImpl tx = null;

    /**
     * setUp
     */
    @Before
    public void setUp() {
        this.tx = new TransactionImpl();
    }

    /**
     * testEquals
     */
    @Test
    public void testEquals() {
        Assert.assertEquals("1", this.tx, this.tx);
        Assert.assertNotNull("2", this.tx);
        Assert.assertNotEquals("3", this.tx, new TransactionImpl());
        Assert.assertNotEquals("4", this.tx, "test");
    }

    /**
     * testBegin
     */
    @Test
    public void testBegin() {
        this.tx.begin();
        Assert.assertEquals("2", Status.STATUS_ACTIVE, this.tx.getStatus());
    }

    /**
     * testSuspend
     * @throws Exception Exception
     */
    @Test
    public void testSuspend() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.suspend();
        Assert.assertTrue("1", this.tx.isSuspended());
        Assert.assertEquals("2", XAResourceStatus.RS_SUSPENDED.value(), xaRes.getStatus());
        try {
            this.tx.suspend();
            Assert.fail("3");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
        this.tx.resume();
        this.tx.commit();
        try {
            this.tx.suspend();
            Assert.fail("4");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testSuspendMarkedRollback
     * @throws Exception Exception
     */
    @Test
    public void testSuspendMarkedRollback() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.setRollbackOnly();
        this.tx.suspend();
        Assert.assertTrue("1", this.tx.isSuspended());
        Assert.assertEquals("2", XAResourceStatus.RS_SUSPENDED.value(), xaRes.getStatus());
        try {
            this.tx.suspend();
            Assert.fail("3");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
        this.tx.resume();
        this.tx.rollback();
        try {
            this.tx.suspend();
            Assert.fail("4");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testResume
     * @throws Exception Exception
     */
    @Test
    public void testResume() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.suspend();
        this.tx.resume();
        Assert.assertTrue("1", !this.tx.isSuspended());
        Assert.assertEquals("2", XAResourceStatus.RS_ACTIVE.value(), xaRes.getStatus());
        try {
            this.tx.resume();
            Assert.fail("3");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testOnePhaseCommit
     * @throws Exception Exception
     */
    @Test
    public void testOnePhaseCommit() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        final Sync sync = new Sync(this.tx);
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.registerSynchronization(sync);
        this.tx.commit();
        Assert.assertTrue("1", sync.isBeforeCompleted());
        Assert.assertTrue("2", sync.isAfterCompleted());
        Assert.assertEquals("3", Status.STATUS_COMMITTED, sync.getCompletedStatus());
        Assert.assertEquals("4", XAResourceStatus.RS_NONE.value(), xaRes.getStatus());
    }

    /**
     * testTwoPhaseCommit
     * @throws Exception Exception
     */
    public void testTwoPhaseCommit() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        final DefaultXAResource xaRes2 = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.enlistResource(xaRes2);
        this.tx.commit();
        Assert.assertEquals("1", Status.STATUS_NO_TRANSACTION, this.tx.getStatus());
        Assert.assertEquals("2", XAResourceStatus.RS_NONE.value(), xaRes.getStatus());
        Assert.assertEquals("3", XAResourceStatus.RS_NONE.value(), xaRes2.getStatus());
    }

    /**
     * testLastResourceCommitOptimization
     * @throws Exception Exception
     */
    @Test
    public void testLastResourceCommitOptimization() throws Exception {
        final boolean[] onePhaseFlags = new boolean[3];
        final DefaultXAResource[] xaRes = new DefaultXAResource[3];
        for (int i = 0; i < 3; ++i) {
            final int index = i;
            xaRes[i] = new DefaultXAResource() {
                @Override
                protected void doCommit(final Xid xid, final boolean onePhase) {
                    onePhaseFlags[index] = onePhase;
                }
            };
        }
        this.tx.begin();
        for (int i = 0; i < 3; ++i) {
            this.tx.enlistResource(xaRes[i]);
        }
        this.tx.commit();
        Assert.assertEquals("1", Status.STATUS_NO_TRANSACTION, this.tx.getStatus());
        Assert.assertTrue("2", onePhaseFlags[0]);
        Assert.assertFalse("3", onePhaseFlags[1]);
        Assert.assertFalse("4", onePhaseFlags[2]);
    }

    /**
     * testCommitForException
     * @throws Exception Exception
     */
    @Test
    public void testCommitForException() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.suspend();
        try {
            this.tx.commit();
            Assert.fail("1");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
        this.tx.begin();
        this.tx.commit();
        try {
            this.tx.commit();
            Assert.fail("2");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testOnePhaseCommitFail
     */
    @Test
    public void testOnePhaseCommitFail() {
        final DefaultXAResource xaRes = new XaRes();

        this.tx.begin();
        this.tx.enlistResource(xaRes);
        try {
            this.tx.commit();
            Assert.fail("1");
        } catch (final RollbackException expected) {
            expected.printStackTrace();
        }
    }

    /**
     * testTwoPhaseCommitFail
     */
    @Test
    public void testTwoPhaseCommitFail() {
        final boolean[] result = new boolean[4];
        final DefaultXAResource xaRes0 = new DefaultXAResource() {
            @Override
            protected int doPrepare(final Xid xid) {
                return XAResource.XA_RDONLY;
            }

            @Override
            protected void doCommit(final Xid xid, final boolean onePhase) {
                Assert.fail("0");
            }

            @Override
            protected void doRollback(final Xid xid) {
                result[0] = true;
            }
        };

        final DefaultXAResource xaRes1 = new DefaultXAResource() {
            @Override
            protected int doPrepare(final Xid xid) {
                return XAResource.XA_OK;
            }

            @Override
            protected void doCommit(final Xid xid, final boolean onePhase) {
                Assert.fail("1");
            }

            @Override
            protected void doRollback(final Xid xid) {
                result[1] = true;
            }
        };

        final DefaultXAResource xaRes2 = new DefaultXAResource() {
            @Override
            protected int doPrepare(final Xid xid) throws XAException {
                throw new XAException();
            }

            @Override
            protected void doCommit(final Xid xid, final boolean onePhase) {
                Assert.fail("2");
            }

            @Override
            protected void doRollback(final Xid xid) {
                result[2] = true;
            }
        };

        final DefaultXAResource xaRes3 = new DefaultXAResource() {
            @Override
            protected int doPrepare(final Xid xid) {
                Assert.fail("3");
                return XAResource.XA_OK;
            }

            @Override
            protected void doCommit(final Xid xid, final boolean onePhase) {
                Assert.fail("4");
            }

            @Override
            protected void doRollback(final Xid xid) {
                result[3] = true;
            }
        };

        this.tx.begin();
        this.tx.enlistResource(xaRes3);
        this.tx.enlistResource(xaRes2);
        this.tx.enlistResource(xaRes1);
        this.tx.enlistResource(xaRes0);
        try {
            this.tx.commit();
            Assert.fail("5");
        } catch (final RollbackException ex) {
            ex.printStackTrace();
        }
        Assert.assertFalse("6", result[0]);
        Assert.assertTrue("7", result[1]);
        Assert.assertFalse("8", result[2]);
        Assert.assertTrue("9", result[3]);
    }

    /**
     * testRollback
     */
    @Test
    public void testRollback() {
        final DefaultXAResource xaRes = new DefaultXAResource();
        final Sync sync = new Sync(this.tx);
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.registerSynchronization(sync);
        this.tx.rollback();
        Assert.assertFalse("1", sync.isBeforeCompleted());
        Assert.assertTrue("2", sync.isAfterCompleted());
        Assert.assertEquals("3", Status.STATUS_ROLLEDBACK, sync.getCompletedStatus());
        Assert.assertEquals("4", XAResourceStatus.RS_NONE.value(), xaRes.getStatus());
    }

    /**
     * testRollbackForException
     * @throws Exception Exception
     */
    @Test
    public void testRollbackForException() throws Exception {
        final DefaultXAResource xaRes = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.suspend();
        try {
            this.tx.rollback();
            Assert.fail("1");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
        this.tx.begin();
        this.tx.rollback();
        try {
            this.tx.rollback();
            Assert.fail("2");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testSetRollbackOnly
     */
    @Test
    public void testSetRollbackOnly() {
        this.tx.begin();
        this.tx.setRollbackOnly();
        Assert.assertEquals("1", Status.STATUS_MARKED_ROLLBACK, this.tx.getStatus());
    }

    /**
     * testSetRollbackOnlyForException
     * @throws Exception Exception
     */
    @Test
    public void testSetRollbackOnlyForException() throws Exception {
        this.tx.begin();
        this.tx.commit();
        try {
            this.tx.setRollbackOnly();
            Assert.fail("1");
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testDelistResource
     */
    @Test
    public void testDelistResource() {
        final DefaultXAResource xaRes = new DefaultXAResource();
        this.tx.begin();
        this.tx.enlistResource(xaRes);
        this.tx.delistResource(xaRes, XAResource.TMSUCCESS);
        Assert.assertEquals("1", XAResourceStatus.RS_SUCCESS.value(), xaRes.getStatus());
    }

    /**
     * testDelistResourceForException
     * @throws Exception Exception
     */
    @Test
    public void testDelistResourceForException() throws Exception {
        this.tx.begin();
        final DefaultXAResource xaRes = new DefaultXAResource();
        try {
            this.tx.delistResource(xaRes, XAResource.TMSUCCESS);
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
        this.tx.suspend();
        this.tx.resume();
        this.tx.commit();
        try {
            this.tx.delistResource(xaRes, XAResource.TMSUCCESS);
        } catch (final IllegalStateException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * testBeforeCompletion
     */
    @Test
    public void testBeforeCompletion() {
        this.tx.begin();
        final Sync sync = new Sync(this.tx);
        this.tx.registerSynchronization(sync);
        this.tx.registerSynchronization(new ExceptionSync());
        try {
            this.tx.commit();
            Assert.fail("1");
        } catch (final RollbackException ex) {
            ex.printStackTrace();
        }
        Assert.assertTrue(sync.isBeforeCompleted());
        Assert.assertTrue(sync.isAfterCompleted());
    }

    /**
     * XaRes
     *
     */
    static final class XaRes extends DefaultXAResource {
        /**
         * @see org.seasar.extension.jta.xa.DefaultXAResource#doPrepare(javax.transaction.xa.Xid)
         */
        @Override
        protected int doPrepare(final Xid xid) {
            Assert.fail("0");
            return XAResource.XA_OK;
        }

        /**
         * @see org.seasar.extension.jta.xa.DefaultXAResource
         * #doCommit(javax.transaction.xa.Xid, boolean)
         */
        @Override
        protected void doCommit(final Xid xid, final boolean onePhase) throws XAException {
            throw new XAException();
        }
    }

    /**
     * Sync
     *
     */
    static final class Sync implements Synchronization {

        /** beforeCompleted */
        private boolean beforeCompleted = false;
        /** afterCompleted */
        private boolean afterCompleted = false;
        /** Status */
        private int completedStatus = Status.STATUS_UNKNOWN;
        /** Transaction */
        private final Transaction transaction;

        /**
         * @param tran Transaction
         */
        Sync(final Transaction tran) {
            this.transaction = tran;
        }

        /**
         * @return the beforeCompleted_
         */
        public boolean isBeforeCompleted() {
            return this.beforeCompleted;
        }

        /**
         * @return the afterCompleted_
         */
        public boolean isAfterCompleted() {
            return this.afterCompleted;
        }

        /**
         * @return the completedStatus_
         */
        public int getCompletedStatus() {
            return this.completedStatus;
        }

        /**
         * @return the transaction
         */
        public Transaction getTransaction() {
            return this.transaction;
        }

        /**
         * @see javax.transaction.Synchronization#beforeCompletion()
         */
        @Override
        public void beforeCompletion() {
            this.beforeCompleted = true;
        }

        /**
         * @see javax.transaction.Synchronization#afterCompletion(int)
         */
        @Override
        public void afterCompletion(final int status) {
            try {
                Assert.assertEquals(Status.STATUS_NO_TRANSACTION, this.transaction.getStatus());
            } catch (final SystemException e) {
                Assert.fail(e.getMessage());
            }
            this.afterCompleted = true;
            this.completedStatus = status;
        }
    }

    /**
     * ExceptionSync
     *
     */
    static final class ExceptionSync implements Synchronization {

        /**
         * @see javax.transaction.Synchronization#beforeCompletion()
         */
        @Override
        public void beforeCompletion() {
            throw new RuntimeException("hoge");
        }

        /**
         * @see javax.transaction.Synchronization#afterCompletion(int)
         */
        @Override
        public void afterCompletion(final int status) {
            return;
        }
    }

}
