#include <test/jtx/TestSuite.h>
#include <test/unit_test/FileDirGuard.h>

#include <xrpld/core/Config.h>
#include <xrpld/core/ConfigSections.h>

#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/server/Port.h>

#include <boost/filesystem.hpp>
#include <boost/format.hpp>

#include <fstream>
#include <regex>

namespace xrpl {
namespace detail {
std::string
configContents(std::string const& dbPath, std::string const& validatorsFile)
{
    static boost::format configContentsTemplate(R"xrpldConfig(
[server]
port_rpc
port_peer
port_wss_admin

[port_rpc]
port = 5005
ip = 127.0.0.1
admin = 127.0.0.1, ::1
protocol = https

[port_peer]
port = 51235
ip = 0.0.0.0
protocol = peer

[port_wss_admin]
port = 6006
ip = 127.0.0.1
admin = 127.0.0.1
protocol = wss

#[port_ws_public]
#port = 5005
#ip = 127.0.0.1
#protocol = wss

#-------------------------------------------------------------------------------

[node_size]
medium

# This is primary persistent datastore for xrpld.  This includes transaction
# metadata, account states, and ledger headers.  Helpful information can be
# found on https://xrpl.org/capacity-planning.html#node-db-type
# delete old ledgers while maintaining at least 2000. Do not require an
# external administrative command to initiate deletion.
[node_db]
type=memory
path=/Users/dummy/xrpld/config/db/rocksdb
open_files=2000
filter_bits=12
cache_mb=256
file_size_mb=8
file_size_mult=2

%1%

%2%

# This needs to be an absolute directory reference, not a relative one.
# Modify this value as required.
[debug_logfile]
/Users/dummy/xrpld/config/log/debug.log

[sntp_servers]
time.windows.com
time.apple.com
time.nist.gov
pool.ntp.org

# Where to find some other servers speaking the Ripple protocol.
#
[ips]
r.ripple.com 51235

# Turn down default logging to save disk space in the long run.
# Valid values here are trace, debug, info, warning, error, and fatal
[rpc_startup]
{ "command": "log_level", "severity": "warning" }

# Defaults to 1 ("yes") so that certificates will be validated. To allow the use
# of self-signed certificates for development or internal use, set to 0 ("no").
[ssl_verify]
0

[sqdb]
backend=sqlite
)xrpldConfig");

    std::string dbPathSection = dbPath.empty() ? "" : "[database_path]\n" + dbPath;
    std::string valFileSection =
        validatorsFile.empty() ? "" : "[validators_file]\n" + validatorsFile;
    return boost::str(configContentsTemplate % dbPathSection % valFileSection);
}

/**
   Write a xrpld config file and remove when done.
 */
class FileCfgGuard : public xrpl::detail::FileDirGuard
{
private:
    path dataDir_;

    bool rmDataDir_{false};

    Config config_;

public:
    FileCfgGuard(
        beast::unit_test::suite& test,
        path subDir,
        path const& dbPath,
        path const& configFile,
        path const& validatorsFile,
        bool useCounter = true,
        std::string confContents = "")
        : FileDirGuard(
              test,
              std::move(subDir),
              configFile,
              confContents.empty() ? configContents(dbPath.string(), validatorsFile.string())
                                   : confContents,
              useCounter)
        , dataDir_(dbPath)
    {
        if (dbPath.empty())
            dataDir_ = subdir() / path(Config::databaseDirName);

        rmDataDir_ = !exists(dataDir_);
        config_.setup(
            file_.string(),
            /* bQuiet */ true,
            /* bSilent */ false,
            /* bStandalone */ false);
    }

    Config const&
    config() const
    {
        return config_;
    }

    std::string
    configFile() const
    {
        return file().string();
    }

    bool
    dataDirExists() const
    {
        return boost::filesystem::is_directory(dataDir_);
    }

    bool
    configFileExists() const
    {
        return fileExists();
    }

    ~FileCfgGuard()
    {
        try
        {
            using namespace boost::filesystem;
            if (rmDataDir_)
                rmDir(dataDir_);
        }
        catch (std::exception& e)
        {
            // if we throw here, just let it die.
            test_.log << "Error in ~FileCfgGuard: " << e.what() << std::endl;
        };
    }
};

std::string
valFileContents()
{
    std::string configContents(R"xrpldConfig(
[validators]
n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7
n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj
n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C
n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS
n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA

[validator_keys]
nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5
nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8
nHUPDdcdb2Y5DZAJne4c2iabFuAP3F34xZUgYQT2NH7qfkdapgnz

[validator_list_sites]
recommended-xrpl-validators.com
more-xrpl-validators.net

[validator_list_keys]
03E74EE14CB525AFBB9F1B7D86CD58ECC4B91452294B42AB4E78F260BD905C091D
030775A669685BD6ABCEBD80385921C7851783D991A8055FD21D2F3966C96F1B56

[validator_list_threshold]
2
)xrpldConfig");
    return configContents;
}

/**
   Write a validators.txt file and remove when done.
 */
class ValidatorsTxtGuard : public detail::FileDirGuard
{
public:
    ValidatorsTxtGuard(
        beast::unit_test::suite& test,
        path subDir,
        path const& validatorsFileName,
        bool useCounter = true)
        : FileDirGuard(
              test,
              std::move(subDir),
              path(validatorsFileName.empty() ? Config::validatorsFileName : validatorsFileName),
              valFileContents(),
              useCounter)
    {
    }

    bool
    validatorsFileExists() const
    {
        return fileExists();
    }

    std::string
    validatorsFile() const
    {
        return absolute(file()).string();
    }

    ~ValidatorsTxtGuard()
    {
    }
};
}  // namespace detail

class Config_test final : public TestSuite
{
private:
    using path = boost::filesystem::path;

public:
    void
    testLegacy()
    {
        testcase("legacy");

        Config c;

        std::string toLoad(R"xrpldConfig(
[server]
port_rpc
port_peer
port_wss_admin

[ssl_verify]
0
)xrpldConfig");

        c.loadFromString(toLoad);

        BEAST_EXPECT(c.legacy("ssl_verify") == "0");
        expectException([&c] { c.legacy("server"); });  // not a single line

        // set a legacy value
        BEAST_EXPECT(c.legacy("not_in_file").empty());
        c.legacy("not_in_file", "new_value");
        BEAST_EXPECT(c.legacy("not_in_file") == "new_value");
    }
    void
    testConfigFile()
    {
        testcase("config_file");

        using namespace boost::filesystem;
        auto const cwd = current_path();

        // Test both config file names.
        char const* configFiles[] = {Config::configFileName, Config::configLegacyName};

        // Config file in current directory.
        for (auto const& configFile : configFiles)
        {
            // Use a temporary directory for testing.
            beast::temp_dir td;
            current_path(td.path());
            path const f = td.file(configFile);
            std::ofstream o(f.string());
            o << detail::configContents("", "");
            o.close();

            // Load the config file from the current directory and verify it.
            Config c;
            c.setup("", true, false, true);
            BEAST_EXPECT(c.section(SECTION_DEBUG_LOGFILE).values().size() == 1);
            BEAST_EXPECT(
                c.section(SECTION_DEBUG_LOGFILE).values()[0] ==
                "/Users/dummy/xrpld/config/log/debug.log");
        }

        // Config file in HOME or XDG_CONFIG_HOME directory.
#if BOOST_OS_LINUX || BOOST_OS_MACOS
        for (auto const& configFile : configFiles)
        {
            // Point the current working directory to a temporary directory, so
            // we don't pick up an actual config file from the repository root.
            beast::temp_dir td;
            current_path(td.path());

            // The XDG config directory is set: the config file must be in a
            // subdirectory named after the system.
            {
                beast::temp_dir tc;

                // Set the HOME and XDG_CONFIG_HOME environment variables. The
                // HOME variable is not used when XDG_CONFIG_HOME is set, but
                // must be set.
                char const* h = getenv("HOME");
                setenv("HOME", tc.path().c_str(), 1);
                char const* x = getenv("XDG_CONFIG_HOME");
                setenv("XDG_CONFIG_HOME", tc.path().c_str(), 1);

                // Create the config file in '${XDG_CONFIG_HOME}/[systemName]'.
                path p = tc.file(systemName());
                create_directory(p);
                p = tc.file(systemName() + "/" + configFile);
                std::ofstream o(p.string());
                o << detail::configContents("", "");
                o.close();

                // Load the config file from the config directory and verify it.
                Config c;
                c.setup("", true, false, true);
                BEAST_EXPECT(c.section(SECTION_DEBUG_LOGFILE).values().size() == 1);
                BEAST_EXPECT(
                    c.section(SECTION_DEBUG_LOGFILE).values()[0] ==
                    "/Users/dummy/xrpld/config/log/debug.log");

                // Restore the environment variables.
                (h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
                (x != nullptr) ? setenv("XDG_CONFIG_HOME", x, 1) : unsetenv("XDG_CONFIG_HOME");
            }

            // The XDG config directory is not set: the config file must be in a
            // subdirectory named .config followed by the system name.
            {
                beast::temp_dir tc;

                // Set only the HOME environment variable.
                char const* h = getenv("HOME");
                setenv("HOME", tc.path().c_str(), 1);
                char const* x = getenv("XDG_CONFIG_HOME");
                unsetenv("XDG_CONFIG_HOME");

                // Create the config file in '${HOME}/.config/[systemName]'.
                std::string s = ".config";
                path p = tc.file(s);
                create_directory(p);
                s += "/" + systemName();
                p = tc.file(s);
                create_directory(p);
                p = tc.file(s + "/" + configFile);
                std::ofstream o(p.string());
                o << detail::configContents("", "");
                o.close();

                // Load the config file from the config directory and verify it.
                Config c;
                c.setup("", true, false, true);
                BEAST_EXPECT(c.section(SECTION_DEBUG_LOGFILE).values().size() == 1);
                BEAST_EXPECT(
                    c.section(SECTION_DEBUG_LOGFILE).values()[0] ==
                    "/Users/dummy/xrpld/config/log/debug.log");

                // Restore the environment variables.
                (h != nullptr) ? setenv("HOME", h, 1) : unsetenv("HOME");
                if (x != nullptr)
                    setenv("XDG_CONFIG_HOME", x, 1);
            }
        }
#endif

        // Restore the current working directory.
        current_path(cwd);
    }
    void
    testDbPath()
    {
        testcase("database_path");

        using namespace boost::filesystem;
        {
            boost::format cc("[database_path]\n%1%\n");

            auto const cwd = current_path();
            path const dataDirRel("test_data_dir");
            path const dataDirAbs(cwd / dataDirRel);
            {
                // Dummy test - do we get back what we put in
                Config c;
                c.loadFromString(boost::str(cc % dataDirAbs.string()));
                BEAST_EXPECT(c.legacy("database_path") == dataDirAbs.string());
            }
            {
                // Rel paths should convert to abs paths
                Config c;
                c.loadFromString(boost::str(cc % dataDirRel.string()));
                BEAST_EXPECT(c.legacy("database_path") == dataDirAbs.string());
            }
            {
                // No db section.
                // N.B. Config::setup will give database_path a default,
                // load will not.
                Config c;
                c.loadFromString("");
                BEAST_EXPECT(c.legacy("database_path").empty());
            }
        }
        {
            // read from file absolute path
            auto const cwd = current_path();
            detail::DirGuard const g0(*this, "test_db");
            path const dataDirRel("test_data_dir");
            path const dataDirAbs(cwd / g0.subdir() / dataDirRel);
            detail::FileCfgGuard const g(
                *this, g0.subdir(), dataDirAbs, Config::configFileName, "", false);
            auto const& c(g.config());
            BEAST_EXPECT(g.dataDirExists());
            BEAST_EXPECT(g.configFileExists());
            BEAST_EXPECT(c.legacy("database_path") == dataDirAbs.string());
        }
        {
            // read from file relative path
            std::string const dbPath("my_db");
            detail::FileCfgGuard const g(*this, "test_db", dbPath, Config::configFileName, "");
            auto const& c(g.config());
            std::string const nativeDbPath = absolute(path(dbPath)).string();
            BEAST_EXPECT(g.dataDirExists());
            BEAST_EXPECT(g.configFileExists());
            BEAST_EXPECT(c.legacy("database_path") == nativeDbPath);
        }
        {
            // read from file no path
            detail::FileCfgGuard const g(*this, "test_db", "", Config::configFileName, "");
            auto const& c(g.config());
            std::string const nativeDbPath =
                absolute(g.subdir() / path(Config::databaseDirName)).string();
            BEAST_EXPECT(g.dataDirExists());
            BEAST_EXPECT(g.configFileExists());
            BEAST_EXPECT(c.legacy("database_path") == nativeDbPath);
        }
    }

    void
    testValidatorKeys()
    {
        testcase("validator keys");

        std::string const validationSeed = "spA4sh1qTvwq92X715tYyGQKmAKfa";

        auto const token =
            "eyJ2YWxpZGF0aW9uX3ByaXZhdGVfa2V5IjoiOWVkNDVmODY2MjQxY2MxOGEyNzQ3Yj"
            "U0Mzg3YzA2MjU5MDc5NzJmNGU3MTkwMjMxZmFhOTM3NDU3ZmE5ZGFmNiIsIm1hbmlm"
            "ZXN0IjoiSkFBQUFBRnhJZTFGdHdtaW12R3RIMmlDY01KcUM5Z1ZGS2lsR2Z3MS92Q3"
            "hIWFhMcGxjMkduTWhBa0UxYWdxWHhCd0R3RGJJRDZPTVNZdU0wRkRBbHBBZ05rOFNL"
            "Rm43TU8yZmRrY3dSUUloQU9uZ3U5c0FLcVhZb3VKK2wyVjBXK3NBT2tWQitaUlM2UF"
            "NobEpBZlVzWGZBaUJzVkpHZXNhYWRPSmMvYUFab2tTMXZ5bUdtVnJsSFBLV1gzWXl3"
            "dTZpbjhIQVNRS1B1Z0JENjdrTWFSRkd2bXBBVEhsR0tKZHZERmxXUFl5NUFxRGVkRn"
            "Y1VEphMncwaTIxZXEzTVl5d0xWSlpuRk9yN0Mwa3cyQWlUelNDakl6ZGl0UTg9In0"
            "=";

        {
            Config c;
            static boost::format configTemplate(R"xrpldConfig(
[validation_seed]
%1%

[validator_token]
%2%
)xrpldConfig");
            std::string error;
            auto const expectedError =
                "Cannot have both [validation_seed] "
                "and [validator_token] config sections";
            try
            {
                c.loadFromString(boost::str(configTemplate % validationSeed % token));
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
    }

    void
    testNetworkID()
    {
        testcase("network id");
        std::string error;
        Config c;
        try
        {
            c.loadFromString(R"xrpldConfig(
[network_id]
main
)xrpldConfig");
        }
        catch (std::runtime_error& e)
        {
            error = e.what();
        }

        BEAST_EXPECT(error.empty());
        BEAST_EXPECT(c.NETWORK_ID == 0);

        try
        {
            c.loadFromString(R"xrpldConfig(
)xrpldConfig");
        }
        catch (std::runtime_error& e)
        {
            error = e.what();
        }

        BEAST_EXPECT(error.empty());
        BEAST_EXPECT(c.NETWORK_ID == 0);

        try
        {
            c.loadFromString(R"xrpldConfig(
[network_id]
255
)xrpldConfig");
        }
        catch (std::runtime_error& e)
        {
            error = e.what();
        }

        BEAST_EXPECT(error.empty());
        BEAST_EXPECT(c.NETWORK_ID == 255);

        try
        {
            c.loadFromString(R"xrpldConfig(
[network_id]
10000
)xrpldConfig");
        }
        catch (std::runtime_error& e)
        {
            error = e.what();
        }

        BEAST_EXPECT(error.empty());
        BEAST_EXPECT(c.NETWORK_ID == 10000);
    }

    void
    testValidatorsFile()
    {
        testcase("validators_file");

        using namespace boost::filesystem;
        {
            // load should throw for missing specified validators file
            boost::format cc("[validators_file]\n%1%\n");
            std::string error;
            std::string const missingPath = "/no/way/this/path/exists";
            auto const expectedError =
                "The file specified in [validators_file] does not exist: " + missingPath;
            try
            {
                Config c;
                c.loadFromString(boost::str(cc % missingPath));
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
        {
            // load should throw for invalid [validators_file]
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
            path const invalidFile = current_path() / vtg.subdir();
            boost::format cc("[validators_file]\n%1%\n");
            std::string error;
            auto const expectedError =
                "Invalid file specified in [validators_file]: " + invalidFile.string();
            try
            {
                Config c;
                c.loadFromString(boost::str(cc % invalidFile.string()));
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
        {
            // load validators from config into single section
            Config c;
            std::string toLoad(R"xrpldConfig(
[validators]
n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7
n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj
n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C

[validator_keys]
nHUhG1PgAG8H8myUENypM35JgfqXAKNQvRVVAFDRzJrny5eZN8d5
nHBu9PTL9dn2GuZtdW4U2WzBwffyX9qsQCd9CNU4Z5YG3PQfViM8
)xrpldConfig");
            c.loadFromString(toLoad);
            BEAST_EXPECT(c.legacy("validators_file").empty());
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 5);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == std::nullopt);
        }
        {
            // load validator list sites and keys from config
            Config c;
            std::string toLoad(R"xrpldConfig(
[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov

[validator_list_keys]
021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566

[validator_list_threshold]
1
)xrpldConfig");
            c.loadFromString(toLoad);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(
                c.section(SECTION_VALIDATOR_LIST_SITES).values()[0] == "xrpl-validators.com");
            BEAST_EXPECT(
                c.section(SECTION_VALIDATOR_LIST_SITES).values()[1] ==
                "trust-these-validators.gov");
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 1);
            BEAST_EXPECT(
                c.section(SECTION_VALIDATOR_LIST_KEYS).values()[0] ==
                "021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801"
                "E566");
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values()[0] == "1");
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == std::size_t(1));
        }
        {
            // load validator list sites and keys from config
            Config c;
            std::string toLoad(R"xrpldConfig(
[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov

[validator_list_keys]
021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566

[validator_list_threshold]
0
)xrpldConfig");
            c.loadFromString(toLoad);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(
                c.section(SECTION_VALIDATOR_LIST_SITES).values()[0] == "xrpl-validators.com");
            BEAST_EXPECT(
                c.section(SECTION_VALIDATOR_LIST_SITES).values()[1] ==
                "trust-these-validators.gov");
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 1);
            BEAST_EXPECT(
                c.section(SECTION_VALIDATOR_LIST_KEYS).values()[0] ==
                "021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801"
                "E566");
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values()[0] == "0");
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == std::nullopt);
        }
        {
            // load should throw if [validator_list_threshold] is greater than
            // the number of [validator_list_keys]
            Config c;
            std::string toLoad(R"xrpldConfig(
[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov

[validator_list_keys]
021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566

[validator_list_threshold]
2
)xrpldConfig");
            std::string error;
            auto const expectedError =
                "Value in config section [validator_list_threshold] exceeds "
                "the number of configured list keys";
            try
            {
                c.loadFromString(toLoad);
                fail();
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
        {
            // load should throw if [validator_list_threshold] is malformed
            Config c;
            std::string toLoad(R"xrpldConfig(
[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov

[validator_list_keys]
021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566

[validator_list_threshold]
value = 2
)xrpldConfig");
            std::string error;
            auto const expectedError =
                "Config section [validator_list_threshold] should contain "
                "single value only";
            try
            {
                c.loadFromString(toLoad);
                fail();
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
        {
            // load should throw if [validator_list_threshold] is negative
            Config c;
            std::string toLoad(R"xrpldConfig(
[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov

[validator_list_keys]
021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566

[validator_list_threshold]
-1
)xrpldConfig");
            bool error = false;
            try
            {
                c.loadFromString(toLoad);
                fail();
            }
            catch (std::bad_cast& e)
            {
                error = true;
            }
            BEAST_EXPECT(error);
        }
        {
            // load should throw if [validator_list_sites] is configured but
            // [validator_list_keys] is not
            Config c;
            std::string toLoad(R"xrpldConfig(
[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov
)xrpldConfig");
            std::string error;
            auto const expectedError = "[validator_list_keys] config section is missing";
            try
            {
                c.loadFromString(toLoad);
                fail();
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
        {
            // load from specified [validators_file] absolute path
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
            BEAST_EXPECT(vtg.validatorsFileExists());
            Config c;
            boost::format cc("[validators_file]\n%1%\n");
            c.loadFromString(boost::str(cc % vtg.validatorsFile()));
            BEAST_EXPECT(c.legacy("validators_file") == vtg.validatorsFile());
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 8);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2);
        }
        {
            // load from specified [validators_file] file name
            // in config directory
            std::string const valFileName = "validators.txt";
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", valFileName);
            detail::FileCfgGuard const rcg(
                *this, vtg.subdir(), "", Config::configFileName, valFileName, false);
            BEAST_EXPECT(vtg.validatorsFileExists());
            BEAST_EXPECT(rcg.configFileExists());
            auto const& c(rcg.config());
            BEAST_EXPECT(c.legacy("validators_file") == valFileName);
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 8);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2);
        }
        {
            // load from specified [validators_file] relative path
            // to config directory
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.txt");
            auto const valFilePath = ".." / vtg.subdir() / "validators.txt";
            detail::FileCfgGuard const rcg(
                *this, vtg.subdir(), "", Config::configFileName, valFilePath, false);
            BEAST_EXPECT(vtg.validatorsFileExists());
            BEAST_EXPECT(rcg.configFileExists());
            auto const& c(rcg.config());
            BEAST_EXPECT(c.legacy("validators_file") == valFilePath);
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 8);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2);
        }
        {
            // load from validators file in default location
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.txt");
            detail::FileCfgGuard const rcg(
                *this, vtg.subdir(), "", Config::configFileName, "", false);
            BEAST_EXPECT(vtg.validatorsFileExists());
            BEAST_EXPECT(rcg.configFileExists());
            auto const& c(rcg.config());
            BEAST_EXPECT(c.legacy("validators_file").empty());
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 8);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2);
        }
        {
            // load from specified [validators_file] instead
            // of default location
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
            BEAST_EXPECT(vtg.validatorsFileExists());
            detail::ValidatorsTxtGuard const vtgDefault(
                *this, vtg.subdir(), "validators.txt", false);
            BEAST_EXPECT(vtgDefault.validatorsFileExists());
            detail::FileCfgGuard const rcg(
                *this, vtg.subdir(), "", Config::configFileName, vtg.validatorsFile(), false);
            BEAST_EXPECT(rcg.configFileExists());
            auto const& c(rcg.config());
            BEAST_EXPECT(c.legacy("validators_file") == vtg.validatorsFile());
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 8);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 2);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2);
        }

        {
            // load validators from both config and validators file
            boost::format cc(R"xrpldConfig(
[validators_file]
%1%

[validators]
n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7
n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj
n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C
n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS
n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA

[validator_keys]
nHB1X37qrniVugfQcuBTAjswphC1drx7QjFFojJPZwKHHnt8kU7v
nHUkAWDR4cB8AgPg7VXMX6et8xRTQb2KJfgv1aBEXozwrawRKgMB

[validator_list_sites]
xrpl-validators.com
trust-these-validators.gov

[validator_list_keys]
021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566
)xrpldConfig");
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
            BEAST_EXPECT(vtg.validatorsFileExists());
            Config c;
            c.loadFromString(boost::str(cc % vtg.validatorsFile()));
            BEAST_EXPECT(c.legacy("validators_file") == vtg.validatorsFile());
            BEAST_EXPECT(c.section(SECTION_VALIDATORS).values().size() == 15);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_SITES).values().size() == 4);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_KEYS).values().size() == 3);
            BEAST_EXPECT(c.section(SECTION_VALIDATOR_LIST_THRESHOLD).values().size() == 1);
            BEAST_EXPECT(c.VALIDATOR_LIST_THRESHOLD == 2);
        }
        {
            // load should throw if [validator_list_threshold] is present both
            // in xrpld.cfg and validators file
            boost::format cc(R"xrpldConfig(
[validators_file]
%1%

[validator_list_threshold]
1
)xrpldConfig");
            std::string error;
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
            BEAST_EXPECT(vtg.validatorsFileExists());
            auto const expectedError =
                "Config section [validator_list_threshold] should contain "
                "single value only";
            try
            {
                Config c;
                c.loadFromString(boost::str(cc % vtg.validatorsFile()));
                fail();
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
        {
            // load should throw if [validators], [validator_keys] and
            // [validator_list_keys] are missing from xrpld.cfg and
            // validators file
            Config c;
            boost::format cc("[validators_file]\n%1%\n");
            std::string error;
            detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
            BEAST_EXPECT(vtg.validatorsFileExists());
            auto const expectedError =
                "The file specified in [validators_file] does not contain a "
                "[validators], [validator_keys] or [validator_list_keys] "
                "section: " +
                vtg.validatorsFile();
            std::ofstream o(vtg.validatorsFile());
            try
            {
                Config c2;
                c2.loadFromString(boost::str(cc % vtg.validatorsFile()));
            }
            catch (std::runtime_error& e)
            {
                error = e.what();
            }
            BEAST_EXPECT(error == expectedError);
        }
    }

    void
    testSetup(bool explicitPath)
    {
        detail::FileCfgGuard const cfg(
            *this, "testSetup", explicitPath ? "test_db" : "", Config::configFileName, "");
        /* FileCfgGuard has a Config object that gets loaded on
            construction, but Config::setup is not reentrant, so we
            need a fresh config for every test case, so ignore it.
        */
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ false,
                /* bSilent */ false,
                /* bStandalone */ false);
            BEAST_EXPECT(!config.quiet());
            BEAST_EXPECT(!config.silent());
            BEAST_EXPECT(!config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 256);
            BEAST_EXPECT(!config.legacy("database_path").empty());
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ true,
                /* bSilent */ false,
                /* bStandalone */ false);
            BEAST_EXPECT(config.quiet());
            BEAST_EXPECT(!config.silent());
            BEAST_EXPECT(!config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 256);
            BEAST_EXPECT(!config.legacy("database_path").empty());
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ false,
                /* bSilent */ true,
                /* bStandalone */ false);
            BEAST_EXPECT(config.quiet());
            BEAST_EXPECT(config.silent());
            BEAST_EXPECT(!config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 256);
            BEAST_EXPECT(!config.legacy("database_path").empty());
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ true,
                /* bSilent */ true,
                /* bStandalone */ false);
            BEAST_EXPECT(config.quiet());
            BEAST_EXPECT(config.silent());
            BEAST_EXPECT(!config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 256);
            BEAST_EXPECT(!config.legacy("database_path").empty());
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ false,
                /* bSilent */ false,
                /* bStandalone */ true);
            BEAST_EXPECT(!config.quiet());
            BEAST_EXPECT(!config.silent());
            BEAST_EXPECT(config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 0);
            BEAST_EXPECT(config.legacy("database_path").empty() == !explicitPath);
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ true,
                /* bSilent */ false,
                /* bStandalone */ true);
            BEAST_EXPECT(config.quiet());
            BEAST_EXPECT(!config.silent());
            BEAST_EXPECT(config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 0);
            BEAST_EXPECT(config.legacy("database_path").empty() == !explicitPath);
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ false,
                /* bSilent */ true,
                /* bStandalone */ true);
            BEAST_EXPECT(config.quiet());
            BEAST_EXPECT(config.silent());
            BEAST_EXPECT(config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 0);
            BEAST_EXPECT(config.legacy("database_path").empty() == !explicitPath);
        }
        {
            Config config;
            config.setup(
                cfg.configFile(),
                /*bQuiet*/ true,
                /* bSilent */ true,
                /* bStandalone */ true);
            BEAST_EXPECT(config.quiet());
            BEAST_EXPECT(config.silent());
            BEAST_EXPECT(config.standalone());
            BEAST_EXPECT(config.LEDGER_HISTORY == 0);
            BEAST_EXPECT(config.legacy("database_path").empty() == !explicitPath);
        }
    }

    void
    testPort()
    {
        detail::FileCfgGuard const cfg(*this, "testPort", "", Config::configFileName, "");
        auto const& conf = cfg.config();
        if (!BEAST_EXPECT(conf.exists("port_rpc")))
            return;
        if (!BEAST_EXPECT(conf.exists("port_wss_admin")))
            return;
        ParsedPort rpc;
        if (!unexcept([&]() { parse_Port(rpc, conf["port_rpc"], log); }))
            return;
        BEAST_EXPECT(rpc.admin_nets_v4.size() + rpc.admin_nets_v6.size() == 2);
        ParsedPort wss;
        if (!unexcept([&]() { parse_Port(wss, conf["port_wss_admin"], log); }))
            return;
        BEAST_EXPECT(wss.admin_nets_v4.size() + wss.admin_nets_v6.size() == 1);
    }

    void
    testZeroPort()
    {
        auto const contents = std::regex_replace(
            detail::configContents("", ""), std::regex("port\\s*=\\s*\\d+"), "port = 0");

        try
        {
            detail::FileCfgGuard const cfg(
                *this, "testPort", "", Config::configFileName, "", true, contents);
            BEAST_EXPECT(false);
        }
        catch (std::exception const& ex)
        {
            BEAST_EXPECT(
                std::string_view(ex.what()).starts_with("Invalid value '0' for key 'port'"));
        }
    }

    void
    testWhitespace()
    {
        Config cfg;
        /* NOTE: this string includes some explicit
         * space chars in order to verify proper trimming */
        std::string toLoad(
            R"(
[port_rpc])"
            "\x20"
            R"(
# comment
    # indented comment
)"
            "\x20\x20"
            R"(
[ips])"
            "\x20"
            R"(
r.ripple.com 51235

  [ips_fixed])"
            "\x20\x20"
            R"(
    # COMMENT
    s1.ripple.com 51235
    s2.ripple.com 51235

)");
        cfg.loadFromString(toLoad);
        BEAST_EXPECT(
            cfg.exists("port_rpc") && cfg.section("port_rpc").lines().empty() &&
            cfg.section("port_rpc").values().empty());
        BEAST_EXPECT(
            cfg.exists(SECTION_IPS) && cfg.section(SECTION_IPS).lines().size() == 1 &&
            cfg.section(SECTION_IPS).values().size() == 1);
        BEAST_EXPECT(
            cfg.exists(SECTION_IPS_FIXED) && cfg.section(SECTION_IPS_FIXED).lines().size() == 2 &&
            cfg.section(SECTION_IPS_FIXED).values().size() == 2);
    }

    void
    testColons()
    {
        Config cfg;
        /* NOTE: this string includes some explicit
         * space chars in order to verify proper trimming */
        std::string toLoad(
            R"(
[port_rpc])"
            "\x20"
            R"(
# comment
    # indented comment
)"
            "\x20\x20"
            R"(
[ips])"
            "\x20"
            R"(
r.ripple.com:51235

  [ips_fixed])"
            "\x20\x20"
            R"(
    # COMMENT
    s1.ripple.com:51235
    s2.ripple.com 51235
    anotherserversansport
    anotherserverwithport:12
    1.1.1.1:1
    1.1.1.1 1
    12.34.12.123:12345
    12.34.12.123 12345
    ::
    2001:db8::
    ::1
    ::1:12345
    [::1]:12345
    2001:db8:3333:4444:5555:6666:7777:8888:12345
    [2001:db8:3333:4444:5555:6666:7777:8888]:1


)");
        cfg.loadFromString(toLoad);
        BEAST_EXPECT(
            cfg.exists("port_rpc") && cfg.section("port_rpc").lines().empty() &&
            cfg.section("port_rpc").values().empty());
        BEAST_EXPECT(
            cfg.exists(SECTION_IPS) && cfg.section(SECTION_IPS).lines().size() == 1 &&
            cfg.section(SECTION_IPS).values().size() == 1);
        BEAST_EXPECT(
            cfg.exists(SECTION_IPS_FIXED) && cfg.section(SECTION_IPS_FIXED).lines().size() == 15 &&
            cfg.section(SECTION_IPS_FIXED).values().size() == 15);
        BEAST_EXPECT(cfg.IPS[0] == "r.ripple.com 51235");

        BEAST_EXPECT(cfg.IPS_FIXED[0] == "s1.ripple.com 51235");
        BEAST_EXPECT(cfg.IPS_FIXED[1] == "s2.ripple.com 51235");
        BEAST_EXPECT(cfg.IPS_FIXED[2] == "anotherserversansport");
        BEAST_EXPECT(cfg.IPS_FIXED[3] == "anotherserverwithport 12");
        BEAST_EXPECT(cfg.IPS_FIXED[4] == "1.1.1.1 1");
        BEAST_EXPECT(cfg.IPS_FIXED[5] == "1.1.1.1 1");
        BEAST_EXPECT(cfg.IPS_FIXED[6] == "12.34.12.123 12345");
        BEAST_EXPECT(cfg.IPS_FIXED[7] == "12.34.12.123 12345");

        // all ipv6 should be ignored by colon replacer, howsoever formatted
        BEAST_EXPECT(cfg.IPS_FIXED[8] == "::");
        BEAST_EXPECT(cfg.IPS_FIXED[9] == "2001:db8::");
        BEAST_EXPECT(cfg.IPS_FIXED[10] == "::1");
        BEAST_EXPECT(cfg.IPS_FIXED[11] == "::1:12345");
        BEAST_EXPECT(cfg.IPS_FIXED[12] == "[::1]:12345");
        BEAST_EXPECT(cfg.IPS_FIXED[13] == "2001:db8:3333:4444:5555:6666:7777:8888:12345");
        BEAST_EXPECT(cfg.IPS_FIXED[14] == "[2001:db8:3333:4444:5555:6666:7777:8888]:1");
    }

    void
    testComments()
    {
        struct TestCommentData
        {
            std::string_view line;
            std::string_view field;
            std::string_view expect;
            bool had_comment;
        };

        std::array<TestCommentData, 13> tests = {
            {{"password = aaaa\\#bbbb", "password", "aaaa#bbbb", false},
             {"password = aaaa#bbbb", "password", "aaaa", true},
             {"password = aaaa #bbbb", "password", "aaaa", true},
             // since the value is all comment, this doesn't parse as k=v :
             {"password = #aaaa #bbbb", "", "password =", true},
             {"password = aaaa\\# #bbbb", "password", "aaaa#", true},
             {"password = aaaa\\##bbbb", "password", "aaaa#", true},
             {"aaaa#bbbb", "", "aaaa", true},
             {"aaaa\\#bbbb", "", "aaaa#bbbb", false},
             {"aaaa\\##bbbb", "", "aaaa#", true},
             {"aaaa #bbbb", "", "aaaa", true},
             {"1 #comment", "", "1", true},
             {"#whole thing is comment", "", "", false},
             {"  #whole comment with space", "", "", false}}};

        for (auto const& t : tests)
        {
            Section s;
            s.append(std::string(t.line));
            BEAST_EXPECT(s.had_trailing_comments() == t.had_comment);
            if (t.field.empty())
            {
                BEAST_EXPECTS(s.legacy() == t.expect, s.legacy());
            }
            else
            {
                std::string field;
                BEAST_EXPECTS(set(field, std::string(t.field), s), t.line);
                BEAST_EXPECTS(field == t.expect, t.line);
            }
        }

        {
            Section s;
            s.append("online_delete = 3000");
            std::uint32_t od = 0;
            BEAST_EXPECT(set(od, "online_delete", s));
            // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
            BEAST_EXPECTS(od == 3000, *(s.get<std::string>("online_delete")));
        }

        {
            Section s;
            s.append("online_delete = 2000 #my comment on this");
            std::uint32_t od = 0;
            BEAST_EXPECT(set(od, "online_delete", s));
            // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
            BEAST_EXPECTS(od == 2000, *(s.get<std::string>("online_delete")));
        }
    }

    void
    testGetters()
    {
        using namespace std::string_literals;
        Section s{"MySection"};
        s.append("a_string = mystring");
        s.append("positive_int = 2");
        s.append("negative_int = -3");
        s.append("bool_ish = 1");

        {
            auto val_1 = "value 1"s;
            BEAST_EXPECT(set(val_1, "a_string", s));
            BEAST_EXPECT(val_1 == "mystring");

            auto val_2 = "value 2"s;
            BEAST_EXPECT(!set(val_2, "not_a_key", s));
            BEAST_EXPECT(val_2 == "value 2");
            BEAST_EXPECT(!set(val_2, "default"s, "not_a_key", s));
            BEAST_EXPECT(val_2 == "default");

            auto val_3 = get<std::string>(s, "a_string");
            BEAST_EXPECT(val_3 == "mystring");
            auto val_4 = get<std::string>(s, "not_a_key");
            BEAST_EXPECT(val_4.empty());
            auto val_5 = get<std::string>(s, "not_a_key", "default");
            BEAST_EXPECT(val_5 == "default");

            auto val_6 = "value 6"s;
            BEAST_EXPECT(get_if_exists(s, "a_string", val_6));
            BEAST_EXPECT(val_6 == "mystring");

            auto val_7 = "value 7"s;
            BEAST_EXPECT(!get_if_exists(s, "not_a_key", val_7));
            BEAST_EXPECT(val_7 == "value 7");
        }

        {
            int val_1 = 1;
            BEAST_EXPECT(set(val_1, "positive_int", s));
            BEAST_EXPECT(val_1 == 2);

            int val_2 = 2;
            BEAST_EXPECT(set(val_2, "negative_int", s));
            BEAST_EXPECT(val_2 == -3);

            int val_3 = 3;
            BEAST_EXPECT(!set(val_3, "a_string", s));
            BEAST_EXPECT(val_3 == 3);

            auto val_4 = get<int>(s, "positive_int");
            BEAST_EXPECT(val_4 == 2);
            auto val_5 = get<int>(s, "not_a_key");
            BEAST_EXPECT(val_5 == 0);
            auto val_6 = get<int>(s, "not_a_key", 5);
            BEAST_EXPECT(val_6 == 5);
            auto val_7 = get<int>(s, "a_string", 6);
            BEAST_EXPECT(val_7 == 6);

            int val_8 = 8;
            BEAST_EXPECT(get_if_exists(s, "positive_int", val_8));
            BEAST_EXPECT(val_8 == 2);

            auto val_9 = 9;
            BEAST_EXPECT(!get_if_exists(s, "not_a_key", val_9));
            BEAST_EXPECT(val_9 == 9);

            auto val_10 = 10;
            BEAST_EXPECT(!get_if_exists(s, "a_string", val_10));
            BEAST_EXPECT(val_10 == 10);

            BEAST_EXPECT(s.get<int>("not_a_key") == std::nullopt);
            try
            {
                s.get<int>("a_string");
                fail();
            }
            catch (boost::bad_lexical_cast&)
            {
                pass();
            }
        }

        {
            bool flag_1 = false;
            BEAST_EXPECT(get_if_exists(s, "bool_ish", flag_1));
            BEAST_EXPECT(flag_1 == true);

            bool flag_2 = false;
            BEAST_EXPECT(!get_if_exists(s, "not_a_key", flag_2));
            BEAST_EXPECT(flag_2 == false);
        }
    }

    void
    testAmendment()
    {
        testcase("amendment");
        struct ConfigUnit
        {
            std::string unit;
            std::uint32_t numSeconds;
            std::uint32_t configVal;
            bool shouldPass;
        };

        std::vector<ConfigUnit> units = {
            {"seconds", 1, 15 * 60, false},
            {"minutes", 60, 14, false},
            {"minutes", 60, 15, true},
            {"hours", 3600, 10, true},
            {"days", 86400, 10, true},
            {"weeks", 604800, 2, true},
            {"months", 2592000, 1, false},
            {"years", 31536000, 1, false}};

        std::string space;
        for (auto& [unit, sec, val, shouldPass] : units)
        {
            Config c;
            std::string toLoad(R"xrpldConfig(
[amendment_majority_time]
)xrpldConfig");
            toLoad += std::to_string(val) + space + unit;
            space = space.empty() ? " " : "";

            try
            {
                c.loadFromString(toLoad);
                if (shouldPass)
                {
                    BEAST_EXPECT(c.AMENDMENT_MAJORITY_TIME.count() == val * sec);
                }
                else
                {
                    fail();
                }
            }
            catch (std::runtime_error&)
            {
                if (!shouldPass)
                {
                    pass();
                }
                else
                {
                    fail();
                }
            }
        }
    }

    void
    testOverlay()
    {
        testcase("overlay: unknown time");

        auto testUnknown = [](std::string value) -> std::optional<std::chrono::seconds> {
            try
            {
                Config c;
                c.loadFromString("[overlay]\nmax_unknown_time=" + value);
                return c.MAX_UNKNOWN_TIME;
            }
            catch (std::runtime_error&)
            {
                return {};
            }
        };

        // Failures
        BEAST_EXPECT(!testUnknown("none"));
        BEAST_EXPECT(!testUnknown("0.5"));
        BEAST_EXPECT(!testUnknown("180 seconds"));
        BEAST_EXPECT(!testUnknown("9 minutes"));

        // Below lower bound
        BEAST_EXPECT(!testUnknown("299"));

        // In bounds
        BEAST_EXPECT(testUnknown("300") == std::chrono::seconds{300});
        BEAST_EXPECT(testUnknown("301") == std::chrono::seconds{301});
        BEAST_EXPECT(testUnknown("1799") == std::chrono::seconds{1799});
        BEAST_EXPECT(testUnknown("1800") == std::chrono::seconds{1800});

        // Above upper bound
        BEAST_EXPECT(!testUnknown("1801"));

        testcase("overlay: diverged time");

        // In bounds:
        auto testDiverged = [](std::string value) -> std::optional<std::chrono::seconds> {
            try
            {
                Config c;
                c.loadFromString("[overlay]\nmax_diverged_time=" + value);
                return c.MAX_DIVERGED_TIME;
            }
            catch (std::runtime_error&)
            {
                return {};
            }
        };

        // Failures
        BEAST_EXPECT(!testDiverged("none"));
        BEAST_EXPECT(!testDiverged("0.5"));
        BEAST_EXPECT(!testDiverged("180 seconds"));
        BEAST_EXPECT(!testDiverged("9 minutes"));

        // Below lower bound
        BEAST_EXPECT(!testDiverged("0"));
        BEAST_EXPECT(!testDiverged("59"));

        // In bounds
        BEAST_EXPECT(testDiverged("60") == std::chrono::seconds{60});
        BEAST_EXPECT(testDiverged("61") == std::chrono::seconds{61});
        BEAST_EXPECT(testDiverged("899") == std::chrono::seconds{899});
        BEAST_EXPECT(testDiverged("900") == std::chrono::seconds{900});

        // Above upper bound
        BEAST_EXPECT(!testDiverged("901"));
    }

    void
    run() override
    {
        testLegacy();
        testConfigFile();
        testDbPath();
        testValidatorKeys();
        testValidatorsFile();
        testSetup(false);
        testSetup(true);
        testPort();
        testZeroPort();
        testWhitespace();
        testColons();
        testComments();
        testGetters();
        testAmendment();
        testOverlay();
        testNetworkID();
    }
};

BEAST_DEFINE_TESTSUITE(Config, core, xrpl);

}  // namespace xrpl
