package ca.recrown.islandsurvivalcraft.utilities.caching;

import static org.junit.Assert.assertSame;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;

@TestInstance(Lifecycle.PER_CLASS)
public class CacheTest {
    private volatile Cache<Integer, String> integerCache;
    private final int LARGE_CACHE_SIZE = 262144;
    private final int[] answers = new int[LARGE_CACHE_SIZE];
    private final Random rand = new Random();
    Cache<Integer, Integer> lCache = new Cache<>(LARGE_CACHE_SIZE);

    @BeforeAll
    public void setUp() {
        integerCache = new Cache<>(3);

        for (int i = 0; i < answers.length; i++) {
            answers[i] = rand.nextInt();
        }
    }

    @AfterEach
    public void cleanUp() {
        lCache.clearCache();
        integerCache.clearCache();
    }

    @Test
    public void testBasicCaching() {
        String val = integerCache.get(0);
        assertTrue(val == null);
        val = "first";
        integerCache.set(0, val);
        assertTrue(integerCache.get(0) != null);
        assertSame(val, integerCache.get(0));

        val = integerCache.get(1);
        assertTrue(val == null);
        val = "second";
        integerCache.set(1, val);
        assertTrue(integerCache.get(1) != null);
        assertSame(val, integerCache.get(1));
    }

    @Test
    public void testUsageBasedClean() {
        integerCache.set(0, "first");
        integerCache.set(1, "second");
        integerCache.set(2, "third");

        assertEquals("first", integerCache.get(0));

        integerCache.set(3, "fourth");
        assertEquals("first", integerCache.get(0));

        integerCache.set(4, "fifth");
        assertTrue(integerCache.get(3) != null);
        assertTrue(integerCache.get(0) != null);
    }

    @Test
    public void testUsageLargeData() {
        int amount = 1024;
        Random random = new Random();

        Cache<Integer, Integer> largeCache = new Cache<>(amount / 2);

        int[] values = new int[amount];
        for (int i = 0; i < amount; i++) {
            values[i] = random.nextInt();
            largeCache.set(i, values[i]);
        }

        for (int i = 0; i < amount / 2; i++) {
            assertEquals(null, largeCache.get(i), "Current accessor: " + i);
        }

        for (int i = amount / 2; i < amount; i++) {
            assertEquals(values[i], largeCache.get(i), "Current accessor: " + i);
        }
    }

    @Test
    public void testUsageLargeDataImportance() {
        int amount = 1024;
        Random random = new Random();

        Cache<Integer, Integer> largeCache = new Cache<>(amount / 2);

        int[] values = new int[amount];
        for (int i = 0; i < amount; i++) {
            values[i] = random.nextInt();
            largeCache.set(i, values[i]);
            largeCache.get(0);
        }

        for (int i = 1; i < (amount / 2) + 1; i++) {
            assertEquals(null, largeCache.get(i), "Current accessor: " + i);
        }

        for (int i = (amount / 2) + 1; i < amount; i++) {
            assertEquals(values[i], largeCache.get(i), "Current accessor: " + i);
        }

        assertEquals(values[0], largeCache.get(0));
    }

    @Test
    public void testMultithreadingWriteConsistency() {
        Runnable write = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
                    lCache.set(i, answers[i]);
                }
            }
        };

        Thread firstThread = new Thread(write);
        firstThread.start();
        Thread secondThread = new Thread(write);
        secondThread.start();
        Thread thirdThread = new Thread(write);
        thirdThread.start();

        try {
            firstThread.join();
            secondThread.join();
            thirdThread.join();
        } catch (InterruptedException e) {
            assertFalse(false, e.getCause().getMessage());
        }

        for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
            assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
        }
    }

    @Test
    public void testMultithreadingReadConsistency() {
        for (int i = 0; i < answers.length; i++) {
            lCache.set(i, answers[i]);
        }

        Runnable read = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
                    assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
                }
            }
        };

        Thread firstThread = new Thread(read);
        firstThread.start();
        Thread secondThread = new Thread(read);
        secondThread.start();
        Thread thirdThread = new Thread(read);
        thirdThread.start();

        try {
            firstThread.join();
            secondThread.join();
            thirdThread.join();
        } catch (InterruptedException e) {
            assertFalse(false, e.getCause().getMessage());
        }
    }

    @Test
    public void testMulthreadedReadWrite() {
        Runnable readWrite = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
                    if (lCache.get(i) == null) {
                        lCache.set(i, answers[i]);
                    } else {
                        assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
                    }
                }
            }
        };

        Runnable readAll = new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
                    assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
                }
            }
        };

        Thread firstThread = new Thread(readWrite);
        firstThread.start();
        Thread secondThread = new Thread(readWrite);
        secondThread.start();
        Thread thirdThread = new Thread(readAll);

        try {
            firstThread.join();
            secondThread.join();
            thirdThread.start();
            thirdThread.join();
        } catch (InterruptedException e) {
            assertFalse(false, e.getCause().getMessage());
        }
    }

    @Test
    public void testMultiThreadConsistency() {
        Runnable readWriteCheck = new Runnable(){
        
            @Override
            public void run() {
                for (int i = 0; i < LARGE_CACHE_SIZE; i++) {
                    if (lCache.get(i) != null) {
                        assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
                    } else {
                        lCache.set(i, answers[i]);
                    }
                }
            }
        };

        ExecutorService executorService = Executors.newFixedThreadPool(6);
        executorService.submit(readWriteCheck);
        executorService.submit(readWriteCheck);
        executorService.submit(readWriteCheck);
        executorService.submit(readWriteCheck);
        executorService.submit(readWriteCheck);
        executorService.submit(readWriteCheck);

        try {
            executorService.shutdown();
            assertTrue(executorService.awaitTermination(1, TimeUnit.MINUTES), "Timed out.");
        } catch (InterruptedException e) {
            assertFalse(false, e.getCause().getMessage());
        }
    }
}