// Copyright (C) 2015-2017 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 <asiolink/io_address.h>
#include <cc/data.h>
#include <dhcp/dhcp4.h>
#include <dhcp_ddns/ncr_msg.h>
#include <dhcp/tests/iface_mgr_test_config.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/subnet_id.h>
#include <dhcp4/tests/dhcp4_test_utils.h>
#include <dhcp4/tests/dhcp4_client.h>

using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
using namespace isc::dhcp::test;

namespace {

/// @brief Set of JSON configurations used throughout the out of range
///  tests.
///
/// - Configuration 0 - Reference configuration, all tests start with the
///   server configured with this configuration:
///   - 1 subnet: 10.0.0.0/24
///   - with one pool  10.0.0.10 - 10.0.0.100
///   - 1 address reservation (fixed host) for HW address:
///     ff:ff:ff:ff:ff:01
///   - 1 hostname reservation (dynamic host) for HW address:
///     dd:dd:dd:dd:dd:01,
///   - DDNS enabled
/// - Configuration 1 - same subnet, different pool
/// - Configuration 2 - same subnet, different pool, no reservations
/// - Configuration 3 - different subnet with reservations
/// - Configuration 4 - different subnet with no reservations
/// - Configuration 5 - same as reference, no reservations
///
const char* OOR_CONFIGS[] = {
// Configuration 0 - reference configuration
    "{ \"interfaces-config\": {"
        "      \"interfaces\": [ \"*\" ]"
        "},"
        "\"valid-lifetime\": 600,"
        "\"subnet4\": [ { "
        "    \"subnet\": \"10.0.0.0/24\", "
        "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
        "    \"reservations\": [ "
        "       {"
        "         \"hw-address\": \"ff:ff:ff:ff:ff:01\","
        "         \"ip-address\": \"10.0.0.7\""
        "       },"
        "       {"
        "         \"hw-address\": \"dd:dd:dd:dd:dd:01\","
        "         \"hostname\": \"test.example.com\""
        "       }"
        "    ]"
        "} ],"
        "\"dhcp-ddns\": {"
        "     \"enable-updates\": true,"
        "     \"qualifying-suffix\": \"\""
        "}"
    "}",

// Configuration 1 - same subnet as reference, different pool
    "{ \"interfaces-config\": {"
        "      \"interfaces\": [ \"*\" ]"
        "},"
        "\"valid-lifetime\": 600,"
        "\"subnet4\": [ { "
        "    \"subnet\": \"10.0.0.0/24\", "
        "    \"pools\": [ { \"pool\": \"10.0.0.101-10.0.0.200\" } ],"
        "    \"reservations\": [ "
        "       {"
        "         \"hw-address\": \"ff:ff:ff:ff:ff:01\","
        "         \"ip-address\": \"10.0.0.7\""
        "       },"
        "       {"
        "         \"hw-address\": \"dd:dd:dd:dd:dd:01\","
        "         \"hostname\": \"reserved.example.com\""
        "       }"
        "    ]"
        "} ],"
        "\"dhcp-ddns\": {"
        "     \"enable-updates\": true,"
        "     \"qualifying-suffix\": \"\""
        "}"
    "}",

// Configuration 2 - same subnet as reference, different pool, no reservations
    "{ \"interfaces-config\": {"
        "      \"interfaces\": [ \"*\" ]"
        "},"
        "\"valid-lifetime\": 600,"
        "\"subnet4\": [ { "
        "    \"subnet\": \"10.0.0.0/24\", "
        "    \"pools\": [ { \"pool\": \"10.0.0.101-10.0.0.200\" } ],"
        "} ],"
        "\"dhcp-ddns\": {"
        "     \"enable-updates\": true,"
        "     \"qualifying-suffix\": \"\""
        "}"
    "}",


// Configuration 3 - different subnet with reservations
    "{ \"interfaces-config\": {"
        "      \"interfaces\": [ \"*\" ]"
        "},"
        "\"valid-lifetime\": 600,"
        "\"subnet4\": [ { "
        "    \"subnet\": \"192.0.2.0/24\", "
        "    \"pools\": [ { \"pool\": \"192.0.2.101-192.0.2.200\" } ],"
        "    \"reservations\": [ "
        "       {"
        "         \"hw-address\": \"ff:ff:ff:ff:ff:01\","
        "         \"ip-address\": \"192.0.2.7\""
        "       },"
        "       {"
        "         \"hw-address\": \"dd:dd:dd:dd:dd:01\","
        "         \"hostname\": \"reserved.example.com\""
        "       }"
        "    ]"
        "} ],"
        "\"dhcp-ddns\": {"
        "     \"enable-updates\": true,"
        "     \"qualifying-suffix\": \"\""
        "}"
    "}",

// Configuration 4 - different subnet with no reservations
    "{ \"interfaces-config\": {"
        "      \"interfaces\": [ \"*\" ]"
        "},"
        "\"valid-lifetime\": 600,"
        "\"subnet4\": [ { "
        "    \"subnet\": \"192.0.2.0/24\", "
        "    \"pools\": [ { \"pool\": \"192.0.2.101-192.0.2.200\" } ]"
        "} ],"
        "\"dhcp-ddns\": {"
        "     \"enable-updates\": true,"
        "     \"qualifying-suffix\": \"\""
        "}"
    "}",

// Configuration 5 - same as reference, no reservations
    "{ \"interfaces-config\": {"
        "      \"interfaces\": [ \"*\" ]"
        "},"
        "\"valid-lifetime\": 600,"
        "\"subnet4\": [ { "
        "    \"subnet\": \"10.0.0.0/24\", "
        "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
        "} ],"
        "\"dhcp-ddns\": {"
        "     \"enable-updates\": true,"
        "     \"qualifying-suffix\": \"\""
        "}"
    "}",

};

/// @brief Enum for indexing into the array of configurations.
/// These were created to make the test cases easier to follow.
enum CfgIndex {
    REF_CFG = 0,
    DIFF_POOL,
    DIFF_POOL_NO_HR,
    DIFF_SUBNET,
    DIFF_SUBNET_NO_HR,
    NO_HR
};

/// @brief Enum for specifying expected response to client renewal attempt
enum RenewOutcome {
    DOES_RENEW,
    DOES_NOT_RENEW
};

/// @brief Enum for specifying expected response to client release attempt
enum ReleaseOutcome {
    DOES_RELEASE,
    DOES_NOT_RELEASE
};

/// @brief Test fixture class for testing various exchanges when the client's
/// leased address is out of range due to configuration changes.
class OutOfRangeTest : public Dhcpv4SrvTest {
public:
    D2ClientMgr& d2_mgr_;

    /// @brief Constructor.
    ///
    /// Sets up fake interfaces.
    OutOfRangeTest()
        : Dhcpv4SrvTest(),
          d2_mgr_(CfgMgr::instance().getD2ClientMgr()),
          iface_mgr_test_config_(true) {
        IfaceMgr::instance().openSockets4();
    }

    /// @brief Destructor.
    ///
    /// Cleans up statistics after the test.
    ~OutOfRangeTest() {
    }

    void configure(const std::string& config, Dhcp4Client& client) {
        NakedDhcpv4Srv& server = *client.getServer();
        ASSERT_NO_FATAL_FAILURE(Dhcpv4SrvTest::configure(config, server));
        if (d2_mgr_.ddnsEnabled()) {
            ASSERT_NO_THROW(server.startD2());
        }
    }

    /// @briefVerify that a NameChangeRequest has been queued
    ///
    /// Checks that a NCR of a given type (ADD or REMOVE) for a given
    /// ip address has been queued.  If the NCR exits, it is processed
    /// off the queue.  Note the function expects there to be 1 and only
    /// 1 NCR queued.
    ///
    /// @param type - NCR type expected, either CHG_ADD or CHG_REMOVE
    /// @param addr - string containing the ip address expected in the NCR
    void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
                                 const std::string& addr) {
        ASSERT_TRUE(d2_mgr_.getQueueSize() > 0);

        isc::dhcp_ddns::NameChangeRequestPtr ncr;
        ASSERT_NO_THROW(ncr = d2_mgr_.peekAt(0));
        ASSERT_TRUE(ncr);

        EXPECT_EQ(type, ncr->getChangeType());
        EXPECT_EQ(addr, ncr->getIpAddress());

        // Process the message off the queue
        ASSERT_NO_THROW(d2_mgr_.runReadyIO());
    }

    /// @brief Conducts a single out-of-range test scenario
    ///
    /// Each test cycles consists of a the following two stages, the first is
    /// a set-up stage during which the server is configured with an initial,
    /// reference, configuration and a client then verifies that it can acquire
    /// and renew a lease.  The second stage verifies that the server, having
    /// been reconfigured such that the original lease is now "out-of-range",
    /// responds correctly to the same client first attempting to renew the
    /// lease and then attempting to release the lease.
    ///
    /// The test also expects the configuration to enable DDNS, and verifies
    /// that a DNS add is done during stage one, and a DNS remove is done
    /// in stage two as part of the release request processing.
    ///
    /// @param cfg_idx - index of the "replacement" configuration used to
    /// reconfigure the server during stage two
    /// @param hwaddress - text value, if not empty, to use as the hardware
    /// address.  This is used for host reservation tests.
    /// in client queries
    /// @param expected_address - text value of the expected address.  If not
    /// empty the test will fail if the server responds with a different lease
    /// address
    /// @param renew_outcome - expected server reaction in response to the
    /// client's stage two renewal attempt.
    /// @param release_outcome - expected server reaction in response to the
    /// client's stage two release attempt.  Currently defaults to DOES_RELEASE
    /// as no cases have been identified which do otherwise.
    void oorRenewReleaseTest(enum CfgIndex cfg_idx,
                             const std::string& hwaddress,
                             const std::string& expected_address,
                             enum RenewOutcome renew_outcome,
                             enum ReleaseOutcome release_outcome = DOES_RELEASE);

    /// @brief Interface Manager's fake configuration control.
    IfaceMgrTestConfig iface_mgr_test_config_;

};

void
OutOfRangeTest::oorRenewReleaseTest(enum CfgIndex cfg_idx,
                             const std::string& hwaddress,
                             const std::string& expected_address,
                             enum RenewOutcome renew_outcome,
                             enum ReleaseOutcome release_outcome) {
    // STAGE ONE:

    // Step 1 is to acquire the lease
    Dhcp4Client client(Dhcp4Client::SELECTING);

    // Configure DHCP server.
    configure(OOR_CONFIGS[REF_CFG], client);

    // Set the host name so DNS updates will be performed
    client.includeHostname("test.example.com");

    // Set the hw address if given one
    if (!hwaddress.empty()) {
        client.setHWAddress(hwaddress);
    }

    // Acquire the lease via DORA
    ASSERT_NO_THROW(client.doDORA());

    // Make sure that the server responded.
    ASSERT_TRUE(client.getContext().response_);
    Pkt4Ptr resp = client.getContext().response_;

    // Make sure that the server has responded with DHCPACK.
    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));

    // Verify a lease was created
    IOAddress leased_address = client.config_.lease_.addr_;
    Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(leased_address);
    ASSERT_TRUE(lease);

    // Check the expected address if given
    if (!expected_address.empty()) {
        ASSERT_EQ(leased_address, expected_address);
    }

    // Verify that a DNS add was requested
    verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, leased_address.toText());

    // The address is still valid, let's verify the lease can be renewed
    // Set the unicast destination address to indicate that it is a renewal.
    client.setState(Dhcp4Client::RENEWING);
    client.setDestAddress(IOAddress("10.0.0.1"));
    ASSERT_NO_THROW(client.doRequest());

    // Verify that we received an ACK to our renewal
    resp = client.getContext().response_;
    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
    EXPECT_EQ(0, d2_mgr_.getQueueSize());

    // STAGE TWO:

    // Now reconfigure which should render our leased address out-of-range
    configure(OOR_CONFIGS[cfg_idx], client);

    // Try to renew after the configuration change..
    ASSERT_NO_THROW(client.doRequest());
    resp = client.getContext().response_;

    // Verify we got ACK'd or NAK'd as expected
    ASSERT_EQ((renew_outcome == DOES_RENEW ? DHCPACK : DHCPNAK),
              static_cast<int>(resp->getType()));

    // Verify that the lease still exists in the database as it has not
    // been explicitly released.
    lease = LeaseMgrFactory::instance().getLease4(leased_address);
    ASSERT_TRUE(lease);

    // Recreate the client lease info, a preceding NAK will have wiped it out
    client.createLease(leased_address, lease->valid_lft_);

    // Send the release. The server should remove it from the DB and
    // request DNS remove.
    ASSERT_NO_THROW(client.doRelease());

    lease = LeaseMgrFactory::instance().getLease4(leased_address);

    if (release_outcome == DOES_RELEASE) {
        EXPECT_FALSE(lease);
        // Verify the DNS remove was queued.
        verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE,
                                leased_address.toText());
    } else {
        // Lease should still exist, and no NCR should be queued.
        EXPECT_TRUE(lease);
        EXPECT_EQ(0, d2_mgr_.getQueueSize());
    }
}


// Verifies that once-valid lease, whose address is no longer
// within the subnet's pool:
//
// a: Is NAKed upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, dynamicOutOfPool) {

    std::string hwaddress = "";
    std::string expected_address = "";

    oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address,
                        DOES_NOT_RENEW);

}

// Verifies that once-valid lease whose address is no longer
// within any configured subnet:
//
// a: Is NAKed upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, dynamicOutOfSubnet) {

    std::string hwaddress = "";
    std::string expected_address = "";

    oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address,
                        DOES_NOT_RENEW);
}

// Test verifies that once-valid dynamic address host reservation,
// whose address is no longer within the subnet's pool:
//
// a: Is NAKed upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, dynamicHostOutOfPool) {
    std::string hwaddress = "dd:dd:dd:dd:dd:01";
    std::string expected_address = "";

    oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_NOT_RENEW);
}

// Test verifies that once-valid dynamic address host reservation,
// whose address is no longer within any configured subnet:
//
// a: Is NAKed upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, dynamicHostOutOfSubnet) {
    std::string hwaddress = "dd:dd:dd:dd:dd:01";
    std::string expected_address = "";

    oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address,
                        DOES_NOT_RENEW);
}

// Test verifies that once-valid dynamic address host reservation,
// whose address is within the configured subnet and pool, but whose
// reservation has been removed:
//
// a: Is allowed to renew
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, dynamicHostReservationRemoved) {
    Dhcp4Client client(Dhcp4Client::SELECTING);

    std::string hwaddress = "dd:dd:dd:dd:dd:01";
    std::string expected_address = "";

    oorRenewReleaseTest(NO_HR, hwaddress, expected_address, DOES_RENEW);
}

// Test verifies that once-valid dynamic address host reservation,
// whose address is no longer within any configured subnet, and which
// no longer has reservation defined:
//
// a: Is NAKed upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, dynamicHostOutOfSubnetReservationRemoved) {
    Dhcp4Client client(Dhcp4Client::SELECTING);

    std::string hwaddress = "dd:dd:dd:dd:dd:01";
    std::string expected_address = "";

    oorRenewReleaseTest(DIFF_SUBNET_NO_HR, hwaddress, expected_address,
                        DOES_NOT_RENEW);
}

// Test verifies that once-valid in-subnet fixed-address host reservation,
// after the subnet pool changes:
//
// a: Is NAK'd upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, fixedHostOutOfSubnet) {
    std::string hwaddress = "ff:ff:ff:ff:ff:01";
    std::string expected_address = "10.0.0.7";

    oorRenewReleaseTest(DIFF_SUBNET, hwaddress, expected_address,
                        DOES_NOT_RENEW);
}


// Test verifies that once-valid in-subnet fixed-address host reservation,
// after the subnet pool is changed:
//
// a: Is ACK'd upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, fixedHostDifferentPool) {
    std::string hwaddress = "ff:ff:ff:ff:ff:01";
    std::string expected_address = "10.0.0.7";

    oorRenewReleaseTest(DIFF_POOL, hwaddress, expected_address, DOES_RENEW);
}

// Test verifies that once-valid in-subnet fixed-address host reservation,
// whose reservation has been removed from the configuration
//
// a: Is NAK'd upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, fixedHostReservationRemoved) {
    std::string hwaddress = "ff:ff:ff:ff:ff:01";
    std::string expected_address = "10.0.0.7";

    oorRenewReleaseTest(NO_HR, hwaddress, expected_address, DOES_NOT_RENEW);
}

// Test verifies that once-valid fixed address host reservation,
// whose address is no longer within any configured subnet
//
// a: Is NAKed upon a renewal attempt
// b: Is released properly upon release, including DNS removal
//
TEST_F(OutOfRangeTest, fixedHostOutOfSubnetReservationRemoved) {
    std::string hwaddress = "ff:ff:ff:ff:ff:01";
    std::string expected_address = "10.0.0.7";

    oorRenewReleaseTest(DIFF_SUBNET_NO_HR, hwaddress, expected_address,
                        DOES_NOT_RENEW);
}

} // end of anonymous namespace
