
//              Copyright Catch2 Authors
// Distributed under the Boost Software License, Version 1.0.
//   (See accompanying file LICENSE_1_0.txt or copy at
//        https://www.boost.org/LICENSE_1_0.txt)

// SPDX-License-Identifier: BSL-1.0
#include <catch2/internal/catch_commandline.hpp>

#include <catch2/internal/catch_compiler_capabilities.hpp>
#include <catch2/catch_config.hpp>
#include <catch2/internal/catch_string_manip.hpp>
#include <catch2/interfaces/catch_interfaces_registry_hub.hpp>
#include <catch2/interfaces/catch_interfaces_reporter_registry.hpp>
#include <catch2/interfaces/catch_interfaces_reporter.hpp>

#include <algorithm>
#include <fstream>
#include <string>

namespace Catch {

    Clara::Parser makeCommandLineParser( ConfigData& config ) {

        using namespace Clara;

        auto const setWarning = [&]( std::string const& warning ) {
            if ( warning == "NoAssertions" ) {
                config.warnings = static_cast<WarnAbout::What>(config.warnings | WarnAbout::NoAssertions);
                return ParserResult::ok( ParseResultType::Matched );
            } else if ( warning == "UnmatchedTestSpec" ) {
                config.warnings = static_cast<WarnAbout::What>(config.warnings | WarnAbout::UnmatchedTestSpec);
                return ParserResult::ok( ParseResultType::Matched );
            }

            return ParserResult ::runtimeError(
                "Unrecognised warning option: '" + warning + '\'' );
        };
        auto const loadTestNamesFromFile = [&]( std::string const& filename ) {
                std::ifstream f( filename.c_str() );
                if( !f.is_open() )
                    return ParserResult::runtimeError( "Unable to load input file: '" + filename + '\'' );

                std::string line;
                while( std::getline( f, line ) ) {
                    line = trim(line);
                    if( !line.empty() && !startsWith( line, '#' ) ) {
                        if( !startsWith( line, '"' ) )
                            line = '"' + line + '"';
                        config.testsOrTags.push_back( line );
                        config.testsOrTags.emplace_back( "," );
                    }
                }
                //Remove comma in the end
                if(!config.testsOrTags.empty())
                    config.testsOrTags.erase( config.testsOrTags.end()-1 );

                return ParserResult::ok( ParseResultType::Matched );
            };
        auto const setTestOrder = [&]( std::string const& order ) {
                if( startsWith( "declared", order ) )
                    config.runOrder = TestRunOrder::Declared;
                else if( startsWith( "lexical", order ) )
                    config.runOrder = TestRunOrder::LexicographicallySorted;
                else if( startsWith( "random", order ) )
                    config.runOrder = TestRunOrder::Randomized;
                else
                    return ParserResult::runtimeError( "Unrecognised ordering: '" + order + '\'' );
                return ParserResult::ok( ParseResultType::Matched );
            };
        auto const setRngSeed = [&]( std::string const& seed ) {
                if( seed == "time" ) {
                    config.rngSeed = generateRandomSeed(GenerateFrom::Time);
                    return ParserResult::ok(ParseResultType::Matched);
                } else if (seed == "random-device") {
                    config.rngSeed = generateRandomSeed(GenerateFrom::RandomDevice);
                    return ParserResult::ok(ParseResultType::Matched);
                }

                CATCH_TRY {
                    std::size_t parsedTo = 0;
                    unsigned long parsedSeed = std::stoul(seed, &parsedTo, 0);
                    if (parsedTo != seed.size()) {
                        return ParserResult::runtimeError("Could not parse '" + seed + "' as seed");
                    }

                    // TODO: Ideally we could parse unsigned int directly,
                    //       but the stdlib doesn't provide helper for that
                    //       type. After this is refactored to use fixed size
                    //       type, we should check the parsed value is in range
                    //       of the underlying type.
                    config.rngSeed = static_cast<unsigned int>(parsedSeed);
                    return ParserResult::ok(ParseResultType::Matched);
                } CATCH_CATCH_ANON(std::exception const&) {
                    return ParserResult::runtimeError("Could not parse '" + seed + "' as seed");
                }
            };
        auto const setColourUsage = [&]( std::string const& useColour ) {
                    auto mode = toLower( useColour );

                    if( mode == "yes" )
                        config.useColour = UseColour::Yes;
                    else if( mode == "no" )
                        config.useColour = UseColour::No;
                    else if( mode == "auto" )
                        config.useColour = UseColour::Auto;
                    else
                        return ParserResult::runtimeError( "colour mode must be one of: auto, yes or no. '" + useColour + "' not recognised" );
                return ParserResult::ok( ParseResultType::Matched );
            };
        auto const setWaitForKeypress = [&]( std::string const& keypress ) {
                auto keypressLc = toLower( keypress );
                if (keypressLc == "never")
                    config.waitForKeypress = WaitForKeypress::Never;
                else if( keypressLc == "start" )
                    config.waitForKeypress = WaitForKeypress::BeforeStart;
                else if( keypressLc == "exit" )
                    config.waitForKeypress = WaitForKeypress::BeforeExit;
                else if( keypressLc == "both" )
                    config.waitForKeypress = WaitForKeypress::BeforeStartAndExit;
                else
                    return ParserResult::runtimeError( "keypress argument must be one of: never, start, exit or both. '" + keypress + "' not recognised" );
            return ParserResult::ok( ParseResultType::Matched );
            };
        auto const setVerbosity = [&]( std::string const& verbosity ) {
            auto lcVerbosity = toLower( verbosity );
            if( lcVerbosity == "quiet" )
                config.verbosity = Verbosity::Quiet;
            else if( lcVerbosity == "normal" )
                config.verbosity = Verbosity::Normal;
            else if( lcVerbosity == "high" )
                config.verbosity = Verbosity::High;
            else
                return ParserResult::runtimeError( "Unrecognised verbosity, '" + verbosity + '\'' );
            return ParserResult::ok( ParseResultType::Matched );
        };
        auto const setReporter = [&]( std::string const& reporterSpec ) {
            if ( reporterSpec.empty() ) {
                return ParserResult::runtimeError( "Received empty reporter spec." );
            }

            IReporterRegistry::FactoryMap const& factories = getRegistryHub().getReporterRegistry().getFactories();

            // clear the default reporter
            if (!config._nonDefaultReporterSpecifications) {
                config.reporterSpecifications.clear();
                config._nonDefaultReporterSpecifications = true;
            }


            // Exactly one of the reporters may be specified without an output
            // file, in which case it defaults to the output specified by "-o"
            // (or standard output).
            static constexpr auto separator = "::";
            static constexpr size_t separatorSize = 2;
            auto fileNameSeparatorPos = reporterSpec.find( separator );
            const bool containsFileName = fileNameSeparatorPos != reporterSpec.npos;
            if ( containsFileName ) {
                auto nextSeparatorPos = reporterSpec.find(
                    separator, fileNameSeparatorPos + separatorSize );
                if ( nextSeparatorPos != reporterSpec.npos ) {
                    return ParserResult::runtimeError(
                        "Too many separators in reporter spec '" + reporterSpec + '\'' );
                }
            }

            std::string reporterName;
            Optional<std::string> outputFileName;
            reporterName = reporterSpec.substr( 0, fileNameSeparatorPos );
            if ( reporterName.empty() ) {
                return ParserResult::runtimeError( "Reporter name cannot be empty." );
            }

            if ( containsFileName ) {
                outputFileName = reporterSpec.substr(
                    fileNameSeparatorPos + separatorSize, reporterSpec.size() );
            }

            auto result = factories.find( reporterName );

            if( result == factories.end() )
                return ParserResult::runtimeError( "Unrecognized reporter, '" + reporterName + "'. Check available with --list-reporters" );
            if( containsFileName && outputFileName->empty() )
                return ParserResult::runtimeError( "Reporter '" + reporterName + "' has empty filename specified as its output. Supply a filename or remove the colons to use the default output." );

            config.reporterSpecifications.push_back({ std::move(reporterName), std::move(outputFileName) });

            // It would be enough to check this only once at the very end, but there is
            // not a place where we could call this check, so do it every time it could fail.
            // For valid inputs, this is still called at most once.
            if (!containsFileName) {
                int n_reporters_without_file = 0;
                for (auto const& spec : config.reporterSpecifications) {
                    if (spec.outputFileName.none()) {
                        n_reporters_without_file++;
                    }
                }
                if (n_reporters_without_file > 1) {
                    return ParserResult::runtimeError( "Only one reporter may have unspecified output file." );
                }
            }

            return ParserResult::ok( ParseResultType::Matched );
        };
        auto const setShardCount = [&]( std::string const& shardCount ) {
            CATCH_TRY{
                std::size_t parsedTo = 0;
                int64_t parsedCount = std::stoll(shardCount, &parsedTo, 0);
                if (parsedTo != shardCount.size()) {
                    return ParserResult::runtimeError("Could not parse '" + shardCount + "' as shard count");
                }
                if (parsedCount <= 0) {
                    return ParserResult::runtimeError("Shard count must be a positive number");
                }

                config.shardCount = static_cast<unsigned int>(parsedCount);
                return ParserResult::ok(ParseResultType::Matched);
            } CATCH_CATCH_ANON(std::exception const&) {
                return ParserResult::runtimeError("Could not parse '" + shardCount + "' as shard count");
            }
        };

        auto const setShardIndex = [&](std::string const& shardIndex) {
            CATCH_TRY{
                std::size_t parsedTo = 0;
                int64_t parsedIndex = std::stoll(shardIndex, &parsedTo, 0);
                if (parsedTo != shardIndex.size()) {
                    return ParserResult::runtimeError("Could not parse '" + shardIndex + "' as shard index");
                }
                if (parsedIndex < 0) {
                    return ParserResult::runtimeError("Shard index must be a non-negative number");
                }

                config.shardIndex = static_cast<unsigned int>(parsedIndex);
                return ParserResult::ok(ParseResultType::Matched);
            } CATCH_CATCH_ANON(std::exception const&) {
                return ParserResult::runtimeError("Could not parse '" + shardIndex + "' as shard index");
            }
        };


        auto cli
            = ExeName( config.processName )
            | Help( config.showHelp )
            | Opt( config.showSuccessfulTests )
                ["-s"]["--success"]
                ( "include successful tests in output" )
            | Opt( config.shouldDebugBreak )
                ["-b"]["--break"]
                ( "break into debugger on failure" )
            | Opt( config.noThrow )
                ["-e"]["--nothrow"]
                ( "skip exception tests" )
            | Opt( config.showInvisibles )
                ["-i"]["--invisibles"]
                ( "show invisibles (tabs, newlines)" )
            | Opt( config.defaultOutputFilename, "filename" )
                ["-o"]["--out"]
                ( "default output filename" )
            | Opt( accept_many, setReporter, "name[:output-file]" )
                ["-r"]["--reporter"]
                ( "reporter to use (defaults to console)" )
            | Opt( config.name, "name" )
                ["-n"]["--name"]
                ( "suite name" )
            | Opt( [&]( bool ){ config.abortAfter = 1; } )
                ["-a"]["--abort"]
                ( "abort at first failure" )
            | Opt( [&]( int x ){ config.abortAfter = x; }, "no. failures" )
                ["-x"]["--abortx"]
                ( "abort after x failures" )
            | Opt( accept_many, setWarning, "warning name" )
                ["-w"]["--warn"]
                ( "enable warnings" )
            | Opt( [&]( bool flag ) { config.showDurations = flag ? ShowDurations::Always : ShowDurations::Never; }, "yes|no" )
                ["-d"]["--durations"]
                ( "show test durations" )
            | Opt( config.minDuration, "seconds" )
                ["-D"]["--min-duration"]
                ( "show test durations for tests taking at least the given number of seconds" )
            | Opt( loadTestNamesFromFile, "filename" )
                ["-f"]["--input-file"]
                ( "load test names to run from a file" )
            | Opt( config.filenamesAsTags )
                ["-#"]["--filenames-as-tags"]
                ( "adds a tag for the filename" )
            | Opt( config.sectionsToRun, "section name" )
                ["-c"]["--section"]
                ( "specify section to run" )
            | Opt( setVerbosity, "quiet|normal|high" )
                ["-v"]["--verbosity"]
                ( "set output verbosity" )
            | Opt( config.listTests )
                ["--list-tests"]
                ( "list all/matching test cases" )
            | Opt( config.listTags )
                ["--list-tags"]
                ( "list all/matching tags" )
            | Opt( config.listReporters )
                ["--list-reporters"]
                ( "list all reporters" )
            | Opt( setTestOrder, "decl|lex|rand" )
                ["--order"]
                ( "test case order (defaults to decl)" )
            | Opt( setRngSeed, "'time'|'random-device'|number" )
                ["--rng-seed"]
                ( "set a specific seed for random numbers" )
            | Opt( setColourUsage, "yes|no" )
                ["--use-colour"]
                ( "should output be colourised" )
            | Opt( config.libIdentify )
                ["--libidentify"]
                ( "report name and version according to libidentify standard" )
            | Opt( setWaitForKeypress, "never|start|exit|both" )
                ["--wait-for-keypress"]
                ( "waits for a keypress before exiting" )
            | Opt( config.benchmarkSamples, "samples" )
                ["--benchmark-samples"]
                ( "number of samples to collect (default: 100)" )
            | Opt( config.benchmarkResamples, "resamples" )
                ["--benchmark-resamples"]
                ( "number of resamples for the bootstrap (default: 100000)" )
            | Opt( config.benchmarkConfidenceInterval, "confidence interval" )
                ["--benchmark-confidence-interval"]
                ( "confidence interval for the bootstrap (between 0 and 1, default: 0.95)" )
            | Opt( config.benchmarkNoAnalysis )
                ["--benchmark-no-analysis"]
                ( "perform only measurements; do not perform any analysis" )
            | Opt( config.benchmarkWarmupTime, "benchmarkWarmupTime" )
                ["--benchmark-warmup-time"]
                ( "amount of time in milliseconds spent on warming up each test (default: 100)" )
            | Opt( setShardCount, "shard count" )
                ["--shard-count"]
                ( "split the tests to execute into this many groups" )
            | Opt( setShardIndex, "shard index" )
                ["--shard-index"]
                ( "index of the group of tests to execute (see --shard-count)" ) |
            Opt( config.allowZeroTests )
                ["--allow-running-no-tests"]
                ( "Treat 'No tests run' as a success" )
            | Arg( config.testsOrTags, "test name|pattern|tags" )
                ( "which test or tests to use" );

        return cli;
    }

} // end namespace Catch
