/*
 * Copyright (C) 2020 Linux Studio Plugins Project <https://lsp-plug.in/>
 *           (C) 2020 Vladimir Sadovnikov <sadko4u@gmail.com>
 *
 * This file is part of lsp-test-fw
 * Created on: 27 июл. 2019 г.
 *
 * lsp-test-fw is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * lsp-test-fw is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with lsp-test-fw. If not, see <https://www.gnu.org/licenses/>.
 */

#include <lsp-plug.in/test-fw/test.h>
#include <lsp-plug.in/test-fw/main/types.h>
#include <lsp-plug.in/test-fw/main/config.h>
#include <lsp-plug.in/test-fw/main/executor.h>
#include <lsp-plug.in/test-fw/main/tools.h>

#include <lsp-plug.in/test-fw/init.h>
#include <lsp-plug.in/test-fw/ptest.h>
#include <lsp-plug.in/test-fw/mtest.h>
#include <lsp-plug.in/test-fw/utest.h>

#include <errno.h>
#include <locale.h>
#include <stdio.h>
#include <string.h>

#ifdef LSP_TEST_FW_PLATFORM_WINDOWS
    #include <windows.h>
#endif /* LSP_TEST_FW_PLATFORM_WINDOWS */

namespace lsp
{
    namespace test
    {
        static int cmp_test_name(const void *a, const void *b)
        {
            const test::Test * const *_a = reinterpret_cast<const test::Test * const *>(a);
            const test::Test * const *_b = reinterpret_cast<const test::Test * const *>(b);
            return ::strcmp((*_a)->full_name(), (*_b)->full_name());
        }

        static test_status_t check_duplicates(const char *tclass, dynarray_t *list)
        {
            size_t n = list->size();

            for (size_t i=0; i<n; ++i)
            {
                test::Test *a       = list->at<test::Test>(i);
                const char *a_grp   = a->group();
                const char *a_name  = a->name();

                for (size_t j=i+1; j<n; ++j)
                {
                    test::Test *b       = list->at<test::Test>(j);
                    if (::strcasecmp(a_grp, b->group()))
                        continue;
                    if (::strcasecmp(a_name, b->name()))
                        continue;

                    ::fprintf(stderr, "%s '%s.%s' has duplicate instance, can not proceed\n", tclass, a_grp, a_name);
                    return LSP_TEST_FW_DUPLICATED;
                }
            }

            ::qsort(list->array<test::Test>(), list->size(), sizeof(test::Test *), cmp_test_name);

            return LSP_TEST_FW_OK;
        }

        test_status_t mtest_init(dynarray_t *list)
        {
            for (ManualTest *test = ManualTest::__root; test != NULL; test = test->__next)
            {
                if (!list->add(static_cast<Test *>(test)))
                    return LSP_TEST_FW_NO_MEM;
            }

            return check_duplicates("Manual test", list);
        }

        test_status_t utest_init(dynarray_t *list)
        {
            for (UnitTest *test = UnitTest::__root; test != NULL; test = test->__next)
            {
                if (!list->add(static_cast<Test *>(test)))
                    return LSP_TEST_FW_NO_MEM;
            }

            return check_duplicates("Unit test", list);
        }

        test_status_t ptest_init(dynarray_t *list)
        {
            for (PerfTest *test = PerfTest::__root; test != NULL; test = test->__next)
            {
                if (!list->add(static_cast<Test *>(test)))
                    return LSP_TEST_FW_NO_MEM;
            }

            return check_duplicates("Performance test", list);
        }

        test_status_t init_init(dynarray_t *list)
        {
            for (Initializer *init = Initializer::__root; init != NULL; init = init->__next)
            {
                if (!list->add(init))
                    return LSP_TEST_FW_NO_MEM;
            }

            return LSP_TEST_FW_OK;
        }

        static void out_system_info(config_t *cfg, dynarray_t *inits)
        {
            for (size_t i=0, n=inits->size(); i<n; ++i)
            {
                test::Initializer *init = inits->at<test::Initializer>(i);
                init->configure(cfg);
                init->info();
            }
        }

        static void initialize_global(config_t *cfg, dynarray_t *inits)
        {
            // Initialize in direct order
            for (size_t i=0, n=inits->size(); i<n; ++i)
            {
                test::Initializer *init = inits->at<test::Initializer>(i);
                init->configure(cfg);
                init->initialize();
            }
        }

        static void finalize_global(dynarray_t *inits)
        {
            // Finalize in reverse order
            for (ssize_t i=inits->size()-1; i>=0; --i)
            {
                test::Initializer *init = inits->at<test::Initializer>(i);
                init->finalize();
            }
        }

        static bool match_string(const char *p, const char *m)
        {
            while (p != NULL)
            {
                // Get pattern token
                const char *pnext = ::strchr(p, '.');
                size_t pcount = (pnext == NULL) ? ::strlen(p) : pnext - p;
                if (pnext != NULL)
                    pnext++;

                // Check wildcard
                if ((pcount == 2) && (p[0] == '*') && (p[1] == '*'))
                    return true;

                // Get match token
                if ((m == NULL) || (*m == '\0'))
                    return false;
                const char *mnext = ::strchr(m, '.');
                size_t mcount = (mnext == NULL) ? ::strlen(m) : mnext - m;
                if (mnext != NULL)
                    mnext++;

                // Check wildcard
                if ((pcount != 1) || (p[0] != '*'))
                {
                    // Check that names match
                    if ((pcount != mcount) || (::memcmp(p, m, pcount) != 0))
                        return false;
                }

                // Move pointers
                p = pnext;
                m = mnext;
            }

            return ((m == NULL) || (*m == '\0'));
        }

        static bool match_list(dynarray_t &list, test::Test *v, bool match_if_empty)
        {
            // Empty list always matches
            if (list.size() <= 0)
                return match_if_empty;

            const char *full = v->full_name();

            for (size_t i=0, n=list.size(); i<n; ++i)
            {
                if (match_string(list.at<const char>(i), full))
                    return true;
            }

            return false;
        }

        static bool check_test_skip(config_t *cfg, stats_t *stats, test::Test *v)
        {
            // Check that test is not ignored
            if (v->ignore())
                return true;

            // Need to check test name and group?
            if (match_list(cfg->ignore, v, false))
            {
                stats->ignored.add(v);
                return true;
            }

            // Is there an explicit list of tests?
            if (!match_list(cfg->list, v, true))
                return true;

            return false;
        }

        static test_status_t list_all(const char *text, const config_t *cfg, dynarray_t *list)
        {
            if (!cfg->suppress)
            {
                ::printf("\n%s (total: %d):\n", text, int(list->size()));
                for (size_t i=0, n=list->size() ; i < n; ++i)
                {
                    test::Test *test = list->at<test::Test>(i);
                    ::printf("  %s\n", test->full_name());
                }
                ::printf("\n");
            }
            else
            {
                for (size_t i=0, n=list->size() ; i < n; ++i)
                {
                    test::Test *test = list->at<test::Test>(i);
                    ::printf("%s\n", test->full_name());
                }
            }

            return LSP_TEST_FW_OK;
        }

        static test_status_t output_stats(const config_t *cfg, stats_t *stats)
        {
            const char *tclass =
                    (cfg->mode == UTEST) ? "unit test" :
                    (cfg->mode == PTEST) ? "performance test" :
                    "manual test";

            printf("\n--------------------------------------------------------------------------------\n");
            printf("Overall %s statistics:\n", tclass);
            printf("  execution time [s]:   %.2f\n", stats->overall);
            printf("  launched:             %d\n", int(stats->total));
            printf("  ignored:              %d\n", int(stats->ignored.size()));
            printf("  succeeded:            %d\n", int(stats->success.size()));
            printf("  failed:               %d\n", int(stats->failed.size()));

            if (stats->failed.size() > 0)
            {
                printf("\n--------------------------------------------------------------------------------\n");
                printf("List of failed tests:\n");
                for (size_t i=0, n=stats->failed.size(); i<n; ++i)
                {
                    test::Test *t = stats->failed.at<test::Test>(i);
                    printf("  %s\n", t->full_name());
                }
                printf("\n");
                return LSP_TEST_FW_FAILED;
            }

            if (stats->ignored.size() > 0)
            {
                printf("\n--------------------------------------------------------------------------------\n");
                printf("List of ignored tests:\n");
                for (size_t i=0, n=stats->ignored.size(); i<n; ++i)
                {
                    test::Test *t = stats->ignored.at<test::Test>(i);
                    printf("  %s\n", t->full_name());
                }
            }

            printf("\n");
            return LSP_TEST_FW_OK;
        }

        static test_status_t create_outfile(config_t *cfg, dynarray_t *inits)
        {
            if (cfg->outfile == NULL)
                return LSP_TEST_FW_OK;

            FILE *fd = fopen(cfg->outfile, "w");
            if (fd != NULL)
            {
                // Update configuration
                FILE *std_out   = cfg->std_out;
                bool verbose    = cfg->verbose;
                cfg->std_out    = fd;
                cfg->verbose    = true;

                // Output information
                out_system_info(cfg, inits);

                // Restore configuration
                cfg->std_out    = std_out;
                cfg->verbose    = verbose;

                fclose(fd);
            }

            return LSP_TEST_FW_OK;
        }

        static int test_main(int argc, const char **argv)
        {
            // Parse configuration
            config_t cfg;
            test_status_t res = cfg.parse(stdout, argc, argv);
            if (res != LSP_TEST_FW_OK)
            {
                cfg.clear();
                return res;
            }

            // Perform initialization
            if (!cfg.is_child)
            {
                // Create temporary directory
                res = mkdirs(cfg.tempdir);
                if (res != LSP_TEST_FW_OK)
                    return res;
            }

            // Initialize list of test initializers
            dynarray_t inits;
            res = init_init(&inits);
            if (res != LSP_TEST_FW_OK)
            {
                ::fprintf(stderr, "Failed to initialize initializer list\n");
                return res;
            }

            // Perform global initialization
            initialize_global(&cfg, &inits);
            if (cfg.sysinfo)
                out_system_info(&cfg, &inits);

            // Obtain the source list of tests for execution
            dynarray_t list;    // List of tests
            switch (cfg.mode)
            {
                case UTEST:
                    res = test::utest_init(&list);
                    if (res != LSP_TEST_FW_OK)
                    {
                        ::fprintf(stderr, "Error initializing unit test subsystem\n");
                        return res;
                    }
                    else if (list.is_empty())
                    {
                        ::fprintf(stderr, "No unit tests available\n");
                        return LSP_TEST_FW_OK;
                    }
                    else if (cfg.list_all)
                        return list_all("List of available unit tests", &cfg, &list);
                    break;
                case PTEST:
                    res = test::ptest_init(&list);
                    if (res != LSP_TEST_FW_OK)
                    {
                        ::fprintf(stderr, "Error initializing performance test subsystem\n");
                        return res;
                    }
                    else if (list.is_empty())
                    {
                        fprintf(stderr, "No performance tests available\n");
                        return LSP_TEST_FW_OK;
                    }
                    else if (cfg.list_all)
                        return list_all("List of available performance tests", &cfg, &list);
                    break;
                case MTEST:
                    res = test::mtest_init(&list);
                    if (res != LSP_TEST_FW_OK)
                    {
                        ::fprintf(stderr, "Error initializing unit test subsystem\n");
                        return res;
                    }
                    else if (list.is_empty())
                    {
                        fprintf(stderr, "No manual tests available\n");
                        return LSP_TEST_FW_OK;
                    }
                    else if (cfg.list_all)
                        return list_all("List of available manual tests", &cfg, &list);
                    break;
                default:
                    return LSP_TEST_FW_BAD_ARGUMENTS;
            }

            // Perform global initialization
            stats_t stats;
            TestExecutor executor;

            stats.total     = 0;
            stats.overall   = 0.0f;
            res             = executor.init(&cfg, &stats, &inits);

            if (res == LSP_TEST_FW_OK)
            {
                // Ensure that there are no duplicates in performance tests
                if (!cfg.is_child)
                    res     = create_outfile(&cfg, &inits);

                // Prepare for test
                if (res == LSP_TEST_FW_OK)
                {
                    test_clock_t start, finish;
                    get_test_time(&start);

                    for (size_t i=0, n=list.size(); i<n; ++i)
                    {
                        test::Test *test = list.at<test::Test>(i);

                        // Check that test is not ignored
                        if (check_test_skip(&cfg, &stats, test))
                            continue;

                        ++stats.total;
                        res = executor.submit(test);
                        if ((res != LSP_TEST_FW_OK) || (cfg.is_child))
                            break;
                    }

                    if (res == LSP_TEST_FW_OK)
                        res = executor.wait();

                    get_test_time(&finish);
                    stats.overall = calc_test_time_difference(&start, &finish);
                }

                // Output statistics
                if (!cfg.is_child)
                    output_stats(&cfg, &stats);
            }

            finalize_global(&inits);

            ::fflush(stdout);
            ::fflush(stderr);

            return res;
        }

        LSP_TEST_FW_PUBLIC
        int main(int argc, const char **argv)
        {
            #ifdef LSP_TEST_FW_PLATFORM_WINDOWS
                // Add a hack to change console encoding to UTF-8
                UINT cp = ::GetConsoleOutputCP();
                ::SetConsoleOutputCP(65001);
                int res = test_main(argc, argv);
                ::SetConsoleCP(cp);
            #else
                int res = test_main(argc, argv);
            #endif /* LSP_TEST_FW_PLATFORM_WINDOWS */
            return res;
        }
    }
}
