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

// SPDX-License-Identifier: BSL-1.0
#include <catch2/catch_config.hpp>
#include <catch2/catch_user_config.hpp>
#include <catch2/internal/catch_enforce.hpp>
#include <catch2/internal/catch_parse_numbers.hpp>
#include <catch2/internal/catch_stdstreams.hpp>
#include <catch2/internal/catch_stringref.hpp>
#include <catch2/internal/catch_string_manip.hpp>
#include <catch2/internal/catch_test_spec_parser.hpp>
#include <catch2/interfaces/catch_interfaces_tag_alias_registry.hpp>
#include <catch2/internal/catch_getenv.hpp>

#include <fstream>

namespace Catch {

    namespace {
        static bool enableBazelEnvSupport() {
#if defined( CATCH_CONFIG_BAZEL_SUPPORT )
            return true;
#else
            return Detail::getEnv( "BAZEL_TEST" ) != nullptr;
#endif
        }

        struct bazelShardingOptions {
            unsigned int shardIndex, shardCount;
            std::string shardFilePath;
        };

        static Optional<bazelShardingOptions> readBazelShardingOptions() {
            const auto bazelShardIndex = Detail::getEnv( "TEST_SHARD_INDEX" );
            const auto bazelShardTotal = Detail::getEnv( "TEST_TOTAL_SHARDS" );
            const auto bazelShardInfoFile = Detail::getEnv( "TEST_SHARD_STATUS_FILE" );


            const bool has_all =
                bazelShardIndex && bazelShardTotal && bazelShardInfoFile;
            if ( !has_all ) {
                // We provide nice warning message if the input is
                // misconfigured.
                auto warn = []( const char* env_var ) {
                    Catch::cerr()
                        << "Warning: Bazel shard configuration is missing '"
                        << env_var << "'. Shard configuration is skipped.\n";
                };
                if ( !bazelShardIndex ) {
                    warn( "TEST_SHARD_INDEX" );
                }
                if ( !bazelShardTotal ) {
                    warn( "TEST_TOTAL_SHARDS" );
                }
                if ( !bazelShardInfoFile ) {
                    warn( "TEST_SHARD_STATUS_FILE" );
                }
                return {};
            }

            auto shardIndex = parseUInt( bazelShardIndex );
            if ( !shardIndex ) {
                Catch::cerr()
                    << "Warning: could not parse 'TEST_SHARD_INDEX' ('" << bazelShardIndex
                    << "') as unsigned int.\n";
                return {};
            }
            auto shardTotal = parseUInt( bazelShardTotal );
            if ( !shardTotal ) {
                Catch::cerr()
                    << "Warning: could not parse 'TEST_TOTAL_SHARD' ('"
                    << bazelShardTotal << "') as unsigned int.\n";
                return {};
            }

            return bazelShardingOptions{
                *shardIndex, *shardTotal, bazelShardInfoFile };

        }
    } // end namespace


    bool operator==( ProcessedReporterSpec const& lhs,
                     ProcessedReporterSpec const& rhs ) {
        return lhs.name == rhs.name &&
               lhs.outputFilename == rhs.outputFilename &&
               lhs.colourMode == rhs.colourMode &&
               lhs.customOptions == rhs.customOptions;
    }

    Config::Config( ConfigData const& data ):
        m_data( data ) {
        // We need to trim filter specs to avoid trouble with superfluous
        // whitespace (esp. important for bdd macros, as those are manually
        // aligned with whitespace).

        for (auto& elem : m_data.testsOrTags) {
            elem = trim(elem);
        }
        for (auto& elem : m_data.sectionsToRun) {
            elem = trim(elem);
        }

        // Insert the default reporter if user hasn't asked for a specific one
        if ( m_data.reporterSpecifications.empty() ) {
#if defined( CATCH_CONFIG_DEFAULT_REPORTER )
            const auto default_spec = CATCH_CONFIG_DEFAULT_REPORTER;
#else
            const auto default_spec = "console";
#endif
            auto parsed = parseReporterSpec(default_spec);
            CATCH_ENFORCE( parsed,
                           "Cannot parse the provided default reporter spec: '"
                               << default_spec << '\'' );
            m_data.reporterSpecifications.push_back( std::move( *parsed ) );
        }

        // Reading bazel env vars can change some parts of the config data,
        // so we have to process the bazel env before acting on the config.
        if ( enableBazelEnvSupport() ) {
            readBazelEnvVars();
        }

        // Bazel support can modify the test specs, so parsing has to happen
        // after reading Bazel env vars.
        TestSpecParser parser( ITagAliasRegistry::get() );
        if ( !m_data.testsOrTags.empty() ) {
            m_hasTestFilters = true;
            for ( auto const& testOrTags : m_data.testsOrTags ) {
                parser.parse( testOrTags );
            }
        }
        m_testSpec = parser.testSpec();


        // We now fixup the reporter specs to handle default output spec,
        // default colour spec, etc
        bool defaultOutputUsed = false;
        for ( auto const& reporterSpec : m_data.reporterSpecifications ) {
            // We do the default-output check separately, while always
            // using the default output below to make the code simpler
            // and avoid superfluous copies.
            if ( reporterSpec.outputFile().none() ) {
                CATCH_ENFORCE( !defaultOutputUsed,
                               "Internal error: cannot use default output for "
                               "multiple reporters" );
                defaultOutputUsed = true;
            }

            m_processedReporterSpecs.push_back( ProcessedReporterSpec{
                reporterSpec.name(),
                reporterSpec.outputFile() ? *reporterSpec.outputFile()
                                          : data.defaultOutputFilename,
                reporterSpec.colourMode().valueOr( data.defaultColourMode ),
                reporterSpec.customOptions() } );
        }
    }

    Config::~Config() = default;


    bool Config::listTests() const          { return m_data.listTests; }
    bool Config::listTags() const           { return m_data.listTags; }
    bool Config::listReporters() const      { return m_data.listReporters; }
    bool Config::listListeners() const      { return m_data.listListeners; }

    std::vector<std::string> const& Config::getTestsOrTags() const { return m_data.testsOrTags; }
    std::vector<std::string> const& Config::getSectionsToRun() const { return m_data.sectionsToRun; }

    std::vector<ReporterSpec> const& Config::getReporterSpecs() const {
        return m_data.reporterSpecifications;
    }

    std::vector<ProcessedReporterSpec> const&
    Config::getProcessedReporterSpecs() const {
        return m_processedReporterSpecs;
    }

    TestSpec const& Config::testSpec() const { return m_testSpec; }
    bool Config::hasTestFilters() const { return m_hasTestFilters; }

    bool Config::showHelp() const { return m_data.showHelp; }

    std::string const& Config::getExitGuardFilePath() const { return m_data.prematureExitGuardFilePath; }

    // IConfig interface
    bool Config::allowThrows() const                   { return !m_data.noThrow; }
    StringRef Config::name() const { return m_data.name.empty() ? m_data.processName : m_data.name; }
    bool Config::includeSuccessfulResults() const      { return m_data.showSuccessfulTests; }
    bool Config::warnAboutMissingAssertions() const {
        return !!( m_data.warnings & WarnAbout::NoAssertions );
    }
    bool Config::warnAboutUnmatchedTestSpecs() const {
        return !!( m_data.warnings & WarnAbout::UnmatchedTestSpec );
    }
    bool Config::zeroTestsCountAsSuccess() const       { return m_data.allowZeroTests; }
    ShowDurations Config::showDurations() const        { return m_data.showDurations; }
    double Config::minDuration() const                 { return m_data.minDuration; }
    TestRunOrder Config::runOrder() const              { return m_data.runOrder; }
    uint32_t Config::rngSeed() const                   { return m_data.rngSeed; }
    unsigned int Config::shardCount() const            { return m_data.shardCount; }
    unsigned int Config::shardIndex() const            { return m_data.shardIndex; }
    ColourMode Config::defaultColourMode() const       { return m_data.defaultColourMode; }
    bool Config::shouldDebugBreak() const              { return m_data.shouldDebugBreak; }
    int Config::abortAfter() const                     { return m_data.abortAfter; }
    bool Config::showInvisibles() const                { return m_data.showInvisibles; }
    Verbosity Config::verbosity() const                { return m_data.verbosity; }

    bool Config::skipBenchmarks() const                           { return m_data.skipBenchmarks; }
    bool Config::benchmarkNoAnalysis() const                      { return m_data.benchmarkNoAnalysis; }
    unsigned int Config::benchmarkSamples() const                 { return m_data.benchmarkSamples; }
    double Config::benchmarkConfidenceInterval() const            { return m_data.benchmarkConfidenceInterval; }
    unsigned int Config::benchmarkResamples() const               { return m_data.benchmarkResamples; }
    std::chrono::milliseconds Config::benchmarkWarmupTime() const { return std::chrono::milliseconds(m_data.benchmarkWarmupTime); }

    void Config::readBazelEnvVars() {
        // Register a JUnit reporter for Bazel. Bazel sets an environment
        // variable with the path to XML output. If this file is written to
        // during test, Bazel will not generate a default XML output.
        // This allows the XML output file to contain higher level of detail
        // than what is possible otherwise.
        const auto bazelOutputFile = Detail::getEnv( "XML_OUTPUT_FILE" );

        if ( bazelOutputFile ) {
            m_data.reporterSpecifications.push_back(
                { "junit", std::string( bazelOutputFile ), {}, {} } );
        }

        const auto bazelTestSpec = Detail::getEnv( "TESTBRIDGE_TEST_ONLY" );
        if ( bazelTestSpec ) {
            // Presumably the test spec from environment should overwrite
            // the one we got from CLI (if we got any)
            m_data.testsOrTags.clear();
            m_data.testsOrTags.push_back( bazelTestSpec );
        }

        const auto bazelShardOptions = readBazelShardingOptions();
        if ( bazelShardOptions ) {
            std::ofstream f( bazelShardOptions->shardFilePath,
                             std::ios_base::out | std::ios_base::trunc );
            if ( f.is_open() ) {
                f << "";
                m_data.shardIndex = bazelShardOptions->shardIndex;
                m_data.shardCount = bazelShardOptions->shardCount;
            }
        }

        const auto bazelExitGuardFile = Detail::getEnv( "TEST_PREMATURE_EXIT_FILE" );
        if (bazelExitGuardFile) {
            m_data.prematureExitGuardFilePath = bazelExitGuardFile;
        }

        const auto bazelRandomSeed = Detail::getEnv( "TEST_RANDOM_SEED" );
        if ( bazelRandomSeed ) {
            auto parsedSeed = parseUInt( bazelRandomSeed, 0 );
            if ( !parsedSeed ) {
                // Currently we handle issues with parsing other Bazel Env
                // options by warning and ignoring the issue. So we do the
                // same for random seed option.
                Catch::cerr()
                    << "Warning: could not parse 'TEST_RANDOM_SEED' ('"
                    << bazelRandomSeed << "') as proper seed.\n";
            } else {
                m_data.rngSeed = *parsedSeed;
            }
        }
    }

} // end namespace Catch
