/*
 * Copyright (C) 2016-2018 Edward d'Auvergne
 *
 * This file is part of the program FlightGear.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <algorithm>
#include <cstring>
#include <iostream>

#include <simgear/debug/logstream.hxx>

#include "dataStore.hxx"
#include "fgTestRunner.hxx"
#include "formatting.hxx"
#include "logging.hxx"

using namespace std;


// The help message.
void helpPrintout(std::ostream &stream) {
    stream << "Usage: fgfs_test_suite [options]\n";
    stream << '\n';
    stream << "Options:\n";
    stream << "  -h, --help            show this help message and exit.\n";
    stream << '\n';
    stream << "  Test selection options:\n";
    stream << "    -s, --system-tests  execute the system/functional tests.\n";
    stream << "    -u, --unit-tests    execute the unit tests.\n";
    stream << "    -g, --gui-tests     execute the GUI tests.\n";
    stream << "    -m, --simgear-tests execute the simgear tests.\n";
    stream << "    -f, --fgdata-tests  execute the FGData tests.\n";
    stream << '\n';
    stream << "    The -s, -u, -g, and -m options accept an optional argument to perform a\n";
    stream << "    subset of all tests.  This argument should either be the name of a test\n";
    stream << "    suite, the full name of an individual test, or a comma separated list.\n";
    stream << '\n';
    stream << "    Full test names consist of the test suite name, the separator '::' and\n";
    stream << "    then the individual test name.  The test names can revealed with the -t\n";
    stream << "    option.\n";
    stream << '\n';
    stream << "    Examples:\n";
    stream << "      - perform and print the names of all available unit tests:\n";
    stream << "        --> fgfs_test_suite -t -u\n";
    stream << "      - run all tests from the NavRadioTests test suite:\n";
    stream << "        --> fgfs_test_suite -u NavRadioTests\n";
    stream << "      - run a specific test without discarding its output:\n";
    stream << "        --> fgfs_test_suite --no-summary -d -u NavRadioTests::testGS\n";
    stream << '\n';
    stream << "  Logging options:\n";
    stream << "    --log-level={bulk,debug,info,warn,alert,popup,dev_warn,dev_alert}\n";
    stream << "                        specify the minimum logging level to output\n";
    stream << "    --log-class=[none, terrain, astro, flight, input, gl, view, cockpit,\n";
    stream << "                 general, math, event, aircraft, autopilot, io, clipper,\n";
    stream << "                 network, atc, nasal, instrumentation, systems, ai, environment,\n";
    stream << "                 sound, navaid, gui, terrasync, particles, headless, osg,\n";
    stream << "                 undefined, all]\n";
    stream << "                        select the logging class(es) to output.\n";
    stream << "    --log-split         output the different non-interleaved log streams\n";
    stream << "                        sequentially.\n";
    stream << '\n';
    stream << "  Verbosity options:\n";
    stream << "    -t, --timings       verbose output including names and timings for all\n";
    stream << "                        tests.\n";
    stream << "    -c, --ctest         simplified output suitable for running via CTest.\n";
    stream << "    -d, --debug         disable IO capture for debugging (super verbose output).\n";
    stream << "    --no-summary        disable the final summary printout.\n";
    stream << '\n';
    stream << "  FG options:\n";
    stream << "    --fg-root           the path to FGData.\n";
    stream << '\n';
    stream << "Environmental variables:\n";
    stream << "  FG_TEST_LOG_LEVEL     equivalent to the --log-level option.\n";
    stream << "  FG_TEST_LOG_CLASS     equivalent to the --log-class option.\n";
    stream << "  FG_TEST_LOG_SPLIT     equivalent to the --log-split option.\n";
    stream << "  FG_TEST_TIMINGS       equivalent to the -t or --timings option.\n";
    stream << "  FG_TEST_DEBUG         equivalent to the -d or --debug option.\n";
    stream << "  FG_ROOT               the path to FGData.  The order of precedence is\n";
    stream << "                         --fg-root, the FG_DATA_DIR CMake option, FG_ROOT,\n";
    stream << "                        '../fgdata/', and '../data/'.\n";
    stream.flush();
}


// Convert the log class comma separated string list into a simgear debug class value.
sgDebugClass processLogClass(std::string classList, bool &failure) {
    // Declarations.
    std::string logClassItem, val;
    unsigned int logClass=0;

    // Convert the command line value into a string array.
    std::replace(classList.begin(), classList.end(), ',', ' ');
    std::vector<std::string> logClasses;
    stringstream temp(classList);
    while (temp >> val)
        logClasses.push_back(val);

    // Build up the value.
    for (auto const& logClassItem: logClasses)
        if (logClassItem == "none")
            logClass |= SG_NONE;
        else if (logClassItem == "terrain")
            logClass |= SG_TERRAIN;
        else if (logClassItem == "astro")
            logClass |= SG_ASTRO;
        else if (logClassItem == "flight")
            logClass |= SG_FLIGHT;
        else if (logClassItem == "input")
            logClass |= SG_INPUT;
        else if (logClassItem == "gl")
            logClass |= SG_GL;
        else if (logClassItem == "view")
            logClass |= SG_VIEW;
        else if (logClassItem == "cockpit")
            logClass |= SG_COCKPIT;
        else if (logClassItem == "general")
            logClass |= SG_GENERAL;
        else if (logClassItem == "math")
            logClass |= SG_MATH;
        else if (logClassItem == "event")
            logClass |= SG_EVENT;
        else if (logClassItem == "aircraft")
            logClass |= SG_AIRCRAFT;
        else if (logClassItem == "autopilot")
            logClass |= SG_AUTOPILOT;
        else if (logClassItem == "io")
            logClass |= SG_IO;
        else if (logClassItem == "clipper")
            logClass |= SG_CLIPPER;
        else if (logClassItem == "network")
            logClass |= SG_NETWORK;
        else if (logClassItem == "atc")
            logClass |= SG_ATC;
        else if (logClassItem == "nasal")
            logClass |= SG_NASAL;
        else if (logClassItem == "instrumentation")
            logClass |= SG_INSTR;
        else if (logClassItem == "systems")
            logClass |= SG_SYSTEMS;
        else if (logClassItem == "ai")
            logClass |= SG_AI;
        else if (logClassItem == "environment")
            logClass |= SG_ENVIRONMENT;
        else if (logClassItem == "sound")
            logClass |= SG_SOUND;
        else if (logClassItem == "navaid")
            logClass |= SG_NAVAID;
        else if (logClassItem == "gui")
            logClass |= SG_GUI;
        else if (logClassItem == "terrasync")
            logClass |= SG_TERRASYNC;
        else if (logClassItem == "particles")
            logClass |= SG_PARTICLES;
        else if (logClassItem == "headless")
            logClass |= SG_HEADLESS;
        else if (logClassItem == "osg")
            logClass |= SG_OSG;
        else if (logClassItem == "undefined")
            logClass |= SG_UNDEFD;
        else if (logClassItem == "all")
            logClass |= SG_ALL;
        else {
            std::cout << "The log class \"" << logClassItem << "\" must be one of:" << std::endl;
            std::cout << "    {none, terrain, astro, flight, input, gl, view, cockpit, general, math," << std::endl;
            std::cout << "    event, aircraft, autopilot, io, clipper, network, atc, nasal," << std::endl;
            std::cout << "    instrumentation, systems, ai, environment, sound, navaid, gui, terrasync," << std::endl;
            std::cout << "    particles, headless, osg, undefined, all}" << std::endl << std::endl;
            std::cout.flush();
            failure = true;
        }

    // Return a simgear debug class.
    return sgDebugClass(logClass);
}


// Convert the log priority string into a simgear debug priority value.
sgDebugPriority processLogPriority(std::string logLevel, bool &failure) {
    // Declarations.
    sgDebugPriority logPriority=SG_INFO;

    // Conversion.
    if (logLevel == "bulk")
        logPriority = SG_BULK;
    else if (logLevel == "debug")
        logPriority = SG_DEBUG;
    else if (logLevel == "info")
        logPriority = SG_INFO;
    else if (logLevel == "warn")
        logPriority = SG_WARN;
    else if (logLevel == "alert")
        logPriority = SG_ALERT;
    else if (logLevel == "popup")
        logPriority = SG_POPUP;
    else if (logLevel == "dev_warn")
        logPriority = SG_DEV_WARN;
    else if (logLevel == "dev_alert")
        logPriority = SG_DEV_ALERT;
    else {
        std::cout << "The log level setting of \"" << logLevel << "\" must be one of {bulk,debug,info,warn,alert,popup,dev_warn,dev_alert}.\n\n";
        std::cout.flush();
        failure = true;
    }

    // Return the simgear debug priority.
    return logPriority;
}


// Print out a summary of the relax test suite.
void summary(CppUnit::OStream &stream, int system_result, int unit_result, int gui_result, int simgear_result, int fgdata_result)
{
    int synopsis = 0;

    // Title.
    string text = "Summary of the FlightGear test suite";
    printTitle(stream, text);

    // Subtitle.
    text = "Synopsis";
    printSection(stream, text);

    // System/functional test summary.
    if (system_result != -1) {
        text = "System/functional tests";
        printSummaryLine(stream, text, system_result);
        synopsis += system_result;
    }

    // Unit test summary.
    if (unit_result != -1) {
        text = "Unit tests";
        printSummaryLine(stream, text, unit_result);
        synopsis += unit_result;
    }

    // GUI test summary.
    if (gui_result != -1) {
        text = "GUI tests";
        printSummaryLine(stream, text, gui_result);
        synopsis += gui_result;
    }

    // Simgear unit test summary.
    if (simgear_result != -1) {
        text = "Simgear unit tests";
        printSummaryLine(stream, text, simgear_result);
        synopsis += simgear_result;
    }

    // FGData test summary.
    if (fgdata_result != -1) {
        text = "FGData tests";
        printSummaryLine(stream, text, fgdata_result);
        synopsis += fgdata_result;
    }

    // Synopsis.
    text ="Synopsis";
    printSummaryLine(stream, text, synopsis);

    // End.
    stream << endl << endl;
}


int main(int argc, char **argv)
{
    // Declarations.
    int         status_gui=-1, status_simgear=-1, status_system=-1, status_unit=-1, status_fgdata=-1;
    bool        run_system=false, run_unit=false, run_gui=false, run_simgear=false, run_fgdata=false;
    bool        logSplit=false;
    bool        timings=false, ctest_output=false, debug=false, printSummary=true, help=false;
    char        *subset_system=NULL, *subset_unit=NULL, *subset_gui=NULL, *subset_simgear=NULL, *subset_fgdata=NULL;
    bool        failure=false;
    char        firstchar;
    std::string arg, delim, fgRoot, logClassVal, logLevel;
    size_t      delimPos;

    // The default logging class and priority to show.
    sgDebugClass    logClass=SG_ALL;
    sgDebugPriority logPriority=SG_INFO;

    // Process environmental variables before the command line options.
    if (getenv("FG_TEST_LOG_LEVEL"))
        logPriority = processLogPriority(getenv("FG_TEST_LOG_LEVEL"), failure);
    if (getenv("FG_TEST_LOG_CLASS"))
        logClass = processLogClass(getenv("FG_TEST_LOG_CLASS"), failure);
    if (getenv("FG_TEST_LOG_SPLIT"))
        logSplit = true;
    if (getenv("FG_TEST_TIMINGS"))
        timings = true;
    if (getenv("FG_TEST_DEBUG"))
        debug = true;
    if (failure)
        return 1;

    // Argument parsing.
    for (int i = 1; i < argc; i++) {
        firstchar = '\0';
        arg = argv[i];

        if (i < argc-1)
            firstchar = argv[i+1][0];

        // System test.
        if (arg == "-s" || arg == "--system-tests") {
            run_system = true;
            if (firstchar != '-')
                subset_system = argv[i+1];

        // Unit test.
        } else if (arg == "-u" || arg == "--unit-tests") {
            run_unit = true;
            if (firstchar != '-')
                subset_unit = argv[i+1];

        // GUI test.
        } else if (arg == "-g" || arg == "--gui-tests") {
            run_gui = true;
            if (firstchar != '-')
                subset_gui = argv[i+1];

        // Simgear test.
        } else if (arg == "-m" || arg == "--simgear-tests") {
            run_simgear = true;
            if (firstchar != '-')
                subset_simgear = argv[i+1];

        // FGData test.
        } else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--fgdata-tests") == 0) {
            run_fgdata = true;
            if (firstchar != '-')
                subset_fgdata = argv[i+1];

        // Log class.
        } else if (arg.find( "--log-class" ) == 0) {
            // Process the command line.
            logClass = SG_NONE;
            delimPos = arg.find('=');
            logClassVal = arg.substr(delimPos + 1);
            logClass = processLogClass(logClassVal, failure);
            if (failure)
                return 1;

        // Log level.
        } else if (arg.find( "--log-level" ) == 0) {
            // Process the command line level.
            delimPos = arg.find('=');
            logLevel = arg.substr(delimPos + 1);
            logPriority = processLogPriority(logLevel, failure);
            if (failure)
                return 1;

        // Log splitting.
        } else if (arg == "--log-split") {
            logSplit = true;

        // Timing output.
        } else if (arg == "-t" || arg == "--timings") {
            timings = true;

        // CTest suitable output.
        } else if (arg == "-c" || arg == "--ctest") {
            ctest_output = true;

        // Debug output.
        } else if (arg == "-d" || arg == "--debug") {
            debug = true;

        // No summary output.
        } else if (arg == "--no-summary") {
            printSummary = false;

        // Help.
        } else if (arg == "-h" || arg == "--help") {
            help = true;

        // FGData path.
        } else if (arg == "--fg-root") {
            if (firstchar != '-')
                fgRoot = argv[i+1];
        }
    }

    // Help.
    if (help) {
        helpPrintout(std::cout);
        return 0;
    }

    // Turn on all tests if no subset was specified.
    if (!run_system && !run_unit && !run_gui && !run_simgear && !run_fgdata) {
        run_system = true;
        run_unit = true;
        run_gui = true;
        run_simgear = true;
        run_fgdata = true;
    }

    // Set up the data store singleton and FGData path.
    DataStore& data = DataStore::get();
    if (data.findFGRoot(fgRoot, debug) != 0) {
        return 1;
    }
    if (data.validateFGRoot() != 0) {
        return 1;
    }

    // Set up logging.
    sglog().setDeveloperMode(true);
    if (debug)
        sglog().setLogLevels(sgDebugClass(logClass), logPriority);
    else
        setupLogging(sgDebugClass(logClass), logPriority, logSplit);

    // Execute each of the test suite categories.
    if (run_system)
        status_system = testRunner("System tests", "System / functional tests", subset_system, timings, ctest_output, debug);
    if (run_unit)
        status_unit = testRunner("Unit tests", "Unit tests", subset_unit, timings, ctest_output, debug);
    if (run_gui && 0) // Disabled as there are no GUI tests yet.
        status_gui = testRunner("GUI tests", "GUI tests", subset_gui, timings, ctest_output, debug);
    if (run_simgear)
        status_simgear = testRunner("Simgear unit tests", "Simgear unit tests", subset_simgear, timings, ctest_output, debug);
    if (run_fgdata)
        status_fgdata = testRunner("FGData tests", "FGData tests", subset_fgdata, timings, ctest_output, debug);

    // Summary printout.
    if (printSummary && !ctest_output)
        summary(cerr, status_system, status_unit, status_gui, status_simgear, status_fgdata);

    // Deactivate the logging.
    if (!debug)
        stopLogging();

    // Failure.
    if (status_system > 0)
        return 1;
    if (status_unit > 0)
        return 1;
    if (status_gui > 0)
        return 1;
    if (status_simgear > 0)
        return 1;
    if (status_fgdata > 0)
        return 1;

    // Success.
    return 0;
}
