/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */

package org.teiid.dqp.internal.process;

import static org.junit.jupiter.api.Assertions.*;

import javax.resource.spi.XATerminator;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAResource;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.teiid.adminapi.Transaction;
import org.teiid.client.xa.XATransactionException;
import org.teiid.client.xa.XidImpl;
import org.teiid.common.queue.FakeWorkManager;
import org.teiid.dqp.service.TransactionContext;

public class TestTransactionServer {

    private TransactionServerImpl server;
    private XATerminator xaTerminator;
    private TransactionManager tm;
	private javax.transaction.Transaction txn;
    
    private static final String THREAD1 = "abc1"; //$NON-NLS-1$
    private static final String THREAD2 = "abc2"; //$NON-NLS-1$

    private static final XidImpl XID1 = new XidImpl(0, new byte[] {
        1
    }, new byte[0]);
    private static final XidImpl XID2 = new XidImpl(0, new byte[] {
        2
    }, new byte[0]);

    @BeforeEach public void setUp() throws Exception {
        server = new TransactionServerImpl();
        xaTerminator = Mockito.mock(XATerminator.class);
        tm = Mockito.mock(TransactionManager.class);
        txn = Mockito.mock(javax.transaction.Transaction.class);
        Mockito.when(tm.getTransaction()).thenReturn(txn);
        Mockito.when(tm.suspend()).thenReturn(txn);
        server.setXaTerminator(xaTerminator);
        server.setTransactionManager(tm);
        server.setWorkManager(new FakeWorkManager());
    }

    /**
     * once in a local, cannot start a global
     */
    @Test public void testTransactionExclusion() throws Exception {
        server.begin(THREAD1);

        try {
            server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100, false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30517 Client thread already involved in a transaction. Transaction nesting is not supported. The current transaction must be completed first.", //$NON-NLS-1$
                         ex.getMessage());
        }
    }

    /**
     * once in a global, cannot start a local
     */
    @Test public void testTransactionExclusion1() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100, false);

        try {
            server.begin(THREAD1);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30517 Client thread already involved in a transaction. Transaction nesting is not supported. The current transaction must be completed first.", //$NON-NLS-1$
                         ex.getMessage());
        }
    }

    /**
     * global can only be started once
     */
    @Test public void testTransactionExclusion2() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);

        try {
            server.start(THREAD2, XID1, XAResource.TMNOFLAGS, 100,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30522 Global transaction Teiid-Xid global:1 branch:null format:0 already exists.", ex.getMessage()); //$NON-NLS-1$
        }
    }

    /**
     * global cannot be nested
     */
    @Test public void testTransactionExclusion3() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);

        try {
            server.start(THREAD1, XID2, XAResource.TMNOFLAGS, 100,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30517 Client thread already involved in a transaction. Transaction nesting is not supported. The current transaction must be completed first.", //$NON-NLS-1$
                         ex.getMessage());
        }
    }

    /**
     * local cannot be nested
     */
    @Test public void testTransactionExclusion4() throws Exception {
        server.begin(THREAD1);

        try {
            server.begin(THREAD1);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30517 Client thread already involved in a transaction. Transaction nesting is not supported. The current transaction must be completed first.", //$NON-NLS-1$
                         ex.getMessage());
        }
    }
    
    /**
     * global cannot be nested
     */
    @Test public void testTransactionExclusion5() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        server.start(THREAD2, XID2, XAResource.TMNOFLAGS, 100,false);
        server.end(THREAD2, XID2, XAResource.TMSUCCESS,false);

        try {
            server.start(THREAD1, XID2, XAResource.TMJOIN, 100,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30517 Client thread already involved in a transaction. Transaction nesting is not supported. The current transaction must be completed first.", //$NON-NLS-1$
                         ex.getMessage());
        }
    }

    @Test public void testLocalCommit() throws Exception {
        server.begin(THREAD1);
        server.commit(THREAD1);
        
        Mockito.verify(tm).commit();

        try {
            server.commit(THREAD1);
        } catch (XATransactionException e) {
            assertEquals("TEIID30526 javax.transaction.InvalidTransactionException: No transaction found for client abc1.", e.getMessage()); //$NON-NLS-1$
        }
    }

    @Test public void testTwoPhaseCommit() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
    	server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        server.commit(THREAD1, XID1, false, false);
        
        Mockito.verify(xaTerminator).commit(XID1, false);
    }     
    
    @Test public void testLocalRollback() throws Exception {
        server.begin(THREAD1);
        server.rollback(THREAD1);
        Mockito.verify(tm).rollback();
        
        try {
            server.rollback(THREAD1);
        } catch (XATransactionException e) {
            assertEquals("TEIID30526 javax.transaction.InvalidTransactionException: No transaction found for client abc1.", e.getMessage()); //$NON-NLS-1$
        }
    }

    @Test public void testConcurrentEnlistment() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);

        try {
            server.start(THREAD1, XID1, XAResource.TMJOIN, 100,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30525 Concurrent enlistment in global transaction Teiid-Xid global:1 branch:null format:0 is not supported.", //$NON-NLS-1$
                         ex.getMessage());
        }
    }

    @Test public void testSuspend() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        server.end(THREAD1, XID1, XAResource.TMSUSPEND,false);

        try {
            server.end(THREAD1, XID1, XAResource.TMSUSPEND,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30524 Client is not currently enlisted in transaction Teiid-Xid global:1 branch:null format:0.", ex.getMessage()); //$NON-NLS-1$
        }
    }
    
    @Test public void testSuspendResume() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        server.end(THREAD1, XID1, XAResource.TMSUSPEND,false);
        server.start(THREAD1, XID1, XAResource.TMRESUME, 100,false);
        server.end(THREAD1, XID1, XAResource.TMSUSPEND,false);

        try {
            server.start(THREAD2, XID1, XAResource.TMRESUME, 100,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30518 Cannot resume, transaction Teiid-Xid global:1 branch:null format:0 was not suspended by client abc2.", ex.getMessage()); //$NON-NLS-1$
        }
    }

    @Test public void testUnknownFlags() throws Exception {
        try {
            server.start(THREAD1, XID1, Integer.MAX_VALUE, 100,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30519 Unknown START flags", ex.getMessage()); //$NON-NLS-1$
        }
    }

    @Test public void testUnknownGlobalTransaction() throws Exception {
        try {
            server.end(THREAD1, XID1, XAResource.TMSUCCESS,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30521 No global transaction found for Teiid-Xid global:1 branch:null format:0.", ex.getMessage()); //$NON-NLS-1$
        }
    }
    
    @Test public void testPrepareWithSuspended() throws Exception {
        server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        server.end(THREAD1, XID1, XAResource.TMSUSPEND,false);

        try {
            server.prepare(THREAD1, XID1,false);
            fail("exception expected"); //$NON-NLS-1$
        } catch (XATransactionException ex) {
            assertEquals("TEIID30505 Suspended work still exists on transaction Teiid-Xid global:1 branch:null format:0.", ex.getMessage()); //$NON-NLS-1$
        }
    }
    
    @Test public void testGetTransactionContext() throws Exception {
        assertSame(server.getOrCreateTransactionContext(THREAD1), server.getOrCreateTransactionContext(THREAD1));
    }
    
    @Test public void testGetTransactions() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        server.begin(THREAD2);
        
        assertEquals(2, server.getTransactions().size());
        
        server.commit(THREAD2);
        assertEquals(1, server.getTransactions().size());
        
        Transaction t = server.getTransactions().iterator().next();
        assertEquals(THREAD1, t.getAssociatedSession());
        assertNotNull(t.getId());
    }
    
    @Test public void testGlobalPrepare() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
        server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        
    	server.prepare(THREAD1, XID1, false);
    	
    	Mockito.verify(xaTerminator).prepare(tc.getXid());
    	
    	server.commit(THREAD1, XID1, true, false);
    }
    
    @Test public void testGlobalPrepareFail() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
        server.end(THREAD1, XID1, XAResource.TMFAIL, false);
        Mockito.verify(txn).setRollbackOnly();
    }    
    
    @Test public void testGlobalOnePhaseCommit() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	
        server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        
        server.prepare(THREAD1, XID1, false);

		
		server.commit(THREAD1, XID1, true, false);
		Mockito.verify(xaTerminator).commit(tc.getXid(), false);
    }  
    
    @Test public void testGlobalOnePhaseCommit_force_prepare_through() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	
        server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        
		
		server.commit(THREAD1, XID1, true, false);
		
		Mockito.verify(xaTerminator).prepare(tc.getXid());
		Mockito.verify(xaTerminator).commit(tc.getXid(), false);
    }  
    
    @Test public void testGlobalOnePhaseCommit_force_prepare() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	
        server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        
		
		server.commit(THREAD1, XID1, true, false);
		
		// since there are two sources the commit is not single phase
		Mockito.verify(xaTerminator).prepare(tc.getXid());
		Mockito.verify(xaTerminator).commit(tc.getXid(), false);
    }  
    
    
    @Test public void testGlobalOnePhase_teiid_multiple() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	
        server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        
        server.prepare(THREAD1, XID1, false);

		
		server.commit(THREAD1, XID1, true, false);
		
		// since there are two sources the commit is not single phase
		Mockito.verify(xaTerminator).commit(tc.getXid(), false);
    }    
    
    @Test public void testGlobalOnePhaseRoolback() throws Exception {
    	server.start(THREAD1, XID1, XAResource.TMNOFLAGS, 100,false);
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	
        server.end(THREAD1, XID1, XAResource.TMSUCCESS, false);
        
        server.prepare(THREAD1, XID1, false);

		
		server.rollback(THREAD1, XID1, false);
		
		// since there are two sources the commit is not single phase
		Mockito.verify(xaTerminator).rollback(tc.getXid());
    }     
    
    @Test public void testRequestCommit() throws Exception{
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	server.begin(tc);
    	server.commit(tc);
    	// EXTENSION
    	// ISSUE: DVCORE-2576
    	// FIXED BY: Roman Pedchenko
    	// DATE: 2015.01.25
    	// ADOPTED FROM: https://issues.jboss.org/browse/TEIID-2632
    	assertEquals(TransactionContext.Scope.NONE, tc.getTransactionType());
    	// END EXTENSION
    	Mockito.verify(tm).commit();
    }
    
    @Test public void testRequestRollback() throws Exception{
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	server.begin(tc);
    	
    	server.rollback(tc);
    	// EXTENSION
    	// ISSUE: DVCORE-2576
    	// FIXED BY: Roman Pedchenko
    	// DATE: 2015.01.25
    	// ADOPTED FROM: https://issues.jboss.org/browse/TEIID-2632
    	assertEquals(TransactionContext.Scope.NONE, tc.getTransactionType());
    	// END EXTENSION
    	Mockito.verify(tm).rollback();
    }     
    
    @Test public void testLocalCancel() throws Exception {
        server.begin(THREAD1);
        
        server.cancelTransactions(THREAD1, false);
        
        Mockito.verify(txn).setRollbackOnly();
    }  
    
    @Test public void testRequestCancel() throws Exception{
    	TransactionContext tc = server.getOrCreateTransactionContext(THREAD1);
    	server.begin(tc);
    	
    	server.cancelTransactions(THREAD1, true);
    	Mockito.verify(txn).setRollbackOnly();
    }      
}
