// Copyright (C) 2014-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 <cc/data.h>
#include <dhcp/classify.h>
#include <dhcp/dhcp6.h>
#include <dhcp/option.h>
#include <dhcp/option_string.h>
#include <dhcp/testutils/iface_mgr_test_config.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/cfg_shared_networks.h>
#include <dhcpsrv/cfg_subnets6.h>
#include <dhcpsrv/iterative_allocator.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <dhcpsrv/parsers/dhcp_parsers.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/subnet_id.h>

#include <dhcpsrv/subnet_selector.h>
#include <dhcpsrv/cfg_hosts.h>
#include <stats/stats_mgr.h>
#include <testutils/gtest_utils.h>
#include <testutils/log_utils.h>
#include <testutils/test_to_element.h>
#include <util/doubles.h>

#include <boost/range/adaptor/reversed.hpp>
#include <gtest/gtest.h>
#include <string>

using namespace isc;
using namespace isc::asiolink;
using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::dhcp::test;
using namespace isc::stats;
using namespace isc::test;
using namespace isc::util;

namespace {

/// @brief An allocator recording calls to @c initAfterConfigure.
class InitRecordingAllocator : public IterativeAllocator {
public:

    /// @brief Constructor.
    ///
    /// @param type specifies the type of allocated leases.
    /// @param subnet weak pointer to the subnet owning the allocator.
    InitRecordingAllocator(Lease::Type type, const WeakSubnetPtr& subnet)
        : IterativeAllocator(type, subnet), callcount_(0) {
    }

    /// @brief Increases the call count of this function.
    ///
    /// The call count can be later examined to check whether or not
    /// the function was called.
    virtual void initAfterConfigureInternal() {
        ++callcount_;
    };

    /// @brief Call count of the @c initAllocatorsAfterConfigure.
    int callcount_;
};

/// @brief Generates interface id option.
///
/// @param text Interface id in a textual format.
OptionPtr
generateInterfaceId(const std::string& text) {
    OptionBuffer buffer(text.begin(), text.end());
    return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, buffer));
}

/// @brief Verifies that a set of subnets contains a given a subnet
///
/// @param cfg_subnets set of subnets in which to look
/// @param exp_subnet_id expected id of the target subnet
/// @param prefix prefix of the target subnet
/// @param exp_valid expected valid lifetime of the subnet
/// @param exp_network  pointer to the subnet's shared-network (if one)
void checkMergedSubnet(CfgSubnets6& cfg_subnets,
                       const std::string& prefix,
                       const SubnetID exp_subnet_id,
                       int exp_valid,
                       SharedNetwork6Ptr exp_network) {
    // Look for the network by prefix.
    auto subnet = cfg_subnets.getByPrefix(prefix);
    ASSERT_TRUE(subnet) << "subnet: " << prefix << " not found";

    // Make sure we have the one we expect.
    ASSERT_EQ(exp_subnet_id, subnet->getID()) << "subnet ID is wrong";
    ASSERT_EQ(exp_valid, subnet->getValid().get()) << "subnetID:"
              << subnet->getID() << ", subnet valid time is wrong";

    SharedNetwork6Ptr shared_network;
    subnet->getSharedNetwork(shared_network);
    if (exp_network) {
        ASSERT_TRUE(shared_network)
            << " expected network: " << exp_network->getName() << " not found";
        ASSERT_TRUE(shared_network == exp_network) << " networks do no match";
    } else {
        ASSERT_FALSE(shared_network) << " unexpected network assignment: "
            << shared_network->getName();
    }
}

// This test verifies that specific subnet can be retrieved by specifying
// subnet identifier or subnet prefix.
TEST(CfgSubnets6Test, getSpecificSubnet) {
    CfgSubnets6 cfg;

    // Create 3 subnets.
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4,
                                   SubnetID(5)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4,
                                   SubnetID(8)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4,
                                   SubnetID(10)));

    // Store the subnets in a vector to make it possible to loop over
    // all configured subnets.
    std::vector<Subnet6Ptr> subnets;
    subnets.push_back(subnet1);
    subnets.push_back(subnet2);
    subnets.push_back(subnet3);

    // Add all subnets to the configuration.
    for (auto const& subnet : subnets) {
        ASSERT_NO_THROW(cfg.add(subnet)) << "failed to add subnet with id: "
            << subnet->getID();
    }

    // Iterate over all subnets and make sure they can be retrieved by
    // subnet identifier.
    for (auto const& subnet : boost::adaptors::reverse(subnets)) {
        ConstSubnet6Ptr subnet_returned = cfg.getBySubnetId(subnet->getID());
        ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: "
            << subnet->getID();
        EXPECT_EQ(subnet->getID(), subnet_returned->getID());
        EXPECT_EQ(subnet->toText(), subnet_returned->toText());
    }

    // Repeat the previous test, but this time retrieve subnets by their
    // prefixes.
    for (auto const& subnet : boost::adaptors::reverse(subnets)) {
        ConstSubnet6Ptr subnet_returned = cfg.getByPrefix(subnet->toText());
        ASSERT_TRUE(subnet_returned) << "failed to return subnet with id: "
            << subnet->getID();
        EXPECT_EQ(subnet->getID(), subnet_returned->getID());
        EXPECT_EQ(subnet->toText(), subnet_returned->toText());
    }

    // Make sure that null pointers are returned for non-existing subnets.
    EXPECT_FALSE(cfg.getBySubnetId(SubnetID(123)));
    EXPECT_FALSE(cfg.getByPrefix("3000::/16"));
}

// This test verifies that a single subnet can be removed from the configuration.
TEST(CfgSubnets6Test, deleteSubnet) {
    CfgSubnets6 cfg;

    // Create 3 subnets.
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));

    ASSERT_NO_THROW(cfg.add(subnet1));
    ASSERT_NO_THROW(cfg.add(subnet2));
    ASSERT_NO_THROW(cfg.add(subnet3));

    // There should be three subnets.
    ASSERT_EQ(3, cfg.getAll()->size());
    // We're going to remove the subnet #2. Let's make sure it exists before
    // we remove it.
    ASSERT_TRUE(cfg.getByPrefix("2001:db8:2::/48"));

    // Remove the subnet and make sure it is gone.
    ASSERT_NO_THROW(cfg.del(subnet2));
    ASSERT_EQ(2, cfg.getAll()->size());
    EXPECT_FALSE(cfg.getByPrefix("2001:db8:2::/48"));

    // Remove another subnet by ID.
    ASSERT_NO_THROW(cfg.del(subnet1->getID()));
    ASSERT_EQ(1, cfg.getAll()->size());
    EXPECT_FALSE(cfg.getByPrefix("2001:db8:1::/48"));
}

// This test verifies that replace a subnet works as expected.
TEST(CfgSubnets6Test, replaceSubnet) {
    CfgSubnets6 cfg;

    // Create 3 subnets.
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
                                   48, 1, 2, 3, 100, SubnetID(10)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
                                   48, 1, 2, 3, 100, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
                                   48, 1, 2, 3, 100, SubnetID(13)));

    ASSERT_NO_THROW(cfg.add(subnet1));
    ASSERT_NO_THROW(cfg.add(subnet2));
    ASSERT_NO_THROW(cfg.add(subnet3));

    // There should be three subnets.
    ASSERT_EQ(3, cfg.getAll()->size());
    // We're going to replace  the subnet #2. Let's make sure it exists before
    // we replace it.
    ASSERT_TRUE(cfg.getByPrefix("2001:db8:2::/48"));

    // Replace the subnet and make sure it was updated.
    Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:2::"),
                                  48, 10, 20, 30, 1000,  SubnetID(2)));
    Subnet6Ptr replaced = cfg.replace(subnet);
    ASSERT_TRUE(replaced);
    EXPECT_TRUE(replaced == subnet2);
    ASSERT_EQ(3, cfg.getAll()->size());
    Subnet6Ptr returned = cfg.getSubnet(SubnetID(2));
    ASSERT_TRUE(returned);
    EXPECT_TRUE(returned == subnet);

    // Restore.
    replaced = cfg.replace(replaced);
    ASSERT_TRUE(replaced);
    EXPECT_TRUE(replaced == subnet);
    ASSERT_EQ(3, cfg.getAll()->size());
    returned = cfg.getSubnet(SubnetID(2));
    ASSERT_TRUE(returned);
    EXPECT_TRUE(returned == subnet2);

    // Prefix conflict returns null.
    subnet.reset(new Subnet6(IOAddress("2001:db8:3::"),
                             48, 10, 20, 30, 1000,  SubnetID(2)));
    replaced = cfg.replace(subnet);
    EXPECT_FALSE(replaced);
    returned = cfg.getSubnet(SubnetID(2));
    ASSERT_TRUE(returned);
    EXPECT_TRUE(returned == subnet2);

    // Changing prefix works even it is highly not recommended.
    subnet.reset(new Subnet6(IOAddress("2001:db8:10::"),
                             48, 10, 20, 30, 1000,  SubnetID(2)));
    replaced = cfg.replace(subnet);
    ASSERT_TRUE(replaced);
    EXPECT_TRUE(replaced == subnet2);
    returned = cfg.getSubnet(SubnetID(2));
    ASSERT_TRUE(returned);
    EXPECT_TRUE(returned == subnet);
}

// This test checks that the subnet can be selected using a relay agent's
// link address.
TEST(CfgSubnets6Test, selectSubnetByRelayAddress) {
    CfgSubnets6 cfg;

    // Let's configure 3 subnets
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));
    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    // Make sure that none of the subnets is selected when there is no relay
    // information configured for them.
    SubnetSelector selector;
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Now specify relay information.
    subnet1->addRelayAddress(IOAddress("2001:db8:ff::1"));
    subnet2->addRelayAddress(IOAddress("2001:db8:ff::2"));
    subnet3->addRelayAddress(IOAddress("2001:db8:ff::3"));

    // And try again. This time relay-info is there and should match.
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
}

// This test checks that subnet can be selected using a relay agent's
// link address specified on the shared network level.
TEST(CfgSubnets6Test, selectSubnetByNetworkRelayAddress) {
    // Create 2 shared networks.
    SharedNetwork6Ptr network1(new SharedNetwork6("net1"));
    SharedNetwork6Ptr network2(new SharedNetwork6("net2"));
    SharedNetwork6Ptr network3(new SharedNetwork6("net3"));

    // Let's configure 3 subnets
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));

    // Allow subnet class of clients to use the subnets.
    subnet1->allowClientClass("subnet");
    subnet2->allowClientClass("subnet");
    subnet3->allowClientClass("subnet");

    // Associate subnets with shared networks.
    network1->add(subnet1);
    network2->add(subnet2);
    network3->add(subnet3);

    // Add subnets to the configuration.
    CfgSubnets6 cfg;

    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    // Make sure that none of the subnets is selected when there is no relay
    // information configured for them.
    SubnetSelector selector;
    selector.client_classes_.insert("subnet");

    // The relays are not set for networks, so none of the subnets should
    // be selected.
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Now specify relay information.
    network1->addRelayAddress(IOAddress("2001:db8:ff::1"));
    network2->addRelayAddress(IOAddress("2001:db8:ff::2"));
    network3->addRelayAddress(IOAddress("2001:db8:ff::3"));

    // And try again. This time relay-info is there and should match.
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));

    // Modify the client classes associated with the first two subnets.
    subnet1->getMutableClientClasses().clear();
    subnet1->allowClientClass("subnet1");
    subnet2->getMutableClientClasses().clear();
    subnet2->allowClientClass("subnet2");

    // This time the non-matching classes should prevent selection.
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::1");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::2");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("2001:db8:ff::3");
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
}

// This test checks that the subnet can be selected using an interface
// name associated with a asubnet.
TEST(CfgSubnets6Test, selectSubnetByInterfaceName) {
    CfgSubnets6 cfg;

    // Let's create 3 subnets.
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));
    subnet1->setIface("foo");
    subnet2->setIface("bar");
    subnet3->setIface("foobar");

    // Until subnets are added to the configuration, there should be nothing
    // returned.
    SubnetSelector selector;
    selector.iface_name_ = "foo";
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Add one of the subnets.
    cfg.add(subnet1);

    // The subnet should be now selected for the interface name "foo".
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));

    // Check that the interface name is checked even when there is
    // only one subnet defined: there should be nothing returned when
    // other interface name is specified.
    selector.iface_name_ = "bar";
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Add other subnets.
    cfg.add(subnet2);
    cfg.add(subnet3);

    // When we specify correct interface names, the subnets should be returned.
    selector.iface_name_ = "foobar";
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
    selector.iface_name_ = "bar";
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));

    // When specifying a non-existing interface the subnet should not be
    // returned.
    selector.iface_name_ = "xyzzy";
    EXPECT_FALSE(cfg.selectSubnet(selector));
}

// This test checks that the subnet can be selected using an Interface ID
// option inserted by a relay.
TEST(CfgSubnets6Test, selectSubnetByInterfaceId) {
    CfgSubnets6 cfg;

    // Create 3 subnets.
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));

    // Create Interface-id options used in subnets 1,2, and 3
    OptionPtr ifaceid1 = generateInterfaceId("relay1.eth0");
    OptionPtr ifaceid2 = generateInterfaceId("VL32");
    // That's a strange interface-id, but this is a real life example
    OptionPtr ifaceid3 = generateInterfaceId("ISAM144|299|ipv6|nt:vp:1:110");

    // Bogus interface-id.
    OptionPtr ifaceid_bogus = generateInterfaceId("non-existent");

    // Assign interface ids to the respective subnets.
    subnet1->setInterfaceId(ifaceid1);
    subnet2->setInterfaceId(ifaceid2);
    subnet3->setInterfaceId(ifaceid3);

    // There shouldn't be any subnet configured at this stage.
    SubnetSelector selector;
    selector.interface_id_ = ifaceid1;
    // Note that some link address must be specified to indicate that it is
    // a relayed message!
    selector.first_relay_linkaddr_ = IOAddress("5000::1");
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Add one of the subnets.
    cfg.add(subnet1);

    // If only one subnet has been specified, it should be returned when the
    // interface id matches. But, for a different interface id there should be
    // no match.
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.interface_id_ = ifaceid2;
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Add other subnets.
    cfg.add(subnet2);
    cfg.add(subnet3);

    // Now that we have all subnets added. we should be able to retrieve them
    // using appropriate interface ids.
    selector.interface_id_ = ifaceid3;
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));
    selector.interface_id_ = ifaceid2;
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));

    // For invalid interface id, there should be nothing returned.
    selector.interface_id_ = ifaceid_bogus;
    EXPECT_FALSE(cfg.selectSubnet(selector));
}

// Test that the client classes are considered when the subnet is selected by
// the relay link address.
TEST(CfgSubnets6Test, selectSubnetByRelayAddressAndClassify) {
    CfgSubnets6 cfg;

    // Let's configure 3 subnets
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));
    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    // Let's sanity check that we can use that configuration.
    SubnetSelector selector;
    selector.first_relay_linkaddr_ = IOAddress("2000::123");
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("3000::345");
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("4000::567");
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));

    // Client now belongs to bar class.
    selector.client_classes_.insert("bar");

    // There are no class restrictions defined, so everything should work
    // as before.
    selector.first_relay_linkaddr_ = IOAddress("2000::123");
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("3000::345");
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("4000::567");
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));

    // Now let's add client class restrictions.
    subnet1->allowClientClass("foo"); // Serve here only clients from foo class
    subnet2->allowClientClass("bar"); // Serve here only clients from bar class
    subnet3->allowClientClass("baz"); // Serve here only clients from baz class

    // The same check as above should result in client being served only in
    // bar class, i.e. subnet2
    selector.first_relay_linkaddr_ = IOAddress("2000::123");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("3000::345");
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("4000::567");
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Now let's check that client with wrong class is not supported
    selector.client_classes_.clear();
    selector.client_classes_.insert("some_other_class");
    selector.first_relay_linkaddr_ = IOAddress("2000::123");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("3000::345");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("4000::567");
    EXPECT_FALSE(cfg.selectSubnet(selector));

    // Finally, let's check that client without any classes is not supported
    selector.client_classes_.clear();
    selector.first_relay_linkaddr_ = IOAddress("2000::123");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("3000::345");
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.first_relay_linkaddr_ = IOAddress("4000::567");
    EXPECT_FALSE(cfg.selectSubnet(selector));
}

// Test that client classes are considered when the subnet is selected by the
// interface name.
TEST(CfgSubnets6Test, selectSubnetByInterfaceNameAndClassify) {
    CfgSubnets6 cfg;

    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));
    subnet1->setIface("foo");
    subnet2->setIface("bar");
    subnet3->setIface("foobar");

    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    // Now we have only one subnet, any request will be served from it
    SubnetSelector selector;
    selector.client_classes_.insert("bar");
    selector.iface_name_ = "foo";
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.iface_name_ = "bar";
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.iface_name_ = "foobar";
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));

    subnet1->allowClientClass("foo"); // Serve here only clients from foo class
    subnet2->allowClientClass("bar"); // Serve here only clients from bar class
    subnet3->allowClientClass("baz"); // Serve here only clients from baz class

    selector.iface_name_ = "foo";
    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.iface_name_ = "bar";
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.iface_name_ = "foobar";
    EXPECT_FALSE(cfg.selectSubnet(selector));
}

// Test that client classes are considered when the interface is selected by
// the interface id.
TEST(CfgSubnets6Test, selectSubnetByInterfaceIdAndClassify) {
    CfgSubnets6 cfg;

    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"),
                                   48, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"),
                                   48, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"),
                                   48, 1, 2, 3, 4, SubnetID(3)));

    // interface-id options used in subnets 1,2, and 3
    OptionPtr ifaceid1 = generateInterfaceId("relay1.eth0");
    OptionPtr ifaceid2 = generateInterfaceId("VL32");
    // That's a strange interface-id, but this is a real life example
    OptionPtr ifaceid3 = generateInterfaceId("ISAM144|299|ipv6|nt:vp:1:110");

    // bogus interface-id
    OptionPtr ifaceid_bogus = generateInterfaceId("non-existent");

    subnet1->setInterfaceId(ifaceid1);
    subnet2->setInterfaceId(ifaceid2);
    subnet3->setInterfaceId(ifaceid3);

    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    // If we have only a single subnet and the request came from a local
    // address, let's use that subnet
    SubnetSelector selector;
    selector.first_relay_linkaddr_ = IOAddress("5000::1");
    selector.client_classes_.insert("bar");
    selector.interface_id_ = ifaceid1;
    EXPECT_EQ(subnet1, cfg.selectSubnet(selector));
    selector.interface_id_ = ifaceid2;
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.interface_id_ = ifaceid3;
    EXPECT_EQ(subnet3, cfg.selectSubnet(selector));

    subnet1->allowClientClass("foo"); // Serve here only clients from foo class
    subnet2->allowClientClass("bar"); // Serve here only clients from bar class
    subnet3->allowClientClass("baz"); // Serve here only clients from baz class

    EXPECT_FALSE(cfg.selectSubnet(selector));
    selector.interface_id_ = ifaceid2;
    EXPECT_EQ(subnet2, cfg.selectSubnet(selector));
    selector.interface_id_ = ifaceid3;
    EXPECT_FALSE(cfg.selectSubnet(selector));
}

// Checks that detection of duplicated subnet IDs works as expected. It should
// not be possible to add two IPv6 subnets holding the same ID.
TEST(CfgSubnets6Test, duplication) {
    CfgSubnets6 cfg;

    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4, 123));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4, 124));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4, 123));
    Subnet6Ptr subnet4(new Subnet6(IOAddress("2000::1"), 48, 1, 2, 3, 4, 125));

    ASSERT_NO_THROW(cfg.add(subnet1));
    EXPECT_NO_THROW(cfg.add(subnet2));
    // Subnet 3 has the same ID as subnet 1. It shouldn't be able to add it.
    EXPECT_THROW(cfg.add(subnet3), isc::dhcp::DuplicateSubnetID);
    // Subnet 4 has a similar but different subnet as subnet 1.
    EXPECT_NO_THROW(cfg.add(subnet4));
}

// This test check if IPv6 subnets can be unparsed in a predictable way,
TEST(CfgSubnets6Test, unparseSubnet) {
    CfgSubnets6 cfg;

    // Add some subnets.
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"),
                                   48, 1, 2, 3, 4, 123));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"),
                                   48, 1, 2, 3, 4, 124));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"),
                                   48, 1, 2, 3, 4, 125));

    OptionPtr ifaceid = generateInterfaceId("relay.eth0");
    subnet1->setInterfaceId(ifaceid);
    subnet1->allowClientClass("foo");
    subnet1->allowClientClass("bar");

    subnet1->setT1Percent(0.45);
    subnet1->setT2Percent(0.70);
    subnet1->setCacheThreshold(0.20);

    subnet2->setIface("lo");
    subnet2->addRelayAddress(IOAddress("2001:db8:ff::2"));
    subnet2->setValid(Triplet<uint32_t>(200));
    subnet2->setPreferred(Triplet<uint32_t>(100));
    subnet2->setStoreExtendedInfo(true);
    subnet2->setCacheMaxAge(80);

    subnet3->setIface("eth1");
    subnet3->addAdditionalClass("foo");
    subnet3->addAdditionalClass("bar");
    subnet3->setReservationsGlobal(false);
    subnet3->setReservationsInSubnet(true);
    subnet3->setReservationsOutOfPool(false);
    subnet3->setRapidCommit(false);
    subnet3->setCalculateTeeTimes(true);
    subnet3->setT1Percent(0.50);
    subnet3->setT2Percent(0.65);
    subnet3->setValid(Triplet<uint32_t>(100, 200, 300));
    subnet3->setPreferred(Triplet<uint32_t>(50, 100, 150));
    subnet3->setDdnsSendUpdates(true);
    subnet3->setDdnsOverrideNoUpdate(true);
    subnet3->setDdnsOverrideClientUpdate(true);
    subnet3->setDdnsReplaceClientNameMode(D2ClientConfig::RCM_ALWAYS);
    subnet3->setDdnsGeneratedPrefix("prefix");
    subnet3->setDdnsQualifyingSuffix("example.com.");
    subnet3->setHostnameCharSet("[^A-Z]");
    subnet3->setHostnameCharReplacement("x");

    data::ElementPtr ctx1 = data::Element::fromJSON("{ \"comment\": \"foo\" }");
    subnet1->setContext(ctx1);
    data::ElementPtr ctx2 = data::Element::createMap();
    subnet2->setContext(ctx2);

    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    // Unparse
    std::string expected = "[\n"
        "{\n"
        "    \"id\": 123,\n"
        "    \"subnet\": \"2001:db8:1::/48\",\n"
        "    \"t1-percent\": 0.45,"
        "    \"t2-percent\": 0.7,"
        "    \"cache-threshold\": .20,\n"
        "    \"interface-id\": \"relay.eth0\",\n"
        "    \"renew-timer\": 1,\n"
        "    \"rebind-timer\": 2,\n"
        "    \"relay\": { \"ip-addresses\": [ ] },\n"
        "    \"preferred-lifetime\": 3,\n"
        "    \"min-preferred-lifetime\": 3,\n"
        "    \"max-preferred-lifetime\": 3,\n"
        "    \"valid-lifetime\": 4,\n"
        "    \"min-valid-lifetime\": 4,\n"
        "    \"max-valid-lifetime\": 4,\n"
        "    \"client-classes\": [ \"foo\", \"bar\" ],\n"
        "    \"pools\": [ ],\n"
        "    \"pd-pools\": [ ],\n"
        "    \"option-data\": [ ],\n"
        "    \"user-context\": { \"comment\": \"foo\" }\n"
        "},{\n"
        "    \"id\": 124,\n"
        "    \"subnet\": \"2001:db8:2::/48\",\n"
        "    \"interface\": \"lo\",\n"
        "    \"renew-timer\": 1,\n"
        "    \"rebind-timer\": 2,\n"
        "    \"relay\": { \"ip-addresses\": [ \"2001:db8:ff::2\" ] },\n"
        "    \"preferred-lifetime\": 100,\n"
        "    \"min-preferred-lifetime\": 100,\n"
        "    \"max-preferred-lifetime\": 100,\n"
        "    \"valid-lifetime\": 200,\n"
        "    \"min-valid-lifetime\": 200,\n"
        "    \"max-valid-lifetime\": 200,\n"
        "    \"user-context\": { },\n"
        "    \"pools\": [ ],\n"
        "    \"pd-pools\": [ ],\n"
        "    \"option-data\": [ ],\n"
        "    \"store-extended-info\": true,\n"
        "    \"cache-max-age\": 80\n"
        "},{\n"
        "    \"id\": 125,\n"
        "    \"subnet\": \"2001:db8:3::/48\",\n"
        "    \"interface\": \"eth1\",\n"
        "    \"renew-timer\": 1,\n"
        "    \"rebind-timer\": 2,\n"
        "    \"relay\": { \"ip-addresses\": [ ] },\n"
        "    \"preferred-lifetime\": 100,\n"
        "    \"min-preferred-lifetime\": 50,\n"
        "    \"max-preferred-lifetime\": 150,\n"
        "    \"valid-lifetime\": 200,\n"
        "    \"min-valid-lifetime\": 100,\n"
        "    \"max-valid-lifetime\": 300,\n"
        "    \"rapid-commit\": false,\n"
        "    \"reservations-global\": false,\n"
        "    \"reservations-in-subnet\": true,\n"
        "    \"reservations-out-of-pool\": false,\n"
        "    \"pools\": [ ],\n"
        "    \"pd-pools\": [ ],\n"
        "    \"option-data\": [ ],\n"
        "    \"evaluate-additional-classes\": [ \"foo\", \"bar\" ],\n"
        "    \"calculate-tee-times\": true,\n"
        "    \"t1-percent\": 0.50,\n"
        "    \"t2-percent\": 0.65,\n"
        "    \"ddns-generated-prefix\": \"prefix\",\n"
        "    \"ddns-override-client-update\": true,\n"
        "    \"ddns-override-no-update\": true,\n"
        "    \"ddns-qualifying-suffix\": \"example.com.\",\n"
        "    \"ddns-replace-client-name\": \"always\",\n"
        "    \"ddns-send-updates\": true,\n"
        "    \"hostname-char-replacement\": \"x\",\n"
        "    \"hostname-char-set\": \"[^A-Z]\"\n"
        "} ]\n";

    runToElementTest<CfgSubnets6>(expected, cfg);
}

// This test check if IPv6 pools can be unparsed in a predictable way,
TEST(CfgSubnets6Test, unparsePool) {
    CfgSubnets6 cfg;

    // Add a subnet with pools
    Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"),
                                  48, 1, 2, 3, 4, 123));
    Pool6Ptr pool1(new Pool6(Lease::TYPE_NA,
                             IOAddress("2001:db8:1::100"),
                             IOAddress("2001:db8:1::199")));
    Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
    pool2->allowClientClass("bar");

    std::string json1 = "{ \"comment\": \"foo\", \"version\": 1 }";
    data::ElementPtr ctx1 = data::Element::fromJSON(json1);
    pool1->setContext(ctx1);
    data::ElementPtr ctx2 = data::Element::fromJSON("{ \"foo\": \"bar\" }");
    pool2->setContext(ctx2);
    pool2->addAdditionalClass("foo");

    subnet->addPool(pool1);
    subnet->addPool(pool2);
    cfg.add(subnet);

    // Unparse
    std::string expected = "[\n"
        "{\n"
        "    \"id\": 123,\n"
        "    \"subnet\": \"2001:db8:1::/48\",\n"
        "    \"renew-timer\": 1,\n"
        "    \"rebind-timer\": 2,\n"
        "    \"relay\": { \"ip-addresses\": [ ] },\n"
        "    \"preferred-lifetime\": 3,\n"
        "    \"min-preferred-lifetime\": 3,\n"
        "    \"max-preferred-lifetime\": 3,\n"
        "    \"valid-lifetime\": 4,\n"
        "    \"min-valid-lifetime\": 4,\n"
        "    \"max-valid-lifetime\": 4,\n"
        "    \"pools\": [\n"
        "        {\n"
        "            \"pool\": \"2001:db8:1::100-2001:db8:1::199\",\n"
        "            \"user-context\": { \"version\": 1,\n"
        "                                \"comment\": \"foo\" },\n"
        "            \"option-data\": [ ]\n"
        "        },{\n"
        "            \"pool\": \"2001:db8:1:1::/64\",\n"
        "            \"user-context\": { \"foo\": \"bar\" },\n"
        "            \"option-data\": [ ],\n"
        "            \"client-classes\": [ \"bar\" ],\n"
        "            \"evaluate-additional-classes\": [ \"foo\" ]\n"
        "        }\n"
        "    ],\n"
        "    \"pd-pools\": [ ],\n"
        "    \"option-data\": [ ]\n"
        "} ]\n";
    runToElementTest<CfgSubnets6>(expected, cfg);
}

// This test check if IPv6 prefix delegation pools can be unparsed
// in a predictable way,
TEST(CfgSubnets6Test, unparsePdPool) {
    CfgSubnets6 cfg;

    // Add a subnet with pd-pools
    Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"),
                                  48, 1, 2, 3, 4, 123));
    Pool6Ptr pdpool1(new Pool6(Lease::TYPE_PD,
                               IOAddress("2001:db8:2::"), 48, 64));
    Pool6Ptr pdpool2(new Pool6(IOAddress("2001:db8:3::"), 48, 56,
                               IOAddress("2001:db8:3::"), 64));
    pdpool2->allowClientClass("bar");

    data::ElementPtr ctx1 = data::Element::fromJSON("{ \"foo\": [ \"bar\" ] }");
    pdpool1->setContext(ctx1);
    pdpool1->addAdditionalClass("bar");
    pdpool2->allowClientClass("bar");

    subnet->addPool(pdpool1);
    subnet->addPool(pdpool2);
    cfg.add(subnet);

    // Unparse
    std::string expected = "[\n"
        "{\n"
        "    \"id\": 123,\n"
        "    \"subnet\": \"2001:db8:1::/48\",\n"
        "    \"renew-timer\": 1,\n"
        "    \"rebind-timer\": 2,\n"
        "    \"relay\": { \"ip-addresses\": [ ] },\n"
        "    \"preferred-lifetime\": 3,\n"
        "    \"min-preferred-lifetime\": 3,\n"
        "    \"max-preferred-lifetime\": 3,\n"
        "    \"valid-lifetime\": 4,\n"
        "    \"min-valid-lifetime\": 4,\n"
        "    \"max-valid-lifetime\": 4,\n"
        "    \"pools\": [ ],\n"
        "    \"pd-pools\": [\n"
        "        {\n"
        "            \"prefix\": \"2001:db8:2::\",\n"
        "            \"prefix-len\": 48,\n"
        "            \"delegated-len\": 64,\n"
        "            \"user-context\": { \"foo\": [ \"bar\" ] },\n"
        "            \"option-data\": [ ],\n"
        "            \"evaluate-additional-classes\": [ \"bar\" ]\n"
        "        },{\n"
        "            \"prefix\": \"2001:db8:3::\",\n"
        "            \"prefix-len\": 48,\n"
        "            \"delegated-len\": 56,\n"
        "            \"excluded-prefix\": \"2001:db8:3::\",\n"
        "            \"excluded-prefix-len\": 64,\n"
        "            \"option-data\": [ ],\n"
        "            \"client-classes\": [ \"bar\" ]\n"
        "        }\n"
        "    ],\n"
        "    \"option-data\": [ ]\n"
        "} ]\n";
    runToElementTest<CfgSubnets6>(expected, cfg);
}

// This test verifies that it is possible to retrieve a subnet using subnet-id.
TEST(CfgSubnets6Test, getSubnet) {
    CfgSubnets6 cfg;

    // Let's configure 3 subnets
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 100));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4, 200));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4, 300));
    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    EXPECT_EQ(subnet1, cfg.getSubnet(100));
    EXPECT_EQ(subnet2, cfg.getSubnet(200));
    EXPECT_EQ(subnet3, cfg.getSubnet(300));
    EXPECT_EQ(Subnet6Ptr(), cfg.getSubnet(400)); // no such subnet
}

// This test verifies that subnets configuration is properly merged.
TEST(CfgSubnets6Test, mergeSubnets) {
    // Create custom options dictionary for testing merge. We're keeping it
    // simple because they are more rigorous tests elsewhere.
    CfgOptionDefPtr cfg_def(new CfgOptionDef());
    cfg_def->add((OptionDefinitionPtr(new OptionDefinition("one", 1, "isc", "string"))));

    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:1::"),
                                   64, 1, 2, 100, 100, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:2::"),
                                   64, 1, 2, 100, 100, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:3::"),
                                   64, 1, 2, 100, 100, SubnetID(3)));
    Subnet6Ptr subnet4(new Subnet6(IOAddress("2001:4::"),
                                   64, 1, 2, 100, 100, SubnetID(4)));

    // Create the "existing" list of shared networks
    CfgSharedNetworks6Ptr networks(new CfgSharedNetworks6());
    SharedNetwork6Ptr shared_network1(new SharedNetwork6("shared-network1"));
    networks->add(shared_network1);
    SharedNetwork6Ptr shared_network2(new SharedNetwork6("shared-network2"));
    networks->add(shared_network2);

    // Empty network pointer.
    SharedNetwork6Ptr no_network;

    // Add Subnets 1, 2 and 4 to shared networks.
    ASSERT_NO_THROW(shared_network1->add(subnet1));
    ASSERT_NO_THROW(shared_network2->add(subnet2));
    ASSERT_NO_THROW(shared_network2->add(subnet4));

    // Create our "existing" configured subnets.
    CfgSubnets6 cfg_to;
    ASSERT_NO_THROW(cfg_to.add(subnet1));
    ASSERT_NO_THROW(cfg_to.add(subnet2));
    ASSERT_NO_THROW(cfg_to.add(subnet3));
    ASSERT_NO_THROW(cfg_to.add(subnet4));

    // Merge in an "empty" config. Should have the original config,
    // still intact.
    CfgSubnets6 cfg_from;
    ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from));

    // We should have all four subnets, with no changes.
    ASSERT_EQ(4, cfg_to.getAll()->size());

    // Should be no changes to the configuration.
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:1::/64",
                                              SubnetID(1), 100, shared_network1));
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:2::/64",
                                              SubnetID(2), 100, shared_network2));
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:3::/64",
                                              SubnetID(3), 100, no_network));
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:4::/64",
                                              SubnetID(4), 100, shared_network2));

    // Fill cfg_from configuration with subnets.
    // subnet 1b updates subnet 1 but leaves it in network 1 with the same ID.
    Subnet6Ptr subnet1b(new Subnet6(IOAddress("2001:10::"),
                                   64, 2, 3, 100, 400, SubnetID(1)));
    subnet1b->setSharedNetworkName("shared-network1");

    // Add generic option 1 to subnet 1b.
    std::string value("Yay!");
    OptionPtr option(new Option(Option::V6, 1));
    option->setData(value.begin(), value.end());
    ASSERT_NO_THROW(subnet1b->getCfgOption()->add(option, false, false, "isc"));

    // subnet 3b updates subnet 3 with different UD and removes it
    // from network 2
    Subnet6Ptr subnet3b(new Subnet6(IOAddress("2001:3::"),
                                   64, 3, 4, 100, 500, SubnetID(30)));

    // Now Add generic option 1 to subnet 3b.
    value = "Team!";
    option.reset(new Option(Option::V6, 1));
    option->setData(value.begin(), value.end());
    ASSERT_NO_THROW(subnet3b->getCfgOption()->add(option, false, false, "isc"));

    // subnet 4b updates subnet 4 and moves it from network2 to network 1
    Subnet6Ptr subnet4b(new Subnet6(IOAddress("2001:4::"),
                                   64, 3, 4, 100, 500, SubnetID(4)));
    subnet4b->setSharedNetworkName("shared-network1");

    // subnet 5 is new and belongs to network 2
    // Has two pools both with an option 1
    Subnet6Ptr subnet5(new Subnet6(IOAddress("2001:5::"),
                                   64, 1, 2, 100, 300, SubnetID(5)));
    subnet5->setSharedNetworkName("shared-network2");

    // Add pool 1
    Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:5::10"), IOAddress("2001:5::20")));
    value = "POOLS";
    option.reset(new Option(Option::V6, 1));
    option->setData(value.begin(), value.end());
    ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, false, "isc"));
    subnet5->addPool(pool);

    // Add pool 2
    pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("2001:6::"), 64));
    value ="RULE!";
    option.reset(new Option(Option::V6, 1));
    option->setData(value.begin(), value.end());
    ASSERT_NO_THROW(pool->getCfgOption()->add(option, false, false, "isc"));
    subnet5->addPool(pool);

    // Add subnets to the merge from config.
    ASSERT_NO_THROW(cfg_from.add(subnet1b));
    ASSERT_NO_THROW(cfg_from.add(subnet3b));
    ASSERT_NO_THROW(cfg_from.add(subnet4b));
    ASSERT_NO_THROW(cfg_from.add(subnet5));

    // Merge again.
    ASSERT_NO_THROW(cfg_to.merge(cfg_def, networks, cfg_from));
    ASSERT_EQ(5, cfg_to.getAll()->size());

    // The subnet1 should be replaced by subnet1b.
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:10::/64",
                                              SubnetID(1), 400, shared_network1));

    // Let's verify that our option is there and populated correctly.
    auto subnet = cfg_to.getByPrefix("2001:10::/64");
    auto desc = subnet->getCfgOption()->get("isc", 1);
    ASSERT_TRUE(desc.option_);
    OptionStringPtr opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
    ASSERT_TRUE(opstr);
    EXPECT_EQ("Yay!", opstr->getValue());

    // The subnet2 should not be affected because it was not present.
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:2::/64",
                                              SubnetID(2), 100, shared_network2));

    // subnet3 should be replaced by subnet3b and no longer assigned to a network.
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:3::/64",
                                              SubnetID(30), 500, no_network));
    // Let's verify that our option is there and populated correctly.
    subnet = cfg_to.getByPrefix("2001:3::/64");
    desc = subnet->getCfgOption()->get("isc", 1);
    ASSERT_TRUE(desc.option_);
    opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
    ASSERT_TRUE(opstr);
    EXPECT_EQ("Team!", opstr->getValue());
    EXPECT_TRUE(subnet->getCfgOption()->isEncapsulated());

    // subnet4 should be replaced by subnet4b and moved to network1.
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:4::/64",
                                              SubnetID(4), 500, shared_network1));

    // subnet5 should have been added to configuration.
    ASSERT_NO_FATAL_FAILURE(checkMergedSubnet(cfg_to, "2001:5::/64",
                                              SubnetID(5), 300, shared_network2));

    // Let's verify that both pools have the proper options.
    subnet = cfg_to.getByPrefix("2001:5::/64");
    const PoolPtr merged_pool = subnet->getPool(Lease::TYPE_NA, IOAddress("2001:5::10"));
    ASSERT_TRUE(merged_pool);
    desc = merged_pool->getCfgOption()->get("isc", 1);
    opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
    ASSERT_TRUE(opstr);
    EXPECT_EQ("POOLS", opstr->getValue());
    EXPECT_TRUE(merged_pool->getCfgOption()->isEncapsulated());

    const PoolPtr merged_pool2 = subnet->getPool(Lease::TYPE_PD, IOAddress("2001:1::"));
    ASSERT_TRUE(merged_pool2);
    desc = merged_pool2->getCfgOption()->get("isc", 1);
    opstr = boost::dynamic_pointer_cast<OptionString>(desc.option_);
    ASSERT_TRUE(opstr);
    EXPECT_EQ("RULE!", opstr->getValue());
    EXPECT_TRUE(merged_pool2->getCfgOption()->isEncapsulated());
}

// This test verifies the Subnet6 parser's validation logic for id parameter.
TEST(CfgSubnets6Test, idValidation) {
    {
        // id 0.
        SCOPED_TRACE("id 0");
        std::string json =
            "        {\n"
            "            \"id\": 0,\n"
            "            \"subnet\": \"2001:db8:1::/64\"\n"
            "        }\n";
        data::ElementPtr elems;
        ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
            << "invalid JSON:" << json  << "\n test is broken";
        std::string expected = "subnet configuration failed: ";
        expected += "The 'id' value (0) is not within ";
        expected += "expected range: (1 - 4294967294)";
        try {
            // Attempt to parse the configuration.
            Subnet6ConfigParser parser;
            Subnet6Ptr subnet = parser.parse(elems);
            ADD_FAILURE() << "expected exception";
        } catch (const std::exception& ex) {
            EXPECT_EQ(expected, ex.what());
        }
    }

    {
        // id 4294967295.
        SCOPED_TRACE("id 4294967295");
        std::string json =
            "        {\n"
            "            \"id\": 4294967295,\n"
            "            \"subnet\": \"2001:db8:1::/64\"\n"
            "        }\n";
        data::ElementPtr elems;
        ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
            << "invalid JSON:" << json  << "\n test is broken";
        std::string expected = "subnet configuration failed: ";
        expected += "The 'id' value (4294967295) is not within ";
        expected += "expected range: (1 - 4294967294)";
        try {
            // Attempt to parse the configuration.
            Subnet6ConfigParser parser;
            Subnet6Ptr subnet = parser.parse(elems);
            ADD_FAILURE() << "expected exception";
        } catch (const std::exception& ex) {
            EXPECT_EQ(expected, ex.what());
        }
    }

    {
        // id 1 (valid).
        SCOPED_TRACE("id 1");
        std::string json =
            "        {\n"
            "            \"id\": 1,\n"
            "            \"subnet\": \"2001:db8:1::/64\"\n"
            "        }\n";
        data::ElementPtr elems;
        ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
            << "invalid JSON:" << json  << "\n test is broken";
        try {
            // Attempt to parse the configuration.
            Subnet6ConfigParser parser;
            Subnet6Ptr subnet = parser.parse(elems);
            EXPECT_TRUE(subnet);
        } catch (const std::exception& ex) {
            ADD_FAILURE() << "unexpected failure " << ex.what();
        }
    }
}

// This test verifies the Subnet6 parser's validation logic for
// t1-percent and t2-percent parameters.
TEST(CfgSubnets6Test, teeTimePercentValidation) {

    // Describes a single test scenario.
    struct Scenario {
        std::string label;         // label used for logging test failures
        bool calculate_tee_times;  // value of calculate-tee-times parameter
        double t1_percent;         // value of t1-percent parameter
        double t2_percent;         // value of t2-percent parameter
        std::string error_message; // expected error message is parsing should fail
    };

    // Test Scenarios.
    std::vector<Scenario> tests = {
        {"off and valid", false, .5, .95, ""},
        {"on and valid", true, .5, .95, ""},
        {"t2_negative", true, .5, -.95,
         "subnet configuration failed: t2-percent:"
         "  -0.95 is invalid, it must be greater than 0.0 and less than 1.0"
        },
        {"t2_too_big", true, .5, 1.95,
         "subnet configuration failed: t2-percent:"
         "  1.95 is invalid, it must be greater than 0.0 and less than 1.0"
        },
        {"t1_negative", true, -.5, .95,
         "subnet configuration failed: t1-percent:"
         "  -0.5 is invalid it must be greater than 0.0 and less than 1.0"
        },
        {"t1_too_big", true, 1.5, .95,
         "subnet configuration failed: t1-percent:"
         "  1.5 is invalid it must be greater than 0.0 and less than 1.0"
        },
        {"t1_bigger_than_t2", true, .85, .45,
         "subnet configuration failed: t1-percent:"
         "  0.85 is invalid, it must be less than t2-percent: 0.45"
        }
    };

    // First we create a set of elements that provides all
    // required for a Subnet6.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/64\", \n"
        "            \"interface\": \"\", \n"
        "            \"renew-timer\": 100, \n"
        "            \"rebind-timer\": 200, \n"
        "            \"valid-lifetime\": 300, \n"
        "            \"client-classes\": [], \n"
        "            \"evaluate-additional-classes\": [], \n"
        "            \"reservations-global\": false, \n"
        "            \"reservations-in-subnet\": true, \n"
        "            \"reservations-out-of-pool\": false \n"
        "        }";


    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
                    << "invalid JSON:" << json << "\n test is broken";

    // Iterate over the test scenarios, verifying each prescribed
    // outcome.
    for (auto const& test : tests) {
        {
            SCOPED_TRACE("test: " + test.label);

            // Set this scenario's configuration parameters
            elems->set("calculate-tee-times", data::Element::create(test.calculate_tee_times));
            elems->set("t1-percent", data::Element::create(test.t1_percent));
            elems->set("t2-percent", data::Element::create(test.t2_percent));

            Subnet6Ptr subnet;
            try {
                // Attempt to parse the configuration.
                Subnet6ConfigParser parser;
                subnet = parser.parse(elems);
            } catch (const std::exception& ex) {
                if (!test.error_message.empty()) {
                    // We expected a failure, did we fail the correct way?
                    EXPECT_EQ(test.error_message, ex.what());
                } else {
                    // Should not have failed.
                    ADD_FAILURE() << "Scenario should not have failed: " << ex.what();
                }

                // Either way we're done with this scenario.
                continue;
            }

            // We parsed correctly, make sure the values are right.
            EXPECT_EQ(test.calculate_tee_times, subnet->getCalculateTeeTimes());
            EXPECT_TRUE(util::areDoublesEquivalent(test.t1_percent, subnet->getT1Percent()))
                << "expected:" << test.t1_percent << " actual: " << subnet->getT1Percent();
            EXPECT_TRUE(util::areDoublesEquivalent(test.t2_percent, subnet->getT2Percent()))
                << "expected:" << test.t2_percent << " actual: " << subnet->getT2Percent();
        }
    }
}

// This test verifies the Subnet6 parser's validation logic for
// preferred-lifetime and indirectly shared lifetime parsing.
// Note the valid-lifetime stuff is similar and already done for Subnet4.
TEST(CfgSubnets6Test, preferredLifetimeValidation) {
    // First we create a set of elements that provides all
    // required for a Subnet6.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/64\", \n"
        "            \"interface\": \"\", \n"
        "            \"renew-timer\": 100, \n"
        "            \"rebind-timer\": 200, \n"
        "            \"valid-lifetime\": 300, \n"
        "            \"client-classes\": [], \n"
        "            \"evaluate-additional-classes\": [], \n"
        "            \"reservations-global\": false, \n"
        "            \"reservations-in-subnet\": true, \n"
        "            \"reservations-out-of-pool\": false \n"
        "        }";


    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
                    << "invalid JSON:" << json << "\n test is broken";

    {
        SCOPED_TRACE("no preferred-lifetime");

        data::ElementPtr copied = data::copy(elems);
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_TRUE(subnet->getPreferred().unspecified());
        data::ConstElementPtr repr = subnet->toElement();
        EXPECT_FALSE(repr->get("preferred-lifetime"));
        EXPECT_FALSE(repr->get("min-preferred-lifetime"));
        EXPECT_FALSE(repr->get("max-preferred-lifetime"));
    }

    {
        SCOPED_TRACE("preferred-lifetime only");

        data::ElementPtr copied = data::copy(elems);
        copied->set("preferred-lifetime", data::Element::create(100));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(100, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(100, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("100", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("100", max_value->str());
    }

    {
        SCOPED_TRACE("min-preferred-lifetime only");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(100));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(100, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(100, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("100", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("100", max_value->str());
    }

    {
        SCOPED_TRACE("max-preferred-lifetime only");
        data::ElementPtr copied = data::copy(elems);
        copied->set("max-preferred-lifetime", data::Element::create(100));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(100, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(100, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("100", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("100", max_value->str());
    }

    {
        SCOPED_TRACE("min-preferred-lifetime and preferred-lifetime");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(100));
        // Use a different (and greater) value for the default.
        copied->set("preferred-lifetime", data::Element::create(200));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(200, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(200, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("200", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("200", max_value->str());
    }

    {
        SCOPED_TRACE("max-preferred-lifetime and preferred-lifetime");
        data::ElementPtr copied = data::copy(elems);
        copied->set("max-preferred-lifetime", data::Element::create(200));
        // Use a different (and smaller) value for the default.
        copied->set("preferred-lifetime", data::Element::create(100));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(100, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(200, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("100", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("200", max_value->str());
    }

    {
        SCOPED_TRACE("min-preferred-lifetime and max-preferred-lifetime");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(100));
        copied->set("max-preferred-lifetime", data::Element::create(200));
        Subnet6ConfigParser parser;
        // No idea about the value to use for the default so failing.
        ASSERT_THROW(parser.parse(copied), DhcpConfigError);
    }

    {
        SCOPED_TRACE("all 3 (min, max and default) preferred-lifetime");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(100));
        // Use a different (and greater) value for the default.
        copied->set("preferred-lifetime", data::Element::create(200));
        // Use a different (and greater than both) value for max.
        copied->set("max-preferred-lifetime", data::Element::create(300));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(200, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(300, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("200", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("300", max_value->str());
    }

    {
        SCOPED_TRACE("default value too small");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(200));
        // 100 < 200 so it will fail.
        copied->set("preferred-lifetime", data::Element::create(100));
        // Use a different (and greater than both) value for max.
        copied->set("max-preferred-lifetime", data::Element::create(300));
        Subnet6ConfigParser parser;
        ASSERT_THROW(parser.parse(copied), DhcpConfigError);
    }

    {
        SCOPED_TRACE("default value too large");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(100));
        // Use a different (and greater) value for the default.
        copied->set("preferred-lifetime", data::Element::create(300));
        // 300 > 200 so it will fail.
        copied->set("max-preferred-lifetime", data::Element::create(200));
        Subnet6ConfigParser parser;
        ASSERT_THROW(parser.parse(copied), DhcpConfigError);
    }

    {
        SCOPED_TRACE("equal bounds are no longer ignored");
        data::ElementPtr copied = data::copy(elems);
        copied->set("min-preferred-lifetime", data::Element::create(100));
        copied->set("preferred-lifetime", data::Element::create(100));
        copied->set("max-preferred-lifetime", data::Element::create(100));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        ASSERT_TRUE(subnet);
        EXPECT_FALSE(subnet->getPreferred().unspecified());
        EXPECT_EQ(100, subnet->getPreferred().get());
        EXPECT_EQ(100, subnet->getPreferred().getMin());
        EXPECT_EQ(100, subnet->getPreferred().getMax());
        data::ConstElementPtr repr = subnet->toElement();
        data::ConstElementPtr value = repr->get("preferred-lifetime");
        ASSERT_TRUE(value);
        EXPECT_EQ("100", value->str());
        data::ConstElementPtr min_value = repr->get("min-preferred-lifetime");
        ASSERT_TRUE(min_value);
        EXPECT_EQ("100", min_value->str());
        data::ConstElementPtr max_value = repr->get("max-preferred-lifetime");
        ASSERT_TRUE(max_value);
        EXPECT_EQ("100", max_value->str());
    }
}

// This test verifies the Subnet6 parser's validation logic for
// hostname sanitizer values.
TEST(CfgSubnets6Test, hostnameSanitizierValidation) {

    // First we create a set of elements that provides all
    // required for a Subnet6.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/64\", \n"
        "            \"interface\": \"\", \n"
        "            \"renew-timer\": 100, \n"
        "            \"rebind-timer\": 200, \n"
        "            \"valid-lifetime\": 300, \n"
        "            \"client-classes\": [], \n"
        "            \"evaluate-additional-classes\": [], \n"
        "            \"reservations-global\": false, \n"
        "            \"reservations-in-subnet\": true, \n"
        "            \"reservations-out-of-pool\": false \n"
        "        }";

    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
                    << "invalid JSON:" << json << "\n test is broken";

    {
        SCOPED_TRACE("invalid regular expression");

        data::ElementPtr copied = data::copy(elems);
        copied->set("hostname-char-set", data::Element::create("^[A-"));
        copied->set("hostname-char-replacement", data::Element::create("x"));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        EXPECT_THROW_MSG(subnet = parser.parse(copied), DhcpConfigError,
                         "subnet configuration failed: hostname-char-set "
                         "'^[A-' is not a valid regular expression");

    }
    {
        SCOPED_TRACE("valid regular expression");

        data::ElementPtr copied = data::copy(elems);
        copied->set("hostname-char-set", data::Element::create("^[A-Z]"));
        copied->set("hostname-char-replacement", data::Element::create("x"));
        Subnet6Ptr subnet;
        Subnet6ConfigParser parser;
        ASSERT_NO_THROW(subnet = parser.parse(copied));
        EXPECT_EQ("^[A-Z]", subnet->getHostnameCharSet().get());
        EXPECT_EQ("x", subnet->getHostnameCharReplacement().get());
    }
}

// This test verifies the Subnet6 parser's validation logic for
// lease cache parameters.
TEST(CfgSubnets6Test, cacheParamValidation) {

    // Describes a single test scenario.
    struct Scenario {
        std::string label;         // label used for logging test failures
        double threshold;          // value of cache-threshold
        std::string error_message; // expected error message is parsing should fail
    };

    // Test Scenarios.
    std::vector<Scenario> tests = {
        {"valid", .25, ""},
        {"valid", 0.0, ""},
        {"negative", -.25,
         "subnet configuration failed: cache-threshold:"
         " -0.25 is invalid, it must be greater than or equal to 0.0 and less than 1.0"
        },
        {"too big", 1.05,
         "subnet configuration failed: cache-threshold:"
         " 1.05 is invalid, it must be greater than or equal to 0.0 and less than 1.0"
        }
    };

    // First we create a set of elements that provides all
    // required for a Subnet6.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/64\", \n"
        "            \"interface\": \"\", \n"
        "            \"renew-timer\": 100, \n"
        "            \"rebind-timer\": 200, \n"
        "            \"valid-lifetime\": 300, \n"
        "            \"client-classes\": [], \n"
        "            \"evaluate-additional-classes\": [], \n"
        "            \"reservations-global\": false, \n"
        "            \"reservations-in-subnet\": true, \n"
        "            \"reservations-out-of-pool\": false \n"
        "        }";

    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
                    << "invalid JSON:" << json << "\n test is broken";

    // Iterate over the test scenarios, verifying each prescribed
    // outcome.
    for (auto const& test : tests) {
        {
            SCOPED_TRACE("test: " + test.label);

            // Set this scenario's configuration parameters
            elems->set("cache-threshold", data::Element::create(test.threshold));

            Subnet6Ptr subnet;
            try {
                // Attempt to parse the configuration.
                Subnet6ConfigParser parser;
                subnet = parser.parse(elems);
            } catch (const std::exception& ex) {
                if (!test.error_message.empty()) {
                    // We expected a failure, did we fail the correct way?
                    EXPECT_EQ(test.error_message, ex.what());
                } else {
                    // Should not have failed.
                    ADD_FAILURE() << "Scenario should not have failed: " << ex.what();
                }

                // Either way we're done with this scenario.
                continue;
            }

            // We parsed correctly, make sure the values are right.
            EXPECT_TRUE(util::areDoublesEquivalent(test.threshold, subnet->getCacheThreshold()));
        }
    }
}

// This test verifies that the optional interface check works as expected.
TEST(CfgSubnets6Test, iface) {
    // Create a configuration.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/64\", \n"
        "            \"interface\": \"eth1961\"\n"
        "        }";

    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
        << "invalid JSON:" << json << "\n test is broken";

    // The interface check can be disabled.
    Subnet6ConfigParser parser_no_check(false);
    Subnet6Ptr subnet;
    EXPECT_NO_THROW(subnet = parser_no_check.parse(elems));
    ASSERT_TRUE(subnet);
    EXPECT_FALSE(subnet->getIface().unspecified());
    EXPECT_EQ("eth1961", subnet->getIface().get());

    // Retry with the interface check enabled.
    Subnet6ConfigParser parser;
    EXPECT_THROW(parser.parse(elems), DhcpConfigError);

    // Configure default test interfaces.
    IfaceMgrTestConfig config(true);

    EXPECT_NO_THROW(subnet = parser_no_check.parse(elems));
    ASSERT_TRUE(subnet);
    EXPECT_FALSE(subnet->getIface().unspecified());
    EXPECT_EQ("eth1961", subnet->getIface().get());

    EXPECT_NO_THROW(subnet = parser.parse(elems));
    ASSERT_TRUE(subnet);
    EXPECT_FALSE(subnet->getIface().unspecified());
    EXPECT_EQ("eth1961", subnet->getIface().get());
}

// This test verifies that update statistics works as expected.
TEST(CfgSubnets6Test, updateStatistics) {
    CfgMgr::instance().clear();

    CfgSubnets6Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6();
    ObservationPtr observation;
    SubnetID subnet_id = 100;

    LeaseMgrFactory::create("type=memfile universe=6 persist=false");

    // remove all statistics
    StatsMgr::instance().removeAll();

    // Create subnet.
    Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 100));

    Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), IOAddress("2001:db8:1::FF")));
    subnet->addPool(pool);

    pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("3001:1:2::"), 96, 112));
    subnet->addPool(pool);

    // Add subnet.
    cfg->add(subnet);

    observation = StatsMgr::instance().getObservation(
        "cumulative-assigned-nas");
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        "cumulative-assigned-pds");
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        "cumulative-registered-nas");
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        "declined-addresses");
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        "reclaimed-declined-addresses");
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        "reclaimed-leases");
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "total-nas")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-pds"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "total-pds")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "assigned-nas")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-pds"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "assigned-pds")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "registered-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "cumulative-assigned-nas")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-pds"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "cumulative-assigned-pds")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-registered-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "declined-addresses"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "declined-addresses")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-declined-addresses"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-leases"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-leases")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "reclaimed-leases")));
    ASSERT_FALSE(observation);

    cfg->updateStatistics();

    observation = StatsMgr::instance().getObservation(
        "cumulative-assigned-nas");
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        "cumulative-assigned-pds");
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        "cumulative-registered-nas");
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        "declined-addresses");
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        "reclaimed-declined-addresses");
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        "reclaimed-leases");
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(256, observation->getBigInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "total-nas")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(256, observation->getBigInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-pds"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(65536, observation->getBigInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "total-pds")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(65536, observation->getBigInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "assigned-nas")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-pds"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "assigned-pds")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "registered-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "cumulative-assigned-nas")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-pds"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "cumulative-assigned-pds")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-registered-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "declined-addresses"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "declined-addresses")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-declined-addresses"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-leases"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-leases")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "reclaimed-leases")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);
}

// This test verifies that remove statistics works as expected.
TEST(CfgSubnets6Test, removeStatistics) {
    CfgSubnets6 cfg;
    ObservationPtr observation;
    SubnetID subnet_id = 100;

    // remove all statistics and then set them all to 0
    StatsMgr::instance().removeAll();

    // Create subnet.
    Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4, 100));

    Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), IOAddress("2001:db8:1::FF")));
    subnet->addPool(pool);

    pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("3001:1:2::"), 96, 112));
    subnet->addPool(pool);

    // Add subnet.
    cfg.add(subnet);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-nas"),
        int128_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getBigInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "total-nas")),
        int128_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "total-nas")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getBigInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-pds"),
        int128_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-pds"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getBigInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "total-pds")),
        int128_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "total-pds")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getBigInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-nas"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "registered-nas"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "registered-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "assigned-nas")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "assigned-nas")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-pds"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-pds"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "assigned-pds")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "assigned-pds")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-nas"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-registered-nas"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-registered-nas"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "cumulative-assigned-nas")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "cumulative-assigned-nas")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-pds"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-pds"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "cumulative-assigned-pds")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "cumulative-assigned-pds")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "declined-addresses"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "declined-addresses"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "declined-addresses")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "declined-addresses")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-declined-addresses"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-declined-addresses"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-leases"),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-leases"));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-leases")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-leases")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    StatsMgr::instance().setValue(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "reclaimed-leases")),
        int64_t(0));

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "reclaimed-leases")));
    ASSERT_TRUE(observation);
    ASSERT_EQ(0, observation->getInteger().first);

    // remove all statistics
    cfg.removeStatistics();

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "total-nas")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "total-pds"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "total-pds")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "registered-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "assigned-nas")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "assigned-pds"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "assigned-pds")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-registered-nas"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "cumulative-assigned-nas")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "cumulative-assigned-pds"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "cumulative-assigned-pds")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "declined-addresses"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "declined-addresses")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-declined-addresses"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-declined-addresses")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               "reclaimed-leases"));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pool", 0, "reclaimed-leases")));
    ASSERT_FALSE(observation);

    observation = StatsMgr::instance().getObservation(
        StatsMgr::generateName("subnet", subnet_id,
                               StatsMgr::generateName("pd-pool", 0, "reclaimed-leases")));
    ASSERT_FALSE(observation);
}

// This test verifies that in range host reservation works as expected.
TEST(CfgSubnets6Test, hostNA) {
    // Create a configuration.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/48\", \n"
        "            \"reservations\": [ {\n"
        "                \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
        "                \"ip-addresses\": [\"2001:db8:1::1\"] } ]\n"
        "        }";

    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
        << "invalid JSON:" << json << "\n test is broken";

    Subnet6ConfigParser parser;
    Subnet6Ptr subnet;
    EXPECT_NO_THROW(subnet = parser.parse(elems));
    ASSERT_TRUE(subnet);
    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
    ASSERT_TRUE(cfg_hosts);
    HostCollection hosts = cfg_hosts->getAll6(SubnetID(1));
    ASSERT_EQ(1, hosts.size());
    ConstHostPtr host = hosts[0];
    ASSERT_TRUE(host);
    EXPECT_EQ(1, host->getIPv6SubnetID());
    EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText());
    IPv6ResrvRange addresses = host->getIPv6Reservations(IPv6Resrv::TYPE_NA);
    ASSERT_EQ(1, std::distance(addresses.first, addresses.second));
    EXPECT_EQ("2001:db8:1::1", addresses.first->second.getPrefix().toText());

    CfgMgr::instance().clear();
}

// This test verifies that an out of range host reservation is rejected.
TEST(CfgSubnets6Test, outOfRangeHost) {
    // Create a configuration.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/48\", \n"
        "            \"reservations\": [ {\n"
        "                \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
        "                \"ip-addresses\": [\"2001:db8:1::1\", \n"
        "                                   \"2001:db8:2::1\"] } ]\n"
        "        }";

    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
        << "invalid JSON:" << json << "\n test is broken";

    Subnet6ConfigParser parser;
    std::string msg = "specified reservation '2001:db8:2::1' is ";
    msg += "not within the IPv6 subnet '2001:db8:1::/48'";
    EXPECT_THROW_MSG(parser.parse(elems), DhcpConfigError, msg);
}

// This test verifies that in range validation does not applies to prefixes.
TEST(CfgSubnets6Test, hostPD) {
    // Create a configuration.
    std::string json =
        "        {"
        "            \"id\": 1,\n"
        "            \"subnet\": \"2001:db8:1::/48\", \n"
        "            \"reservations\": [ {\n"
        "                \"hw-address\": \"aa:bb:cc:dd:ee:ff\", \n"
        "                \"prefixes\": [\"2001:db8:2::/64\"] } ]\n"
        "        }";

    data::ElementPtr elems;
    ASSERT_NO_THROW(elems = data::Element::fromJSON(json))
        << "invalid JSON:" << json << "\n test is broken";

    Subnet6ConfigParser parser;
    Subnet6Ptr subnet;
    try {
        subnet = parser.parse(elems);
    } catch (const std::exception& ex) {
        std::cerr << "parse fail with " << ex.what() << std::endl;
    }
    //EXPECT_NO_THROW(subnet = parser.parse(elems));
    ASSERT_TRUE(subnet);
    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
    ASSERT_TRUE(cfg_hosts);
    HostCollection hosts = cfg_hosts->getAll6(SubnetID(1));
    ASSERT_EQ(1, hosts.size());
    ConstHostPtr host = hosts[0];
    ASSERT_TRUE(host);
    EXPECT_EQ(1, host->getIPv6SubnetID());
    EXPECT_EQ("hwaddr=AABBCCDDEEFF", host->getIdentifierAsText());
    IPv6ResrvRange prefixes = host->getIPv6Reservations(IPv6Resrv::TYPE_PD);
    ASSERT_EQ(1, std::distance(prefixes.first, prefixes.second));
    EXPECT_EQ("2001:db8:2::", prefixes.first->second.getPrefix().toText());
    EXPECT_EQ(64, prefixes.first->second.getPrefixLen());

    // Verify the prefix is not in the subnet range.
    EXPECT_FALSE(subnet->inRange(prefixes.first->second.getPrefix()));

    CfgMgr::instance().clear();
}

// This test verifies that the getLinks tool works as expected.
TEST(CfgSubnets6Test, getLinks) {
    CfgSubnets6 cfg;

    // Create 4 subnets.
    Subnet6Ptr subnet0(new Subnet6(IOAddress("::"), 48, 1, 2, 3, 4,
                                   SubnetID(111)));
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4,
                                   SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 64, 1, 2, 3, 4,
                                   SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 64, 1, 2, 3, 4,
                                   SubnetID(3)));

    // Add all subnets to the configuration.
    ASSERT_NO_THROW(cfg.add(subnet0));
    ASSERT_NO_THROW(cfg.add(subnet1));
    ASSERT_NO_THROW(cfg.add(subnet2));
    ASSERT_NO_THROW(cfg.add(subnet3));

    // No 2001:db8:4:: subnet.
    SubnetIDSet links;
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:4::")));
    EXPECT_TRUE(links.empty());

    // A 2001:db8:2::/64 subnet.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:2::")));
    SubnetIDSet expected = { 2 };
    EXPECT_EQ(expected, links);

    // Check that any address in the subnet works.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:2::1234:5678:9abc:def0")));
    EXPECT_EQ(expected, links);

    // Check that an address outside the subnet does not work.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:2:1::")));
    EXPECT_TRUE(links.empty());

    // Add a second 2001:db8:2::/64 subnet.
    Subnet6Ptr subnet10(new Subnet6(IOAddress("2001:db8:2::10"), 64, 1, 2, 3,
                                    4, SubnetID(10)));
    ASSERT_NO_THROW(cfg.add(subnet10));

    // Now we should get 2 subnets.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:2::")));
    expected = { 2, 10 };
    EXPECT_EQ(expected, links);

    // Add a larger subnet.
    Subnet6Ptr subnet20(new Subnet6(IOAddress("2001:db8:2::20"), 56, 1, 2, 3,
                                    4, SubnetID(20)));
    ASSERT_NO_THROW(cfg.add(subnet20));

    // Now we should get 3 subnets.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:2::")));
    expected = { 2, 10, 20 };
    EXPECT_EQ(expected, links);

    // But only the larger subnet if the address is only in it.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("2001:db8:2:1::")));
    expected = { 20 };
    EXPECT_EQ(expected, links);

    // Even it is not used for Bulk Leasequery, it works for :: too.
    links.clear();
    EXPECT_NO_THROW(links = cfg.getLinks(IOAddress("::")));
    expected = { 111 };
    EXPECT_EQ(expected, links);
}

// This test verifies that for each subnet in the configuration it calls
// the registerLeaseMgrCallback function.
TEST(CfgSubnets6Test, initAllocatorsAfterConfigure) {
    CfgSubnets6 cfg;

    // Create 4 subnets.
    Subnet6Ptr subnet0(new Subnet6(IOAddress("2001:db8:1::"),
                                   64, 1, 2, 3, 4, SubnetID(111)));
    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:2::"),
                                   64, 1, 2, 3, 4, SubnetID(1)));
    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:3::"),
                                   64, 1, 2, 3, 4, SubnetID(2)));
    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:4::"),
                                   64, 1, 2, 3, 4, SubnetID(3)));

    auto allocator0 = boost::make_shared<InitRecordingAllocator>(Lease::TYPE_NA, subnet0);
    subnet0->setAllocator(Lease::TYPE_NA, allocator0);

    auto allocator2 = boost::make_shared<InitRecordingAllocator>(Lease::TYPE_PD, subnet2);
    subnet2->setAllocator(Lease::TYPE_PD, allocator2);

    cfg.add(subnet0);
    cfg.add(subnet1);
    cfg.add(subnet2);
    cfg.add(subnet3);

    EXPECT_EQ(0, allocator0->callcount_);
    EXPECT_EQ(0, allocator2->callcount_);

    cfg.initAllocatorsAfterConfigure();

    EXPECT_EQ(1, allocator0->callcount_);
    EXPECT_EQ(1, allocator2->callcount_);
}


/// @brief Test fixture for parsing v6 Subnets that can verify log output.
class Subnet6ParserTest : public LogContentTest {
public:

    /// @brief virtual destructor
    virtual ~Subnet6ParserTest() = default;
};

// This test verifies that subnet parser for IPv6 works properly
// when using invalid renew and rebind timers.
TEST_F(Subnet6ParserTest, parseWithInvalidRenewRebind) {
    std::string config =
        "{\n"
        "    \"id\": 1,\n"
        "    \"subnet\": \"2001:db8:1::/64\",\n"
        "    \"valid-lifetime\": 399,\n"
        "    \"rebind-timer\": 199,\n"
        "    \"renew-timer\": 200\n"
        "}";

    // Basic configuration for subnet6 but with a renew-timer value
    // larger than rebind-timer.
    ElementPtr config_element = Element::fromJSON(config);

    // Parse configuration specified above.
    Subnet6ConfigParser parser(false);
    Subnet6Ptr subnet;

    // Parser should not throw.
    ASSERT_NO_THROW(subnet = parser.parse(config_element));
    ASSERT_TRUE(subnet);

    // Veriy we emitted the proper log message.
    addString("DHCPSRV_CFGMGR_RENEW_GTR_REBIND in subnet-id 1,"
              " the value of renew-timer 200 is greater than the value"
              " of rebind-timer 199, ignoring renew-timer");
    EXPECT_TRUE(checkFile());
}

} // end of anonymous namespace
