package com.antkorwin.xsync;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

import com.jupiter.tools.stress.test.concurrency.ExecutionMode;
import com.jupiter.tools.stress.test.concurrency.StressTestRunner;
import org.junit.jupiter.api.Test;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Created on 27/12/2019
 * <p>
 * multiple keys test cases
 *
 * @author Korovin Anatoliy
 */
public class MultiKeysXSyncTest {

	private static final int ITERATIONS = 100_000;
	private static final int THREADS_COUNT = 8;
	private static final long INITIAL_BALANCE = 1000L;

	private XSync<Long> xsync = new XSync<>();

	@Test
	void twoKeysSynchronizeExecute() {
		List<Account> accounts = LongStream.range(0, 10)
		                                   .boxed()
		                                   .map(i -> new Account(i, INITIAL_BALANCE))
		                                   .collect(Collectors.toList());

		// add two instance with the same id
		accounts.add(new Account(1L, INITIAL_BALANCE));

		StressTestRunner.test()
		                .mode(ExecutionMode.EXECUTOR_MODE)
		                .threads(THREADS_COUNT)
		                .iterations(ITERATIONS)
		                // deadlock prevention
		                .timeout(1, TimeUnit.MINUTES)
		                .run(() -> {
			                // select 3 different accounts
			                int fromId = randomExclude(accounts.size());
			                Account from = accounts.get(fromId);
			                int toId = randomExclude(accounts.size(), fromId);
			                Account to = accounts.get(toId);
			                // Act
			                transfer(from, to);
		                });
		// Assert
		long sum = accounts.stream()
		                   .peek(System.out::println)
		                   .mapToLong(Account::getBalance)
		                   .sum();

		System.out.println("SUM=" + sum);
		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}

	@Test
	void twoKeysSynchronizeEvaluate() {
		List<Account> accounts = LongStream.range(0, 10)
		                                   .boxed()
		                                   .map(i -> new Account(i, INITIAL_BALANCE))
		                                   .collect(Collectors.toList());

		// add two instance with the same id
		accounts.add(new Account(1L, INITIAL_BALANCE));

		StressTestRunner.test()
		                .mode(ExecutionMode.EXECUTOR_MODE)
		                .threads(THREADS_COUNT)
		                .iterations(ITERATIONS)
		                // deadlock prevention
		                .timeout(1, TimeUnit.MINUTES)
		                .run(() -> {
			                // select 3 different accounts
			                int fromId = randomExclude(accounts.size());
			                Account from = accounts.get(fromId);
			                int toId = randomExclude(accounts.size(), fromId);
			                Account to = accounts.get(toId);
			                // Act
			                long balance = transferEval(from, to);
			                assertThat(balance).isGreaterThan(0);
		                });
		// Assert
		long sum = accounts.stream()
		                   .peek(System.out::println)
		                   .mapToLong(Account::getBalance)
		                   .sum();

		System.out.println("SUM=" + sum);
		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}

	@Test
	void sameKeysSynchronizeExecute() {
		List<Account> accounts = LongStream.range(0, 10)
										   .boxed()
										   .map(i -> new Account(i, INITIAL_BALANCE))
										   .collect(Collectors.toList());

		// add two instance with the same id
		accounts.add(new Account(1L, INITIAL_BALANCE));

		StressTestRunner.test()
						.mode(ExecutionMode.EXECUTOR_MODE)
						.threads(THREADS_COUNT)
						.iterations(ITERATIONS)
						// deadlock prevention
						.timeout(1, TimeUnit.MINUTES)
						.run(() -> {
							// select 3 different accounts
							int fromId = randomExclude(accounts.size());
							Account from = accounts.get(fromId);
							int toId = randomExclude(accounts.size(), fromId);
							Account to = accounts.get(toId);
							// Act
							transferOnSameKeys(from, to);
						});
		// Assert
		long sum = accounts.stream()
						   .peek(System.out::println)
						   .mapToLong(Account::getBalance)
						   .sum();

		System.out.println("SUM=" + sum);
		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}

	/**
	 * check the correct synchronization on the local(for XSync instance) mutex
	 * when internal hash led to a collision of multiple mutexes
	 */
	@Test
	void collectionWithTheSameKeysSynchronizeExecute() {
		List<Account> accounts = LongStream.range(0, 10)
										   .boxed()
										   .map(i -> new Account(i, INITIAL_BALANCE))
										   .collect(Collectors.toList());

		// add two instance with the same id
		accounts.add(new Account(1L, INITIAL_BALANCE));

		StressTestRunner.test()
						.mode(ExecutionMode.EXECUTOR_MODE)
						.threads(THREADS_COUNT)
						.iterations(ITERATIONS)
						// deadlock prevention
						.timeout(1, TimeUnit.MINUTES)
						.run(() -> {
							// select 3 different accounts
							int fromId = randomExclude(accounts.size());
							Account from = accounts.get(fromId);
							int toId = randomExclude(accounts.size(), fromId);
							Account to = accounts.get(toId);
							// Act
							transferOnCollectionWithTheSameKeys(from, to);
						});
		// Assert
		long sum = accounts.stream()
						   .peek(System.out::println)
						   .mapToLong(Account::getBalance)
						   .sum();

		System.out.println("SUM=" + sum);
		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}

	@Test
	void multipleKeysExecute() {
		List<Account> accounts = LongStream.range(0, 10)
		                                   .boxed()
		                                   .map(i -> new Account(i, INITIAL_BALANCE))
		                                   .collect(Collectors.toList());

		// add two instance with the same id
		accounts.add(new Account(1L, INITIAL_BALANCE));

		StressTestRunner.test()
		                .mode(ExecutionMode.EXECUTOR_MODE)
		                .threads(THREADS_COUNT)
		                .iterations(ITERATIONS)
		                // deadlock prevention
		                .timeout(1, TimeUnit.MINUTES)
		                .run(() -> {
			                // select 3 different accounts
			                int fromId = randomExclude(accounts.size());
			                Account from = accounts.get(fromId);
			                int toId = randomExclude(accounts.size(), fromId);
			                Account to = accounts.get(toId);
			                int collectorId = randomExclude(accounts.size(), fromId, toId);
			                Account collector = accounts.get(collectorId);
			                // Act
			                transfer(from, to, collector);
		                });
		// Assert
		long sum = accounts.stream()
		                   .peek(System.out::println)
		                   .mapToLong(Account::getBalance)
		                   .sum();

		System.out.println("SUM=" + sum);
		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}


	@Test
	void multipleKeysEvaluate() {
		List<Account> accounts = LongStream.range(0, 10)
		                                   .boxed()
		                                   .map(i -> new Account(i, INITIAL_BALANCE))
		                                   .collect(Collectors.toList());

		StressTestRunner.test()
		                .mode(ExecutionMode.EXECUTOR_MODE)
		                .threads(THREADS_COUNT)
		                .iterations(ITERATIONS)
		                // deadlock prevention
		                .timeout(1, TimeUnit.MINUTES)
		                .run(() -> {
			                int fromId = randomExclude(accounts.size());
			                Account from = accounts.get(fromId);
			                int toId = randomExclude(accounts.size(), fromId);
			                Account to = accounts.get(toId);
			                int collectorId = randomExclude(accounts.size(), fromId, toId);
			                Account collector = accounts.get(collectorId);
			                // Act
			                long resultBalance = transferEval(from, to, collector);
			                // Assert
			                assertThat(resultBalance).isGreaterThan(0);
		                });

		// Assert concurrency flow
		long sum = accounts.stream()
		                   .mapToLong(Account::getBalance)
		                   .sum();

		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}

	/**
	 * evaluate with multiple same keys
	 */
	@Test
	void multipleKeysEvaluateWithCollision() {
		List<Account> accounts = LongStream.range(0, 10)
										   .boxed()
										   .map(i -> new Account(i, INITIAL_BALANCE))
										   .collect(Collectors.toList());

		StressTestRunner.test()
						.mode(ExecutionMode.EXECUTOR_MODE)
						.threads(THREADS_COUNT)
						.iterations(ITERATIONS)
						// deadlock prevention
						.timeout(1, TimeUnit.MINUTES)
						.run(() -> {
							int fromId = randomExclude(accounts.size());
							Account from = accounts.get(fromId);
							int toId = randomExclude(accounts.size(), fromId);
							Account to = accounts.get(toId);
							int collectorId = randomExclude(accounts.size(), fromId, toId);
							Account collector = accounts.get(collectorId);
							// Act
							long resultBalance = transferEvalForCollectionWithTheSameKeys(from, to, collector);
							// Assert
							assertThat(resultBalance).isGreaterThan(0);
						});

		// Assert concurrency flow
		long sum = accounts.stream()
						   .mapToLong(Account::getBalance)
						   .sum();

		assertThat(sum).isEqualTo(accounts.size() * INITIAL_BALANCE);
	}

	@Test
	void syncByTheEmptyListOfKeys() {

		Exception exception = null;
		try {
			xsync.execute(Arrays.asList(), () -> {
				// nop
			});
		} catch (Exception e) {
			exception = e;
		}

		assertThat(exception).isNotNull();
		assertThat(exception.getClass()).isEqualTo(RuntimeException.class);
		assertThat(exception.getMessage()).isEqualTo("Empty key list");
	}

	@Test
	void evaluateEmptyListOfKeys() {

		Exception exception = null;
		try {
			xsync.evaluate(Arrays.asList(), () -> {
				// nop
				return null;
			});
		} catch (Exception e) {
			exception = e;
		}

		assertThat(exception).isNotNull();
		assertThat(exception.getClass()).isEqualTo(RuntimeException.class);
		assertThat(exception.getMessage()).isEqualTo("Empty key list");
	}


	@Test
	void throwExceptionInFunction() {

		Exception exception = null;
		try {
			xsync.evaluate(Arrays.asList(123L), () -> {
				// nop
				throw new NotImplementedException();
			});
		} catch (Exception e) {
			exception = e;
		}

		assertThat(exception).isNotNull();
		assertThat(exception.getClass()).isEqualTo(NotImplementedException.class);
	}


	private void transfer(Account first, Account second) {
		xsync.execute(first.getId(), second.getId(),
		              () -> {
			              second.balance += first.balance;
			              first.balance -= first.balance;
		              });
	}

	private long transferEval(Account first, Account second) {
		return xsync.evaluate(first.getId(), second.getId(),
		                      () -> {
			                      second.balance += first.balance / 2;
			                      first.balance -= first.balance / 2;
			                      return second.balance;
		                      });
	}

	private void transferOnSameKeys(Account first, Account second) {
		xsync.execute(first.getId(), first.getId(),
					  () -> {
						  second.balance += first.balance;
						  first.balance -= first.balance;
					  });
	}

	private void transfer(Account first, Account second, Account collector) {
		xsync.execute(Arrays.asList(first.getId(), second.getId(), collector.getId()),
		              () -> {
			              collector.balance += first.balance / 2 + second.balance / 2;
			              first.balance -= first.balance / 2;
			              second.balance -= second.balance / 2;
		              });
	}

	private long transferEval(Account first, Account second, Account collector) {
		return xsync.evaluate(Arrays.asList(first.getId(), second.getId(), collector.getId()),
		                      () -> {
			                      collector.balance += first.balance / 2 + second.balance / 2;
			                      first.balance -= first.balance / 2;
			                      second.balance -= second.balance / 2;
			                      return collector.balance;
		                      });
	}

	private long transferEvalForCollectionWithTheSameKeys(Account first, Account second, Account collector) {
		return xsync.evaluate(Arrays.asList(first.getId(), second.getId(), first.getId(), collector.getId()),
							  () -> {
								  collector.balance += first.balance / 2 + second.balance / 2;
								  first.balance -= first.balance / 2;
								  second.balance -= second.balance / 2;
								  return collector.balance;
							  });
	}

	private void transferOnCollectionWithTheSameKeys(Account first, Account second) {
		xsync.execute(Arrays.asList(first.getId(), first.getId(), first.getId()),
					  () -> {
						  second.balance += first.balance;
						  first.balance -= first.balance;
					  });
	}

	private int randomExclude(int maxValue, Integer... excludingValue) {
		List<Integer> exclude = Arrays.asList(excludingValue);
		int rnd = new Random().nextInt(maxValue);
		while (exclude.contains(rnd)) {
			rnd = new Random().nextInt(maxValue);
		}
		return rnd;
	}

	class Account {
		private Long id;
		private long balance;

		public Account(Long id, long balance) {
			this.id = id;
			this.balance = balance;
		}

		public Long getId() {
			return id;
		}

		public long getBalance() {
			return balance;
		}

		@Override
		public String toString() {
			return "Account{" +
			       "id=" + id +
			       ", balance=" + balance +
			       '}';
		}
	}
}