// Copyright (C) 2013-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>

#include <config/http_command_config.h>
#include <config/unix_command_config.h>
#include <d2/parser_context.h>
#include <d2/tests/parser_unittest.h>
#include <d2/tests/test_callout_libraries.h>
#include <d2srv/d2_cfg_mgr.h>
#include <d2srv/d2_config.h>
#include <d2srv/d2_simple_parser.h>
#include <dhcpsrv/testutils/config_result_check.h>
#include <hooks/hooks_parser.h>
#include <process/testutils/d_test_stubs.h>
#include <test_data_files_config.h>
#include <util/encode/encode.h>
#include <util/filesystem.h>

#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>

using namespace std;
using namespace isc;
using namespace isc::d2;
using namespace isc::hooks;
using namespace isc::process;
using namespace isc::config;
using namespace isc::util;

namespace {

/// @brief Function to create full path to test data file
///
/// The full path is dependent upon the value of D2_TEST_DATA_DIR which
/// whose value is generated from test_data_files_config.h.in
///
/// @param name file name to which the path should be prepended
std::string testDataFile(const std::string& name) {
    return (std::string(D2_TEST_DATA_DIR) + "/" + name);
}

/// @brief Test fixture class for testing D2CfgMgr class.
/// It maintains an member instance of D2CfgMgr and provides methods for
/// converting JSON strings to configuration element sets, checking parse
/// results, and accessing the configuration context.
class D2CfgMgrTest : public ConfigParseTest {
public:

    /// @brief Constructor
    D2CfgMgrTest():cfg_mgr_(new D2CfgMgr()), d2_params_() {
        resetHooksPath();
        resetSocketPath();
    }

    /// @brief Destructor
    ~D2CfgMgrTest() {
        resetHooksPath();
        resetSocketPath();
    }

    /// @brief Sets the Hooks path from which hooks can be loaded.
    /// @param explicit_path path to use as the hooks path.
    void setHooksTestPath(const std::string explicit_path = "") {
        HooksLibrariesParser::getHooksPath(true,
                                           (!explicit_path.empty() ?
                                            explicit_path : D2_HOOKS_TEST_PATH));
    }

    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
    void resetHooksPath() {
        HooksLibrariesParser::getHooksPath(true);
    }

    /// @brief Sets the path in which the socket can be created.
    /// @param explicit_path path to use as the socket path.
    void setSocketTestPath(const std::string explicit_path = "") {
        UnixCommandConfig::getSocketPath(true, (!explicit_path.empty() ?
                                         explicit_path : TEST_DATA_BUILDDIR));

        auto path = UnixCommandConfig::getSocketPath();
        UnixCommandConfig::setSocketPathPerms(file::getPermissions(path));
    }

    /// @brief Resets the socket path to the default.
    void resetSocketPath() {
        UnixCommandConfig::getSocketPath(true);
        UnixCommandConfig::setSocketPathPerms();
    }

    /// @brief Configuration manager instance.
    D2CfgMgrPtr cfg_mgr_;

    /// @brief Build JSON configuration string for a D2Params element
    ///
    /// Constructs a JSON string for "params" element using replaceable
    /// parameters.
    ///
    /// @param ip_address string to insert as ip_address value
    /// @param port integer to insert as port value
    /// @param dns_server_timeout integer to insert as dns_server_timeout value
    /// @param ncr_protocol string to insert as ncr_protocol value
    /// @param ncr_format string to insert as ncr_format value
    ///
    /// @return std::string containing the JSON configuration text
    std::string makeParamsConfigString(const std::string& ip_address,
                                       const int port,
                                       const int dns_server_timeout,
                                       const std::string& ncr_protocol,
                                       const std::string& ncr_format) {
        std::ostringstream config;
        config <<
            "{"
            " \"ip-address\": \"" << ip_address << "\" , "
            " \"port\": " << port << " , "
            " \"dns-server-timeout\": " << dns_server_timeout << " , "
            " \"ncr-protocol\": \"" << ncr_protocol << "\" , "
            " \"ncr-format\": \"" << ncr_format << "\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {} "
            "}";

        return (config.str());
    }

    /// @brief Enumeration to select between expected configuration outcomes
    enum RunConfigMode {
        NO_ERROR,
        SYNTAX_ERROR,
        LOGIC_ERROR
    };

    /// @brief Parses a configuration string and tests against a given outcome
    ///
    /// Convenience method which accepts JSON text and an expected pass or fail
    /// outcome.  It uses the D2ParserContext to parse the text under the
    /// PARSE_SUB_DHCPDDNS context, then adds the D2 defaults to the resultant
    /// element tree. Assuming that's successful the element tree is passed
    /// to D2CfgMgr::parseConfig() method.
    ///
    /// @param config_str the JSON configuration text to parse
    /// @param error_type  indicates the type error expected, NONE, SYNTAX,
    /// or LOGIC. SYNTAX errors are emitted by JSON parser, logic errors
    /// are emitted by element parser(s).
    /// @param exp_error exact text of the error message expected
    /// defaults to SHOULD_PASS.
    ///
    /// @return AssertionSuccess if test passes, AssertionFailure otherwise
    ::testing::AssertionResult runConfigOrFail(const std::string& json,
                                               const RunConfigMode mode,
                                               const std::string& exp_error) {

        try {
            // Invoke the JSON parser, casting the returned element tree
            // into mutable form.
            D2ParserContext parser_context;
            data::ElementPtr elem =
                boost::const_pointer_cast<Element>
                (parser_context.parseString(json, D2ParserContext::
                                                  PARSER_SUB_DHCPDDNS));

            // If parsing succeeded when we expected a syntax error, then fail.
            if (mode == SYNTAX_ERROR) {
               return ::testing::AssertionFailure()
                             << "Unexpected  JSON parsing success"
                             << "\njson: [" << json << " ]";
            }

            // JSON parsed ok, so add the defaults to the element tree it produced.
            D2SimpleParser::setAllDefaults(elem);
            config_set_ = elem;
        } catch (const std::exception& ex) {
            // JSON Parsing failed
            if (exp_error.empty()) {
                // We did not expect an error, so fail.
                return ::testing::AssertionFailure()
                          << "Unexpected syntax error:" << ex.what()
                          << "\njson: [" << json << " ]";
            }

            if (ex.what() != exp_error) {
                // Expected an error not the one we got, so fail
                return ::testing::AssertionFailure()
                          << "Wrong syntax error detected, expected: "
                          << exp_error << ", got: " << ex.what()
                          << "\njson: [" << json << " ]";
            }

            // We go the syntax error we expected, so return success
            return ::testing::AssertionSuccess();
        }

        // The JSON parsed ok and we've added the defaults, pass the config
        // into the Element parser and check for the expected outcome.
        data::ConstElementPtr answer;
        answer = cfg_mgr_->simpleParseConfig(config_set_, false);

        // Extract the result and error text from the answer.
        int rcode = 0;
        isc::data::ConstElementPtr comment;
        comment = isc::config::parseAnswer(rcode, answer);

        if (rcode != 0) {
            // Element Parsing failed.
            if (exp_error.empty()) {
                // We didn't expect it to, fail the test.
                return ::testing::AssertionFailure()
                              << "Unexpected logic error: " << *comment
                              << "\njson: [" << json << " ]";
            }

            if (comment->stringValue() != exp_error) {
                // We 't expect a different error, fail the test.
                return ::testing::AssertionFailure()
                              << "Wrong logic error detected, expected: "
                              << exp_error << ", got: " << *comment
                              << "\njson: [" << json << " ]";
            }
        } else {
            // Element parsing succeeded.
            if (!exp_error.empty()) {
                // It was supposed to fail, so fail the test.
                return ::testing::AssertionFailure()
                              << "Unexpected logic success, expected error:"
                              << exp_error
                              << "\njson: [" << json << " ]";
            }
        }

        // Verify that the D2 context can be retrieved and is not null.
        D2CfgContextPtr context;
        context = cfg_mgr_->getD2CfgContext();
        if (!context) {
            return ::testing::AssertionFailure() << "D2CfgContext is null";
        }

        // Verify that the global scalar container has been created.
        d2_params_ = context->getD2Params();
        if (!d2_params_) {
            return ::testing::AssertionFailure() << "D2Params is null";
        }

        return ::testing::AssertionSuccess();
    }

    /// @brief Replaces %LIBRARY% with specified library name
    ///
    /// @param config input config text (should contain "%LIBRARY%" string)
    /// @param lib_name %LIBRARY% will be replaced with that name
    /// @return configuration text with library name replaced
    std::string pathReplacer(const char* config, const char* lib_name) {
        string txt(config);
        txt.replace(txt.find("%LIBRARY%"), strlen("%LIBRARY%"), string(lib_name));
        return (txt);
    }

    /// @brief Pointer the D2Params most recently parsed.
    D2ParamsPtr d2_params_;
};

/// @brief Convenience macros for invoking runOrConfig()
#define RUN_CONFIG_OK(a) (static_cast<void>(runConfigOrFail(a, NO_ERROR, "")))
#define SYNTAX_ERROR(a,b) ASSERT_TRUE(runConfigOrFail(a,SYNTAX_ERROR,b))
#define LOGIC_ERROR(a,b) ASSERT_TRUE(runConfigOrFail(a,LOGIC_ERROR,b))

/// @brief Tests a basic valid configuration for D2Param.
TEST_F(D2CfgMgrTest, validParamsEntry) {
    // Verify that ip_address can be valid v4 address.
    std::string config = makeParamsConfigString ("192.0.0.1", 777, 333,
                                           "UDP", "JSON");
    RUN_CONFIG_OK(config);

    EXPECT_EQ(isc::asiolink::IOAddress("192.0.0.1"),
              d2_params_->getIpAddress());
    EXPECT_EQ(777, d2_params_->getPort());
    EXPECT_EQ(333, d2_params_->getDnsServerTimeout());
    EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_params_->getNcrProtocol());
    EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_params_->getNcrFormat());

    // Verify that ip_address can be valid v6 address.
    config = makeParamsConfigString ("3001::5", 777, 333, "UDP", "JSON");
    RUN_CONFIG_OK(config);

    // Verify that the global scalars have the proper values.
    EXPECT_EQ(isc::asiolink::IOAddress("3001::5"),
              d2_params_->getIpAddress());

    // Verify the configuration summary.
    EXPECT_EQ("listening on 3001::5, port 777, using UDP",
              d2_params_->getConfigSummary());
}

/// @brief Tests default values for D2Params.
/// It verifies that D2Params is populated with default value for optional
/// parameter if not supplied in the configuration.
/// Currently they are all optional.
TEST_F(D2CfgMgrTest, defaultValues) {

    ElementPtr defaults = isc::d2::test::parseJSON("{ }");
    ASSERT_NO_THROW(D2SimpleParser::setAllDefaults(defaults));

    // Check that omitting ip_address gets you its default
    std::string config =
            "{"
            " \"port\": 777 , "
            " \"dns-server-timeout\": 333 , "
            " \"ncr-protocol\": \"UDP\" , "
            " \"ncr-format\": \"JSON\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {} "
            "}";

    RUN_CONFIG_OK(config);
    ConstElementPtr deflt;
    ASSERT_NO_THROW(deflt = defaults->get("ip-address"));
    ASSERT_TRUE(deflt);
    EXPECT_EQ(deflt->stringValue(), d2_params_->getIpAddress().toText());

    // Check that omitting port gets you its default
    config =
            "{"
            " \"ip-address\": \"192.0.0.1\" , "
            " \"dns-server-timeout\": 333 , "
            " \"ncr-protocol\": \"UDP\" , "
            " \"ncr-format\": \"JSON\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {} "
            "}";

    RUN_CONFIG_OK(config);
    ASSERT_NO_THROW(deflt = defaults->get("port"));
    ASSERT_TRUE(deflt);
    EXPECT_EQ(deflt->intValue(), d2_params_->getPort());

    // Check that omitting timeout gets you its default
    config =
            "{"
            " \"ip-address\": \"192.0.0.1\" , "
            " \"port\": 777 , "
            " \"ncr-protocol\": \"UDP\" , "
            " \"ncr-format\": \"JSON\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {} "
            "}";

    RUN_CONFIG_OK(config);
    ASSERT_NO_THROW(deflt = defaults->get("dns-server-timeout"));
    ASSERT_TRUE(deflt);
    EXPECT_EQ(deflt->intValue(), d2_params_->getDnsServerTimeout());

    // Check that omitting protocol gets you its default
    config =
            "{"
            " \"ip-address\": \"192.0.0.1\" , "
            " \"port\": 777 , "
            " \"dns-server-timeout\": 333 , "
            " \"ncr-format\": \"JSON\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {} "
            "}";

    RUN_CONFIG_OK(config);
    ASSERT_NO_THROW(deflt = defaults->get("ncr-protocol"));
    ASSERT_TRUE(deflt);
    EXPECT_EQ(dhcp_ddns::stringToNcrProtocol(deflt->stringValue()),
              d2_params_->getNcrProtocol());

    // Check that omitting format gets you its default
    config =
            "{"
            " \"ip-address\": \"192.0.0.1\" , "
            " \"port\": 777 , "
            " \"dns-server-timeout\": 333 , "
            " \"ncr-protocol\": \"UDP\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {} "
            "}";

    RUN_CONFIG_OK(config);
    ASSERT_NO_THROW(deflt = defaults->get("ncr-format"));
    ASSERT_TRUE(deflt);
    EXPECT_EQ(dhcp_ddns::stringToNcrFormat(deflt->stringValue()),
              d2_params_->getNcrFormat());
}

/// @brief Tests the unsupported scalar parameters and objects are detected.
TEST_F(D2CfgMgrTest, unsupportedTopLevelItems) {
    // Check that an unsupported top level parameter fails.
    std::string config =
            "{"
            " \"ip-address\": \"127.0.0.1\", "
            " \"port\": 777 , "
            " \"dns-server-timeout\": 333 , "
            " \"ncr-protocol\": \"UDP\" , "
            " \"ncr-format\": \"JSON\", "
            " \"tsig-keys\": [], "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {}, "
            " \"bogus-param\" : true "
            "}";

    SYNTAX_ERROR(config, "<string>:1.185-197: got unexpected "
                         "keyword \"bogus-param\" in DhcpDdns map.");

    // Check that unsupported top level objects fails.  For
    // D2 these fail as they are not in the parse order.
    config =
            "{"
            " \"ip-address\": \"127.0.0.1\", "
            " \"port\": 777 , "
            " \"dns-server-timeout\": 333 , "
            " \"ncr-protocol\": \"UDP\" , "
            " \"ncr-format\": \"JSON\", "
            " \"tsig-keys\": [], "
            " \"bogus-object-one\" : {}, "
            " \"forward-ddns\" : {}, "
            " \"reverse-ddns\" : {}, "
            " \"bogus-object-two\" : {} "
            "}";

    SYNTAX_ERROR(config, "<string>:1.141-158: got unexpected"
                         " keyword \"bogus-object-one\" in DhcpDdns map.");
}


/// @brief Tests the enforcement of data validation when parsing D2Params.
/// It verifies that:
/// -# ip_address cannot be "0.0.0.0"
/// -# ip_address cannot be "::"
/// -# port cannot be 0
/// -# dns_server_timeout cannot be 0
/// -# ncr_protocol must be valid
/// -# ncr_format must be valid
TEST_F(D2CfgMgrTest, invalidEntry) {
    // Cannot use port  0
    std::string config = makeParamsConfigString ("127.0.0.1", 0, 333, "UDP", "JSON");
    SYNTAX_ERROR(config, "<string>:1.40: port must be greater than zero but less than 65536");

    // Cannot use dns server timeout of 0
    config = makeParamsConfigString ("127.0.0.1", 777, 0, "UDP", "JSON");
    SYNTAX_ERROR(config, "<string>:1.69: dns-server-timeout"
                         " must be greater than zero");

    // Invalid protocol
    config = makeParamsConfigString ("127.0.0.1", 777, 333, "BOGUS", "JSON");
    SYNTAX_ERROR(config, "<string>:1.92-98: syntax error,"
                         " unexpected constant string, expecting UDP or TCP");

    // Unsupported protocol
    config = makeParamsConfigString ("127.0.0.1", 777, 333, "TCP", "JSON");
    LOGIC_ERROR(config, "ncr-protocol : TCP is not yet supported"
                        " (<string>:1:92)");

    // Invalid format
    config = makeParamsConfigString ("127.0.0.1", 777, 333, "UDP", "BOGUS");
    SYNTAX_ERROR(config, "<string>:1.115-121: syntax error,"
                         " unexpected constant string, expecting JSON");
}

// Control socket tests in d2_process_unittests.cc

// DdnsDomainList and TSIGKey tests moved to d2_simple_parser_unittest.cc

/// @brief Tests construction of D2CfgMgr
/// This test verifies that a D2CfgMgr constructs properly.
TEST(D2CfgMgr, construction) {
    boost::scoped_ptr<D2CfgMgr> cfg_mgr;

    // Verify that configuration manager constructions without error.
    ASSERT_NO_THROW(cfg_mgr.reset(new D2CfgMgr()));

    // Verify that the context can be retrieved and is not null.
    D2CfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr->getD2CfgContext());
    EXPECT_TRUE(context);

    // Verify that the forward manager can be retrieved and is not null.
    EXPECT_TRUE(context->getForwardMgr());

    // Verify that the reverse manager can be retrieved and is not null.
    EXPECT_TRUE(context->getReverseMgr());

    // Verify that the manager can be destructed without error.
    EXPECT_NO_THROW(cfg_mgr.reset());
}

/// @brief Tests the parsing of a complete, valid DHCP-DDNS configuration.
/// This tests passes the configuration into an instance of D2CfgMgr just
/// as it would be done by d2_process in response to a configuration update
/// event.
TEST_F(D2CfgMgrTest, fullConfig) {
    setHooksTestPath();
    setSocketTestPath();

    // Create a configuration with all of application level parameters, plus
    // both the forward and reverse ddns managers.  Both managers have two
    // domains with three servers per domain.
    std::string config = "{ "
                        "\"ip-address\" : \"192.168.1.33\" , "
                        "\"port\" : 88 , "
                        "\"dns-server-timeout\": 333 , "
                        "\"ncr-protocol\": \"UDP\" , "
                        "\"ncr-format\": \"JSON\", "
                        "\"control-socket\" : {"
                        " \"socket-type\" : \"unix\" ,"
                        " \"socket-name\" : \"d2-ctrl-channel\" "
                        "},"
                        "\"hooks-libraries\": ["
                        "{"
                        "  \"library\": \"%LIBRARY%\" , "
                        "  \"parameters\": "
                        "  { \"param1\": \"foo\" } "
                        "}"
                        "],"
                        "\"tsig-keys\": ["
                        "{"
                        "  \"name\": \"d2_key.example.com\" , "
                        "  \"algorithm\": \"hmac-md5\" , "
                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                        "},"
                        "{"
                        "  \"name\": \"d2_key.billcat.net\" , "
                        "  \"algorithm\": \"hmac-md5\" , "
                        " \"digest-bits\": 120 , "
                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                        "}"
                        "],"
                        "\"forward-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"name\": \"example.com\" , "
                        "  \"key-name\": \"d2_key.example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } , "
                        "  { \"ip-address\": \"127.0.0.2\" } , "
                        "  { \"ip-address\": \"127.0.0.3\"} "
                        "  ] } "
                        ", "
                        "{ \"name\": \"billcat.net\" , "
                        "  \"key-name\": \"d2_key.billcat.net\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.4\" } , "
                        "  { \"ip-address\": \"127.0.0.5\" } , "
                        "  { \"ip-address\": \"127.0.0.6\" } "
                        "  ] } "
                        "] },"
                        "\"reverse-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"name\": \" 0.168.192.in.addr.arpa.\" , "
                        "  \"key-name\": \"d2_key.example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.1.1\" } , "
                        "  { \"ip-address\": \"127.0.2.1\" } , "
                        "  { \"ip-address\": \"127.0.3.1\" } "
                        "  ] } "
                        ", "
                        "{ \"name\": \" 0.247.106.in.addr.arpa.\" , "
                        "  \"key-name\": \"d2_key.billcat.net\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.4.1\" }, "
                        "  { \"ip-address\": \"127.0.5.1\" } , "
                        "  { \"ip-address\": \"127.0.6.1\" } "
                        "  ] } "
                        "] } }";

    // Replace the library path.
    std::string pr_config = pathReplacer(config.c_str(), CALLOUT_LIBRARY);
    // Should parse without error.
    RUN_CONFIG_OK(pr_config);

    // Verify that the D2 context can be retrieved and is not null.
    D2CfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());

    // Verify that the global scalars have the proper values.
    D2ParamsPtr& d2_params = context->getD2Params();
    ASSERT_TRUE(d2_params);

    EXPECT_EQ(isc::asiolink::IOAddress("192.168.1.33"),
              d2_params->getIpAddress());
    EXPECT_EQ(88, d2_params->getPort());
    EXPECT_EQ(333, d2_params->getDnsServerTimeout());
    EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_params->getNcrProtocol());
    EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_params->getNcrFormat());

    // Verify that the UNIX control socket can be retrieved.
    ConstElementPtr ctrl_sock = context->getUnixControlSocketInfo();
    ASSERT_TRUE(ctrl_sock);
    ASSERT_EQ(Element::list, ctrl_sock->getType());
    ASSERT_EQ(ctrl_sock->size(), 1);
    ctrl_sock = ctrl_sock->get(0);
    ASSERT_EQ(Element::map, ctrl_sock->getType());
    EXPECT_EQ(2, ctrl_sock->size());
    ASSERT_TRUE(ctrl_sock->get("socket-type"));
    EXPECT_EQ("\"unix\"", ctrl_sock->get("socket-type")->str());
    ASSERT_TRUE(ctrl_sock->get("socket-name"));
    EXPECT_EQ("\"d2-ctrl-channel\"", ctrl_sock->get("socket-name")->str());

    // Verify that the hooks libraries can be retrieved.
    const HookLibsCollection libs = context->getHooksConfig().get();
    ASSERT_EQ(1, libs.size());
    EXPECT_EQ(string(CALLOUT_LIBRARY), libs[0].libname_);
    ASSERT_TRUE(libs[0].parameters_);
    EXPECT_EQ("{ \"param1\": \"foo\" }", libs[0].parameters_->str());

    // Verify that the forward manager can be retrieved.
    DdnsDomainListMgrPtr mgr = context->getForwardMgr();
    ASSERT_TRUE(mgr);
    EXPECT_EQ("forward-ddns", mgr->getName());

    // Verify that the forward manager has the correct number of domains.
    DdnsDomainMapPtr domains = mgr->getDomains();
    ASSERT_TRUE(domains);
    int count =  domains->size();
    EXPECT_EQ(2, count);

    // Verify that the server count in each of the forward manager domains.
    // NOTE that since prior tests have validated server parsing, we are are
    // assuming that the servers did in fact parse correctly if the correct
    // number of them are there.
    for (auto const& domain_pair : *domains) {
        DdnsDomainPtr domain = domain_pair.second;
        DnsServerInfoStoragePtr servers = domain->getServers();
        count = servers->size();
        EXPECT_TRUE(servers);
        EXPECT_EQ(3, count);
    }

    // Verify that the reverse manager can be retrieved.
    mgr = context->getReverseMgr();
    ASSERT_TRUE(mgr);
    EXPECT_EQ("reverse-ddns", mgr->getName());

    // Verify that the reverse manager has the correct number of domains.
    domains = mgr->getDomains();
    count =  domains->size();
    EXPECT_EQ(2, count);

    // Verify that the server count in each of the reverse manager domains.
    // NOTE that since prior tests have validated server parsing, we are are
    // assuming that the servers did in fact parse correctly if the correct
    // number of them are there.
    for (auto const& domain_pair : *domains) {
        DdnsDomainPtr domain = domain_pair.second;
        DnsServerInfoStoragePtr servers = domain->getServers();
        count = servers->size();
        EXPECT_TRUE(servers);
        EXPECT_EQ(3, count);
    }

    // Test directional update flags.
    EXPECT_TRUE(cfg_mgr_->forwardUpdatesEnabled());
    EXPECT_TRUE(cfg_mgr_->reverseUpdatesEnabled());

    // Verify that parsing the exact same configuration a second time
    // does not cause a duplicate value errors.
    answer_ = cfg_mgr_->simpleParseConfig(config_set_, false);
    ASSERT_TRUE(checkAnswer(0));
}

/// @brief Tests the basics of the D2CfgMgr FQDN-domain matching
/// This test uses a valid configuration to exercise the D2CfgMgr
/// forward FQDN-to-domain matching.
/// It verifies that:
/// 1. Given an FQDN which exactly matches a domain's name, that domain is
/// returned as match.
/// 2. Given a FQDN for sub-domain in the list, returns the proper match.
/// 3. Given a FQDN that matches no domain name, returns the wild card domain
/// as a match.
TEST_F(D2CfgMgrTest, forwardMatch) {
    // Create  configuration with one domain, one sub domain, and the wild
    // card.
    std::string config = "{ "
                        "\"ip-address\" : \"192.168.1.33\" , "
                        "\"port\" : 88 , "
                        "\"tsig-keys\": [] ,"
                        "\"forward-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"name\": \"example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] } "
                        ", "
                        "{ \"name\": \"one.example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.2\" } "
                        "  ] } "
                        ", "
                        "{ \"name\": \"*\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.3\" } "
                        "  ] } "
                        "] }, "
                        "\"reverse-ddns\" : {} "
                        "}";

    // Verify that we can parse the configuration.
    RUN_CONFIG_OK(config);

    // Verify that the D2 context can be retrieved and is not null.
    D2CfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());

    // Test directional update flags.
    EXPECT_TRUE(cfg_mgr_->forwardUpdatesEnabled());
    EXPECT_FALSE(cfg_mgr_->reverseUpdatesEnabled());

    DdnsDomainPtr match;
    // Verify that an exact match works.
    EXPECT_TRUE(cfg_mgr_->matchForward("example.com", match));
    EXPECT_EQ("example.com", match->getName());

    // Verify that search is case insensitive.
    EXPECT_TRUE(cfg_mgr_->matchForward("EXAMPLE.COM", match));
    EXPECT_EQ("example.com", match->getName());

    // Verify that an exact match works.
    EXPECT_TRUE(cfg_mgr_->matchForward("one.example.com", match));
    EXPECT_EQ("one.example.com", match->getName());

    // Verify that a FQDN for sub-domain matches.
    EXPECT_TRUE(cfg_mgr_->matchForward("blue.example.com", match));
    EXPECT_EQ("example.com", match->getName());

    // Verify that a FQDN for sub-domain matches.
    EXPECT_TRUE(cfg_mgr_->matchForward("red.one.example.com", match));
    EXPECT_EQ("one.example.com", match->getName());

    // Verify that an FQDN with no match, returns the wild card domain.
    EXPECT_TRUE(cfg_mgr_->matchForward("shouldbe.wildcard", match));
    EXPECT_EQ("*", match->getName());

    // Verify that an attempt to match an empty FQDN throws.
    ASSERT_THROW(cfg_mgr_->matchForward("", match), D2CfgError);
}

/// @brief Tests domain matching when there is no wild card domain.
/// This test verifies that matches are found only for FQDNs that match
/// some or all of a domain name.  FQDNs without matches should not return
/// a match.
TEST_F(D2CfgMgrTest, matchNoWildcard) {
    // Create a configuration with one domain, one sub-domain, and NO wild card.
    std::string config = "{ "
                        "\"ip-address\" : \"192.168.1.33\" , "
                        "\"port\" : 88 , "
                        "\"tsig-keys\": [] ,"
                        "\"forward-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"name\": \"example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] } "
                        ", "
                        "{ \"name\": \"one.example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.2\" } "
                        "  ] } "
                        "] }, "
                        "\"reverse-ddns\" : {} "
                        " }";

    // Verify that we can parse the configuration.
    RUN_CONFIG_OK(config);

    // Verify that the D2 context can be retrieved and is not null.
    D2CfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());

    DdnsDomainPtr match;
    // Verify that full or partial matches, still match.
    EXPECT_TRUE(cfg_mgr_->matchForward("example.com", match));
    EXPECT_EQ("example.com", match->getName());

    EXPECT_TRUE(cfg_mgr_->matchForward("blue.example.com", match));
    EXPECT_EQ("example.com", match->getName());

    EXPECT_TRUE(cfg_mgr_->matchForward("red.one.example.com", match));
    EXPECT_EQ("one.example.com", match->getName());

    // Verify that a FQDN with no match, fails to match.
    EXPECT_FALSE(cfg_mgr_->matchForward("shouldbe.wildcard", match));
}

/// @brief Tests domain matching when there is ONLY a wild card domain.
/// This test verifies that any FQDN matches the wild card.
TEST_F(D2CfgMgrTest, matchAll) {
    std::string config = "{ "
                        "\"ip-address\" : \"192.168.1.33\" , "
                        "\"port\" : 88 , "
                        "\"tsig-keys\": [] ,"
                        "\"forward-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"name\": \"*\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] } "
                        "] }, "
                        "\"reverse-ddns\" : {} "
                        "}";

    // Verify that we can parse the configuration.
    RUN_CONFIG_OK(config);

    // Verify that the D2 context can be retrieved and is not null.
    D2CfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());

    // Verify that wild card domain is returned for any FQDN.
    DdnsDomainPtr match;
    EXPECT_TRUE(cfg_mgr_->matchForward("example.com", match));
    EXPECT_EQ("*", match->getName());
    EXPECT_TRUE(cfg_mgr_->matchForward("shouldbe.wildcard", match));
    EXPECT_EQ("*", match->getName());

    // Verify that an attempt to match an empty FQDN still throws.
    ASSERT_THROW(cfg_mgr_->matchReverse("", match), D2CfgError);

}

/// @brief Tests the basics of the D2CfgMgr reverse FQDN-domain matching
/// This test uses a valid configuration to exercise the D2CfgMgr's
/// reverse FQDN-to-domain matching.
/// It verifies that:
/// 1. Given an FQDN which exactly matches a domain's name, that domain is
/// returned as match.
/// 2. Given a FQDN for sub-domain in the list, returns the proper match.
/// 3. Given a FQDN that matches no domain name, returns the wild card domain
/// as a match.
TEST_F(D2CfgMgrTest, matchReverse) {
    std::string config = "{ "
                        "\"ip-address\" : \"192.168.1.33\" , "
                        "\"port\" : 88 , "
                        "\"tsig-keys\": [] ,"
                        "\"forward-ddns\" : {}, "
                        "\"reverse-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"name\": \"5.100.168.192.in-addr.arpa.\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] }, "
                        "{ \"name\": \"100.200.192.in-addr.arpa.\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] }, "
                        "{ \"name\": \"170.192.in-addr.arpa.\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] }, "
                        // Note mixed case to test case insensitivity.
                        "{ \"name\": \"2.0.3.0.8.b.d.0.1.0.0.2.IP6.ARPA.\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] },"
                        "{ \"name\": \"*\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" } "
                        "  ] } "
                        "] } }";

    // Verify that we can parse the configuration.
    RUN_CONFIG_OK(config);

    // Verify that the D2 context can be retrieved and is not null.
    D2CfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());

    // Test directional update flags.
    EXPECT_FALSE(cfg_mgr_->forwardUpdatesEnabled());
    EXPECT_TRUE(cfg_mgr_->reverseUpdatesEnabled());

    DdnsDomainPtr match;

    // Verify an exact match.
    EXPECT_TRUE(cfg_mgr_->matchReverse("192.168.100.5", match));
    EXPECT_EQ("5.100.168.192.in-addr.arpa.", match->getName());

    // Verify a sub-domain match.
    EXPECT_TRUE(cfg_mgr_->matchReverse("192.200.100.27", match));
    EXPECT_EQ("100.200.192.in-addr.arpa.", match->getName());

    // Verify a sub-domain match.
    EXPECT_TRUE(cfg_mgr_->matchReverse("192.170.50.30", match));
    EXPECT_EQ("170.192.in-addr.arpa.", match->getName());

    // Verify a wild card match.
    EXPECT_TRUE(cfg_mgr_->matchReverse("1.1.1.1", match));
    EXPECT_EQ("*", match->getName());

    // Verify a IPv6 match.
    EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:302:99::",match));
    EXPECT_EQ("2.0.3.0.8.b.d.0.1.0.0.2.IP6.ARPA.", match->getName());

    // Verify a IPv6 wild card match.
    EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:99:302::",match));
    EXPECT_EQ("*", match->getName());

    // Verify that an attempt to match an invalid IP address throws.
    ASSERT_THROW(cfg_mgr_->matchReverse("", match), D2CfgError);
}

/// @brief Tests D2 config parsing against a wide range of config permutations.
///
/// It tests for both syntax errors that the JSON parsing (D2ParserContext)
/// should detect as well as post-JSON parsing logic errors generated by
/// the Element parsers (i.e...SimpleParser/DhcpParser derivations)
///
/// It iterates over all of the test configurations described in given file.
/// The file content is JSON specialized to this test. The format of the file
/// is:
///
/// @code
/// # The file must open with a list. It's name is arbitrary.
///
/// { "test_list" :
/// [
///
/// #    Test one starts here:
///      {
///
/// #    Each test has:
/// #      1. description - optional text description
/// #      2. syntax-error - error JSON parser should emit (omit if none)
/// #      3. logic-error - error element parser(s) should emit (omit if none)
/// #      4. data - configuration text to parse
/// #
///      "description" : "<text describing test>",
///      "syntax-error" : "<exact text from JSON parser including position>" ,
///      "logic-error" : "<exact text from element parser including position>" ,
///      "data" :
///          {
/// #        configuration elements here
///          "bool_val" : false,
///          "some_map" :  {}
/// #         :
///          }
///      }
///
/// #    Next test would start here
///      ,
///      {
///      }
///
/// ]}
///
/// @endcode
///
/// (The file supports comments per Element::fromJSONFile())
///
TEST_F(D2CfgMgrTest, configPermutations) {
    std::string test_file = testDataFile("d2_cfg_tests.json");
    isc::data::ConstElementPtr tests;

    // Read contents of the file and parse it as JSON. Note it must contain
    // all valid JSON, we aren't testing JSON parsing.
    try {
        tests = isc::data::Element::fromJSONFile(test_file, true);
    } catch (const std::exception& ex) {
        FAIL() << "ERROR parsing file : " << test_file << " : " << ex.what();
    }

    // Read in each test For each test, read:
    //
    //  1. description - optional text description
    //  2. syntax-error or logic-error or neither
    //  3. data - configuration text to parse
    //  4. convert data into JSON text
    //  5. submit JSON for parsing
    ASSERT_TRUE(tests->get("test-list"));
    for (auto const& test : tests->get("test-list")->listValue()) {
        // Grab the description.
        std::string description = "<no desc>";
        isc::data::ConstElementPtr elem = test->get("description");
        if (elem) {
            elem->getValue(description);
        }

        // Grab the expected error message, if there is one.
        std::string expected_error = "";
        RunConfigMode mode = NO_ERROR;
        elem = test->get("syntax-error");
        if (elem) {
            elem->getValue(expected_error);
            mode = SYNTAX_ERROR;
        } else {
            elem = test->get("logic-error");
            if (elem) {
                elem->getValue(expected_error);
                mode = LOGIC_ERROR;
            }
        }

        // Grab the test's configuration data.
        isc::data::ConstElementPtr data = test->get("data");
        ASSERT_TRUE(data) << "No data for test: " << test->getPosition();

        // Convert the test data back to JSON text, then submit it for parsing.
        stringstream os;
        data->toJSON(os);
        EXPECT_TRUE(runConfigOrFail(os.str(), mode, expected_error))
            << " failed for test: " << test->getPosition() << std::endl;
    }
}

/// @brief Tests comments.
TEST_F(D2CfgMgrTest, comments) {
    setSocketTestPath();
    std::string config = "{ "
                        "\"comment\": \"D2 config\" , "
                        "\"ip-address\" : \"192.168.1.33\" , "
                        "\"port\" : 88 , "
                        "\"control-sockets\": ["
                        "{"
                        " \"socket-type\": \"unix\","
                        " \"socket-name\": \"d2-ctrl-socket\","
                        " \"user-context\": { \"comment\":"
                        "  \"Indirect comment\" }"
                        "},"
                        "{"
                        " \"comment\": \"HTTP control socket\","
                        " \"socket-type\": \"http\","
                        " \"socket-address\": \"::1\","
                        " \"socket-port\": 8053,"
                        " \"authentication\": {"
                        "  \"comment\": \"basic HTTP authentication\","
                        "  \"type\": \"basic\","
                        "  \"clients\": [ {"
                        "   \"comment\": \"admin is authorized\","
                        "   \"user\": \"admin\","
                        "   \"password\": \"foobar\""
                        "  } ]"
                        " }"
                        "}"
                        "],"
                        "\"tsig-keys\": ["
                        "{"
                        "  \"user-context\": { "
                        "    \"comment\": \"Indirect comment\" } , "
                        "  \"name\": \"d2_key.example.com\" , "
                        "  \"algorithm\": \"hmac-md5\" , "
                        "  \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                        "}"
                        "],"
                        "\"forward-ddns\" : {"
                        "\"ddns-domains\": [ "
                        "{ \"comment\": \"A DDNS domain\" , "
                        "  \"name\": \"example.com\" , "
                        "  \"key-name\": \"d2_key.example.com\" , "
                        "  \"dns-servers\" : [ "
                        "  { \"ip-address\": \"127.0.0.1\" , "
                        "    \"user-context\": { \"version\": 1 } } "
                        "  ] } "
                        "] } }";

    // Should parse without error.
    RUN_CONFIG_OK(config);

    // Check the D2 context.
    D2CfgContextPtr d2_context;
    ASSERT_NO_THROW(d2_context = cfg_mgr_->getD2CfgContext());
    ASSERT_TRUE(d2_context);

    // Check global user context.
    ConstElementPtr ctx = d2_context->getContext();
    ASSERT_TRUE(ctx);
    ASSERT_EQ(1, ctx->size());
    ASSERT_TRUE(ctx->get("comment"));
    EXPECT_EQ("\"D2 config\"", ctx->get("comment")->str());

    // There is a UNIX control socket.
    ConstElementPtr socket = d2_context->getUnixControlSocketInfo();
    ASSERT_TRUE(socket);
    ASSERT_EQ(Element::list, socket->getType());
    ASSERT_EQ(socket->size(), 1);
    socket = socket->get(0);
    ASSERT_TRUE(socket);
    ConstElementPtr ctx_socket = socket->get("user-context");
    ASSERT_TRUE(ctx_socket);
    ASSERT_EQ(1, ctx_socket->size());
    ASSERT_TRUE(ctx_socket->get("comment"));
    EXPECT_EQ("\"Indirect comment\"", ctx_socket->get("comment")->str());

    // There is a HTTP control socket with authentication.
    socket = d2_context->getHttpControlSocketInfo();
    ASSERT_TRUE(socket);
    ASSERT_EQ(Element::list, socket->getType());
    ASSERT_EQ(socket->size(), 1);
    socket = socket->get(0);
    ASSERT_TRUE(socket);
    /// @todo use the configuration object.
    ctx_socket = socket->get("user-context");
    ASSERT_TRUE(ctx_socket);
    ASSERT_EQ(1, ctx_socket->size());
    ASSERT_TRUE(ctx_socket->get("comment"));
    EXPECT_EQ("\"HTTP control socket\"", ctx_socket->get("comment")->str());

    // HTTP authentication.
    ConstElementPtr auth = socket->get("authentication");
    ASSERT_TRUE(auth);
    ConstElementPtr ctx_auth = auth->get("user-context");
    ASSERT_TRUE(ctx_auth);
    ASSERT_EQ(1, ctx_auth->size());
    ASSERT_TRUE(ctx_auth->get("comment"));
    EXPECT_EQ("\"basic HTTP authentication\"", ctx_auth->get("comment")->str());

    // Authentication client.
    ConstElementPtr clients = auth->get("clients");
    ASSERT_TRUE(clients);
    ASSERT_EQ(1, clients->size());
    ConstElementPtr client;
    ASSERT_NO_THROW(client = clients->get(0));
    ASSERT_TRUE(client);
    ConstElementPtr ctx_client = client->get("user-context");
    ASSERT_TRUE(ctx_client);
    ASSERT_EQ(1, ctx_client->size());
    ASSERT_TRUE(ctx_client->get("comment"));
    EXPECT_EQ("\"admin is authorized\"", ctx_client->get("comment")->str());

    // Check TSIG keys.
    TSIGKeyInfoMapPtr keys = d2_context->getKeys();
    ASSERT_TRUE(keys);
    ASSERT_EQ(1, keys->size());

    // Check the TSIG key.
    TSIGKeyInfoMap::iterator gotkey = keys->find("d2_key.example.com");
    ASSERT_TRUE(gotkey != keys->end());
    TSIGKeyInfoPtr key = gotkey->second;
    ASSERT_TRUE(key);

    // Check the TSIG key user context.
    ConstElementPtr key_ctx = key->getContext();
    ASSERT_TRUE(key_ctx);
    ASSERT_EQ(1, key_ctx->size());
    ASSERT_TRUE(key_ctx->get("comment"));
    EXPECT_EQ("\"Indirect comment\"", key_ctx->get("comment")->str());

    // Check the forward manager.
    DdnsDomainListMgrPtr mgr = d2_context->getForwardMgr();
    ASSERT_TRUE(mgr);
    EXPECT_EQ("forward-ddns", mgr->getName());
    DdnsDomainMapPtr domains = mgr->getDomains();
    ASSERT_TRUE(domains);
    ASSERT_EQ(1, domains->size());

    // Check the DDNS domain.
    DdnsDomainMap::iterator gotdns = domains->find("example.com");
    ASSERT_TRUE(gotdns != domains->end());
    DdnsDomainPtr domain = gotdns->second;
    ASSERT_TRUE(domain);

    // Check the DNS server.
    DnsServerInfoStoragePtr servers = domain->getServers();
    ASSERT_TRUE(servers);
    ASSERT_EQ(1, servers->size());
    DnsServerInfoPtr server = (*servers)[0];
    ASSERT_TRUE(server);

    // Check the DNS server user context.
    ConstElementPtr srv_ctx = server->getContext();
    ASSERT_TRUE(srv_ctx);
    ASSERT_EQ(1, srv_ctx->size());
    ASSERT_TRUE(srv_ctx->get("version"));
    EXPECT_EQ("1", srv_ctx->get("version")->str());
}

/// @brief Tests a basic valid configuration for D2Param.
TEST_F(D2CfgMgrTest, listenOnANYAddresses) {
    // Verify that ip_address 0.0.0.0 is valid.
    std::string config = makeParamsConfigString ("0.0.0.0", 777, 333,
                                           "UDP", "JSON");
    RUN_CONFIG_OK(config);

    EXPECT_EQ(isc::asiolink::IOAddress("0.0.0.0"),
              d2_params_->getIpAddress());

    // Verify the configuration summary.
    EXPECT_EQ("listening on 0.0.0.0, port 777, using UDP",
              d2_params_->getConfigSummary());

    EXPECT_EQ(777, d2_params_->getPort());
    EXPECT_EQ(333, d2_params_->getDnsServerTimeout());
    EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_params_->getNcrProtocol());
    EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_params_->getNcrFormat());

    // Verify that ip_address :: valid.
    config = makeParamsConfigString ("::", 777, 333, "UDP", "JSON");
    RUN_CONFIG_OK(config);

    // Verify that the global scalars have the proper values.
    EXPECT_EQ(isc::asiolink::IOAddress("::"),
              d2_params_->getIpAddress());

    // Verify the configuration summary.
    EXPECT_EQ("listening on ::, port 777, using UDP",
              d2_params_->getConfigSummary());
}

} // end of anonymous namespace
