/**
 * Email: bt19ex@brocku.ca
 * Student Number: 6920201
 * Licence: GPL3.0 see https://www.gnu.org/licenses/gpl-3.0.en.html for more detail
 * Basic random test case generator and tester with partially formatted output. Requires terminal unicode and ANSI escape code support for full effect.
 *
 * Please use Linux to run this software, tested on Debian stable using CLion 2023.2.2 // GCC version 12.2
 * This should work on any linux distro with a C++20 compiler.
 * CMake is optional as this file can be compiled via g++
 * An compile command would be:
 *      `g++ -O3 -o main.run -Wall -Wpedantic -Wextra -Werror main.cpp && ./main.run`
 * If you use CMake please make sure CMAKE_BUILD_TYPE is release otherwise the program will take a long time to run.
 */


#include <iostream>
#include <random>
#include <vector>
#include <cstdint>
#include <algorithm>

/**
 * Config Variables
 * ---------------------------------------
 */
static constexpr size_t DEFAULT_TEST_COUNT = 10;                      // Default: 10 tests
static constexpr size_t MAX_RAM_USAGE = 1ul * 1024ul * 1024ul;        // Default: 1mb per test


/**
 * Do not change anything below this line!
 * ---------------------------------------
 */

static constexpr auto maxSize = MAX_RAM_USAGE / sizeof(std::int32_t);
static const auto maxChars = std::to_string(maxSize).size();

struct failPoint
{
    size_t i, j;
};

// fp has no meaning if the test doesn't fail.
bool validateTest(const std::vector<std::int32_t>& in, failPoint& fp)
{
    // empty sets are sorted. if you have a problem with this take it up with god
    if (in.empty())
        return true;
    for (size_t i = 0; i < in.size(); i++)
    {
        for (size_t j = i; j < in.size(); j++)
        {
            // if any value after the current value is less, then we have failed to sort.
            if (in[i] > in[j])
            {
                fp = {i, j};
                return false;
            }
        }
    }
    return true;
}

std::vector<std::int32_t> generateRandomData()
{
    // setup random numbers
    static std::random_device dev;
    static std::mt19937_64 engine(dev());
    // don't want the int array to be too big otherwise we'll run out of ram. you can change this for your machine but im on my laptop rn
    // if it mattered I'd test it on my desktop with 4gb array allocations, but it doesn't
    std::uniform_int_distribution dist(std::numeric_limits<std::int32_t>::min(), std::numeric_limits<std::int32_t>::max());
    std::uniform_int_distribution dist_size(0ul, maxSize);
    // pick a random size
    size_t size = dist_size(engine);
    // populate the array
    std::vector<std::int32_t> ret;
    for (size_t i = 0; i < size; i++)
        ret.push_back(dist(engine));
    return ret;
}

template<typename T>
void runTest(T t, std::string name)
{
    auto data = generateRandomData();
    // little extra formatting goes a long way =3
    name += "; Data Size: ";
    auto dataSizeStr = std::to_string(data.size());
    name += dataSizeStr;
    
    // run the sort on the data
    t(data);
    
    // add padding for nice output :P
    std::string padding;
    for (size_t i = 0; i < maxChars - dataSizeStr.size(); i++)
        padding += ' ';
    
    failPoint fp{};
    // input and output arrays cannot be shown due to their potential size. Instead, the software will output the failure value and locations.
    // sometimes they are neighbours sometimes they are far apart. it depends on the randomness
    if (validateTest(data, fp))
        std::cout << "\033[36m[\033[32m✓\033[36m]:\033[0m Test '" << name << "'" << padding << "\t\033[32mPASSED\033[0m" << std::endl;
    else
        std::cout << "\033[36m[\033[31mX\033[36m]:\033[0m Test '" << name << "'" << padding << "\t\033[31mFAILED\t\t" << "Value '" << data[fp.i]
                  << "' @ " << fp.i << " is greater than '" << data[fp.j] << "' @ " << fp.j << "\033[0m" << std::endl;
}

void brettSort(std::vector<std::int32_t>& v)
{
    if (v.empty())
        return;
    static std::random_device dev;
    static std::mt19937_64 engine(dev());
    // since the STL won't fail (it's pretty good as long as you follow the rules)
    std::sort(v.begin(), v.end());
    // we can add some errors (sometimes) that will cause our test to fail (also sometimes)!
    std::uniform_int_distribution<int> dist(0, (int) v.size() - 1);
    std::uniform_real_distribution realDist(0.0, 1.0);
    int p1 = dist(engine);
    int p2 = dist(engine);
    if (!v.empty() && realDist(engine) > 0.85)
        v[p1] = v[p2];
}

int main(int argc, char** argv)
{
    std::cout << "You can provide a test count via ./program [test count]" << std::endl;
    auto testCount = DEFAULT_TEST_COUNT;
    if (argc > 1)
        testCount = std::stoi(argv[1]);
    std::cout << "Running " << testCount << " tests" << std::endl;
    // the brett sort should fail at some point
    for (size_t i = 0; i < testCount; i++)
        runTest(brettSort, "brettSort");
    return 0;
}
