// Copyright (C) 2013-2018 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/.

/// @file   dhcp4_test_utils.h
///
/// @brief  This file contains utility classes used for DHCPv4 server testing

#ifndef DHCP4_TEST_UTILS_H
#define DHCP4_TEST_UTILS_H

#include <gtest/gtest.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/option4_addrlst.h>
#include <dhcp/pkt4.h>
#include <dhcp/pkt_filter.h>
#include <dhcp/pkt_filter_inet.h>
#include <dhcpsrv/alloc_engine.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/lease.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <dhcp4/dhcp4_srv.h>
#include <dhcp4/parser_context.h>
#include <asiolink/io_address.h>
#include <cc/command_interpreter.h>
#include <list>

#include <boost/shared_ptr.hpp>

namespace isc {
namespace dhcp {
namespace test {

/// @brief Dummy Packet Filtering class.
///
/// This class reports capability to respond directly to the client which
/// doesn't have address configured yet.
///
/// All packet and socket handling functions do nothing because they are not
/// used in unit tests.
class PktFilterTest : public PktFilter {
public:

    /// @brief Constructor.
    ///
    /// Sets the 'direct response' capability to true.
    PktFilterTest()
        : direct_resp_supported_(true) {
    }

    /// @brief Reports 'direct response' capability.
    ///
    /// @return always true.
    virtual bool isDirectResponseSupported() const {
        return (direct_resp_supported_);
    }

    /// Does nothing.
    virtual SocketInfo openSocket(Iface&,
                                  const isc::asiolink::IOAddress& addr,
                                  const uint16_t port, const bool, const bool) {
        return (SocketInfo(addr, port, 0));
    }

    /// Does nothing.
    virtual Pkt4Ptr receive(Iface&, const SocketInfo&) {
        return Pkt4Ptr();
    }

    /// Does nothing.
    virtual int send(const Iface&, uint16_t, const Pkt4Ptr&) {
        return (0);
    }

    /// @brief Holds a boolean value which indicates whether direct response
    /// capability is supported (true) or not (false).
    bool direct_resp_supported_;

};

typedef boost::shared_ptr<PktFilterTest> PktFilterTestPtr;

/// Forward definition for Dhcp4Client defined in dhcp4_client.h
/// dhcp4_client.h includes dhcp_test_utils.h (this file), so to avoid
/// circular dependencies, we need a forward class declaration.
class Dhcp4Client;

/// @brief "Naked" DHCPv4 server, exposes internal fields
class NakedDhcpv4Srv: public Dhcpv4Srv {
public:

    /// @brief Constructor.
    ///
    /// This constructor disables default modes of operation used by the
    /// Dhcpv4Srv class:
    /// - Send/receive broadcast messages through sockets on interfaces
    /// which support broadcast traffic.
    /// - Direct DHCPv4 traffic - communication with clients which do not
    /// have IP address assigned yet.
    ///
    /// Enabling these modes requires root privileges so they must be
    /// disabled for unit testing.
    ///
    /// Note, that disabling broadcast options on sockets does not impact
    /// the operation of these tests because they use local loopback
    /// interface which doesn't have broadcast capability anyway. It rather
    /// prevents setting broadcast options on other (broadcast capable)
    /// sockets which are opened on other interfaces in Dhcpv4Srv constructor.
    ///
    /// The Direct DHCPv4 Traffic capability can be disabled here because
    /// it is tested with PktFilterLPFTest unittest. The tests which belong
    /// to PktFilterLPFTest can be enabled on demand when root privileges can
    /// be guaranteed.
    ///
    /// @param port port number to listen on; the default value 0 indicates
    /// that sockets should not be opened.
    NakedDhcpv4Srv(uint16_t port = 0)
        : Dhcpv4Srv(port, false, false) {
        // Create a default lease database backend.
        std::string dbconfig = "type=memfile universe=4 persist=false";
        isc::dhcp::LeaseMgrFactory::create(dbconfig);
        // Create fixed server id.
        server_id_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
                                            asiolink::IOAddress("192.0.3.1")));
    }

    /// @brief Returns fixed server identifier assigned to the naked server
    /// instance.
    OptionPtr getServerID() const {
        return (server_id_);
    }

    /// @brief fakes packet reception
    /// @param timeout ignored
    ///
    /// The method receives all packets queued in receive queue, one after
    /// another. Once the queue is empty, it initiates the shutdown procedure.
    ///
    /// See fake_received_ field for description
    virtual Pkt4Ptr receivePacket(int /*timeout*/) {

        // If there is anything prepared as fake incoming traffic, use it
        if (!fake_received_.empty()) {
            Pkt4Ptr pkt = fake_received_.front();
            fake_received_.pop_front();
            return (pkt);
        }

        // If not, just trigger shutdown and return immediately
        shutdown();
        return (Pkt4Ptr());
    }

    /// @brief fake packet sending
    ///
    /// Pretend to send a packet, but instead just store it in fake_send_ list
    /// where test can later inspect server's response.
    virtual void sendPacket(const Pkt4Ptr& pkt) {
        fake_sent_.push_back(pkt);
    }

    /// @brief adds a packet to fake receive queue
    ///
    /// See fake_received_ field for description
    void fakeReceive(const Pkt4Ptr& pkt) {
        fake_received_.push_back(pkt);
    }

    virtual ~NakedDhcpv4Srv() {
    }

    /// @brief Runs processing DHCPREQUEST.
    ///
    /// @param request a message received from client
    /// @return DHCPACK or DHCPNAK message
    Pkt4Ptr processRequest(Pkt4Ptr& request) {
        AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
        return (processRequest(request, context));
    }

    /// @brief Runs processing DHCPRELEASE.
    ///
    /// @param release message received from client
    void processRelease(Pkt4Ptr& release) {
        AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
        processRelease(release, context);
    }

    /// @brief Runs processing DHCPDECLINE
    ///
    /// @param decline message received from client
    void processDecline(Pkt4Ptr& decline) {
        AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4());
        processDecline(decline, context);
    }

    /// @brief Dummy server identifier option used by various tests.
    OptionPtr server_id_;

    /// @brief packets we pretend to receive
    ///
    /// Instead of setting up sockets on interfaces that change between OSes, it
    /// is much easier to fake packet reception. This is a list of packets that
    /// we pretend to have received. You can schedule new packets to be received
    /// using fakeReceive() and NakedDhcpv4Srv::receivePacket() methods.
    std::list<Pkt4Ptr> fake_received_;

    std::list<Pkt4Ptr> fake_sent_;

    using Dhcpv4Srv::adjustIfaceData;
    using Dhcpv4Srv::appendServerID;
    using Dhcpv4Srv::processDiscover;
    using Dhcpv4Srv::processRequest;
    using Dhcpv4Srv::processRelease;
    using Dhcpv4Srv::processDecline;
    using Dhcpv4Srv::processInform;
    using Dhcpv4Srv::processClientName;
    using Dhcpv4Srv::createNameChangeRequests;
    using Dhcpv4Srv::acceptServerId;
    using Dhcpv4Srv::sanityCheck;
    using Dhcpv4Srv::srvidToString;
    using Dhcpv4Srv::classifyPacket;
    using Dhcpv4Srv::deferredUnpack;
    using Dhcpv4Srv::accept;
    using Dhcpv4Srv::acceptMessageType;
    using Dhcpv4Srv::selectSubnet;
    using Dhcpv4Srv::VENDOR_CLASS_PREFIX;
    using Dhcpv4Srv::shutdown_;
    using Dhcpv4Srv::alloc_engine_;
    using Dhcpv4Srv::client_port_;
};

// We need to pass one reference to the Dhcp4Client, which is defined in
// dhcp4_client.h. That header includes this file. To avoid circular
// dependencies, we use forward declaration here.
class Dhcp4Client;

/// @brief Base class for DHCPv4 server testing.
///
/// Currently it configures the test data path directory in
/// the @c CfgMgr. When the object is destroyed, the original
/// path is reverted.
class BaseServerTest : public ::testing::Test {
public:

    /// @brief Constructor.
    BaseServerTest();

    /// @brief Destructor.
    virtual ~BaseServerTest();

private:

    /// @brief Holds the original data directory.
    std::string original_datadir_;

};

class Dhcpv4SrvTest : public BaseServerTest {
public:

    enum ExpectedResult {
        SHOULD_PASS, // pass = accept decline, move lease to declined state.
        SHOULD_FAIL  // fail = reject the decline
    };

    /// @brief Constructor
    ///
    /// Initializes common objects used in many tests.
    /// Also sets up initial configuration in CfgMgr.
    Dhcpv4SrvTest();

    /// @brief destructor
    virtual ~Dhcpv4SrvTest();

    /// @brief Add 'Parameter Request List' option to the packet.
    ///
    /// This function adds PRL option comprising the following option codes:
    /// - 5 - Name Server
    /// - 15 - Domain Name
    /// - 7 - Log Server
    /// - 8 - Quotes Server
    /// - 9 - LPR Server
    ///
    /// @param pkt packet to add PRL option to.
    void addPrlOption(Pkt4Ptr& pkt);

    /// @brief Configures options being requested in the PRL option.
    ///
    /// The lpr-servers option is NOT configured here although it is
    /// added to the 'Parameter Request List' option in the
    /// \ref addPrlOption. When requested option is not configured
    /// the server should not return it in its response. The goal
    /// of not configuring the requested option is to verify that
    /// the server will not return it.
    void configureRequestedOptions();

    /// @brief checks that the response matches request
    /// @param q query (client's message)
    /// @param a answer (server's message)
    void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a);

    /// @brief Check that certain basic (always added when lease is acquired)
    /// are present in a message.
    ///
    /// @param pkt A message to be checked.
    /// @return Assertion result which indicates whether test passed or failed.
    ::testing::AssertionResult basicOptionsPresent(const Pkt4Ptr& pkt);

    /// @brief Check that certain basic (always added when lease is acquired)
    /// are not present.
    ///
    /// @param pkt A packet to be checked.
    /// @return Assertion result which indicates whether test passed or failed.
    ::testing::AssertionResult noBasicOptions(const Pkt4Ptr& pkt);

    /// @brief Check that certain requested options are present in the message.
    ///
    /// @param pkt A message to be checked.
    /// @return Assertion result which indicates whether test passed or failed.
    ::testing::AssertionResult requestedOptionsPresent(const Pkt4Ptr& pkt);

    /// @brief Check that certain options (requested with PRL option)
    /// are not present.
    ///
    /// @param pkt A packet to be checked.
    /// @return Assertion result which indicates whether test passed or failed.
    ::testing::AssertionResult noRequestedOptions(const Pkt4Ptr& pkt);

    /// @brief generates client-id option
    ///
    /// Generate client-id option of specified length
    /// Ids with different lengths are sufficient to generate
    /// unique ids. If more fine grained control is required,
    /// tests generate client-ids on their own.
    /// Sets client_id_ field.
    /// @param size size of the client-id to be generated
    OptionPtr generateClientId(size_t size = 4);

    /// @brief generate hardware address
    ///
    /// @param size size of the generated MAC address
    /// @param pointer to Hardware Address object
    HWAddrPtr generateHWAddr(size_t size = 6);

    /// Check that address was returned from proper range, that its lease
    /// lifetime is correct, that T1 and T2 are returned properly
    /// @param rsp response to be checked
    /// @param subnet subnet that should be used to verify assigned address
    ///        and options
    /// @param t1_present check that t1 must be present (true) or must not be
    /// present (false)
    /// @param t2_present check that t2 must be present (true) or must not be
    /// present (false)
    void checkAddressParams(const Pkt4Ptr& rsp, const Subnet4Ptr subnet,
                            bool t1_present = false,
                            bool t2_present = false);

    /// @brief Basic checks for generated response (message type and trans-id).
    ///
    /// @param rsp response packet to be validated
    /// @param expected_message_type expected message type
    /// @param expected_transid expected transaction-id
    void checkResponse(const Pkt4Ptr& rsp, int expected_message_type,
                       uint32_t expected_transid);

    /// @brief Checks if the lease sent to client is present in the database
    ///
    /// @param rsp response packet to be validated
    /// @param client_id expected client-identifier (or NULL)
    /// @param HWAddr expected hardware address (not used now)
    /// @param expected_addr expected address
    Lease4Ptr checkLease(const Pkt4Ptr& rsp, const OptionPtr& client_id,
                         const HWAddrPtr&,
                         const isc::asiolink::IOAddress& expected_addr);

    /// @brief Checks if server response (OFFER, ACK, NAK) includes proper server-id
    /// @param rsp response packet to be validated
    /// @param expected_srvid expected value of server-id
    void checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid);

    /// @brief Checks if server response (OFFER, ACK, NAK) includes proper client-id
    ///
    /// This method follows values reported by CfgMgr in echoClientId() method.
    /// Depending on its configuration, the client-id is either mandatory or
    /// forbidden to appear in the response.
    ///
    /// @param rsp response packet to be validated
    /// @param expected_clientid expected value of client-id
    void checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid);

    /// @brief Create packet from output buffer of another packet.
    ///
    /// This function creates a packet using an output buffer from another
    /// packet. This imitates reception of a packet from the wire. The
    /// unpack function is then called to parse packet contents and to
    /// create a collection of the options carried by this packet.
    ///
    /// This function is useful for unit tests which verify that the received
    /// packet is parsed correctly. In those cases it is usually inappropriate
    /// to create an instance of the packet, add options, set packet
    /// fields and use such packet as an input to processDiscover or
    /// processRequest function. This is because, such a packet has certain
    /// options already initialized and there is no way to verify that they
    /// have been initialized when packet instance was created or wire buffer
    /// processing. By creating a packet from the buffer we guarantee that the
    /// new packet is entirely initialized during wire data parsing.
    ///
    /// @param src_pkt A source packet, to be copied.
    /// @param [out] dst_pkt A destination packet.
    ///
    /// @return assertion result indicating if a function completed with
    /// success or failure.
    static ::testing::AssertionResult
    createPacketFromBuffer(const isc::dhcp::Pkt4Ptr& src_pkt,
                           isc::dhcp::Pkt4Ptr& dst_pkt);

    /// @brief Tests if Discover or Request message is processed correctly
    ///
    /// This test verifies that the Parameter Request List option is handled
    /// correctly, i.e. it checks that certain options are present in the
    /// server's response when they are requested and that they are not present
    /// when they are not requested or NAK occurs.
    ///
    /// @todo We need an additional test for PRL option using real traffic
    /// capture.
    ///
    /// @param msg_type DHCPDISCOVER or DHCPREQUEST
    void testDiscoverRequest(const uint8_t msg_type);

    /// @brief Runs DHCPv4 configuration from the JSON string.
    ///
    /// @param config String holding server configuration in JSON format.
    /// @param commit A boolean flag indicating if the new configuration
    /// should be committed (if true), or not (if false).
    void configure(const std::string& config, const bool commit = true);

    /// @brief Configure specified DHCP server using JSON string.
    ///
    /// @param config String holding server configuration in JSON format.
    /// @param srv Instance of the server to be configured.
    /// @param commit A boolean flag indicating if the new configuration
    /// should be committed (if true), or not (if false).
    void configure(const std::string& config, NakedDhcpv4Srv& srv,
                   const bool commit = true);

    /// @brief Pretends a packet of specified type was received.
    ///
    /// Instantiates fake network interfaces, configures passed Dhcpv4Srv,
    /// then creates a message of specified type and sends it to the
    /// server and then checks whether expected statistics were set
    /// appropriately.
    ///
    /// @param srv the DHCPv4 server to be used
    /// @param config JSON configuration to be used
    /// @param pkt_type type of the packet to be faked
    /// @param stat_name name of the expected statistic
    void pretendReceivingPkt(NakedDhcpv4Srv& srv, const std::string& config,
                             uint8_t pkt_type, const std::string& stat_name);

    /// @brief Create @c Dhcpv4Exchange from client's query.
    Dhcpv4Exchange createExchange(const Pkt4Ptr& query);

    /// @brief Performs 4-way exchange to obtain new lease.
    ///
    /// This is used as a preparatory step for Decline operation.
    ///
    /// @param client Client to be used to obtain a lease.
    void acquireLease(Dhcp4Client& client);

    /// @brief Tests if the acquired lease is or is not declined.
    ///
    /// @param client Dhcp4Client instance
    /// @param hw_address_1 HW Address to be used to acquire the lease.
    /// @param client_id_1 Client id to be used to acquire the lease.
    /// @param hw_address_2 HW Address to be used to decline the lease.
    /// @param client_id_2 Client id to be used to decline the lease.
    /// @param expected_result SHOULD_PASS if the lease is expected to
    /// be successfully declined, or SHOULD_FAIL if the lease is expected
    /// to not be declined.
    void acquireAndDecline(Dhcp4Client& client,
                           const std::string& hw_address_1,
                           const std::string& client_id_1,
                           const std::string& hw_address_2,
                           const std::string& client_id_2,
                           ExpectedResult expected_result);

    /// @brief This function cleans up after the test.
    virtual void TearDown();

    /// @brief A subnet used in most tests
    Subnet4Ptr subnet_;

    /// @brief A pool used in most tests
    Pool4Ptr pool_;

    /// @brief A client-id used in most tests
    ClientIdPtr client_id_;

    int rcode_;

    isc::data::ConstElementPtr comment_;

    /// @brief Server object under test.
    NakedDhcpv4Srv srv_;
};

/// @brief Patch the server config to add interface-config/re-detect=false
/// @param json the server config
inline void
disableIfacesReDetect(isc::data::ConstElementPtr json) {
    isc::data::ConstElementPtr ifaces_cfg = json->get("interfaces-config");
    if (ifaces_cfg) {
        isc::data::ElementPtr mutable_cfg =
            boost::const_pointer_cast<isc::data::Element>(ifaces_cfg);
        mutable_cfg->set("re-detect", isc::data::Element::create(false));
    }
}

/// @brief Runs parser in JSON mode, useful for parser testing
///
/// @param in string to be parsed
/// @return ElementPtr structure representing parsed JSON
inline isc::data::ElementPtr
parseJSON(const std::string& in)
{
    isc::dhcp::Parser4Context ctx;
    return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_JSON));
}

/// @brief Runs parser in Dhcp4 mode
///
/// This is a simplified Dhcp4 mode, so no outer { } and "Dhcp4" is
/// needed. This format is used by most of the tests.
///
/// @param in string to be parsed
/// @param verbose display the exception message when it fails
/// @return ElementPtr structure representing parsed JSON
inline isc::data::ElementPtr
parseDHCP4(const std::string& in, bool verbose = false)
{
    try {
        isc::dhcp::Parser4Context ctx;
        isc::data::ElementPtr json;
        json = ctx.parseString(in, isc::dhcp::Parser4Context::SUBPARSER_DHCP4);
        disableIfacesReDetect(json);
        return (json);
    }
    catch (const std::exception& ex) {
        if (verbose) {
            std::cout << "EXCEPTION: " << ex.what() << std::endl;
        }
        throw;
    }
}

/// @brief Runs parser in option definition mode
///
/// This function parses specified text as JSON that defines option definitions.
///
/// @param in string to be parsed
/// @param verbose display the exception message when it fails
/// @return ElementPtr structure representing parsed JSON
inline isc::data::ElementPtr
parseOPTION_DEFS(const std::string& in, bool verbose = false)
{
    try {
        isc::dhcp::Parser4Context ctx;
        return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_OPTION_DEFS));
    }
    catch (const std::exception& ex) {
        if (verbose) {
            std::cout << "EXCEPTION: " << ex.what() << std::endl;
        }
        throw;
    }
}

}; // end of isc::dhcp::test namespace
}; // end of isc::dhcp namespace
}; // end of isc namespace

#endif // DHCP4_TEST_UTILS_H
