/*
 * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * 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.switchyard.handlers;

import java.util.UUID;

import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAResource;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.switchyard.ExchangePhase;
import org.switchyard.HandlerException;
import org.switchyard.MockExchange;
import org.switchyard.policy.PolicyUtil;
import org.switchyard.policy.TransactionPolicy;

public class TransactionHandlerTest {

    private MockTransactionManager tm;
    private MockExchange exchange;
    private TransactionHandler handler;

    @Before
    public void setUp() {
        tm = new MockTransactionManager();
        exchange = new MockExchange();
        handler = new TransactionHandler();
        handler.setTransactionManager(tm);
    }

    /* policy combination error */
    
    @Test
    public void incompatibleRequirements_PropagatesAndSuspends() {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.SUSPENDS_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            // expected
            System.out.println(handlerEx.toString());
            return;
        }

        Assert.fail("Expected a handler exception due to incompatible policy");
    }

    @Test
    public void incompatibleRequirements_managedGlobalAndmanagedLocal() {
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            System.out.println(handlerEx.toString());
            return;
        }

        Assert.fail("Expected a handler exception due to incompatible policy");
    }

    @Test
    public void incompatibleRequirements_managedGlobalAndNoManaged() {
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        PolicyUtil.require(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            // expected
            System.out.println(handlerEx.toString());
            return;
        }

        Assert.fail("Expected a handler exception due to incompatible policy");
    }

    @Test
    public void incompatibleRequirements_managedLocalAndNoManaged() {
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL);
        PolicyUtil.require(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            // expected
            System.out.println(handlerEx.toString());
            return;
        }

        Assert.fail("Expected a handler exception due to incompatible policy");
    }

    @Test
    public void incompatibleRequirements_PropagatesAndManagedLocal() {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            // expected
            System.out.println(handlerEx.toString());
            return;
        }

        Assert.fail("Expected a handler exception due to incompatible policy");
    }

    @Test
    public void incompatibleRequirements_PropagatesAndNoManaged() {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            // expected
            System.out.println(handlerEx.toString());
            return;
        }

        Assert.fail("Expected a handler exception due to incompatible policy");
    }

    /* invalid transaction status */
    
    @Test
    public void propagatesRequiredButNoTransaction() {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);

        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            // expected
            System.out.println(handlerEx.toString());
            return;
        }
        Assert.fail("Expected a handler exception due to invalid policy");
    }
    
    @Test
    public void propagateProvidedButNotRequired() throws Exception {
        // We currently view a propagated transaction without a requirement
        // as harmless. This tests confirms this behavior and acts as a guard
        // in case our behavior changes.
        PolicyUtil.provide(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);
        tm.begin();
        
        try {
            handler.handleMessage(exchange);
        } catch (HandlerException handlerEx) {
            Assert.fail("Exception not expected when transaction is provided but not required: "
                    + handlerEx);
        }
    }

    /* transaction propagation */
    
    @Test
    public void propagateRequiredAndProvided() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        PolicyUtil.provide(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);

        tm.begin();
        Transaction tx = tm.getTransaction();

        handler.handleMessage(exchange);
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);

        Transaction tx2 = tm.getTransaction();
        Assert.assertEquals(tx, tx2);
        Assert.assertEquals(Status.STATUS_ACTIVE, tx2.getStatus());
    }

    @Test
    public void propagateRequiredAndProvidedWithManagedGlobalRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        propagateRequiredAndProvided();
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL));
    }

    /* Creates new transaction */
    
    @Test
    public void propagateRequiredButNotProvidedWithManagedGlobalRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        exchange.setPhase(ExchangePhase.IN);

        Assert.assertEquals(null, tm.getTransaction());

        handler.handleMessage(exchange);
        Transaction tx = tm.getTransaction();
        Assert.assertEquals(Status.STATUS_ACTIVE, tx.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.PROPAGATES_TRANSACTION));
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL));

        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Assert.assertEquals(Status.STATUS_COMMITTED, tx.getStatus());
    }

    @Test
    public void managedLocalRequiredWithNoTransactionProvided() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL);
        exchange.setPhase(ExchangePhase.IN);
        
        Assert.assertEquals(null, tm.getTransaction());
        
        handler.handleMessage(exchange);
        Transaction tx = tm.getTransaction();
        Assert.assertEquals(Status.STATUS_ACTIVE, tx.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL));
        
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Assert.assertEquals(Status.STATUS_COMMITTED, tx.getStatus());
    }
    
    @Test
    public void transactionRolledbackByHandler() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.PROPAGATES_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        exchange.setPhase(ExchangePhase.IN);

        Assert.assertEquals(null, tm.getTransaction());

        handler.handleMessage(exchange);
        Transaction tx = tm.getTransaction();
        Assert.assertEquals(Status.STATUS_ACTIVE, tx.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.PROPAGATES_TRANSACTION));
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL));

        tx.setRollbackOnly();
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Assert.assertEquals(Status.STATUS_ROLLEDBACK, tx.getStatus());
    }

    @Test
    public void suspendAndManagedGlobalRequiredButNoTransaction() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.SUSPENDS_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        exchange.setPhase(ExchangePhase.IN);

        handler.handleMessage(exchange);
        Transaction tx2 = tm.getTransaction();
        Assert.assertEquals(Status.STATUS_ACTIVE, tx2.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.SUSPENDS_TRANSACTION));
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL));
        
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Assert.assertEquals(Status.STATUS_COMMITTED, tx2.getStatus());
    }

    /* Suspends existing transaction and create new transaction */
    
    @Test
    public void suspendAndManagedGlobalRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.SUSPENDS_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL);
        exchange.setPhase(ExchangePhase.IN);
        tm.begin();
        Transaction tx1 = tm.getTransaction();

        handler.handleMessage(exchange);
        Transaction tx2 = tm.getTransaction();
        Assert.assertNotSame(tx1, tx2);
        Assert.assertEquals(Status.STATUS_ACTIVE, tx1.getStatus());
        Assert.assertEquals(Status.STATUS_ACTIVE, tx2.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.SUSPENDS_TRANSACTION));
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_GLOBAL));
        
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Assert.assertEquals(tx1, tm.getTransaction());
        Assert.assertEquals(Status.STATUS_ACTIVE, tx1.getStatus());
        Assert.assertEquals(Status.STATUS_COMMITTED, tx2.getStatus());
    }

    @Test
    public void suspendsAndManagedLocalRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.SUSPENDS_TRANSACTION);
        PolicyUtil.require(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL);
        exchange.setPhase(ExchangePhase.IN);
        tm.begin();
        Transaction tx1 = tm.getTransaction();
        
        handler.handleMessage(exchange);
        Transaction tx2 = tm.getTransaction();
        Assert.assertNotSame(tx1, tx2);
        Assert.assertEquals(Status.STATUS_ACTIVE, tx1.getStatus());
        Assert.assertEquals(Status.STATUS_ACTIVE, tx2.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.SUSPENDS_TRANSACTION));
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.MANAGED_TRANSACTION_LOCAL));
        
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Transaction tx3 = tm.getTransaction();
        Assert.assertEquals(tx1, tx3);
        Assert.assertEquals(Status.STATUS_COMMITTED, tx2.getStatus());
        Assert.assertEquals(Status.STATUS_ACTIVE, tx3.getStatus());
    }

    /* Suspends existing transaction and run under no managed transaction */
    
    @Test
    public void noManagedRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);
        tm.begin();
        Transaction tx1 = tm.getTransaction();
        
        handler.handleMessage(exchange);
        Transaction tx2 = tm.getTransaction();
        Assert.assertNotNull(tx1);
        Assert.assertNull(tx2);
        Assert.assertEquals(Status.STATUS_ACTIVE, tx1.getStatus());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION));
        
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Transaction tx3 = tm.getTransaction();
        Assert.assertEquals(tx1, tx3);
        Assert.assertEquals(Status.STATUS_ACTIVE, tx3.getStatus());
    }

    @Test
    public void suspendsRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.SUSPENDS_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);
        tm.begin();
        Transaction tx1 = tm.getTransaction();

        handler.handleMessage(exchange);
        // transaction should be disabled
        Assert.assertNull(tm.getTransaction());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.SUSPENDS_TRANSACTION));

        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Transaction tx2 = tm.getTransaction();
        // transaction should be enabled
        Assert.assertEquals(tx1, tx2);
        Assert.assertEquals(Status.STATUS_ACTIVE, tx2.getStatus());
    }
    
    @Test
    public void suspendAndNoManagedRequired() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION);
        suspendsRequired();
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION));
    }
    
    /* no transaction */
    
    @Test
    public void suspendRequiredButNoTransaction() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.SUSPENDS_TRANSACTION);
        exchange.setPhase(ExchangePhase.IN);
        handler.handleMessage(exchange);
        Assert.assertNull(tm.getTransaction());
        exchange.setPhase(ExchangePhase.OUT);
        handler.handleMessage(exchange);
        Assert.assertNull(tm.getTransaction());
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.SUSPENDS_TRANSACTION));
    }
    
    @Test
    public void suspendAndNoManagedRequiredButNoTransaction() throws Exception {
        PolicyUtil.require(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION);
        suspendRequiredButNoTransaction();
        Assert.assertTrue(PolicyUtil.isProvided(exchange, TransactionPolicy.NO_MANAGED_TRANSACTION));
    }
}

class MockTransactionManager implements TransactionManager {

    private ThreadLocal<MockTransaction> transaction = new ThreadLocal<MockTransaction>();

    @Override
    public void begin() throws NotSupportedException, SystemException {
        transaction.set(new MockTransaction());
    }

    @Override
    public void commit() throws RollbackException, HeuristicMixedException,
            HeuristicRollbackException, SecurityException,
            IllegalStateException, SystemException {
        transaction.get().commit();
        transaction.remove();
    }

    @Override
    public int getStatus() throws SystemException {
        if (transaction.get() != null) {
            return transaction.get().getStatus();
        } else {
            return Status.STATUS_NO_TRANSACTION;
        }
    }

    @Override
    public MockTransaction getTransaction() throws SystemException {
        return transaction.get();
    }

    @Override
    public void resume(Transaction arg0) throws InvalidTransactionException,
            IllegalStateException, SystemException {
        transaction.set(MockTransaction.class.cast(arg0));
    }

    @Override
    public void rollback() throws IllegalStateException, SecurityException,
            SystemException {
        transaction.get().rollback();
        transaction.remove();
    }

    @Override
    public void setRollbackOnly() throws IllegalStateException, SystemException {
        transaction.get().setRollbackOnly();
    }

    @Override
    public void setTransactionTimeout(int arg0) throws SystemException {
        // TODO Auto-generated method stub

    }

    @Override
    public Transaction suspend() throws SystemException {
        MockTransaction t = transaction.get();
        transaction.remove();
        return t;
    }
}

class MockTransaction implements Transaction {

    private int status = Status.STATUS_ACTIVE;
    private String uuid;
    
    public MockTransaction() {
        uuid = UUID.randomUUID().toString();
    }
    
    @Override
    public void commit() throws RollbackException, HeuristicMixedException,
            HeuristicRollbackException, SecurityException,
            IllegalStateException, SystemException {
        status = Status.STATUS_COMMITTED;
    }

    @Override
    public boolean delistResource(XAResource arg0, int arg1)
            throws IllegalStateException, SystemException {
        return false;
    }

    @Override
    public boolean enlistResource(XAResource arg0) throws RollbackException,
            IllegalStateException, SystemException {
        return false;
    }

    @Override
    public int getStatus() throws SystemException {
        return status;
    }

    @Override
    public void registerSynchronization(Synchronization arg0)
            throws RollbackException, IllegalStateException, SystemException {
    }

    @Override
    public void rollback() throws IllegalStateException, SystemException {
        status = Status.STATUS_ROLLEDBACK;
    }

    @Override
    public void setRollbackOnly() throws IllegalStateException, SystemException {
        status = Status.STATUS_MARKED_ROLLBACK;
    }

    public String getUUID() {
        return uuid;
    }
    
    @Override
    public boolean equals(Object obj) {
        MockTransaction target = MockTransaction.class.cast(obj);
        return uuid.equals(target.getUUID());
    }
}