package org.multiverse.stms.gamma.integration.isolation.writeskew;

import org.junit.Before;
import org.junit.Test;
import org.multiverse.TestThread;
import org.multiverse.api.TxnExecutor;
import org.multiverse.api.IsolationLevel;
import org.multiverse.api.LockMode;
import org.multiverse.api.Txn;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnLong;
import org.multiverse.stms.gamma.transactions.GammaTxn;

import java.util.concurrent.atomic.AtomicBoolean;

import static java.lang.String.format;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.multiverse.TestUtils.*;
import static org.multiverse.api.GlobalStmInstance.getGlobalStmInstance;
import static org.multiverse.api.TxnThreadLocal.clearThreadLocalTxn;

/**
 * A Test that checks if the writeskew problem is happening. When
 * pessimisticRead/LockLevel.Read/writeskew=false is used, no writeskew is possible. Otherwise
 * it can happen.
 *
 * @author Peter Veentjer.
 */
public class WriteSkewStressTest {
    private volatile boolean stop;
    private Customer customer1;
    private Customer customer2;

    enum Mode {
        snapshot,
        pessimisticReadLevel,
        pessimisticWriteLevel,
        pessimisticReads,
        pessimisticWrites,
        serialized
    }

    private Mode mode;
    private TransferThread[] threads;
    private AtomicBoolean writeSkewEncountered = new AtomicBoolean();
    private GammaStm stm;
    private int threadCount = 8;

    @Before
    public void setUp() {
        stm = (GammaStm) getGlobalStmInstance();
        clearThreadLocalTxn();
        customer1 = new Customer();
        customer2 = new Customer();
        stop = false;
        writeSkewEncountered.set(false);

        threads = new TransferThread[threadCount];
        for (int k = 0; k < threads.length; k++) {
            threads[k] = new TransferThread(k);
        }

        GammaTxnLong account = customer1.getRandomAccount();
        GammaTxn tx = stm.newDefaultTxn();
        account.openForWrite(tx, LOCKMODE_NONE).long_value = 1000;
        tx.commit();
    }

    @Test
    public void whenPessimisticRead_thenNoWriteSkewPossible() {
        mode = Mode.pessimisticReads;
        startAll(threads);
        sleepMs(getStressTestDurationMs(30 * 1000));
        stop = true;

        joinAll(threads);

        System.out.println("User1: " + customer1);
        System.out.println("User2: " + customer2);

        assertFalse("writeskew detected", writeSkewEncountered.get());
    }

    /**
     * If this test fails, the anomaly we are looking for, hasn't occurred yet. Try increasing the
     * running time.
     */
    @Test
    public void whenPessimisticWrite_thenWriteSkewPossible() {
        mode = Mode.pessimisticWrites;
        startAll(threads);
        sleepMs(getStressTestDurationMs(30 * 1000));
        stop = true;

        joinAll(threads);

        System.out.println("User1: " + customer1);
        System.out.println("User2: " + customer2);

        assertTrue("no writeskew detected", writeSkewEncountered.get());
    }

    @Test
    public void whenPessimisticWriteSanityTest() {
        Customer customer1 = new Customer();
        Customer customer2 = new Customer();

        GammaTxn tx1 = stm.newDefaultTxn();
        GammaTxn tx2 = stm.newDefaultTxn();

        customer1.account1.get(tx1);
        customer1.account2.get(tx1);
        customer1.account2.get(tx1);
        customer2.account2.get(tx1);

        customer1.account1.get(tx2);
        customer1.account2.get(tx2);
        customer2.account1.get(tx2);
        customer2.account2.get(tx2);

        customer1.account1.openForWrite(tx1, LOCKMODE_READ).long_value++;
        customer2.account1.openForWrite(tx1, LOCKMODE_READ).long_value++;

        customer1.account2.openForWrite(tx2, LOCKMODE_READ).long_value++;
        customer2.account2.openForWrite(tx2, LOCKMODE_READ).long_value++;

        tx1.commit();
        tx2.commit();
    }


    @Test
    public void whenPessimisticWriteLevel_thenWriteSkewPossible() {
        mode = Mode.pessimisticWriteLevel;
        startAll(threads);
        sleepMs(getStressTestDurationMs(30 * 1000));
        stop = true;

        joinAll(threads);

        System.out.println("User1: " + customer1);
        System.out.println("User2: " + customer2);

        assertTrue("no writeskew detected", writeSkewEncountered.get());
    }

    @Test
    public void whenPessimisticWriteLevelSanityTest() {
        Customer customer1 = new Customer();
        Customer customer2 = new Customer();

        GammaTxn tx1 = stm.newTxnFactoryBuilder()
                .setSpeculative(false)
                .setWriteLockMode(LockMode.Read)
                .newTransactionFactory()
                .newTxn();
        GammaTxn tx2 = stm.newTxnFactoryBuilder()
                .setSpeculative(false)
                .setWriteLockMode(LockMode.Read)
                .newTransactionFactory()
                .newTxn();

        customer1.account1.get(tx1);
        customer1.account2.get(tx1);
        customer1.account2.get(tx1);
        customer2.account2.get(tx1);

        customer1.account1.get(tx2);
        customer1.account2.get(tx2);
        customer2.account1.get(tx2);
        customer2.account2.get(tx2);

        customer1.account1.openForWrite(tx1, LOCKMODE_NONE).long_value++;
        customer2.account1.openForWrite(tx1, LOCKMODE_NONE).long_value++;

        customer1.account2.openForWrite(tx2, LOCKMODE_NONE).long_value++;
        customer2.account2.openForWrite(tx2, LOCKMODE_NONE).long_value++;

        tx1.commit();
        tx2.commit();
    }

    @Test
    public void whenPessimisticReadLevel_thenNoWriteSkewPossible() {
        mode = Mode.pessimisticReadLevel;
        startAll(threads);
        sleepMs(getStressTestDurationMs(30 * 1000));
        stop = true;

        joinAll(threads);

        System.out.println("User1: " + customer1);
        System.out.println("User2: " + customer2);

        assertFalse("writeskew detected", writeSkewEncountered.get());
    }


    @Test
    public void whenSnapshotIsolation_thenWriteSkewPossible() {
        mode = Mode.snapshot;
        startAll(threads);
        sleepMs(getStressTestDurationMs(30 * 1000));
        stop = true;
        joinAll(threads);

        System.out.println("User1: " + customer1);
        System.out.println("User2: " + customer2);

        System.out.println("User1.account1: " + customer1.account1.toDebugString());
        System.out.println("User1.account2: " + customer1.account2.toDebugString());

        System.out.println("User2.account1: " + customer2.account1.toDebugString());
        System.out.println("User2.account2: " + customer2.account2.toDebugString());

        assertTrue(writeSkewEncountered.get());
    }

    @Test
    public void whenSerializedIsolationLevel_thenWriteSkewNotPossible() {
        mode = Mode.serialized;
        startAll(threads);
        sleepMs(getStressTestDurationMs(30 * 1000));
        stop = true;

        joinAll(threads);

        System.out.println("User1: " + customer1);
        System.out.println("User2: " + customer2);

        assertFalse("writeskew detected", writeSkewEncountered.get());
    }

    public class TransferThread extends TestThread {

        private final TxnExecutor snapshotBlock = stm.newTxnFactoryBuilder()
                .setSpeculative(false)
                .setMaxRetries(10000)
                .newTxnExecutor();
        private final TxnExecutor serializedBlock = stm.newTxnFactoryBuilder()
                .setSpeculative(false)
                .setIsolationLevel(IsolationLevel.Serializable)
                .setMaxRetries(10000)
                .newTxnExecutor();
        private final TxnExecutor pessimisticReadsBlock = stm.newTxnFactoryBuilder()
                .setSpeculative(false)
                .setReadLockMode(LockMode.Read)
                .setMaxRetries(10000)
                .newTxnExecutor();
        private final TxnExecutor pessimisticWritesBlock = stm.newTxnFactoryBuilder()
                .setSpeculative(false)
                .setWriteLockMode(LockMode.Read)
                .setMaxRetries(10000)
                .newTxnExecutor();


        public TransferThread(int id) {
            super("TransferThread-" + id);
        }

        @Override
        public void doRun() throws Exception {
            int k = 0;
            while (!stop) {
                if (k % 100 == 0) {
                    System.out.printf("%s is at %s\n", getName(), k);
                }

                switch (mode) {
                    case snapshot:
                        runWithSnapshotIsolation();
                        break;
                    case serialized:
                        runWithSerializedIsolation();
                        break;
                    case pessimisticReadLevel:
                        runWithPessimisticReadLevel();
                        break;
                    case pessimisticWriteLevel:
                        runWithPessimisticWriteLevel();
                        break;
                    case pessimisticReads:
                        runWithPessimisticReads();
                        break;
                    case pessimisticWrites:
                        runWithPessimisticWrites();
                        break;
                    default:
                        throw new IllegalStateException();
                }

                k++;
            }
        }

        private void runWithPessimisticReadLevel() {
            pessimisticReadsBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    run(btx, LockMode.None, LockMode.None);
                }
            });
        }

        private void runWithPessimisticWriteLevel() {
            pessimisticWritesBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    run(btx, LockMode.None, LockMode.None);
                }
            });
        }

        private void runWithSerializedIsolation() {
            serializedBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    run(btx, LockMode.None, LockMode.None);
                }
            });
        }

        private void runWithSnapshotIsolation() {
            snapshotBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    run(btx, LockMode.None, LockMode.None);
                }
            });
        }

        private void runWithPessimisticReads() {
            snapshotBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    run(btx, LockMode.Read, LockMode.None);
                }
            });
        }

        private void runWithPessimisticWrites() {
            snapshotBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    run(btx, LockMode.None, LockMode.Read);
                }
            });
        }

        public void run(GammaTxn tx, LockMode readLockMode, LockMode writeLockMode) {
            int amount = randomInt(100);

            Customer from = random(customer1, customer2);
            Customer to = random(customer1, customer2);

            long sum = from.account1.openForRead(tx, readLockMode.asInt()).long_value
                    + from.account2.openForRead(tx, readLockMode.asInt()).long_value;

            if (sum < 0) {
                if (writeSkewEncountered.compareAndSet(false, true)) {
                    System.out.println("writeskew detected");
                }
            }

            sleepRandomMs(5);

            if (sum >= amount) {
                GammaTxnLong fromAccount = from.getRandomAccount();
                fromAccount.openForWrite(tx, writeLockMode.asInt()).long_value -= amount;

                GammaTxnLong toAccount = to.getRandomAccount();
                toAccount.openForWrite(tx, writeLockMode.asInt()).long_value += amount;
            }

            sleepRandomMs(5);
        }
    }

    public Customer random(Customer customer1, Customer customer2) {
        return randomBoolean() ? customer1 : customer2;
    }

    public class Customer {
        private GammaTxnLong account1 = new GammaTxnLong(stm);
        private GammaTxnLong account2 = new GammaTxnLong(stm);

        public GammaTxnLong getRandomAccount() {
            return randomBoolean() ? account1 : account2;
        }

        public String toString() {
            return format("User(account1 = %s, account2 = %s, sum=%s)",
                    account1.atomicToString(), account2.atomicToString(), account1.atomicGet() + account2.atomicGet());
        }
    }
}
