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.LockMode;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.api.functions.Function;
import org.multiverse.api.lifecycle.TxnListener;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnLong;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnRef;
import org.multiverse.stms.gamma.transactions.GammaTxn;
import org.multiverse.stms.gamma.transactions.fat.FatMonoGammaTxn;
import org.multiverse.stms.gamma.transactions.fat.FatVariableLengthGammaTxn;
import org.multiverse.stms.gamma.transactions.lean.LeanFixedLengthGammaTxn;
import org.multiverse.stms.gamma.transactions.lean.LeanMonoGammaTxn;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.multiverse.TestUtils.assertInstanceof;
import static org.multiverse.api.TxnThreadLocal.clearThreadLocalTxn;
import static org.multiverse.api.TxnThreadLocal.getThreadLocalTxn;

public class GammaTxnExecutor_speculativeTest implements GammaConstants {

    private GammaStm stm;

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

    @Test
    public void whenTransactionGrowing() {
        final GammaTxnRef<Long>[] refs = new GammaTxnRef[1000];
        for (int k = 0; k < refs.length; k++) {
            refs[k] = new GammaTxnRef<Long>(stm, 0L);
        }

        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();
        final AtomicInteger attempt = new AtomicInteger(1);

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setSpeculative(true)
                .setControlFlowErrorsReused(false)
                .setDirtyCheckEnabled(false)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                System.out.println(tx.getClass());
                assertSame(tx, getThreadLocalTxn());
                GammaTxn btx = (GammaTxn) tx;
                assertEquals(attempt.get(), tx.getAttempt());
                attempt.incrementAndGet();

                transactions.add(btx);

                for (GammaTxnRef<Long> ref : refs) {
                    ref.set(1L);
                }
            }
        });

        for (GammaTxnRef ref : refs) {
            assertEquals(1L, ref.atomicGet());
        }

        assertEquals(4, transactions.size());
        assertInstanceof(LeanMonoGammaTxn.class, transactions.get(0));
        assertInstanceof(LeanFixedLengthGammaTxn.class, transactions.get(1));
        assertInstanceof(FatVariableLengthGammaTxn.class, transactions.get(2));
        assertInstanceof(FatVariableLengthGammaTxn.class, transactions.get(3));
    }

    @Test
    public void whenCommute() {
        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();
        final GammaTxnRef<String> ref = new GammaTxnRef<String>(stm);
        final Function<String> function = mock(Function.class);

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setDirtyCheckEnabled(false)
                .setSpeculative(true)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);
                ref.commute(btx, function);
            }
        });

        assertEquals(2, transactions.size());
        assertTrue(transactions.get(0) instanceof LeanMonoGammaTxn);
        assertTrue(transactions.get(1) instanceof FatMonoGammaTxn);
    }

    @Test
    public void whenEnsure() {
        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();
        final GammaTxnRef<String> ref = new GammaTxnRef<String>(stm);

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setDirtyCheckEnabled(false)
                .setSpeculative(true)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);
                ref.get(btx);
                ref.ensure(btx);
            }
        });

        assertEquals(2, transactions.size());
        assertTrue(transactions.get(0) instanceof LeanMonoGammaTxn);
        assertTrue(transactions.get(1) instanceof FatMonoGammaTxn);
    }


    @Test
    public void whenConstructing() {
        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setSpeculative(true)
                .setDirtyCheckEnabled(false)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);

                new GammaTxnRef(btx);
            }
        });

        assertEquals(2, transactions.size());
        assertTrue(transactions.get(0) instanceof LeanMonoGammaTxn);
        assertTrue(transactions.get(1) instanceof FatMonoGammaTxn);
    }

    @Test
    public void whenNonRef() {
        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();
        final GammaTxnLong ref = new GammaTxnLong(stm);

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setSpeculative(true)
                .setDirtyCheckEnabled(false)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);
                ref.get(tx);
            }
        });

        assertEquals(2, transactions.size());
        assertTrue(transactions.get(0) instanceof LeanMonoGammaTxn);
        assertTrue(transactions.get(1) instanceof FatMonoGammaTxn);
    }

    @Test
    public void whenExplicitLocking() {
        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();
        final GammaTxnRef ref = new GammaTxnRef(stm);

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setSpeculative(true)
                .setDirtyCheckEnabled(false)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);
                ref.getLock().acquire(LockMode.Read);
            }
        });

        assertEquals(2, transactions.size());
        assertTrue(transactions.get(0) instanceof LeanMonoGammaTxn);
        assertTrue(transactions.get(1) instanceof FatMonoGammaTxn);
    }

    @Test
    public void whenNormalListenerAdded() {
        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();
        final AtomicBoolean added = new AtomicBoolean();
        final TxnListener listener = mock(TxnListener.class);

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setSpeculative(true)
                .setDirtyCheckEnabled(false)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                assertSame(tx, getThreadLocalTxn());
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);

                if (!added.get()) {
                    btx.register(listener);
                }

            }
        });

        assertEquals(2, transactions.size());
        assertTrue(transactions.get(0) instanceof LeanMonoGammaTxn);
        assertTrue(transactions.get(1) instanceof FatMonoGammaTxn);
    }

    @Test
    public void whenTimeoutAvailable_thenCopied() {
        final GammaTxnLong ref1 = new GammaTxnLong(stm);
        final GammaTxnLong ref2 = new GammaTxnLong(stm);

        final List<GammaTxn> transactions = new LinkedList<GammaTxn>();

        TxnExecutor executor = stm.newTxnFactoryBuilder()
                .setTimeoutNs(1000)
                .setSpeculative(true)
                .setDirtyCheckEnabled(false)
                .newTxnExecutor();

        executor.execute(new TxnVoidCallable() {
            @Override
            public void call(Txn tx) throws Exception {
                assertSame(tx, getThreadLocalTxn());
                GammaTxn btx = (GammaTxn) tx;
                transactions.add(btx);

                if (transactions.size() == 1) {
                    btx.remainingTimeoutNs = 500;
                } else {
                    assertEquals(500, btx.getRemainingTimeoutNs());
                }

                ref1.openForWrite(btx, LOCKMODE_NONE);
                ref2.openForWrite(btx, LOCKMODE_NONE);
            }
        });
    }
}
