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

import static org.easymock.EasyMock.createControl;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
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.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.easymock.EasyMock;
import org.easymock.IMocksControl;
import org.junit.Before;
import org.junit.Test;

public class TranStrategyTest {

    TransactionManager tm;
    Transaction t;
    private IMocksControl c;
   
    @Before
    public void clean()
    {
      c = createControl();
      tm = c.createMock(TransactionManager.class);
      t = c.createMock(Transaction.class);
    }
    
    @Test
    public void testMandatoryBegin() throws Exception
    {
      // MANDATORY strategy should throw IllegalStateException when tran manager 
      // status is Status.STATUS_NO_TRANSACTION it should not return null.
      expect(tm.getStatus()).andReturn(Status.STATUS_NO_TRANSACTION);

      assertThrows(IllegalStateException.class, () -> {
        assertNotNull("TransactionStrategy.MANDATORY.begin(tm) returned null when manager " +
              "status is STATUS_NO_TRANSACTION", TransactionAttribute.MANDATORY.begin(tm).getActiveTransaction());
      });
          
      // MANDATORY strategy should return null for all tran manager states other
      // than Status.STATUS_NO_TRANSACTION.
      int[] invalids = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_MARKED_ROLLBACK,
          Status.STATUS_ACTIVE, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
          Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };
      
      for (int i = 0; i < invalids.length ; i++) {
        c.reset();
        expect(tm.getStatus()).andReturn(invalids[i]);
        expect(tm.getTransaction()).andReturn(null);
        c.replay();
          Transaction tran = TransactionAttribute.MANDATORY.begin(tm).getActiveTransaction();
          assertNull("TransactionStrategy.MANDATORY.begin() did not return null when manager status value is " + invalids[i], tran);
        c.verify();
      }
    }
     
    @Test
    public void testMandatoryFinish() throws HeuristicRollbackException, SystemException, HeuristicMixedException, InvalidTransactionException, RollbackException {
        TransactionToken tranToken = new TransactionToken(t, null, TransactionAttribute.MANDATORY);
        TransactionAttribute.MANDATORY.finish(tm, tranToken);

    }
    
    
    @Test
    public void testNeverBegin() throws Exception
    {
        // NEVER strategy should throw IllegalStateException when tran manager 
        // status is Status.STATUS_ACTIVE it should not return null.
        expect(tm.getStatus()).andReturn(Status.STATUS_ACTIVE);

        assertThrows(IllegalStateException.class, () -> {
            assertNotNull("TransactionStrategy.NEVER.begin() returned null when manager status is STATUS_ACTIVE", TransactionAttribute.NEVER.begin(tm));
        });

        // NEVER strategy should return null for all tran manager states other
        // than Status.STATUS_ACTIVE.
        int[] invalids = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_MARKED_ROLLBACK,
                                    Status.STATUS_NO_TRANSACTION, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
                                    Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };

        for (int i = 0; i < invalids.length ; i++) {
            c.reset();
            expect(tm.getStatus()).andReturn(invalids[i]);
            expect(tm.getTransaction()).andReturn(null).anyTimes();
            c.replay();
                assertNull("TransactionStrategy.NEVER.begin() did not return null when manager status value is " + invalids[i], TransactionAttribute.NEVER.begin(tm).getActiveTransaction());
            c.verify();
        }

    }
    
    @Test
    public void testNeverFinish() throws HeuristicRollbackException, SystemException, HeuristicMixedException, InvalidTransactionException, RollbackException {
        TransactionToken tranToken = new TransactionToken(null, null, TransactionAttribute.NEVER);
        TransactionAttribute.NEVER.finish(tm, tranToken);
    }
    
    @Test
    public void testNotSupportedBegin() throws Exception
    {
      // NOT_SUPPORTED strategy should suspend an active transaction
      // and _NOT_ begin a new one
      expect(tm.getStatus()).andReturn(Status.STATUS_ACTIVE);
      expect(tm.suspend()).andReturn(null);
      c.replay();
      TransactionAttribute.NOT_SUPPORTED.begin(tm);
      c.verify();
       
      // For all situations where there is no active transaction the
      // NOT_SUPPORTED strategy should return null
      int[] invalids = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_MARKED_ROLLBACK,
          Status.STATUS_NO_TRANSACTION, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
          Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };
      
      for (int i = 0; i < invalids.length ; i++) {
          c.reset();
          expect(tm.getStatus()).andReturn(invalids[i]);
          expect(tm.getTransaction()).andReturn(null).anyTimes();
          c.replay();
          assertNull("TransactionStrategy.NOT_SUPPORTED.begin() did not return null when manager status value is " + invalids[i], TransactionAttribute.NOT_SUPPORTED.begin(tm).getActiveTransaction());
        c.verify();
      }
     
    }
    
    @Test
    public void testNotSupportedFinish() throws Exception {
      // If finish is called with a previously active transaction, then
      // we expect this transaction to be resumed for a NOT_SUPPORTED strategy
        tm.resume(t);
        EasyMock.expectLastCall();
        c.replay();
        TransactionToken tranToken = new TransactionToken(null, t, TransactionAttribute.NOT_SUPPORTED);
        TransactionAttribute.NOT_SUPPORTED.finish(tm, tranToken);
        c.verify();
        
        c.reset();
        tranToken = new TransactionToken(null, null, TransactionAttribute.NOT_SUPPORTED);
        TransactionAttribute.NOT_SUPPORTED.finish(tm, tranToken);
    }
    
    @Test
    public void testRequiredBegin() throws Exception
    {
      // If there is no previously active transaction when the REQUIRED strategy
      // is invoked then we expect a call to begin one
      expect(tm.getStatus()).andReturn(Status.STATUS_NO_TRANSACTION);
      expect(tm.getTransaction()).andReturn(null);
      tm.begin();
      expectLastCall();
      c.replay();
      TransactionAttribute.REQUIRED.begin(tm);
      c.verify();
      c.reset();
       
      // For all cases where there is a transaction we expect REQUIRED to return null
      int[] invalids = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_MARKED_ROLLBACK,
          Status.STATUS_ACTIVE, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
          Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };
      
      for (int i = 0; i < invalids.length ; i++) {
          c.reset();
          expect(tm.getStatus()).andReturn(invalids[i]);
          expect(tm.getTransaction()).andReturn(null);
          c.replay();
          assertNull("TransactionStrategy.REQUIRED.begin() did not return null when manager status value is " + invalids[i], TransactionAttribute.REQUIRED.begin(tm).getActiveTransaction());
        c.verify();
      }
    }
    
    @Test
    public void testRequiredFinish() throws Exception
    {
      // In the REQUIRED strategy we expect a call to rollback when a call to finish()
      // is made with a tran where the tran manager status shows Status.STATUS_MARKED_ROLLBACK
        expect(tm.getStatus()).andReturn(Status.STATUS_MARKED_ROLLBACK);
      
        TransactionToken tranToken = new TransactionToken(t, null, TransactionAttribute.REQUIRED, true);
        tm.rollback();
        expectLastCall();
        c.replay();
        TransactionAttribute.REQUIRED.finish(tm, tranToken);
        c.verify();
        
        int[] invalids = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_NO_TRANSACTION,
            Status.STATUS_ACTIVE, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
            Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };
        
        // For all other tran manager states we expect a call to commit
        for (int i = 0; i < invalids.length ; i++) {
          c.reset();
          expect(tm.getStatus()).andReturn(invalids[i]);
          tm.commit();
          expectLastCall();
          c.replay();
          TransactionAttribute.REQUIRED.finish(tm, tranToken);
          c.verify();
        }
        
        // If null is passed instead of a tran we expect nothing to happen
        c.reset();
        c.replay();
        tranToken = new TransactionToken(null, null, TransactionAttribute.REQUIRED);
        c.verify();
        TransactionAttribute.REQUIRED.finish(tm, tranToken);
      
    }
    
    @Test
    public void testRequiresNew_BeginActiveTran() throws Exception
    {
      // Suspend case (no exception from tm.begin())
      expect(tm.getStatus()).andReturn(Status.STATUS_ACTIVE);
      
      // In the case of the REQUIRES_NEW strategy we expect an active tran to be suspended
      // a new new transaction to begin
      expect(tm.suspend()).andReturn(null);
      expectLastCall();
      tm.begin();
      expectLastCall();
      expect(tm.getTransaction()).andReturn(null);
      c.replay();
      TransactionAttribute.REQUIRES_NEW.begin(tm);
      c.verify();
    }
    
    @Test
    public void testRequiresNew_BeginNoActiveTran() throws Exception
    {
      // No active tran cases (no exception from tm.begin())

      int[] manStatus = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_MARKED_ROLLBACK,
          Status.STATUS_NO_TRANSACTION, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
          Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };
      
      // For all cases where the tran manager state is _not_ Status.STATUS_ACTIVE 
      // we expect a call to begin a new tran, no call to suspend and null to be
      // returned from TransactionStrategy.REQUIRES_NEW.begin(tm)
      for (int i = 0; i < manStatus.length ; i++) {
          c.reset();
          expect(tm.getStatus()).andReturn(manStatus[i]);
          expect(tm.getTransaction()).andReturn(null);
          tm.begin();
          expectLastCall();
          c.replay();
          assertNull("TransactionStrategy.REQUIRES_NEW.begin() did not return null when manager status value is " + manStatus[i], TransactionAttribute.REQUIRES_NEW.begin(tm).getActiveTransaction());
        c.verify();
      }
     
    }
    
    @Test
    public void testRequiresNew_TmExceptions() throws Exception
    {
      int[] allStates = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_NO_TRANSACTION,
          Status.STATUS_ACTIVE, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
          Status.STATUS_MARKED_ROLLBACK, Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };
      
      // SystemException and NotSupportedException from tm.begin()
      Set<Exception> ees = new HashSet<Exception>();
      ees.add(new SystemException("KABOOM!"));
      ees.add(new NotSupportedException("KABOOM!"));
      
      // Loop through all states states twice changing the exception thrown
      // from tm.begin()
      for (int i = 0 ; i < allStates.length ; i++ ) {
        Iterator<Exception> iterator = ees.iterator();
        while (iterator.hasNext()) {
          Exception e = iterator.next();
          c.reset();
          expect(tm.getStatus()).andReturn(allStates[i]);
          expect(tm.getTransaction()).andReturn(null).anyTimes();
          tm.begin();
          expectLastCall().andThrow(e);
          requiresNewExceptionCheck(tm, allStates[i]);
        }
      }
    }
    
    private void requiresNewExceptionCheck(TransactionManager tm, int managerStatus) throws Exception
    {
      // If an ACTIVE tran is already present we expect a call to suspend this tran.
      // All states should call begin(), whereupon we receive our exception resulting
      // in calls to resume(t)
      if (managerStatus == Status.STATUS_ACTIVE) {
          expect(tm.suspend()).andReturn(null);
      }
      tm.resume(EasyMock.anyObject(Transaction.class));
      expectLastCall();
      c.replay();
      try {
          TransactionAttribute.REQUIRES_NEW.begin(tm);
      } catch (SystemException se) {
          // Expect to be in here
      } catch (NotSupportedException nse) {
          // or to be in here
      } catch (Exception thrownE) {
          fail("TransactionStrategy.REQUIRES_NEW.begin() threw unexpected exception when manager status is " + managerStatus);
      }
      c.verify();
      c.reset();
    }
    
    @Test
    public void testRequiresNew_Finish() throws Exception
    {
        int[] allStates = new int[]{ Status.STATUS_COMMITTED, Status.STATUS_COMMITTING, Status.STATUS_NO_TRANSACTION,
                                     Status.STATUS_ACTIVE, Status.STATUS_PREPARED, Status.STATUS_PREPARING, Status.STATUS_ROLLEDBACK, 
                                     Status.STATUS_MARKED_ROLLBACK, Status.STATUS_ROLLING_BACK, Status.STATUS_UNKNOWN };

        // Loop through all states calling TransactionStrategy.REQUIRES_NEW.finish
        // passing tran manager and a tran, then passing tran manager and null
        for (int i = 0 ; i < allStates.length ; i++ ) {
            c.reset();
            expect(tm.getStatus()).andReturn(allStates[i]);
            requiresNew_assertion(tm, allStates[i]);
            tm.resume(EasyMock.anyObject(Transaction.class));
            expectLastCall();
            c.replay();
                TransactionToken tranToken = new TransactionToken(t, t, TransactionAttribute.REQUIRES_NEW, true);
                TransactionAttribute.REQUIRES_NEW.finish(tm, tranToken);
            c.verify();
            c.reset();
                expect(tm.getStatus()).andReturn(allStates[i]);
                requiresNew_assertion(tm, allStates[i]);
                c.replay();
                TransactionToken tranToken2 = new TransactionToken(t, null, TransactionAttribute.REQUIRES_NEW, true);
                TransactionAttribute.REQUIRES_NEW.finish(tm, tranToken2);
            c.verify();
        }

    }

    // If tran manager status reports Status.STATUS_MARKED_ROLLBACK we expect
    // a call to rollback ... otherwise we expect a call to commit
    private void requiresNew_assertion(TransactionManager tm, int status) throws Exception
    {
      if (status == Status.STATUS_MARKED_ROLLBACK) {
          tm.rollback();
      }
      else {
          tm.commit();
      }
      expectLastCall();
    }
    
    @Test
    public void testSupports() throws Exception
    {
      expect(tm.getTransaction()).andReturn(null);
      expect(tm.getStatus()).andReturn(Status.STATUS_ACTIVE);
      c.replay();
      assertNull("TransTransactionStrategy.SUPPORTS.begin(tm) did not return null", TransactionAttribute.SUPPORTS.begin(tm).getActiveTransaction());
      c.verify();
    }
}
