package org.multiverse.stms.gamma;

import org.junit.Before;
import org.junit.Test;
import org.multiverse.api.Txn;
import org.multiverse.api.TxnExecutor;
import org.multiverse.api.PropagationLevel;
import org.multiverse.api.callables.TxnIntCallable;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.api.exceptions.TxnMandatoryException;
import org.multiverse.api.exceptions.TxnNotAllowedException;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnLong;
import org.multiverse.stms.gamma.transactions.GammaTxn;
import org.multiverse.stms.gamma.transactions.GammaTxnFactory;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.multiverse.TestUtils.assertIsActive;
import static org.multiverse.api.TxnThreadLocal.*;

public class FatGammaTxnExecutor_propagationLevelTest implements GammaConstants {
    private GammaStm stm;

    @Before
    public void setUp() {
        stm = new GammaStm();
        clearThreadLocalTxn();
    }

    @Test
    public void whenNeverAndTransactionAvailable_thenNoTransactionAllowedException() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Never)
                .newTxnExecutor();

        GammaTxn otherTx = stm.newDefaultTxn();
        setThreadLocalTxn(otherTx);

        TxnVoidCallable callable = mock(TxnVoidCallable.class);

        try {
            executor.execute(callable);
            fail();
        } catch (TxnNotAllowedException expected) {
        }

        verifyZeroInteractions(callable);
        assertIsActive(otherTx);
        assertSame(otherTx, getThreadLocalTxn());
    }

    @Test
    public void whenNeverAndNoTransactionAvailable() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Never)
                .newTxnExecutor();

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertNull(tx);
                return 10;
            }
        };

        int result = executor.execute(callable);

        assertEquals(10, result);
        assertNull(getThreadLocalTxn());
    }

    @Test
    public void whenMandatoryAndNoTransactionAvailable_thenNoTransactionFoundException() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Mandatory)
                .newTxnExecutor();

        TxnVoidCallable callable = mock(TxnVoidCallable.class);

        try {
            executor.execute(callable);
            fail();
        } catch (TxnMandatoryException expected) {
        }

        verifyZeroInteractions(callable);
        assertNull(getThreadLocalTxn());
    }

    @Test
    public void whenMandatoryAndTransactionAvailable_thenExistingTransactionUsed() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Mandatory)
                .newTxnExecutor();

        final GammaTxn otherTx = stm.newDefaultTxn();
        setThreadLocalTxn(otherTx);

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertSame(otherTx, tx);
                return 10;
            }
        };

        int result = executor.execute(callable);

        assertEquals(10, result);
        assertIsActive(otherTx);
        assertSame(otherTx, getThreadLocalTxn());
    }

    @Test
    public void whenRequiresAndNoTransactionAvailable_thenNewTransactionUsed() {
        GammaTxnFactory txFactory = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Requires)
                .newTransactionFactory();

        final GammaTxnLong ref = new GammaTxnLong(stm);

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertNotNull(tx);
                GammaTxn btx = (GammaTxn) tx;
                ref.incrementAndGet(1);
                return 10;
            }
        };

        int result = new FatGammaTxnExecutor(txFactory).execute(callable);

        assertEquals(10, result);
        assertNull(getThreadLocalTxn());
        assertEquals(1, ref.atomicGet());
    }

    @Test
    public void whenRequiresAndTransactionAvailable_thenExistingTransactionUsed() {
        GammaTxnFactory txFactory = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Requires)
                .newTransactionFactory();

        final GammaTxn existingTx = stm.newDefaultTxn();
        setThreadLocalTxn(existingTx);

        final GammaTxnLong ref = new GammaTxnLong(stm);

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertSame(existingTx, tx);
                GammaTxn btx = (GammaTxn) tx;
                ref.incrementAndGet(btx, 1);
                return 10;
            }
        };

        int result = new FatGammaTxnExecutor(txFactory).execute(callable);

        assertEquals(10, result);
        assertSame(existingTx, getThreadLocalTxn());
        assertIsActive(existingTx);
        //since the value hasn't committed yet, it still is zero (the value before the transaction began).
        assertEquals(0, ref.atomicGet());
    }

    @Test
    public void whenRequiresNewAndNoTransactionAvailable_thenNewTransactionCreated() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.RequiresNew)
                .newTxnExecutor();

        final GammaTxnLong ref = new GammaTxnLong(stm, 0);

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertNotNull(tx);
                GammaTxn btx = (GammaTxn) tx;
                ref.incrementAndGet(btx, 1);
                return 10;
            }
        };

        int result = executor.execute(callable);

        assertEquals(10, result);
        assertEquals(1, ref.atomicGet());
        assertNull(getThreadLocalTxn());
    }

    @Test
    public void whenRequiresNewAndTransactionAvailable_thenExistingTransactionSuspended() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.RequiresNew)
                .newTxnExecutor();

        final GammaTxn otherTx = stm.newDefaultTxn();
        setThreadLocalTxn(otherTx);

        final GammaTxnLong ref = new GammaTxnLong(stm, 10);

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertNotNull(tx);
                assertNotSame(otherTx, tx);
                GammaTxn btx = (GammaTxn) tx;
                ref.incrementAndGet(btx, 1);
                return 1;
            }
        };

        int result = executor.execute(callable);

        assertEquals(1, result);
        assertEquals(11, ref.atomicGet());
        assertSame(otherTx, getThreadLocalTxn());
        assertIsActive(otherTx);
    }

    @Test
    public void whenSupportsAndTransactionAvailable() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Supports)
                .newTxnExecutor();

        final GammaTxn otherTx = stm.newDefaultTxn();
        setThreadLocalTxn(otherTx);

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertSame(otherTx, tx);
                return 10;
            }
        };

        int result = executor.execute(callable);

        assertEquals(10, result);
        assertIsActive(otherTx);
        assertSame(otherTx, getThreadLocalTxn());
    }

    @Test
    public void whenSupportsAndNoTransactionAvailable() {
        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setPropagationLevel(PropagationLevel.Supports)
                .newTxnExecutor();

        TxnIntCallable callable = new TxnIntCallable() {
            @Override
            public int call(Txn tx) throws Exception {
                assertNull(tx);
                return 10;
            }
        };

        int result = executor.execute(callable);

        assertEquals(10, result);
        assertNull(getThreadLocalTxn());
    }
}
