// Copyright (C) 2019-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 <arpa/inet.h>
#include <gtest/gtest.h>

#include <database/backend_selector.h>
#include <dhcp/option_int.h>
#include <dhcp/option_string.h>
#include <dhcp/testutils/iface_mgr_test_config.h>
#include <dhcp6/dhcp6_srv.h>
#include <dhcp6/ctrl_dhcp6_srv.h>
#include <dhcp6/json_config_parser.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/cfg_subnets6.h>
#include <dhcpsrv/testutils/generic_backend_unittest.h>
#include <dhcpsrv/testutils/test_config_backend_dhcp6.h>

#include <dhcp6/tests/dhcp6_test_utils.h>
#include <dhcp6/tests/get_config_unittest.h>

#include <boost/scoped_ptr.hpp>

#include <iostream>
#include <fstream>
#include <sstream>
#include <limits.h>

using namespace isc::asiolink;
using namespace isc::config;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::dhcp::test;
using namespace isc::db;
using namespace std;

namespace {

/// @brief Test fixture for testing external configuration merging
class Dhcp6CBTest : public GenericBackendTest {
protected:
    /// @brief Pre test set up
    /// Called prior to each test.  It creates two configuration backends
    /// that differ by host name ("db1" and "db2"). It then registers
    /// a backend factory that will return them rather than create
    /// new instances. The backends need to pre-exist so they can be
    /// populated prior to calling server configure.  It uses
    /// TestConfigBackend instances but with a type of "memfile" to pass
    /// parsing.  Doing it all here allows us to use ASSERTs if we feel like
    /// it.
    virtual void SetUp() {
        DatabaseConnection::ParameterMap params;
        params[std::string("type")] = std::string("memfile");
        params[std::string("host")] = std::string("db1");
        db1_.reset(new TestConfigBackendDHCPv6(params));

        params[std::string("host")] = std::string("db2");
        db2_.reset(new TestConfigBackendDHCPv6(params));

        ConfigBackendDHCPv6Mgr::instance().registerBackendFactory("memfile",
            [this](const DatabaseConnection::ParameterMap& parameters)
                -> ConfigBackendDHCPv6Ptr {
                    auto host = parameters.find("host");
                    if (host != parameters.end()) {
                        if (host->second == "db1") {
                            return (db1_);
                        } else if (host->second == "db2") {
                            return (db2_);
                        }
                    }

                    // Apparently we're looking for a new one.
                    return (TestConfigBackendDHCPv6Ptr(new TestConfigBackendDHCPv6(parameters)));
                });
    }

    /// @brief Clean up after each test
    virtual void TearDown() {
        // Unregister the factory to be tidy.
        ConfigBackendDHCPv6Mgr::instance().unregisterBackendFactory("memfile");
    }

public:

    /// Constructor
    Dhcp6CBTest()
    : rcode_(-1), db1_selector("db1"), db2_selector("db1")   {
        // Open port 0 means to not do anything at all. We don't want to
        // deal with sockets here, just check if configuration handling
        // is sane.
        srv_.reset(new ControlledDhcpv6Srv(0));

        // Create fresh context.
        resetConfiguration();
        CfgMgr::instance().setFamily(AF_INET6);
    }

    /// Destructor
    virtual ~Dhcp6CBTest() {
        resetConfiguration();
    };

    /// @brief Reset configuration singletons.
    void resetConfiguration() {
        CfgMgr::instance().clear();
        ConfigBackendDHCPv6Mgr::destroy();
    }

    /// @brief Convenience method for running configuration
    ///
    /// This method does not throw, but signals errors using gtest macros.
    ///
    /// @param config text to be parsed as JSON
    /// @param expected_code expected code (see cc/command_interpreter.h)
    /// @param exp_error expected text error (check skipped if empty)
    void configure(std::string config, int expected_code,
                   std::string exp_error = "") {
        ConstElementPtr json;
        try {
            json = parseDHCP6(config, true);
        } catch(const std::exception& ex) {
            ADD_FAILURE() << "parseDHCP6 failed: " << ex.what();
        }

        ConstElementPtr status;
        ASSERT_NO_THROW(status = configureDhcp6Server(*srv_, json));
        ASSERT_TRUE(status);

        int rcode;
        ConstElementPtr comment = parseAnswerText(rcode, status);
        ASSERT_EQ(expected_code, rcode) << " comment: "
                    << comment->stringValue();

        string text;
        ASSERT_NO_THROW(text = comment->stringValue());

        if (expected_code != rcode) {
            std::cout << "Reported status: " << text << std::endl;
        }

        if ((rcode != 0)) {
            if (!exp_error.empty()) {
                ASSERT_EQ(exp_error, text);
            }
        }
    }

    boost::scoped_ptr<Dhcpv6Srv> srv_;  ///< DHCP6 server under test
    int rcode_;                         ///< Return code from element parsing
    ConstElementPtr comment_;           ///< Reason for parse fail

    BackendSelector db1_selector; ///< BackendSelector by host for first config backend
    BackendSelector db2_selector; ///< BackendSelector by host for second config backend

    TestConfigBackendDHCPv6Ptr db1_; ///< First configuration backend instance
    TestConfigBackendDHCPv6Ptr db2_; ///< Second configuration backend instance
};

// This test verifies that externally configured globals are
// merged correctly into staging configuration.
TEST_F(Dhcp6CBTest, mergeGlobals) {
    string base_config =
        "{ \n"
        "    \"interfaces-config\": { \n"
        "        \"interfaces\": [\"*\" ] \n"
        "    }, \n"
        "    \"decline-probation-period\": 7000, \n"
        "    \"valid-lifetime\": 1000, \n"
        "    \"rebind-timer\": 800, \n"
        "    \"server-tag\": \"first-server\", \n"
        "    \"config-control\": { \n"
        "       \"config-databases\": [ { \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db1\" \n"
        "           },{ \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db2\" \n"
        "           } \n"
        "       ] \n"
        "   } \n"
        "} \n";

    extractConfig(base_config);

    // Make some globals:
    StampedValuePtr server_tag(new StampedValue("server-tag", "second-server"));
    StampedValuePtr decline_period(new StampedValue("decline-probation-period", Element::create(86400)));
    StampedValuePtr renew_timer(new StampedValue("renew-timer", Element::create(500)));
    StampedValuePtr mt_enabled(new StampedValue("multi-threading.enable-multi-threading", Element::create(true)));
    StampedValuePtr mt_pool_size(new StampedValue("multi-threading.thread-pool-size", Element::create(256)));
    StampedValuePtr hr_identifiers(new StampedValue("host-reservation-identifiers", "[ \"hw-address\", \"flex-id\" ]"));

    // Let's add all of the globals to the second backend.  This will verify
    // we find them there.
    db2_->createUpdateGlobalParameter6(ServerSelector::ALL(), server_tag);
    db2_->createUpdateGlobalParameter6(ServerSelector::ALL(), decline_period);
    db2_->createUpdateGlobalParameter6(ServerSelector::ALL(), renew_timer);
    db2_->createUpdateGlobalParameter6(ServerSelector::ALL(), mt_enabled);
    db2_->createUpdateGlobalParameter6(ServerSelector::ALL(), mt_pool_size);
    db2_->createUpdateGlobalParameter6(ServerSelector::ALL(), hr_identifiers);

    // Should parse and merge without error.
    ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));

    // Verify the composite staging is correct.  (Remember that
    // CfgMgr::instance().commit() hasn't been called)
    SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();

    // decline-probation-period is an explicit member that should come
    // from the backend.
    EXPECT_EQ(86400, staging_cfg->getDeclinePeriod());

    // Verify that the implicit globals from JSON are there.
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, "valid-lifetime",
                                                  Element::create(1000)));
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, "rebind-timer",
                                                  Element::create(800)));

    // Verify that the implicit globals from the backend are there.
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, server_tag));
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, renew_timer));
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, mt_enabled));
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, mt_pool_size));
    ASSERT_NO_FATAL_FAILURE(checkConfiguredGlobal(staging_cfg, hr_identifiers, true));

    auto const& ex_hr_i = staging_cfg->getCfgHostOperations6()->getIdentifierTypes();
    EXPECT_EQ(ex_hr_i.size(), 2);
    EXPECT_EQ(ex_hr_i.front(), Host::IDENT_HWADDR);
    EXPECT_EQ(ex_hr_i.back(), Host::IDENT_FLEX);
}

// This test verifies that externally configured option definitions
// merged correctly into staging configuration.
TEST_F(Dhcp6CBTest, mergeOptionDefs) {
    string base_config =
        "{ \n"
        "    \"option-def\": [ { \n"
        "        \"name\": \"one\", \n"
        "        \"code\": 1, \n"
        "        \"type\": \"ipv6-address\", \n"
        "        \"space\": \"isc\" \n"
        "     }, \n"
        "     { \n"
        "        \"name\": \"two\", \n"
        "        \"code\": 2, \n"
        "        \"type\": \"string\", \n"
        "        \"space\": \"isc\" \n"
        "     } \n"
        "   ], \n"
        "    \"config-control\": { \n"
        "       \"config-databases\": [ { \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db1\" \n"
        "           },{ \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db2\" \n"
        "           } \n"
        "       ] \n"
        "   } \n"
        "} \n";

    extractConfig(base_config);

    // Create option one replacement and add it to first backend.
    OptionDefinitionPtr def;
    def.reset(new OptionDefinition("one", 101, "isc", "uint16"));
    db1_->createUpdateOptionDef6(ServerSelector::ALL(), def);

    // Create option three and add it to first backend.
    def.reset(new OptionDefinition("three", 3, "isc", "string"));
    db1_->createUpdateOptionDef6(ServerSelector::ALL(), def);

    // Create option four and add it to second backend.
    def.reset(new OptionDefinition("four", 4, "isc", "string"));
    db2_->createUpdateOptionDef6(ServerSelector::ALL(), def);

    // Should parse and merge without error.
    ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));

    // Verify the composite staging is correct.
    SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
    ConstCfgOptionDefPtr option_defs = staging_cfg->getCfgOptionDef();

    // Definition "one" from first backend should be there.
    OptionDefinitionPtr found_def = option_defs->get("isc", "one");
    ASSERT_TRUE(found_def);
    EXPECT_EQ(101, found_def->getCode());
    EXPECT_EQ(OptionDataType::OPT_UINT16_TYPE, found_def->getType());

    // Definition "two" from JSON config should be there.
    found_def = option_defs->get("isc", "two");
    ASSERT_TRUE(found_def);
    EXPECT_EQ(2, found_def->getCode());

    // Definition "three" from first backend should be there.
    found_def = option_defs->get("isc", "three");
    ASSERT_TRUE(found_def);
    EXPECT_EQ(3, found_def->getCode());

    // Definition "four" from first backend should not be there.
    found_def = option_defs->get("isc", "four");
    ASSERT_FALSE(found_def);
}

// This test verifies that externally configured options
// merged correctly into staging configuration.
TEST_F(Dhcp6CBTest, mergeOptions) {
    string base_config =
        "{ \n"
        "    \"option-data\": [ { \n"
        "        \"name\": \"solmax-rt\", \n"
        "        \"data\": \"500\" \n"
        "     },{ \n"
        "        \"name\": \"bootfile-url\", \n"
        "        \"data\": \"orig-boot-file\" \n"
        "     } \n"
        "    ], \n"
        "    \"config-control\": { \n"
        "       \"config-databases\": [ { \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db1\" \n"
        "           },{ \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db2\" \n"
        "           } \n"
        "       ] \n"
        "   } \n"
        "} \n";

    OptionDescriptorPtr opt;
    // Add solmax-rt to the first backend.
    opt.reset(new OptionDescriptor(
              createOption<OptionString>(Option::V6, D6O_BOOTFILE_URL,
                                         true, false, false,
                                         "updated-boot-file")));
    opt->space_name_ = DHCP6_OPTION_SPACE;
    db1_->createUpdateOption6(ServerSelector::ALL(), opt);

    // Add solmax-rt to the second backend.
    opt.reset(new OptionDescriptor(
              createOption<OptionUint32>(Option::V6, D6O_SOL_MAX_RT,
                                         false, false, true, 700)));
    opt->space_name_ = DHCP6_OPTION_SPACE;
    db2_->createUpdateOption6(ServerSelector::ALL(), opt);

    // Should parse and merge without error.
    ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));

    //  Now let's verify that composite staging options are correct.
    SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();
    CfgOptionPtr options = staging_cfg->getCfgOption();

    // bootfile-url should come from the first config back end.
    // (overwriting the original).
    OptionDescriptor found_opt =
        options->get(DHCP6_OPTION_SPACE, D6O_BOOTFILE_URL);
    ASSERT_TRUE(found_opt.option_);
    OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(found_opt.option_);
    ASSERT_TRUE(opstr);
    EXPECT_EQ("updated-boot-file", opstr->getValue());

    // sol-maxt-rt should come from the original config
    found_opt = options->get(DHCP6_OPTION_SPACE, D6O_SOL_MAX_RT);
    ASSERT_TRUE(found_opt.option_);
    OptionUint32Ptr opint = boost::dynamic_pointer_cast<OptionUint32>(found_opt.option_);
    ASSERT_TRUE(opint);
    EXPECT_EQ(500, opint->getValue());
}

// This test verifies that DHCP options fetched from the config backend
// encapsulate their suboptions.
TEST_F(Dhcp6CBTest, mergeOptionsWithSuboptions) {
    string base_config =
        "{ \n"
        "    \"option-def\": [ { \n"
        "        \"name\": \"option-1024\", \n"
        "        \"code\": 1024, \n"
        "        \"type\": \"empty\", \n"
        "        \"space\": \"dhcp6\", \n"
        "        \"encapsulate\": \"option-1024-space\" \n"
        "    }, \n"
        "    { \n"
        "        \"name\": \"option-1025\", \n"
        "        \"code\": 1025, \n"
        "        \"type\": \"string\", \n"
        "        \"space\": \"option-1024-space\" \n"
        "    } ], \n"
        "    \"config-control\": { \n"
        "       \"config-databases\": [ { \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db1\" \n"
        "           },{ \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db2\" \n"
        "           } \n"
        "       ] \n"
        "   } \n"
        "} \n";

    extractConfig(base_config);

    // Create option 1024 instance and store it in the database.
    OptionDescriptorPtr opt;
    opt.reset(new OptionDescriptor(
              createEmptyOption(Option::V6, 1024, true, false)));
    opt->space_name_ = DHCP6_OPTION_SPACE;
    db1_->createUpdateOption6(ServerSelector::ALL(), opt);

    // Create option 1024 suboption and store it in the database.
    opt.reset(new OptionDescriptor(
              createOption<OptionString>(Option::V6, 1025, true, false, false,
                                         "http://server:8080")
          )
    );
    opt->space_name_ = "option-1024-space";
    db1_->createUpdateOption6(ServerSelector::ALL(), opt);

    // Fetch the configuration from the config backend.
    ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));

    auto staging_cfg = CfgMgr::instance().getStagingCfg();

    // Make sure that option 1024 has been fetched.
    auto found_opt_desc = staging_cfg->getCfgOption()->get(DHCP6_OPTION_SPACE, 1024);
    ASSERT_TRUE(found_opt_desc.option_);

    // Make sure that the option 1024 contains its suboption.
    auto found_subopt = found_opt_desc.option_->getOption(1025);
    EXPECT_TRUE(found_subopt);
}

// This test verifies that externally configured shared-networks are
// merged correctly into staging configuration.
TEST_F(Dhcp6CBTest, mergeSharedNetworks) {
    string base_config =
        "{ \n"
        "    \"interfaces-config\": { \n"
        "        \"interfaces\": [\"*\" ] \n"
        "    }, \n"
        "    \"valid-lifetime\": 4000, \n"
        "    \"config-control\": { \n"
        "       \"config-databases\": [ { \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db1\" \n"
        "           },{ \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db2\" \n"
        "           } \n"
        "       ] \n"
        "   }, \n"
        "   \"shared-networks\": [ { \n"
        "       \"name\": \"two\" \n"
        "   }] \n"
        "} \n";

    extractConfig(base_config);

    // Make a few networks
    SharedNetwork6Ptr network1(new SharedNetwork6("one"));
    SharedNetwork6Ptr network3(new SharedNetwork6("three"));

    // Add network1 to db1 and network3 to db2
    db1_->createUpdateSharedNetwork6(ServerSelector::ALL(), network1);
    db2_->createUpdateSharedNetwork6(ServerSelector::ALL(), network3);

    // Should parse and merge without error.
    ASSERT_NO_FATAL_FAILURE(configure(base_config, CONTROL_RESULT_SUCCESS, ""));

    // Verify the composite staging is correct.  (Remember that
    // CfgMgr::instance().commit() hasn't been called)
    SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();

    CfgSharedNetworks6Ptr networks = staging_cfg->getCfgSharedNetworks6();
    SharedNetwork6Ptr staged_network;

    // SharedNetwork One should have been added from db1 config
    staged_network = networks->getByName("one");
    ASSERT_TRUE(staged_network);

    // Subnet2 should have come from the json config
    staged_network = networks->getByName("two");
    ASSERT_TRUE(staged_network);

    // Subnet3, which is in db2 should not have been merged.
    // We queried db1 first and the query returned data. In
    // other words, we iterate over the backends, asking for
    // data.  We use the first data, we find.
    staged_network = networks->getByName("three");
    ASSERT_FALSE(staged_network);
}

// This test verifies that externally configured subnets are
// merged correctly into staging configuration.
TEST_F(Dhcp6CBTest, mergeSubnets) {
    string base_config =
        "{ \n"
        "    \"interfaces-config\": { \n"
        "        \"interfaces\": [\"*\" ] \n"
        "    }, \n"
        "    \"valid-lifetime\": 4000, \n"
        "    \"config-control\": { \n"
        "       \"config-databases\": [ { \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db1\" \n"
        "           },{ \n"
        "               \"type\": \"memfile\", \n"
        "               \"host\": \"db2\" \n"
        "           } \n"
        "       ] \n"
        "   }, \n"
        "   \"subnet6\": [ \n"
        "   { \n"
        "       \"id\": 2,\n"
        "       \"subnet\": \"2001:2::/64\" \n"
        "   } ]\n"
        "} \n";

    extractConfig(base_config);

    // Make a few subnets
    auto subnet1 = Subnet6::create(IOAddress("2001:1::"), 64, 1, 2, 100, 100, SubnetID(1));
    auto subnet3 = Subnet6::create(IOAddress("2001:3::"), 64, 1, 2, 100, 100, SubnetID(3));

    // Add subnet1 to db1 and subnet3 to db2
    db1_->createUpdateSubnet6(ServerSelector::ALL(), subnet1);
    db2_->createUpdateSubnet6(ServerSelector::ALL(), subnet3);

    // Should parse and merge without error.
    configure(base_config, CONTROL_RESULT_SUCCESS, "");

    // Verify the composite staging is correct.  (Remember that
    // CfgMgr::instance().commit() hasn't been called)

    SrvConfigPtr staging_cfg = CfgMgr::instance().getStagingCfg();

    CfgSubnets6Ptr subnets = staging_cfg->getCfgSubnets6();
    ConstSubnet6Ptr staged_subnet;

    // Subnet1 should have been added from db1 config
    staged_subnet = subnets->getBySubnetId(1);
    ASSERT_TRUE(staged_subnet);

    // Subnet2 should have come from the json config
    staged_subnet = subnets->getBySubnetId(2);
    ASSERT_TRUE(staged_subnet);

    // Subnet3, which is in db2 should not have been merged, since it is
    // first found, first used?
    staged_subnet = subnets->getBySubnetId(3);
    ASSERT_FALSE(staged_subnet);
}

}
