/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */

#include "CTest/cmCTestLaunch.h"
#include "CTest/cmCTestScriptHandler.h"
#include "cmCTest.h"
#include "cmDocumentation.h"
#include "cmSystemTools.h"

#include "cmsys/Encoding.hxx"
#if defined(_WIN32) && defined(CMAKE_BUILD_WITH_CMAKE)
#    include "cmsys/ConsoleBuf.hxx"
#endif
#include <iostream>
#include <string.h>
#include <string>
#include <vector>

static const char* cmDocumentationName[][2] = {
    { nullptr, "  ctest - Testing driver provided by CMake." },
    { nullptr, nullptr }
};

static const char* cmDocumentationUsage[][2] = { { nullptr,
                                                   "  ctest [options]" },
                                                 { nullptr, nullptr } };

static const char* cmDocumentationOptions[][2] = {
    { "-C <cfg>, --build-config <cfg>", "Choose configuration to test." },
    { "--progress", "Enable short progress output from tests." },
    { "-V,--verbose", "Enable verbose output from tests." },
    { "-VV,--extra-verbose", "Enable more verbose output from tests." },
    { "--debug", "Displaying more verbose internals of CTest." },
    { "--output-on-failure", "Output anything outputted by the test program "
                             "if the test should fail." },
    { "--test-output-size-passed <size>", "Limit the output for passed tests "
                                          "to <size> bytes" },
    { "--test-output-size-failed <size>", "Limit the output for failed tests "
                                          "to <size> bytes" },
    { "-F", "Enable failover." },
    { "-j <jobs>, --parallel <jobs>", "Run the tests in parallel using the "
                                      "given number of jobs." },
    { "-Q,--quiet", "Make ctest quiet." },
    { "-O <file>, --output-log <file>", "Output to log file" },
    { "-N,--show-only", "Disable actual execution of tests." },
    { "-L <regex>, --label-regex <regex>", "Run tests with labels matching "
                                           "regular expression." },
    { "-R <regex>, --tests-regex <regex>", "Run tests matching regular "
                                           "expression." },
    { "-E <regex>, --exclude-regex <regex>", "Exclude tests matching regular "
                                             "expression." },
    { "-LE <regex>, --label-exclude <regex>", "Exclude tests with labels "
                                              "matching regular expression." },
    { "-FA <regex>, --fixture-exclude-any <regex>", "Do not automatically "
                                                    "add any tests for "
                                                    "fixtures matching "
                                                    "regular expression." },
    { "-FS <regex>, --fixture-exclude-setup <regex>", "Do not automatically "
                                                      "add setup tests for "
                                                      "fixtures matching "
                                                      "regular expression." },
    { "-FC <regex>, --fixture-exclude-cleanup <regex>", "Do not automatically "
                                                        "add cleanup tests for "
                                                        "fixtures matching "
                                                        "regular expression." },
    { "-D <dashboard>, --dashboard <dashboard>", "Execute dashboard test" },
    { "-D <var>:<type>=<value>", "Define a variable for script mode" },
    { "-M <model>, --test-model <model>", "Sets the model for a dashboard" },
    { "-T <action>, --test-action <action>", "Sets the dashboard action to "
                                             "perform" },
    { "--track <track>", "Specify the track to submit dashboard to" },
    { "-S <script>, --script <script>", "Execute a dashboard for a "
                                        "configuration" },
    { "-SP <script>, --script-new-process <script>",
      "Execute a dashboard for a "
      "configuration" },
    { "-A <file>, --add-notes <file>", "Add a notes file with submission" },
    { "-I [Start,End,Stride,test#,test#|Test file], --tests-information",
      "Run a specific number of tests by number." },
    { "-U, --union", "Take the Union of -I and -R" },
    { "--rerun-failed", "Run only the tests that failed previously" },
    { "--repeat-until-fail <n>", "Require each test to run <n> "
                                 "times without failing in order to pass" },
    { "--max-width <width>", "Set the max width for a test name to output" },
    { "--interactive-debug-mode [0|1]", "Set the interactive mode to 0 or 1." },
    { "--no-label-summary", "Disable timing summary information for labels." },
    { "--no-subproject-summary", "Disable timing summary information for "
                                 "subprojects." },
    { "--build-and-test", "Configure, build and run a test." },
    { "--build-target", "Specify a specific target to build." },
    { "--build-nocmake", "Run the build without running cmake first." },
    { "--build-run-dir", "Specify directory to run programs from." },
    { "--build-two-config", "Run CMake twice" },
    { "--build-exe-dir", "Specify the directory for the executable." },
    { "--build-generator", "Specify the generator to use." },
    { "--build-generator-platform",
      "Specify the generator-specific platform." },
    { "--build-generator-toolset", "Specify the generator-specific toolset." },
    { "--build-project", "Specify the name of the project to build." },
    { "--build-makeprogram", "Specify the make program to use." },
    { "--build-noclean", "Skip the make clean step." },
    { "--build-config-sample",
      "A sample executable to use to determine the configuration" },
    { "--build-options", "Add extra options to the build step." },

    { "--test-command", "The test to run with the --build-and-test option." },
    { "--test-timeout", "The time limit in seconds, internal use only." },
    { "--test-load", "CPU load threshold for starting new parallel tests." },
    { "--tomorrow-tag", "Nightly or experimental starts with next day tag." },
    { "--overwrite", "Overwrite CTest configuration option." },
    { "--extra-submit <file>[;<file>]",
      "Submit extra files to the dashboard." },
    { "--force-new-ctest-process",
      "Run child CTest instances as new processes" },
    { "--schedule-random", "Use a random order for scheduling tests" },
    { "--submit-index",
      "Submit individual dashboard tests with specific index" },
    { "--timeout <seconds>", "Set a global timeout on all tests." },
    { "--stop-time <time>",
      "Set a time at which all tests should stop running." },
    { "--http1.0", "Submit using HTTP 1.0." },
    { "--no-compress-output", "Do not compress test output when submitting." },
    { "--print-labels", "Print all available test labels." },
    { nullptr, nullptr }
};

// this is a test driver program for cmCTest.
int
main(int argc, char const* const* argv)
{
#if defined(_WIN32) && defined(CMAKE_BUILD_WITH_CMAKE)
    // Replace streambuf so we can output Unicode to console
    cmsys::ConsoleBuf::Manager consoleOut(std::cout);
    consoleOut.SetUTF8Pipes();
    cmsys::ConsoleBuf::Manager consoleErr(std::cerr, true);
    consoleErr.SetUTF8Pipes();
#endif
    cmsys::Encoding::CommandLineArguments encoding_args =
        cmsys::Encoding::CommandLineArguments::Main(argc, argv);
    argc = encoding_args.argc();
    argv = encoding_args.argv();

    cmSystemTools::DoNotInheritStdPipes();
    cmSystemTools::EnableMSVCDebugHook();
    cmSystemTools::InitializeLibUV();
    cmSystemTools::FindCMakeResources(argv[0]);

    // Dispatch 'ctest --launch' mode directly.
    if(argc >= 2 && strcmp(argv[1], "--launch") == 0)
    {
        return cmCTestLaunch::Main(argc, argv);
    }

    cmCTest inst;

    if(cmSystemTools::GetCurrentWorkingDirectory().empty())
    {
        cmCTestLog(&inst, ERROR_MESSAGE,
                   "Current working directory cannot be established."
                       << std::endl);
        return 1;
    }

    // If there is a testing input file, check for documentation options
    // only if there are actually arguments.  We want running without
    // arguments to run tests.
    if(argc > 1 || !(cmSystemTools::FileExists("CTestTestfile.cmake") ||
                     cmSystemTools::FileExists("DartTestfile.txt")))
    {
        if(argc == 1)
        {
            cmCTestLog(&inst, ERROR_MESSAGE,
                       "*********************************"
                           << std::endl
                           << "No test configuration file found!" << std::endl
                           << "*********************************" << std::endl);
        }
        cmDocumentation doc;
        doc.addCTestStandardDocSections();
        if(doc.CheckOptions(argc, argv))
        {
            // Construct and print requested documentation.
            cmCTestScriptHandler* ch =
                static_cast<cmCTestScriptHandler*>(inst.GetHandler("script"));
            ch->CreateCMake();

            doc.SetShowGenerators(false);
            doc.SetName("ctest");
            doc.SetSection("Name", cmDocumentationName);
            doc.SetSection("Usage", cmDocumentationUsage);
            doc.PrependSection("Options", cmDocumentationOptions);
            return doc.PrintRequestedDocumentation(std::cout) ? 0 : 1;
        }
    }

    // copy the args to a vector
    std::vector<std::string> args;
    args.reserve(argc);
    for(int i = 0; i < argc; ++i)
    {
        args.push_back(argv[i]);
    }
    // run ctest
    std::string output;
    int         res = inst.Run(args, &output);
    cmCTestLog(&inst, OUTPUT, output);

    return res;
}
