/*************************************
 * You can use this code to test the BlockingQueue class you wrote in BlockingQueue.chpl.
 *
 * Usage: 
 * $ chpl -MYourFolderName testBlockingQueue.chpl
 * $ ./testBlockingQueue --v=true
 * Change the v constant to false to remove all the print statements.
 *
 * Author: Kyle Burke <https://github.com/paithan>
 * Author: Mat Goonan <https://github.com/matgoonan>
 */

use BlockingQueue;
use Time;
use Random;

//short named version
config const v = true;
var verbose = v; //long named-version


//This section tests that the queue can take different types.

writeln("Testing on different types.");

writeln("Testing on strings...");
var typeStringTest = false;
begin with (ref typeStringTest) {
    var q = new owned BlockingQueue(string, 10);
    q.add("");
    q.add("Cookie Monster!");
    typeStringTest = true;
}

writeln("Testing on ints...");
var typeIntTest = false;
begin with (ref typeIntTest) {
    var q = new owned BlockingQueue(int, 10);
    q.add(17);
    q.add(-4);
    typeIntTest = true;
}

writeln("Testing on Semaphores...");
var typeSemaphoreTest = false;
begin with (ref typeSemaphoreTest) {
    var q = new owned BlockingQueue(owned Semaphore, 10);
    var s1 = new owned Semaphore(200);
    var s2 = new owned Semaphore(2);
    q.add(s1);
    q.add(s2);
    typeSemaphoreTest = true;
}

writeln("Done testing types.");


writeln("Starting the block-on-removal tests.");

//This section tests that the queue blocks when empty.
//First, starting from empty.
var blockEmptyOnRemoveTestA = true; //Becomes false if it doesn't block
begin with (ref blockEmptyOnRemoveTestA) {
    var q = new owned BlockingQueue(int, 10);
    //writeln("Running the first test!  Trying to remove from an empty queue.  Nothing should happen.");
    //writeln("Ctrl + C to kill this.");
    q.remove();
    //we should never get here
    writeln("BlockingQueue.remove() doesn't block when the queue is empty (Test A)!");
    blockEmptyOnRemoveTestA = false;
}

//Second, adding one and removing two.
var blockEmptyOnRemoveTestB = true; //becomes false if it doesn't block
begin with (ref blockEmptyOnRemoveTestB) {
    var q = new owned BlockingQueue(int, 10);
    q.add(0);
    //writeln("Running the second test!  Trying to remove from an empty queue.  Nothing should happen.");
    //writeln("Ctrl + C to kill this.");
    q.remove();
    q.remove();
    //code should never reach here
    writeln("BlockingQueue.remove() doesn't block when the queue is empty (Test B)!");
    blockEmptyOnRemoveTestB = false;
}

//Third, fill it up, then remove them all.
var blockEmptyOnRemoveTestC = true; //becomes false if it doesn't block
begin with (ref blockEmptyOnRemoveTestC) {
    var size = 10;
    var q = new owned BlockingQueue(int, size);
    for i in 1..size {
        q.add(0);
    }
    //writeln("Running the third test!  Trying to remove from an empty queue.  Nothing should happen.");
    //writeln("Ctrl + C to kill this.");
    for i in 1..size + 1 {
        q.remove();
    }
    //this should be dead code
    writeln("BlockingQueue.remove() doesn't block when the queue is empty (Test C)!");
    blockEmptyOnRemoveTestC = false;
}


writeln("Finished launching the block-on-removal tests...");


writeln("Launching the block-on-add tests...");


//This section tests that add blocks when the queue is full.
//First test: just fill it up
var blockFullOnAddTestA = true; //becomes false if it doesn't block
begin with (ref blockFullOnAddTestA) {
    var size = 10;
    var q = new owned BlockingQueue(int, size);
    for i in 1..size {
        q.add(0);
    }
    q.add(5); //should block here
    blockFullOnAddTestA = false; //should never be executed.
}

//Second test: fill it up, remove everything, fill it up again
var blockFullOnAddTestB = true; //becomes false if it doesn't block
begin with (ref blockFullOnAddTestB) {
    var size = 10;
    var q = new owned BlockingQueue(int, size);
    for i in 1..size {
        q.add(0);
    }
    for i in 1..size {
        q.remove();
    }
    for i in 1..size {
        q.add(0);
    }
    q.add(5); //should block here
    blockFullOnAddTestB = false; //should never be executed.
}


writeln("Finished launching the block-on-add tests...");


writeln("Testing getNumElements...");


//This section tests that getNumElements works.

var intQueue = new owned BlockingQueue(int, 10);
var getNumElementsTestA = intQueue.getNumElements(); //should be 0
intQueue.add(5);
intQueue.add(5);
intQueue.remove();
intQueue.add(5);
intQueue.add(5);
var getNumElementsTestB = intQueue.getNumElements(); //should be 3
for i in 4..10 {
    intQueue.add(5);
}
var getNumElementsTestC = intQueue.getNumElements(); //should be 10


writeln("Testing the order of things...");


//This section tests that the queue adds and removes things in the proper order.
var testDomainMax = 5;
intQueue = new owned BlockingQueue(int, testDomainMax);
var testDomain = 1..testDomainMax * 2;
begin { //use a new thread so that some of the removes can happen too.
    for i in testDomain {
        intQueue.add(i);
    }
}
var orderTest : [testDomain] int;
for i in testDomain {
    orderTest[i] = intQueue.remove();
}




writeln("Launching the stress tests...");


//time for stress tests!
//numRounds is the
proc blockingQueueStressTest(capacity : int, initialNumElements : int, maxAddWait : real, maxRemoveWait : real, numRounds : int, verbose : bool = true) {
    var inOrder : bool = true;
    var sizeRestricted : atomic bool;
    sizeRestricted.write(true);
    if (initialNumElements > capacity) {
        writeln("Stress test launched with bigger initial number of elements than the capacity!  Quitting!");
        return -1.0;
    }
    writeln("Stress test launched!");
    var timer : Timer;
    timer.start();
    var q = new owned BlockingQueue(int, capacity);
    for i in 1..initialNumElements {
        q.add(i);
    }
    sync {
        //the thread that adds elements
        begin with (ref sizeRestricted) {
            var rng = new owned NPBRandomStream(real);
            for i in initialNumElements + 1..initialNumElements + (capacity * numRounds) {
                var waitSeconds = rng.getNext() * maxAddWait;
                sleep(waitSeconds);
                q.add(i % capacity);
                //print out the queue or the size
                if(verbose) {
                  writeln(q);
                } else {
                  writeln(q.getNumElements());
                }
                //check that it's not too big
                if (q.getNumElements() > capacity) {
                    sizeRestricted.write(false);
                }
            }
        }
        //the thread that removes elements
        var rng = new owned NPBRandomStream(real);
        for i in initialNumElements + 1..initialNumElements + (capacity * numRounds) {
            var waitSeconds = rng.getNext() * maxRemoveWait;
            sleep(waitSeconds);
            q.remove();
            //print out the queue or the size
            if(verbose) {
              writeln(q);
            } else {
              writeln(q.getNumElements());
            }
            if (q.getNumElements() > capacity) {
                sizeRestricted.write(false);
            }
        }
    }
    writeln("Stress test completed!");
    timer.stop();

    //make sure all the elements are in the right place.
    for i in 1..initialNumElements {
        if (q.remove() != i) {
            inOrder = false;
        }
    }

    //return the results
    if (!sizeRestricted.read()) {
        return -1.0; //the size is not properly restricted
    } else if (!inOrder) {
        return -2.0; //the elements are not in the right place
    } else {
        //everything's okay!  Return the time the test took.
        return timer.elapsed();
    }
}
writeln("Launching Stress Test A...");
var stressTestA = blockingQueueStressTest(10, 5, .0001, .0001, 200, verbose);
writeln("Stress Test A Complete!");
writeln("Launching Stress Test B...");
var stressTestB = blockingQueueStressTest(20, 10, .09, .01, 20, verbose);
writeln("Stress Test B Complete!");
writeln("Launching Stress Test C...");
var stressTestC = blockingQueueStressTest(40, 20, .02, .09, 20, verbose);
writeln("Stress Test C Complete!");
writeln("Launching Stress Test D...");
var stressTestD = blockingQueueStressTest(200, 50, .01, .02, 20, false);
writeln("Stress Test D Complete!");

/*
var newQueue = new BlockingQueue(int, 13);

begin{
    for i in 0..299 {
        forall j in 1..500 {
            newQueue.add(i);
        }
    }
}

var i = 0;
while (i < 299)  {
    var y = newQueue.remove();
    if (y == i) {
        //it's cool
    } else if (y == i + 1) {
        if (i % 20 == 0) { writeln("Finished with i = ", i); }
        i += 1;
    } else if (y == i - 1) {
        //it's cool
    } else {
        writeln("Not cool!  Got a ", y, ", but was expecting something like ", i);
    }
}
writeln("Finished the stress test!");


writeln("Test for remove...");

var allCorrect = true;
for i in outputs.domain {
    allCorrect = allCorrect && outputs[i] == i;
}
if (allCorrect) {
    writeln("Correct!");
} else {
    writeln("Incorrect!!!!!!!!!!!!!!  -8 ");
}
*/



//Tests have finished.  Print out the report
writeln("******************************");
writeln("* Here is the report:");
writeln("*");

//report about the type tests
if (typeStringTest && typeIntTest && typeSemaphoreTest) {
    writeln("* All of the type tests passed!");
} else {
    if (!typeStringTest) {
        writeln("* Does not work with strings.");
    }
    if (!typeIntTest) {
        writeln("* Does not work with ints.");
    }
    if (!typeSemaphoreTest) {
        writeln("* Does not work with Semaphores.");
    }
}
writeln("* ");

//report about the blocking on remove tests
if (blockEmptyOnRemoveTestA && blockEmptyOnRemoveTestB && blockEmptyOnRemoveTestC) {
    writeln("* Blocks on remove when empty, good!");
} else {
    if (!blockEmptyOnRemoveTestA) {
        writeln("* The queue did not block on remove in Test A.");
    }
    if (!blockEmptyOnRemoveTestB) {
        writeln("* The queue did not block on remove in Test B.");
    }

    if (!blockEmptyOnRemoveTestC) {
        writeln("* The queue did not block on remove in Test C.");
    }
}
writeln("* ");

//report about the blocking on add tests
if (blockFullOnAddTestA && blockFullOnAddTestB) {
    writeln("* Blocks on add when full, great!");
} else {
    if (!blockFullOnAddTestA) {
        writeln("* The queue did not block on add in Test A.");
    }
    if (!blockFullOnAddTestB) {
        writeln("* The queue did not block on add in Test B.");
    }
}
writeln("* ");

//report about getNumElements
if (getNumElementsTestA == 0 && getNumElementsTestB == 3 && getNumElementsTestC == 10) {
    writeln("* getNumElements tests all passed!");
} else {
    if (getNumElementsTestA != 0) {
        writeln("* getNumElements returned ", getNumElementsTestA, " when it should have returned 0.");
    }
    if (getNumElementsTestB != 3) {
        writeln("* getNumElements returned ", getNumElementsTestB, " when it should have returned 3.");
    }
    if (getNumElementsTestC != 10) {
        writeln("* getNumElements returned ", getNumElementsTestC, " when it should have returned 10.");
    }
}
writeln("* ");

//report about FIFO-ness of Queue
if (orderTest[1] == 1 && orderTest[2] == 2 && orderTest[3] == 3 && orderTest[4] == 4 && orderTest[5] == 5 && orderTest[6] == 6 && orderTest[7] == 7 && orderTest[8] == 8 && orderTest[9] == 9 && orderTest[10] == 10) {
    writeln("* The queue is FIFO, good.");
} else {
    writeln("* After the order test, the queue should be [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], but instead is ", orderTest, ".");
}
writeln("* ");

//report about Stress Tests
if (stressTestA >= 0 && stressTestB >= 0 && stressTestC >= 0 && stressTestD >= 0) {
    writeln("* Stress tests all passed!");
}
if (stressTestA < 0) {
    writeln("* Stress Test A failed.");
    if (stressTestA == -1.0) {
        writeln("  Sometimes there were more than the allowed number of elements.");
    } else if (stressTestA == -2.0) {
        writeln("  The final array was out of order!  Something went wrong!");
    }
} else {
    writeln("* Stress Test A ran in ", stressTestA, " seconds.");
}
if (stressTestB < 0) {
    writeln("* Stress Test B failed.");
    if (stressTestB == -1.0) {
        writeln("  Sometimes there were more than the allowed number of elements.");
    } else if (stressTestB == -2.0) {
        writeln("  The final array was out of order!  Something went wrong!");
    }
} else {
    writeln("* Stress Test B ran in ", stressTestB, " seconds.");
}
if (stressTestC < 0) {
    writeln("* Stress Test C failed.");
    if (stressTestC == -1.0) {
        writeln("  Sometimes there were more than the allowed number of elements.");
    } else if (stressTestC == -2.0) {
        writeln("  The final array was out of order!  Something went wrong!");
    }
} else {
    writeln("* Stress Test C ran in ", stressTestC, " seconds.");
}
if (stressTestD < 0) {
    writeln("* Stress Test D failed.");
    if (stressTestD == -1.0) {
        writeln("  Sometimes there were more than the allowed number of elements.");
    } else if (stressTestD == -2.0) {
        writeln("  The final array was out of order!  Something went wrong!");
    }
} else {
    writeln("* Stress Test D ran in ", stressTestD, " seconds.");
}

writeln("******************************");
writeln("If all the tests passed, you will need to hit Ctrl + C to kill the extra threads.");