// Example usage: dub -c unittest-runner -b unittest
module tests.runner;

import core.stdc.stdio;
import core.stdc.string;
import core.sys.posix.setjmp;

import bubel.ecs.vector;
import bubel.ecs.simple_vector;
import bubel.ecs.std;

import std.traits;

import tests.time;

version (LDC)
{
    import ldc.attributes;
}
else
{
    enum optStrategy = 0;
}

enum int ASSERTED = 123;
enum string OUT_FILE = "test_report.xml";

static jmp_buf gEnvBuffer;
static AssertInfo gAssertInfo;

version (D_BetterC)
{
}
else version = notBetterC;

struct AssertInfo
{
    const(char)* msg;
    const(char)* file;
    int line;
}

extern (C) void __assert(const char* msg, const char* file, int line)
{
    gAssertInfo = AssertInfo(msg, file, line);
    longjmp(gEnvBuffer, ASSERTED);
}

struct Test
{
    string file;
    string msg;
    int file_line;
    string name;
    //string classname;
    int time;
    bool passed;
}

struct TestSuite
{
    string name;
    uint passed = 0;
    uint failed = 0;
    uint skipped = 0;
    Vector!Test tests;
}

string copyString(const char* str)
{
    auto length = strlen(str);
    char[] arr = cast(char[]) str[0 .. length + 1];
    return cast(string) Mallocator.makeArray(arr);
}

string copyString(string str)
{
    return cast(string) Mallocator.makeArray((cast(char*)str)[0 .. str.length + 1]);
}

struct TestRunner(Args...)
{
    void generateJUnit()
    {
        write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n");

        write("<testsuites tests=\"");
        write(passed + failed);
        write("\" failures=\"");
        write(failed);
        write("\">\n");

        foreach (ref TestSuite suite; suites)
        {
            write("\t<testsuite name=\"");
            write(suite.name);
            write("\" tests=\"");
            write(suite.passed + suite.failed);
            write("\" failures=\"");
            write(suite.failed);
            write("\" skipped=\"");
            write(suite.skipped);
            write("\">\n");

            foreach (ref Test test; suite.tests)
            {
                write("\t\t<testcase name=\"");
                write(test.name);
                write("\" classname=\"");
                write(suite.name);
                write("\" time=\"");
                write(cast(double)test.time*0.000001);
                write("\">");
                if (test.msg)
                {
                    write("\n\t\t\t<failure type=\"Fail\" message=\"");
                    write(test.msg[0 .. $ - 1]);
                    write("\">");
                    write("Assert!  File: ");
                    write(test.file[0 .. $ - 1]);
                    write(":");
                    write(test.file_line);
                    write(" Message: ");
                    write(test.msg[0 .. $ - 1]);
                    write("</failure>\n");
                    write("\t\t</testcase>\n");
                }
                else write("</testcase>\n");
                
            }

            write("\t</testsuite>\n");
        }

        write("</testsuites>");
    }

    @(optStrategy,"none")
    void runTests(string[] include = null, string[] exclude = null)
    {
        foreach (i, module_; Args)
        {
            TestSuite* suite = &suites[i];
            suite.name = module_.stringof;

            void function() before;
            void function() after;

            foreach (index, unittest_; __traits(getUnitTests, module_))
            {
                enum attributes = __traits(getAttributes, unittest_);

                static if (attributes.length != 0)
                {
                    foreach(attr_id, attr; attributes)
                    {
                        static if(isFunctionPointer!(attr)){}
                        else static if(attr == "_tr_before")
                        {
                            static assert(attr_id+1 < attributes.length);
                            enum attr2 = attributes[attr_id + 1];
                            //static assert(__traits(hasMember, module_, attr2));
                            //alias func = __traits(getMember, module_, attr2);
                            //attr2();
                            before = attr2;
                            //static assert(is(typeof(__traits(getMember, module_, attr2)) == void function()));
                        }
                        else
                        {
                            if (include.length > 0)
                            {
                                bool matched = false;
                                foreach (str; include)
                                {
                                    if (match(str, attr))
                                    {
                                        matched = true;
                                        break;
                                    }
                                }

                                foreach (str; exclude)
                                {
                                    if (match(str, attr))
                                    {
                                        matched = false;
                                        break;
                                    }
                                }

                                if (!matched)
                                {
                                    suite.skipped++;
                                    continue;
                                }
                            }
                        }
                    }
                }
                else if (include.length > 0)
                {
                    suite.skipped++;
                    continue;
                }

                Test test;

                static if (attributes.length == 0)
                    test.name = "None";
                else
                    test.name = attributes[0];

                static if (__traits(hasMember, module_, "beforeEveryTest"))
                    module_.beforeEveryTest();
                if(before)before();

                version(D_BetterC)
                {
                    // Save calling environment for longjmp 
                    int jmp_ret = setjmp(gEnvBuffer);

                    if (jmp_ret == ASSERTED)
                    {
                        test.passed = false;
                        test.file = copyString(gAssertInfo.file);
                        test.file_line = gAssertInfo.line;
                        test.msg = copyString(gAssertInfo.msg);
                    }
                    else
                    {
                        long time = Time.getUSecTime();
                        unittest_();
                        test.passed = true;
                        test.time = cast(int)(Time.getUSecTime() - time);
                    }
                }
                else
                {
                    import core.exception : AssertError;
                    try 
                    {
                        unittest_();
                        test.passed = true;
                    }
                    catch(AssertError error)
                    {
                        test.passed = false;
                        test.file = copyString(error.file);
                        test.file_line = cast(int)error.line;
                        test.msg = copyString(error.msg);
                    }
                }

                if (test.passed)
                    suite.passed++;
                else
                    suite.failed++;
                suite.tests ~= test;
                static if (__traits(hasMember, module_, "afterEveryTest"))
                    module_.afterEveryTest();
            }
            passed += suite.passed;
            failed += suite.failed;
            skipped += suite.skipped;
        }
    }

    void writeFile()
    {
        if (junit.length == 0)
            generateJUnit();
        auto file = fopen(OUT_FILE, "w");
        fwrite(junit.data.ptr, junit.length, 1, file);
        fclose(file);
    }

    void writeOutput()
    {
        foreach (ref TestSuite suite; suites)
        {
            printf("Suite: \"%s\" Passed: %u/%u Skipped: %u\n", suite.name.ptr,
                    suite.passed, suite.passed + suite.failed, suite.skipped);
            foreach (ref Test test; suite.tests)
            {
                if (!test.passed)
                {
                    printf("\tTest: \"%s\" Failed! Line: %u Message: %s\n",
                            test.name.ptr, test.file_line, test.msg.ptr);
                }
            }
        }
        printf("Passed %u/%u tests. Skipped: %u.\n", passed, passed + failed, skipped);
    }

    bool match(string a, string b)
    {
        uint i = 0;
        foreach (char c; b)
        {
            if (i > a.length)
                return false;
            if (a[i] == c)
                i++;
            else
            {
                if (a[i] == '*')
                {
                    if (i + 1 >= a.length)
                        return true;
                    else if (a[i + 1] == c)
                        i += 2;
                }
                else
                    return false;
            }
        }
        return i == a.length;
    }

    void write(string txt)
    {
        junit.add(cast(ubyte[]) txt);
    }

    void write(double num)
    {
        ubyte[40] buffer;
        int len;
        len = sprintf(cast(char*) buffer.ptr, "%2.8lf", num);
        junit.add(buffer[0 .. len]);
    }

    void write(uint num)
    {
        ubyte[20] buffer;
        int len;
        len = sprintf(cast(char*) buffer.ptr, "%u", num);
        junit.add(buffer[0 .. len]);
    }

    TestSuite[Args.length] suites;
    uint passed = 0;
    uint failed = 0;
    uint skipped = 0;

    SimpleVector junit;
}

version (notBetterC)
{
    extern (C) int rt_init();
    extern (C) int rt_term();
    version (D_Coverage) extern (C) void dmd_coverDestPath(string path);
}

enum before = "_tr_before";

void extractStrings(ref Vector!string container, string str)
{
    uint s = 0;
    foreach (j, char c; str)
    {
        if (c == ',')
        {
            if (s < j)
            {
                container.add(str[s .. j]);
            }
            s = cast(uint) j + 1;
        }
    }
    if (s < str.length)
    {
        container.add(str[s .. $]);
    }
}

extern (C) int main(int argc, char** args)
{
    Vector!string include;
    Vector!string exclude;

    version (notBetterC)
    {
        rt_init();
        version (D_Coverage)
            dmd_coverDestPath("reports");
    }

    for (int i = 1; i < argc; i++)
    {
        string arg = cast(string) args[i][0 .. strlen(args[i])];
        if (arg.length < 2)
            continue;
        else if (arg == "-i")
        {
            if (i + 1 >= argc)
                break;
            i++;
            arg = cast(string) args[i][0 .. strlen(args[i])];
            extractStrings(include, arg);
        }
        else if (arg == "-e")
        {
            if (i + 1 >= argc)
                break;
            i++;
            arg = cast(string) args[i][0 .. strlen(args[i])];
            extractStrings(exclude, arg);
        }
        else if (arg.length < 10)
            continue;
        else if (arg[0 .. 10] == "--include=")
        {
            extractStrings(include, arg[10 .. $]);
        }
        else if (arg[0 .. 10] == "--exclude=")
        {
            extractStrings(exclude, arg[10 .. $]);
        }
    }

    TestRunner!(tests.id_manager, tests.vector, tests.basic, tests.perf, tests.access_perf) runner;

    runner.runTests(include[], exclude[]);

    runner.writeFile();

    runner.writeOutput();

    version (notBetterC)
        rt_term();

    if (!runner.failed)
        return 0;
    else
        return 1;
}

version (D_BetterC)
{
    version(LDC)
    {
        extern (C) __gshared int _d_eh_personality(int, int, size_t, void*, void*)
        {
            return 0;
        }
    }
}