// Copyright (C) 2011-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 <asiolink/io_address.h>
#include <dhcp/testutils/pkt_captures.h>
#include <dhcp/dhcp4.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/docsis3_option_defs.h>
#include <dhcp/option_int.h>
#include <dhcp/option_string.h>
#include <dhcp/option4_addrlst.h>
#include <dhcp/option_vendor.h>
#include <dhcp/pkt4.h>
#include <exceptions/exceptions.h>
#include <testutils/gtest_utils.h>
#include <util/buffer.h>
#include <util/str.h>
#include <util/encode/encode.h>

#include <boost/shared_array.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/static_assert.hpp>
#include <gtest/gtest.h>

#include <iostream>
#include <sstream>

#include <arpa/inet.h>

using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
using namespace isc::util;
// Don't import the entire boost namespace.  It will unexpectedly hide uint8_t
// for some systems.
using boost::scoped_ptr;

namespace {

/// V4 Options being used for pack/unpack testing.
/// For test simplicity, all selected options have
/// variable length data so as there are no restrictions
/// on a length of their data.
static uint8_t v4_opts[] = {
    53, 1, 2, // Message Type (required to not throw exception during unpack)
    12,  3, 0,   1,  2, // Hostname
    14,  3, 10, 11, 12, // Merit Dump File
    60,  3, 20, 21, 22, // Class Id
    128, 3, 30, 31, 32, // Vendor specific
    254, 3, 40, 41, 42, // Reserved
};

// Sample data
const uint8_t dummyOp = BOOTREQUEST;
const uint8_t dummyHtype = 6;
const uint8_t dummyHlen = 6;
const uint8_t dummyHops = 13;
const uint32_t dummyTransid = 0x12345678;
const uint16_t dummySecs = 42;
const uint16_t dummyFlags = BOOTP_BROADCAST;

const IOAddress dummyCiaddr("192.0.2.1");
const IOAddress dummyYiaddr("1.2.3.4");
const IOAddress dummySiaddr("192.0.2.255");
const IOAddress dummyGiaddr("255.255.255.255");

// a dummy MAC address
const uint8_t dummyMacAddr[] = {0, 1, 2, 3, 4, 5};

// A dummy MAC address, padded with 0s
const uint8_t dummyChaddr[16] = {0, 1, 2, 3, 4, 5, 0, 0,
                                 0, 0, 0, 0, 0, 0, 0, 0 };

// Let's use some creative test content here (128 chars + \0)
const uint8_t dummyFile[] = "Lorem ipsum dolor sit amet, consectetur "
    "adipiscing elit. Proin mollis placerat metus, at "
    "lacinia orci ornare vitae. Mauris amet.";

// Yet another type of test content (64 chars + \0)
const uint8_t dummySname[] = "Lorem ipsum dolor sit amet, consectetur "
    "adipiscing elit posuere.";

BOOST_STATIC_ASSERT(sizeof(dummyFile)  == Pkt4::MAX_FILE_LEN + 1);
BOOST_STATIC_ASSERT(sizeof(dummySname) == Pkt4::MAX_SNAME_LEN + 1);


class Pkt4Test : public ::testing::Test {
public:
    Pkt4Test() {
    }

    /// @brief Generates test packet.
    ///
    /// Allocates and generates test packet, with all fixed fields set to non-zero
    /// values. Content is not always reasonable.
    ///
    /// See generateTestPacket2() function that returns exactly the same packet in
    /// on-wire format.
    ///
    /// @return pointer to allocated Pkt4 object.
    Pkt4Ptr generateTestPacket1() {

        boost::shared_ptr<Pkt4> pkt(new Pkt4(DHCPDISCOVER, dummyTransid));

        vector<uint8_t> vectorMacAddr(dummyMacAddr, dummyMacAddr
                                      + sizeof(dummyMacAddr));

        // hwType = 6(ETHERNET), hlen = 6(MAC address len)
        pkt->setHWAddr(dummyHtype, dummyHlen, vectorMacAddr);
        pkt->setHops(dummyHops); // 13 relays. Wow!
        // Transaction-id is already set.
        pkt->setSecs(dummySecs);
        pkt->setFlags(dummyFlags); // all flags set
        pkt->setCiaddr(dummyCiaddr);
        pkt->setYiaddr(dummyYiaddr);
        pkt->setSiaddr(dummySiaddr);
        pkt->setGiaddr(dummyGiaddr);
        // Chaddr already set with setHWAddr().
        pkt->setSname(dummySname, 64);
        pkt->setFile(dummyFile, 128);

        return (pkt);
    }

    /// @brief Generates test packet.
    ///
    /// Allocates and generates on-wire buffer that represents test packet, with all
    /// fixed fields set to non-zero values.  Content is not always reasonable.
    ///
    /// See generateTestPacket1() function that returns exactly the same packet as
    /// Pkt4 object.
    ///
    /// @return pointer to allocated Pkt4 object
    // Returns a vector containing a DHCPv4 packet header.
    vector<uint8_t> generateTestPacket2() {

        // That is only part of the header. It contains all "short" fields,
        // larger fields are constructed separately.
        uint8_t hdr[] = {
            1, 6, 6, 13,            // op, htype, hlen, hops,
            0x12, 0x34, 0x56, 0x78, // transaction-id
            0, 42, 0x80, 0x00,      // 42 secs, BROADCAST flags
            192, 0, 2, 1,           // ciaddr
            1, 2, 3, 4,             // yiaddr
            192, 0, 2, 255,         // siaddr
            255, 255, 255, 255,     // giaddr
        };

        // Initialize the vector with the header fields defined above.
        vector<uint8_t> buf(hdr, hdr + sizeof(hdr));

        // Append the large header fields.
        copy(dummyChaddr, dummyChaddr + Pkt4::MAX_CHADDR_LEN, back_inserter(buf));
        copy(dummySname, dummySname + Pkt4::MAX_SNAME_LEN, back_inserter(buf));
        copy(dummyFile, dummyFile + Pkt4::MAX_FILE_LEN, back_inserter(buf));

        // Should now have all the header, so check.  The "static_cast" is used
        // to get round an odd bug whereby the linker appears not to find the
        // definition of DHCPV4_PKT_HDR_LEN if it appears within an EXPECT_EQ().
        EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), buf.size());

        return (buf);
    }

    /// @brief Verify that the options are correct after parsing.
    ///
    /// @param pkt A packet holding parsed options.
    void verifyParsedOptions(const Pkt4Ptr& pkt) {
        EXPECT_TRUE(pkt->getOption(12));
        EXPECT_TRUE(pkt->getOption(60));
        EXPECT_TRUE(pkt->getOption(14));
        EXPECT_TRUE(pkt->getOption(128));
        EXPECT_TRUE(pkt->getOption(254));

        // Verify the packet type is correct.
        ASSERT_EQ(DHCPOFFER, pkt->getType());

        // First option after message type starts at 3.
        uint8_t *opt_data_ptr = v4_opts + 3;

        // Option 12 is represented by the OptionString class so let's do
        // the appropriate conversion.
        boost::shared_ptr<Option> x = pkt->getOption(12);
        ASSERT_TRUE(x); // option 1 should exist
        OptionStringPtr option12 = boost::static_pointer_cast<OptionString>(x);

        ASSERT_TRUE(option12);
        EXPECT_EQ(12, option12->getType());  // this should be option 12
        ASSERT_EQ(3, option12->getValue().length()); // it should be of length 3
        EXPECT_EQ(5, option12->len()); // total option length 5
        EXPECT_EQ(0, memcmp(&option12->getValue()[0], opt_data_ptr + 2, 2)); // data len=3
        opt_data_ptr += x->len();

        x = pkt->getOption(14);
        ASSERT_TRUE(x); // option 14 should exist
        // Option 14 is represented by the OptionString class so let's do
        // the appropriate conversion.
        OptionStringPtr option14 = boost::static_pointer_cast<OptionString>(x);
        ASSERT_TRUE(option14);
        EXPECT_EQ(14, option14->getType());  // this should be option 14
        ASSERT_EQ(3, option14->getValue().length()); // it should be of length 3
        EXPECT_EQ(5, option14->len()); // total option length 5

        EXPECT_EQ(0, memcmp(&option14->getValue()[0], opt_data_ptr + 2, 3)); // data len=3
        opt_data_ptr += x->len();

        x = pkt->getOption(60);
        ASSERT_TRUE(x); // option 60 should exist
        EXPECT_EQ(60, x->getType());  // this should be option 60
        ASSERT_EQ(3, x->getData().size()); // it should be of length 3
        EXPECT_EQ(5, x->len()); // total option length 5
        EXPECT_EQ(0, memcmp(&x->getData()[0], opt_data_ptr + 2, 3)); // data len=3
        opt_data_ptr += x->len();

        x = pkt->getOption(128);
        ASSERT_TRUE(x); // option 3 should exist
        EXPECT_EQ(128, x->getType());  // this should be option 254
        ASSERT_EQ(3, x->getData().size()); // it should be of length 3
        EXPECT_EQ(5, x->len()); // total option length 5
        EXPECT_EQ(0, memcmp(&x->getData()[0], opt_data_ptr + 2, 3)); // data len=3
        opt_data_ptr += x->len();

        x = pkt->getOption(254);
        ASSERT_TRUE(x); // option 3 should exist
        EXPECT_EQ(254, x->getType());  // this should be option 254
        ASSERT_EQ(3, x->getData().size()); // it should be of length 3
        EXPECT_EQ(5, x->len()); // total option length 5
        EXPECT_EQ(0, memcmp(&x->getData()[0], opt_data_ptr + 2, 3)); // data len=3
    }

};


TEST_F(Pkt4Test, constructor) {

    ASSERT_EQ(236U, static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) );
    scoped_ptr<Pkt4> pkt;

    // Just some dummy payload.
    uint8_t testData[250];
    for (uint8_t i = 0; i < 250; i++) {
        testData[i] = i;
    }

    // Positive case1. Normal received packet.
    EXPECT_NO_THROW(pkt.reset(new Pkt4(testData, Pkt4::DHCPV4_PKT_HDR_LEN)));

    EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), pkt->len());

    EXPECT_NO_THROW(pkt.reset());

    // Positive case2. Normal outgoing packet.
    EXPECT_NO_THROW(pkt.reset(new Pkt4(DHCPDISCOVER, 0xffffffff)));

    // DHCPv4 packet must be at least 236 bytes long, with Message Type
    // Option taking extra 3 bytes it is 239
    EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) + 3, pkt->len());
    EXPECT_EQ(DHCPDISCOVER, pkt->getType());
    EXPECT_EQ(0xffffffff, pkt->getTransid());
    EXPECT_NO_THROW(pkt.reset());

    // Negative case. Should drop truncated messages.
    EXPECT_THROW(
        pkt.reset(new Pkt4(testData, Pkt4::DHCPV4_PKT_HDR_LEN - 1)),
        OutOfRange
    );
}


TEST_F(Pkt4Test, fixedFields) {

    boost::shared_ptr<Pkt4> pkt = generateTestPacket1();

    // OK, let's check packet values
    EXPECT_EQ(dummyOp, pkt->getOp());
    EXPECT_EQ(dummyHtype, pkt->getHtype());
    EXPECT_EQ(dummyHlen, pkt->getHlen());
    EXPECT_EQ(dummyHops, pkt->getHops());
    EXPECT_EQ(dummyTransid, pkt->getTransid());
    EXPECT_EQ(dummySecs, pkt->getSecs());
    EXPECT_EQ(dummyFlags, pkt->getFlags());

    EXPECT_EQ(dummyCiaddr, pkt->getCiaddr());
    EXPECT_EQ(dummyYiaddr, pkt->getYiaddr());
    EXPECT_EQ(dummySiaddr, pkt->getSiaddr());
    EXPECT_EQ(dummyGiaddr, pkt->getGiaddr());

    // Chaddr contains link-layer addr (MAC). It is no longer always 16 bytes
    // long and its length depends on hlen value (it is up to 16 bytes now).
    ASSERT_EQ(pkt->getHWAddr()->hwaddr_.size(), dummyHlen);
    EXPECT_EQ(0, memcmp(dummyChaddr, &pkt->getHWAddr()->hwaddr_[0], dummyHlen));

    EXPECT_EQ(0, memcmp(dummySname, &pkt->getSname()[0], 64));

    EXPECT_EQ(0, memcmp(dummyFile, &pkt->getFile()[0], 128));

    EXPECT_EQ(DHCPDISCOVER, pkt->getType());
}

TEST_F(Pkt4Test, fixedFieldsPack) {
    boost::shared_ptr<Pkt4> pkt = generateTestPacket1();
    vector<uint8_t> expectedFormat = generateTestPacket2();

    EXPECT_NO_THROW(
        pkt->pack();
    );

    // Minimum packet size is 236 bytes + 3 bytes of mandatory
    // DHCP Message Type Option
    ASSERT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) + 3, pkt->len());

    // Redundant but MUCH easier for debug in gdb
    const uint8_t* exp = &expectedFormat[0];
    const uint8_t* got = pkt->getBuffer().getData();

    EXPECT_EQ(0, memcmp(exp, got, Pkt4::DHCPV4_PKT_HDR_LEN));
}

/// TODO Uncomment when ticket #1226 is implemented
TEST_F(Pkt4Test, fixedFieldsUnpack) {
    vector<uint8_t> expectedFormat = generateTestPacket2();

    expectedFormat.push_back(0x63); // magic cookie
    expectedFormat.push_back(0x82);
    expectedFormat.push_back(0x53);
    expectedFormat.push_back(0x63);

    expectedFormat.push_back(0x35); // message-type
    expectedFormat.push_back(0x1);
    expectedFormat.push_back(0x1);

    boost::shared_ptr<Pkt4> pkt(new Pkt4(&expectedFormat[0],
                                         expectedFormat.size()));;


    EXPECT_NO_THROW(
        pkt->unpack()
    );

    // OK, let's check packet values
    EXPECT_EQ(dummyOp, pkt->getOp());
    EXPECT_EQ(dummyHtype, pkt->getHtype());
    EXPECT_EQ(dummyHlen, pkt->getHlen());
    EXPECT_EQ(dummyHops, pkt->getHops());
    EXPECT_EQ(dummyTransid, pkt->getTransid());
    EXPECT_EQ(dummySecs, pkt->getSecs());
    EXPECT_EQ(dummyFlags, pkt->getFlags());

    EXPECT_EQ(dummyCiaddr, pkt->getCiaddr());
    EXPECT_EQ("1.2.3.4", pkt->getYiaddr().toText());
    EXPECT_EQ("192.0.2.255", pkt->getSiaddr().toText());
    EXPECT_EQ("255.255.255.255", pkt->getGiaddr().toText());

    // chaddr is always 16 bytes long and contains link-layer addr (MAC)
    EXPECT_EQ(0, memcmp(dummyChaddr, &pkt->getHWAddr()->hwaddr_[0], dummyHlen));

    ASSERT_EQ(static_cast<size_t>(Pkt4::MAX_SNAME_LEN), pkt->getSname().size());
    EXPECT_EQ(0, memcmp(dummySname, &pkt->getSname()[0], Pkt4::MAX_SNAME_LEN));

    ASSERT_EQ(static_cast<size_t>(Pkt4::MAX_FILE_LEN), pkt->getFile().size());
    EXPECT_EQ(0, memcmp(dummyFile, &pkt->getFile()[0], Pkt4::MAX_FILE_LEN));

    EXPECT_EQ(DHCPDISCOVER, pkt->getType());
}

// This test is for hardware addresses (htype, hlen and chaddr fields)
TEST_F(Pkt4Test, hwAddr) {

    vector<uint8_t> mac;
    uint8_t expectedChaddr[Pkt4::MAX_CHADDR_LEN];

    // We resize vector to specified length. It is more natural for fixed-length
    // field, than clear it (shrink size to 0) and push_back each element
    // (growing length back to MAX_CHADDR_LEN).
    mac.resize(Pkt4::MAX_CHADDR_LEN);

    scoped_ptr<Pkt4> pkt;
    // let's test each hlen, from 0 till 16
    for (size_t macLen = 0; macLen < Pkt4::MAX_CHADDR_LEN; macLen++) {
        for (size_t i = 0; i < Pkt4::MAX_CHADDR_LEN; i++) {
            mac[i] = 0;
            expectedChaddr[i] = 0;
        }
        for (size_t i = 0; i < macLen; i++) {
            mac[i] = 128 + i;
            expectedChaddr[i] = 128 + i;
        }

        // type and transaction doesn't matter in this test
        pkt.reset(new Pkt4(DHCPOFFER, 1234));
        pkt->setHWAddr(255 - macLen * 10, // just weird htype
                       macLen,
                       mac);
        EXPECT_EQ(0, memcmp(expectedChaddr, &pkt->getHWAddr()->hwaddr_[0],
                            Pkt4::MAX_CHADDR_LEN));

        EXPECT_NO_THROW(
            pkt->pack();
        );

        // CHADDR starts at offset 28 in DHCP packet
        const uint8_t* ptr = pkt->getBuffer().getData() + 28;

        EXPECT_EQ(0, memcmp(ptr, expectedChaddr, Pkt4::MAX_CHADDR_LEN));

        pkt.reset();
    }

    /// TODO: extend this test once options support is implemented. HW address
    /// longer than 16 bytes should be stored in client-identifier option
}

TEST_F(Pkt4Test, msgTypes) {

    struct msgType {
        uint8_t dhcp;
        uint8_t bootp;
    };

    msgType types[] = {
        {DHCPDISCOVER, BOOTREQUEST},
        {DHCPOFFER, BOOTREPLY},
        {DHCPREQUEST, BOOTREQUEST},
        {DHCPDECLINE, BOOTREQUEST},
        {DHCPACK, BOOTREPLY},
        {DHCPNAK, BOOTREPLY},
        {DHCPRELEASE, BOOTREQUEST},
        {DHCPINFORM, BOOTREQUEST},
        {DHCPLEASEQUERY, BOOTREQUEST},
        {DHCPLEASEUNASSIGNED, BOOTREPLY},
        {DHCPLEASEUNKNOWN, BOOTREPLY},
        {DHCPLEASEACTIVE, BOOTREPLY}
    };

    scoped_ptr<Pkt4> pkt;
    for (size_t i = 0; i < sizeof(types) / sizeof(msgType); i++) {
        pkt.reset(new Pkt4(types[i].dhcp, 0));
        EXPECT_EQ(types[i].dhcp, pkt->getType());
        EXPECT_EQ(types[i].bootp, pkt->getOp());
        pkt.reset();
    }

    EXPECT_THROW(
        pkt.reset(new Pkt4(100, 0)), // There's no message type 100
        OutOfRange
    );
}

// This test verifies handling of sname field
TEST_F(Pkt4Test, sname) {

    uint8_t sname[Pkt4::MAX_SNAME_LEN];

    scoped_ptr<Pkt4> pkt;
    // Let's test each sname length, from 0 till 64 (included)
    for (size_t snameLen = 0; snameLen <= Pkt4::MAX_SNAME_LEN; ++snameLen) {
        for (size_t i = 0; i < snameLen; ++i) {
            sname[i] = i + 1;
        }
        if (snameLen < Pkt4::MAX_SNAME_LEN) {
                for (size_t i = snameLen; i < Pkt4::MAX_SNAME_LEN; ++i) {
                        sname[i] = 0;
                }
        }

        // Type and transaction doesn't matter in this test
        pkt.reset(new Pkt4(DHCPOFFER, 1234));
        pkt->setSname(sname, snameLen);

        EXPECT_EQ(0, memcmp(sname, &pkt->getSname()[0], Pkt4::MAX_SNAME_LEN));

        EXPECT_NO_THROW(
            pkt->pack();
        );

        // SNAME starts at offset 44 in DHCP packet
        const uint8_t* ptr = pkt->getBuffer().getData() + 44;
        EXPECT_EQ(0, memcmp(ptr, sname, Pkt4::MAX_SNAME_LEN));

        pkt.reset();
    }

    // Check that a null argument generates an exception.
    Pkt4 pkt4(DHCPOFFER, 1234);
    EXPECT_THROW(pkt4.setSname(NULL, Pkt4::MAX_SNAME_LEN), InvalidParameter);
    EXPECT_THROW(pkt4.setSname(NULL, 0), InvalidParameter);

    // Check that a too long argument generates an exception
    // (the actual content doesn't matter).
    uint8_t bigsname[Pkt4::MAX_SNAME_LEN + 1];
    EXPECT_THROW(pkt4.setSname(bigsname, Pkt4::MAX_SNAME_LEN + 1), OutOfRange);
}

TEST_F(Pkt4Test, file) {

    uint8_t file[Pkt4::MAX_FILE_LEN];

    scoped_ptr<Pkt4> pkt;
    // Let's test each file length, from 0 till 128 (included).
    for (size_t fileLen = 0; fileLen <= Pkt4::MAX_FILE_LEN; ++fileLen) {
        for (size_t i = 0; i < fileLen; ++i) {
            file[i] = i + 1;
        }
        if (fileLen < Pkt4::MAX_FILE_LEN) {
                for (size_t i = fileLen; i < Pkt4::MAX_FILE_LEN; ++i) {
                        file[i] = 0;
                }
        }

        // Type and transaction doesn't matter in this test.
        pkt.reset(new Pkt4(DHCPOFFER, 1234));
        pkt->setFile(file, fileLen);

        EXPECT_EQ(0, memcmp(file, &pkt->getFile()[0], Pkt4::MAX_FILE_LEN));

        EXPECT_NO_THROW(
            pkt->pack();
        );

        // FILE starts at offset 108 in DHCP packet.
        const uint8_t* ptr = pkt->getBuffer().getData() + 108;
        EXPECT_EQ(0, memcmp(ptr, file, Pkt4::MAX_FILE_LEN));

        pkt.reset();
    }

    // Check that a null argument generates an exception.
    Pkt4 pkt4(DHCPOFFER, 1234);
    EXPECT_THROW(pkt4.setFile(NULL, Pkt4::MAX_FILE_LEN), InvalidParameter);
    EXPECT_THROW(pkt4.setFile(NULL, 0), InvalidParameter);

    // Check that a too long argument generates an exception
    // (the actual content doesn't matter).
    uint8_t bigfile[Pkt4::MAX_FILE_LEN + 1];
    EXPECT_THROW(pkt4.setFile(bigfile, Pkt4::MAX_FILE_LEN + 1), OutOfRange);
}

TEST_F(Pkt4Test, options) {
    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 0));

    vector<uint8_t> payload[5];
    for (uint8_t i = 0; i < 5; i++) {
        payload[i].push_back(i * 10);
        payload[i].push_back(i * 10 + 1);
        payload[i].push_back(i * 10 + 2);
    }

    boost::shared_ptr<Option> opt1(new Option(Option::V4, 12, payload[0]));
    boost::shared_ptr<Option> opt3(new Option(Option::V4, 14, payload[1]));
    boost::shared_ptr<Option> opt2(new Option(Option::V4, 60, payload[2]));
    boost::shared_ptr<Option> opt5(new Option(Option::V4,128, payload[3]));
    boost::shared_ptr<Option> opt4(new Option(Option::V4,254, payload[4]));

    pkt->addOption(opt1);
    pkt->addOption(opt2);
    pkt->addOption(opt3);
    pkt->addOption(opt4);
    pkt->addOption(opt5);

    EXPECT_TRUE(pkt->getOption(12));
    EXPECT_TRUE(pkt->getOption(60));
    EXPECT_TRUE(pkt->getOption(14));
    EXPECT_TRUE(pkt->getOption(128));
    EXPECT_TRUE(pkt->getOption(254));
    EXPECT_FALSE(pkt->getOption(127)); //  no such option

    // Options are unique in DHCPv4. It should not be possible
    // to add more than one option of the same type.
    EXPECT_THROW(
        pkt->addOption(opt1),
        BadValue
    );

    EXPECT_NO_THROW(
        pkt->pack();
    );

    const OutputBuffer& buf = pkt->getBuffer();
    // Check that all options are stored, they should take sizeof(v4_opts),
    // DHCP magic cookie (4 bytes), and OPTION_END added (just one byte)
    ASSERT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) +
              sizeof(DHCP_OPTIONS_COOKIE) + sizeof(v4_opts) + 1,
              buf.getLength());

    // That that this extra data actually contain our options
    const uint8_t* ptr = buf.getData();

    // Rewind to end of fixed part.
    ptr += Pkt4::DHCPV4_PKT_HDR_LEN + sizeof(DHCP_OPTIONS_COOKIE);

    EXPECT_EQ(0, memcmp(ptr, v4_opts, sizeof(v4_opts)));
    EXPECT_EQ(DHO_END, static_cast<uint8_t>(*(ptr + sizeof(v4_opts))));

    // delOption() checks
    EXPECT_TRUE(pkt->getOption(12));  // Sanity check: option 12 is still there
    EXPECT_TRUE(pkt->delOption(12));  // We should be able to remove it
    EXPECT_FALSE(pkt->getOption(12)); // It should not be there anymore
    EXPECT_FALSE(pkt->delOption(12)); // And removal should fail

    EXPECT_NO_THROW(pkt.reset());
}

// Check that multiple options of the same type may be retrieved by
// using getOptions, Also check that retrieved options are copied when
// setCopyRetrievedOptions is enabled.
TEST_F(Pkt4Test, getOptions) {
    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 0));
    OptionPtr opt1(new Option(Option::V4, 1));
    OptionPtr opt2(new Option(Option::V4, 1));
    OptionPtr opt3(new Option(Option::V4, 2));
    OptionPtr opt4(new Option(Option::V4, 2));

    pkt->addOption(opt1);
    pkt->Pkt::addOption(opt2);
    pkt->Pkt::addOption(opt3);
    pkt->Pkt::addOption(opt4);

    // Retrieve options with option code 1.
    OptionCollection options = pkt->getOptions(1);
    ASSERT_EQ(2, options.size());

    OptionCollection::const_iterator opt_it;

    // Make sure that the first option is returned. We're using the pointer
    // to opt1 to find the option.
    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(1, opt1));
    EXPECT_TRUE(opt_it != options.end());

    // Make sure that the second option is returned.
    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(1, opt2));
    EXPECT_TRUE(opt_it != options.end());

    // Retrieve options with option code 2.
    options = pkt->getOptions(2);
    ASSERT_EQ(2, options.size());

    // opt3 and opt4 should exist.
    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(2, opt3));
    EXPECT_TRUE(opt_it != options.end());

    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(2, opt4));
    EXPECT_TRUE(opt_it != options.end());

    // Enable copying options when they are retrieved.
    pkt->setCopyRetrievedOptions(true);

    options = pkt->getOptions(1);
    ASSERT_EQ(2, options.size());

    // Both retrieved options should be copied so an attempt to find them
    // using option pointer should fail. Original pointers should have
    // been replaced with new instances.
    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(1, opt1));
    EXPECT_TRUE(opt_it == options.end());

    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(1, opt2));
    EXPECT_TRUE(opt_it == options.end());

    // Return instances of options with the option code 1 and make sure
    // that copies of the options were used to replace original options
    // in the packet.
    pkt->setCopyRetrievedOptions(false);
    OptionCollection options_modified = pkt->getOptions(1);
    for (auto const& opt_it_modified : options_modified) {
        opt_it = std::find(options.begin(), options.end(), opt_it_modified);
        ASSERT_TRUE(opt_it != options.end());
    }

    // Let's check that remaining two options haven't been affected by
    // retrieving the options with option code 1.
    options = pkt->getOptions(2);
    ASSERT_EQ(2, options.size());

    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(2, opt3));
    EXPECT_TRUE(opt_it != options.end());

    opt_it = std::find(options.begin(), options.end(),
                       std::pair<const unsigned int, OptionPtr>(2, opt4));
    EXPECT_TRUE(opt_it != options.end());
}

// This test verifies that it is possible to control whether a pointer
// to an option or a pointer to a copy of an option is returned by the
// packet object.
TEST_F(Pkt4Test, setCopyRetrievedOptions) {
    // Create option 1 with two sub options.
    OptionPtr option1(new Option(Option::V4, 1));
    OptionPtr sub1(new Option(Option::V4, 1));
    OptionPtr sub2(new Option(Option::V4, 2));

    option1->addOption(sub1);
    option1->addOption(sub2);

    // Create option 2 with two sub options.
    OptionPtr option2(new Option(Option::V4, 2));
    OptionPtr sub3(new Option(Option::V4, 1));
    OptionPtr sub4(new Option(Option::V4, 2));

    option2->addOption(sub3);
    option2->addOption(sub4);

    // Add both options to a packet.
    Pkt4Ptr pkt(new Pkt4(DHCPDISCOVER, 1234));
    pkt->addOption(option1);
    pkt->addOption(option2);

    // Retrieve options and make sure that the pointers to the original
    // option instances are returned.
    ASSERT_TRUE(option1 == pkt->getOption(1));
    ASSERT_TRUE(option2 == pkt->getOption(2));

    // Now force copying the options when they are retrieved.
    pkt->setCopyRetrievedOptions(true);
    EXPECT_TRUE(pkt->isCopyRetrievedOptions());

    // Option pointer returned must point to a new instance of option 2.
    OptionPtr option2_copy = pkt->getOption(2);
    EXPECT_FALSE(option2 == option2_copy);

    // Disable copying.
    pkt->setCopyRetrievedOptions(false);
    EXPECT_FALSE(pkt->isCopyRetrievedOptions());

    // Expect that the original pointer is returned. This guarantees that
    // option1 wasn't affected by copying option 2.
    OptionPtr option1_copy = pkt->getOption(1);
    EXPECT_TRUE(option1 == option1_copy);

    // Again, enable copying options.
    pkt->setCopyRetrievedOptions(true);

    // This time a pointer to new option instance should be returned.
    option1_copy = pkt->getOption(1);
    EXPECT_FALSE(option1 == option1_copy);
}

// This test verifies that the options are unpacked from the packet correctly.
TEST_F(Pkt4Test, unpackOptions) {

    vector<uint8_t> expectedFormat = generateTestPacket2();

    expectedFormat.push_back(0x63);
    expectedFormat.push_back(0x82);
    expectedFormat.push_back(0x53);
    expectedFormat.push_back(0x63);

    for (size_t i = 0; i < sizeof(v4_opts); i++) {
        expectedFormat.push_back(v4_opts[i]);
    }

    // now expectedFormat contains fixed format and 5 options

    boost::shared_ptr<Pkt4> pkt(new Pkt4(&expectedFormat[0],
                                expectedFormat.size()));

    EXPECT_NO_THROW(
        pkt->unpack()
    );

    verifyParsedOptions(pkt);
}

// Checks if the code is able to handle a malformed option
TEST_F(Pkt4Test, unpackMalformed) {

    vector<uint8_t> orig = generateTestPacket2();

    orig.push_back(0x63);
    orig.push_back(0x82);
    orig.push_back(0x53);
    orig.push_back(0x63);

    orig.push_back(53); // Message Type
    orig.push_back(1); // length=1
    orig.push_back(2); // type=2

    orig.push_back(12); // Hostname
    orig.push_back(3); // length=3
    orig.push_back(102); // data="foo"
    orig.push_back(111);
    orig.push_back(111);

    // That's our original content. It should be sane.
    Pkt4Ptr success(new Pkt4(&orig[0], orig.size()));
    EXPECT_NO_THROW(success->unpack());

    // With the exception of END and PAD an option must have a length byte
    vector<uint8_t> nolength = orig;
    nolength.resize(orig.size() - 4);
    Pkt4Ptr no_length_pkt(new Pkt4(&nolength[0], nolength.size()));
    EXPECT_NO_THROW(no_length_pkt->unpack());

    // The unpack() operation doesn't throw but there is no option 12
    EXPECT_FALSE(no_length_pkt->getOption(12));

    // Truncated data is not accepted too but doesn't throw
    vector<uint8_t> shorty = orig;
    shorty.resize(orig.size() - 1);
    Pkt4Ptr too_short_pkt(new Pkt4(&shorty[0], shorty.size()));
    EXPECT_NO_THROW(too_short_pkt->unpack());

    // The unpack() operation doesn't throw but there is no option 12
    EXPECT_FALSE(no_length_pkt->getOption(12));
}

// Checks if the code is able to handle a malformed vendor option
TEST_F(Pkt4Test, unpackVendorMalformed) {

    vector<uint8_t> orig = generateTestPacket2();

    orig.push_back(0x63);
    orig.push_back(0x82);
    orig.push_back(0x53);
    orig.push_back(0x63);

    orig.push_back(53); // Message Type
    orig.push_back(1); // length=1
    orig.push_back(2); // type=2

    orig.push_back(125); // vivso suboptions
    size_t full_len_index = orig.size();
    orig.push_back(15); // length=15
    orig.push_back(1); // vendor_id=0x1020304
    orig.push_back(2);
    orig.push_back(3);
    orig.push_back(4);
    size_t data_len_index = orig.size();
    orig.push_back(10); // data-len=10
    orig.push_back(128); // suboption type=128
    orig.push_back(3); // suboption length=3
    orig.push_back(102); // data="foo"
    orig.push_back(111);
    orig.push_back(111);
    orig.push_back(129); // suboption type=129
    orig.push_back(3); // suboption length=3
    orig.push_back(99); // data="bar"
    orig.push_back(98);
    orig.push_back(114);

    // That's our original content. It should be sane.
    Pkt4Ptr success(new Pkt4(&orig[0], orig.size()));
    EXPECT_NO_THROW(success->unpack());

    // Data-len must match
    vector<uint8_t> baddatalen = orig;
    baddatalen.resize(orig.size() - 5);
    baddatalen[full_len_index] = 10;
    Pkt4Ptr bad_data_len_pkt(new Pkt4(&baddatalen[0], baddatalen.size()));
    EXPECT_THROW(bad_data_len_pkt->unpack(), SkipRemainingOptionsError);

    // A suboption must have a length byte
    vector<uint8_t> nolength = orig;
    nolength.resize(orig.size() - 4);
    nolength[full_len_index] = 11;
    nolength[data_len_index] = 6;
    Pkt4Ptr no_length_pkt(new Pkt4(&nolength[0], nolength.size()));
    EXPECT_THROW(no_length_pkt->unpack(), SkipRemainingOptionsError);

    // Truncated data is not accepted either
    vector<uint8_t> shorty = orig;
    shorty.resize(orig.size() - 1);
    shorty[full_len_index] = 14;
    shorty[data_len_index] = 9;
    Pkt4Ptr too_short_pkt(new Pkt4(&shorty[0], shorty.size()));
    EXPECT_THROW(too_short_pkt->unpack(), SkipRemainingOptionsError);
}

// This test verifies methods that are used for manipulating meta fields
// i.e. fields that are not part of DHCPv4 (e.g. interface name).
TEST_F(Pkt4Test, metaFields) {

    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
    pkt->setIface("loooopback");
    pkt->setIndex(42);
    pkt->setRemoteAddr(IOAddress("1.2.3.4"));
    pkt->setLocalAddr(IOAddress("4.3.2.1"));

    EXPECT_EQ("loooopback", pkt->getIface());
    EXPECT_EQ(42, pkt->getIndex());
    EXPECT_EQ("1.2.3.4", pkt->getRemoteAddr().toText());
    EXPECT_EQ("4.3.2.1", pkt->getLocalAddr().toText());
}

TEST_F(Pkt4Test, Timestamp) {
    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));

    // Just after construction timestamp is invalid
    ASSERT_TRUE(pkt->getTimestamp().is_not_a_date_time());

    // Update packet time.
    pkt->updateTimestamp();

    // Get updated packet time.
    boost::posix_time::ptime ts_packet = pkt->getTimestamp();

    // After timestamp is updated it should be date-time.
    ASSERT_FALSE(ts_packet.is_not_a_date_time());

    // Check current time.
    boost::posix_time::ptime ts_now =
        boost::posix_time::microsec_clock::universal_time();

    // Calculate period between packet time and now.
    boost::posix_time::time_period ts_period(ts_packet, ts_now);

    // Duration should be positive or zero.
    EXPECT_TRUE(ts_period.length().total_microseconds() >= 0);
}

TEST_F(Pkt4Test, hwaddr) {
    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
    const uint8_t hw[] = { 2, 4, 6, 8, 10, 12 }; // MAC
    const uint8_t hw_type = 123; // hardware type

    HWAddrPtr hwaddr(new HWAddr(hw, sizeof(hw), hw_type));

    // setting NULL hardware address is not allowed
    EXPECT_THROW(pkt->setHWAddr(HWAddrPtr()), BadValue);

    pkt->setHWAddr(hwaddr);

    EXPECT_EQ(hw_type, pkt->getHtype());

    EXPECT_EQ(sizeof(hw), pkt->getHlen());

    EXPECT_TRUE(hwaddr == pkt->getHWAddr());
}

// This test verifies that the packet remote and local HW address can
// be set and returned.
TEST_F(Pkt4Test, hwaddrSrcRemote) {
    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
    const uint8_t src_hw[] = { 1, 2, 3, 4, 5, 6 };
    const uint8_t dst_hw[] = { 7, 8, 9, 10, 11, 12 };
    const uint8_t hw_type = 123;

    HWAddrPtr dst_hwaddr(new HWAddr(dst_hw, sizeof(src_hw), hw_type));
    HWAddrPtr src_hwaddr(new HWAddr(src_hw, sizeof(src_hw), hw_type));

    // Check that we can set the local address.
    EXPECT_NO_THROW(pkt->setLocalHWAddr(dst_hwaddr));
    EXPECT_TRUE(dst_hwaddr == pkt->getLocalHWAddr());

    // Check that we can set the remote address.
    EXPECT_NO_THROW(pkt->setRemoteHWAddr(src_hwaddr));
    EXPECT_TRUE(src_hwaddr == pkt->getRemoteHWAddr());

    // Can't set the NULL addres.
    EXPECT_THROW(pkt->setRemoteHWAddr(HWAddrPtr()), BadValue);
    EXPECT_THROW(pkt->setLocalHWAddr(HWAddrPtr()), BadValue);

    // Test alternative way to set local address.
    const uint8_t dst_hw2[] = { 19, 20, 21, 22, 23, 24 };
    std::vector<uint8_t> dst_hw_vec(dst_hw2, dst_hw2 + sizeof(dst_hw2));
    const uint8_t hw_type2 = 234;
    EXPECT_NO_THROW(pkt->setLocalHWAddr(hw_type2, sizeof(dst_hw2), dst_hw_vec));
    HWAddrPtr local_addr = pkt->getLocalHWAddr();
    ASSERT_TRUE(local_addr);
    EXPECT_EQ(hw_type2, local_addr->htype_);
    EXPECT_TRUE(std::equal(dst_hw_vec.begin(), dst_hw_vec.end(),
                           local_addr->hwaddr_.begin()));

    // Set remote address.
    const uint8_t src_hw2[] = { 25, 26, 27, 28, 29, 30 };
    std::vector<uint8_t> src_hw_vec(src_hw2, src_hw2 + sizeof(src_hw2));
    EXPECT_NO_THROW(pkt->setRemoteHWAddr(hw_type2, sizeof(src_hw2), src_hw_vec));
    HWAddrPtr remote_addr = pkt->getRemoteHWAddr();
    ASSERT_TRUE(remote_addr);
    EXPECT_EQ(hw_type2, remote_addr->htype_);
    EXPECT_TRUE(std::equal(src_hw_vec.begin(), src_hw_vec.end(),
                           remote_addr->hwaddr_.begin()));
}

// This test verifies that the check for a message being relayed is correct.
TEST_F(Pkt4Test, isRelayed) {
    Pkt4 pkt(DHCPDISCOVER, 1234);
    // By default, the hops and giaddr should be 0.
    ASSERT_TRUE(pkt.getGiaddr().isV4Zero());
    ASSERT_EQ(0, pkt.getHops());
    // For zero giaddr the packet is non-relayed.
    EXPECT_FALSE(pkt.isRelayed());
    // Set giaddr but leave hops = 0.
    pkt.setGiaddr(IOAddress("10.0.0.1"));
    EXPECT_TRUE(pkt.isRelayed());
    // After setting hops the message should still be relayed.
    pkt.setHops(10);
    EXPECT_TRUE(pkt.isRelayed());
    // Set giaddr to 0. The message is now not-relayed.
    pkt.setGiaddr(IOAddress(IOAddress::IPV4_ZERO_ADDRESS()));
    EXPECT_FALSE(pkt.isRelayed());
    // Setting the giaddr to 255.255.255.255 should not cause it to
    // be relayed message.
    pkt.setGiaddr(IOAddress(IOAddress::IPV4_BCAST_ADDRESS()));
    EXPECT_FALSE(pkt.isRelayed());
}

// Tests whether a packet can be assigned to a class and later
// checked if it belongs to a given class
TEST_F(Pkt4Test, clientClasses) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // Default values (do not belong to any class)
    EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
    EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_MODEM));
    EXPECT_TRUE(pkt.getClasses().empty());

    // Add to the first class
    pkt.addClass(DOCSIS3_CLASS_EROUTER);
    EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
    EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_MODEM));
    ASSERT_FALSE(pkt.getClasses().empty());

    // Add to a second class
    pkt.addClass(DOCSIS3_CLASS_MODEM);
    EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
    EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_MODEM));

    // Check that it's ok to add to the same class repeatedly
    EXPECT_NO_THROW(pkt.addClass("foo"));
    EXPECT_NO_THROW(pkt.addClass("foo"));
    EXPECT_NO_THROW(pkt.addClass("foo"));

    // Check that the packet belongs to 'foo'
    EXPECT_TRUE(pkt.inClass("foo"));
}

// Tests operations on additional classes list.
TEST_F(Pkt4Test, additionalClientClasses) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // Default values (do not belong to any class)
    EXPECT_TRUE(pkt.getAdditionalClasses().empty());

    // Add to the first class
    pkt.addAdditionalClass(DOCSIS3_CLASS_EROUTER);
    EXPECT_EQ(1, pkt.getAdditionalClasses().size());

    // Add to a second class
    pkt.addAdditionalClass(DOCSIS3_CLASS_MODEM);
    EXPECT_EQ(2, pkt.getAdditionalClasses().size());
    EXPECT_TRUE(pkt.getAdditionalClasses().contains(DOCSIS3_CLASS_EROUTER));
    EXPECT_TRUE(pkt.getAdditionalClasses().contains(DOCSIS3_CLASS_MODEM));
    EXPECT_FALSE(pkt.getAdditionalClasses().contains("foo"));

    // Check that it's ok to add to the same class repeatedly
    EXPECT_NO_THROW(pkt.addAdditionalClass("foo"));
    EXPECT_NO_THROW(pkt.addAdditionalClass("foo"));
    EXPECT_NO_THROW(pkt.addAdditionalClass("foo"));

    // Check that the packet belongs to 'foo'
    EXPECT_TRUE(pkt.getAdditionalClasses().contains("foo"));
}

// Tests whether a packet can be assigned to a subclass and later
// checked if it belongs to a given subclass
TEST_F(Pkt4Test, templateClasses) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // Default values (do not belong to any subclass)
    EXPECT_FALSE(pkt.inClass("SPAWN_template-interface-name_eth0"));
    EXPECT_FALSE(pkt.inClass("SPAWN_template-interface-id_interface-id0"));
    EXPECT_TRUE(pkt.getClasses().empty());

    // Add to the first subclass
    pkt.addSubClass("template-interface-name", "SPAWN_template-interface-name_eth0");
    EXPECT_TRUE(pkt.inClass("SPAWN_template-interface-name_eth0"));
    EXPECT_FALSE(pkt.inClass("SPAWN_template-interface-id_interface-id0"));
    ASSERT_FALSE(pkt.getClasses().empty());

    // Add to a second subclass
    pkt.addSubClass("template-interface-id", "SPAWN_template-interface-id_interface-id0");
    EXPECT_TRUE(pkt.inClass("SPAWN_template-interface-name_eth0"));
    EXPECT_TRUE(pkt.inClass("SPAWN_template-interface-id_interface-id0"));

    // Verify the order is as expected.
    const ClientClasses& classes = pkt.getClasses();
    auto cclass = classes.cbegin();
    ASSERT_NE(cclass, classes.cend());
    EXPECT_EQ("SPAWN_template-interface-name_eth0", (*cclass));
    ++cclass;
    ASSERT_NE(cclass, classes.cend());
    EXPECT_EQ("template-interface-name", (*cclass));
    ++cclass;
    ASSERT_NE(cclass, classes.cend());
    EXPECT_EQ("SPAWN_template-interface-id_interface-id0", (*cclass));
    ++cclass;
    ASSERT_NE(cclass, classes.cend());
    EXPECT_EQ("template-interface-id", (*cclass));

    // Check that it's ok to add to the same subclass repeatedly
    EXPECT_NO_THROW(pkt.addSubClass("template-foo", "SPAWN_template-foo_bar"));
    EXPECT_NO_THROW(pkt.addSubClass("template-foo", "SPAWN_template-foo_bar"));
    EXPECT_NO_THROW(pkt.addSubClass("template-bar", "SPAWN_template-bar_bar"));

    // Check that the packet belongs to 'SPAWN_template-foo_bar'
    EXPECT_TRUE(pkt.inClass("SPAWN_template-foo_bar"));

    // Check that the packet belongs to 'SPAWN_template-bar_bar'
    EXPECT_TRUE(pkt.inClass("SPAWN_template-bar_bar"));
}

// Tests whether MAC can be obtained and that MAC sources are not
// confused.
TEST_F(Pkt4Test, getMAC) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // DHCPv4 packet by default doesn't have MAC address specified.
    EXPECT_FALSE(pkt.getMAC(HWAddr::HWADDR_SOURCE_ANY));
    EXPECT_FALSE(pkt.getMAC(HWAddr::HWADDR_SOURCE_RAW));

    // Let's invent a MAC
    const uint8_t hw[] = { 2, 4, 6, 8, 10, 12 }; // MAC
    const uint8_t hw_type = 123; // hardware type
    HWAddrPtr dummy_hwaddr(new HWAddr(hw, sizeof(hw), hw_type));

    // Now let's pretend that we obtained it from raw sockets
    pkt.setRemoteHWAddr(dummy_hwaddr);

    // Now we should be able to get something
    ASSERT_TRUE(pkt.getMAC(HWAddr::HWADDR_SOURCE_ANY));
    ASSERT_TRUE(pkt.getMAC(HWAddr::HWADDR_SOURCE_RAW));

    // Check that the returned MAC is indeed the expected one
    ASSERT_TRUE(*dummy_hwaddr == *pkt.getMAC(HWAddr::HWADDR_SOURCE_ANY));
    ASSERT_TRUE(*dummy_hwaddr == *pkt.getMAC(HWAddr::HWADDR_SOURCE_RAW));
}

// Tests that getLabel/makeLabel methods produces the expected strings based on
// packet content.
TEST_F(Pkt4Test, getLabel) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // Verify makeLabel() handles empty values
    EXPECT_EQ ("[no hwaddr info], cid=[no info], tid=0x0",
               Pkt4::makeLabel(HWAddrPtr(), ClientIdPtr(), 0));

    // Verify an "empty" packet label is as we expect
    EXPECT_EQ ("[hwtype=1 ], cid=[no info], tid=0x4d2",
               pkt.getLabel());

    // Set that packet hardware address, then verify getLabel
    const uint8_t hw[] = { 2, 4, 6, 8, 10, 12 }; // MAC
    const uint8_t hw_type = 123; // hardware type
    HWAddrPtr dummy_hwaddr(new HWAddr(hw, sizeof(hw), hw_type));
    pkt.setHWAddr(dummy_hwaddr);

    EXPECT_EQ ("[hwtype=123 02:04:06:08:0a:0c],"
               " cid=[no info], tid=0x4d2", pkt.getLabel());

    // Add a client id to the packet then verify getLabel
    OptionBuffer clnt_id(4);
    for (uint8_t i = 0; i < 4; i++) {
        clnt_id[i] = 100 + i;
    }

    OptionPtr opt(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
                             clnt_id.begin(), clnt_id.begin() + 4));
    pkt.addOption(opt);

    EXPECT_EQ ("[hwtype=123 02:04:06:08:0a:0c],"
               " cid=[64:65:66:67], tid=0x4d2",
               pkt.getLabel());

}

// Test that empty client identifier option doesn't cause an exception from
// Pkt4::getLabel.
TEST_F(Pkt4Test, getLabelEmptyClientId) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // Create empty client identifier option.
    OptionPtr empty_opt(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER));
    pkt.addOption(empty_opt);

    EXPECT_EQ("[hwtype=1 ], cid=[no info], tid=0x4d2"
              " (malformed client-id)", pkt.getLabel());
}

// Tests that the variant of makeLabel which doesn't include transaction
// id produces expected output.
TEST_F(Pkt4Test, makeLabelWithoutTransactionId) {
    EXPECT_EQ("[no hwaddr info], cid=[no info]",
              Pkt4::makeLabel(HWAddrPtr(), ClientIdPtr()));

    // Test non-null hardware address.
    HWAddrPtr hwaddr(new HWAddr(HWAddr::fromText("01:02:03:04:05:06", 123)));
    EXPECT_EQ("[hwtype=123 01:02:03:04:05:06], cid=[no info]",
              Pkt4::makeLabel(hwaddr, ClientIdPtr()));

    // Test non-null client identifier and non-null hardware address.
    ClientIdPtr cid = ClientId::fromText("01:02:03:04");
    EXPECT_EQ("[hwtype=123 01:02:03:04:05:06], cid=[01:02:03:04]",
              Pkt4::makeLabel(hwaddr, cid));

    // Test non-nnull client identifier and null hardware address.
    EXPECT_EQ("[no hwaddr info], cid=[01:02:03:04]",
              Pkt4::makeLabel(HWAddrPtr(), cid));
}

// Tests that the correct DHCPv4 message name is returned for various
// message types.
TEST_F(Pkt4Test, getName) {
    // Check all possible packet types
    for (int itype = 0; itype < 256; ++itype) {
        uint8_t type = itype;

        switch (type) {
        case DHCPDISCOVER:
            EXPECT_STREQ("DHCPDISCOVER", Pkt4::getName(type));
            break;

        case DHCPOFFER:
            EXPECT_STREQ("DHCPOFFER", Pkt4::getName(type));
            break;

        case DHCPREQUEST:
            EXPECT_STREQ("DHCPREQUEST", Pkt4::getName(type));
            break;

        case DHCPDECLINE:
            EXPECT_STREQ("DHCPDECLINE", Pkt4::getName(type));
            break;

        case DHCPACK:
            EXPECT_STREQ("DHCPACK", Pkt4::getName(type));
            break;

        case DHCPNAK:
            EXPECT_STREQ("DHCPNAK", Pkt4::getName(type));
            break;

        case DHCPRELEASE:
            EXPECT_STREQ("DHCPRELEASE", Pkt4::getName(type));
            break;

        case DHCPINFORM:
            EXPECT_STREQ("DHCPINFORM", Pkt4::getName(type));
            break;

        case DHCPLEASEQUERY:
            EXPECT_STREQ("DHCPLEASEQUERY", Pkt4::getName(type));
            break;

        case DHCPLEASEUNASSIGNED:
            EXPECT_STREQ("DHCPLEASEUNASSIGNED", Pkt4::getName(type));
            break;

        case DHCPLEASEUNKNOWN:
            EXPECT_STREQ("DHCPLEASEUNKNOWN", Pkt4::getName(type));
            break;

        case DHCPLEASEACTIVE:
            EXPECT_STREQ("DHCPLEASEACTIVE", Pkt4::getName(type));
            break;

        case DHCPBULKLEASEQUERY:
            EXPECT_STREQ("DHCPBULKLEASEQUERY", Pkt4::getName(type));
            break;

        case DHCPLEASEQUERYDONE:
            EXPECT_STREQ("DHCPLEASEQUERYDONE", Pkt4::getName(type));
            break;

        case DHCPLEASEQUERYSTATUS:
            EXPECT_STREQ("DHCPLEASEQUERYSTATUS", Pkt4::getName(type));
            break;

        case DHCPTLS:
            EXPECT_STREQ("DHCPTLS", Pkt4::getName(type));
            break;

        default:
            EXPECT_STREQ("UNKNOWN", Pkt4::getName(type));
        }
    }
}

// This test checks that the packet data are correctly converted to the
// textual format.
TEST_F(Pkt4Test, toText) {
    Pkt4 pkt(DHCPDISCOVER, 2543);
    pkt.setLocalAddr(IOAddress("192.0.2.34"));
    pkt.setRemoteAddr(IOAddress("192.10.33.4"));

    pkt.addOption(OptionPtr(new Option4AddrLst(123, IOAddress("192.0.2.3"))));
    pkt.addOption(OptionPtr(new OptionUint32(Option::V4, 156, 123456)));
    pkt.addOption(OptionPtr(new OptionString(Option::V4, 87, "lorem ipsum")));
    OptionBuffer data = { 0x61, 0x62, 0x63, 0x64, 0x65, 0x66 };
    OptionPtr opt(new Option(Option::V4, 231, data));
    pkt.addOption(opt);
    OptionBuffer data_sub = { 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39 };
    OptionPtr sub_opt(new Option(Option::V4, 1, data_sub));
    opt->addOption(sub_opt);
    data_sub.clear();
    sub_opt.reset(new Option(Option::V4, 2, data_sub));
    opt->addOption(sub_opt);

    EXPECT_EQ("local_address=192.0.2.34:67, remote_address=192.10.33.4:68,\n"
              "msg_type=DHCPDISCOVER (1), trans_id=0x9ef,\n"
              "options:\n"
              "  type=053, len=001: 1 (uint8)\n"
              "  type=087, len=011: \"lorem ipsum\" (string)\n"
              "  type=123, len=004: 192.0.2.3\n"
              "  type=156, len=004: 123456 (uint32)\n"
              "  type=231, len=020: 61:62:63:64:65:66 'abcdef',\n"
              "options:\n"
              "    type=001, len=010: 30:31:32:33:34:35:36:37:38:39 '0123456789'\n"
              "    type=002, len=000: ''",
              pkt.toText());

    // Now remove all options, including Message Type and check if the
    // information about lack of any options is displayed properly.
    pkt.delOption(123);
    pkt.delOption(156);
    pkt.delOption(87);
    pkt.delOption(53);
    pkt.delOption(231);

    EXPECT_EQ("local_address=192.0.2.34:67, remote_address=192.10.33.4:68,\n"
              "msg_type=(missing), trans_id=0x9ef,\n"
              "message contains no options",
              pkt.toText());

}

// This test checks that the packet data are correctly converted to the
// textual format.
TEST_F(Pkt4Test, toTextVerbose) {
    Pkt4 pkt(DHCPDISCOVER, 2543);
    pkt.setLocalAddr(IOAddress("192.0.2.34"));
    pkt.setRemoteAddr(IOAddress("192.10.33.4"));

    pkt.addOption(OptionPtr(new Option4AddrLst(123, IOAddress("192.0.2.3"))));
    pkt.addOption(OptionPtr(new OptionUint32(Option::V4, 156, 123456)));
    pkt.addOption(OptionPtr(new OptionString(Option::V4, 87, "lorem ipsum")));
    OptionBuffer data = { 0x61, 0x62, 0x63, 0x64, 0x65, 0x66 };
    OptionPtr opt(new Option(Option::V4, 231, data));
    pkt.addOption(opt);
    OptionBuffer data_sub = { 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39 };
    OptionPtr sub_opt(new Option(Option::V4, 1, data_sub));
    opt->addOption(sub_opt);
    data_sub.clear();
    sub_opt.reset(new Option(Option::V4, 2, data_sub));
    opt->addOption(sub_opt);

    // Set verbose true but with verbose members not set.
    EXPECT_EQ("local_address=192.0.2.34:67, remote_address=192.10.33.4:68,\n"
              "msg_type=DHCPDISCOVER (1), trans_id=0x9ef, secs=0, flags=0x0,\n"
              "options:\n"
              "  type=053, len=001: 1 (uint8)\n"
              "  type=087, len=011: \"lorem ipsum\" (string)\n"
              "  type=123, len=004: 192.0.2.3\n"
              "  type=156, len=004: 123456 (uint32)\n"
              "  type=231, len=020: 61:62:63:64:65:66 'abcdef',\n"
              "options:\n"
              "    type=001, len=010: 30:31:32:33:34:35:36:37:38:39 '0123456789'\n"
              "    type=002, len=000: ''",
              pkt.toText(true));

    // Now populate verbose members.
    pkt.setSecs(2);
    pkt.setFlags(0x80);
    pkt.setCiaddr(IOAddress("1.1.1.1"));
    pkt.setYiaddr(IOAddress("2.2.2.2"));
    pkt.setSiaddr(IOAddress("3.3.3.3"));
    pkt.setGiaddr(IOAddress("4.4.4.4"));

    std::vector<uint8_t> sname = { 'x', 'y', 'z' };
    std::vector<uint8_t> file = { 'A', 'B', 'C' };
    pkt.setSname(sname.data(), 3);
    pkt.setFile(file.data(), 3);

    // Verbose defaulting to false.
    EXPECT_EQ("local_address=192.0.2.34:67, remote_address=192.10.33.4:68,\n"
              "msg_type=DHCPDISCOVER (1), trans_id=0x9ef,\n"
              "options:\n"
              "  type=053, len=001: 1 (uint8)\n"
              "  type=087, len=011: \"lorem ipsum\" (string)\n"
              "  type=123, len=004: 192.0.2.3\n"
              "  type=156, len=004: 123456 (uint32)\n"
              "  type=231, len=020: 61:62:63:64:65:66 'abcdef',\n"
              "options:\n"
              "    type=001, len=010: 30:31:32:33:34:35:36:37:38:39 '0123456789'\n"
              "    type=002, len=000: ''",
              pkt.toText());

    // Set verbose true.
    EXPECT_EQ("local_address=192.0.2.34:67, remote_address=192.10.33.4:68,\n"
              "msg_type=DHCPDISCOVER (1), trans_id=0x9ef, secs=2, flags=0x80, ciaddr=1.1.1.1,"
              " yiaddr=2.2.2.2, siaddr=3.3.3.3, giaddr=4.4.4.4, sname=[xyz], file=[ABC],\n"
              "options:\n"
              "  type=053, len=001: 1 (uint8)\n"
              "  type=087, len=011: \"lorem ipsum\" (string)\n"
              "  type=123, len=004: 192.0.2.3\n"
              "  type=156, len=004: 123456 (uint32)\n"
              "  type=231, len=020: 61:62:63:64:65:66 'abcdef',\n"
              "options:\n"
              "    type=001, len=010: 30:31:32:33:34:35:36:37:38:39 '0123456789'\n"
              "    type=002, len=000: ''",
              pkt.toText(true));
}

// Sanity check. Verifies that the getName() and getType()
// don't throw.
TEST_F(Pkt4Test, getType) {

    Pkt4 pkt(DHCPDISCOVER, 2543);
    pkt.delOption(DHO_DHCP_MESSAGE_TYPE);

    ASSERT_NO_THROW(pkt.getType());
    ASSERT_NO_THROW(pkt.getName());

    // The method has to return something that is not NULL,
    // even if the packet doesn't have Message Type option.
    EXPECT_TRUE(pkt.getName());
}

// Verifies that when the VIVSO option 125 has length that is too
// short (i.e. less than sizeof(uint8_t), unpack throws a
// SkipRemainingOptionsError exception
TEST_F(Pkt4Test, truncatedVendorLength) {

    // Build a good discover packet
    Pkt4Ptr pkt = dhcp::test::PktCaptures::discoverWithValidVIVSO();

    // Unpacking should not throw
    ASSERT_NO_THROW(pkt->unpack());
    ASSERT_EQ(DHCPDISCOVER, pkt->getType());

    // VIVSO option should be there
    OptionPtr x = pkt->getOption(DHO_VIVSO_SUBOPTIONS);
    ASSERT_TRUE(x);
    ASSERT_EQ(DHO_VIVSO_SUBOPTIONS, x->getType());
    OptionVendorPtr vivso = boost::dynamic_pointer_cast<OptionVendor>(x);
    ASSERT_TRUE(vivso);
    EXPECT_EQ(133+2, vivso->len()); // data + opt code + len

    // Build a bad discover packet
    pkt = dhcp::test::PktCaptures::discoverWithTruncatedVIVSO();

    // Unpack should throw Skip exception
    ASSERT_THROW(pkt->unpack(), SkipRemainingOptionsError);
    ASSERT_EQ(DHCPDISCOVER, pkt->getType());

    // VIVSO option should not be there
    x = pkt->getOption(DHO_VIVSO_SUBOPTIONS);
    ASSERT_FALSE(x);
}

// Verifies that we handle text options that contain trailing
// and embedded NULLs correctly.  Per RFC 2132, Sec 2 we should
// be stripping trailing NULLs.  We've agreed to permit
// embedded NULLs (for now).
TEST_F(Pkt4Test, nullTerminatedOptions) {
    // Construct the onwire packet.
    vector<uint8_t> base_msg = generateTestPacket2();
    base_msg.push_back(0x63); // magic cookie
    base_msg.push_back(0x82);
    base_msg.push_back(0x53);
    base_msg.push_back(0x63);

    base_msg.push_back(0x35); // message-type
    base_msg.push_back(0x1);
    base_msg.push_back(0x1);

    int base_size = base_msg.size();

    // We'll create four text options, with various combinations of NULLs.
    vector<uint8_t> hostname = { DHO_HOST_NAME, 5, 't', 'w', 'o', 0, 0 };
    vector<uint8_t> merit_dump = { DHO_MERIT_DUMP, 4, 'o', 'n', 'e', 0 };
    vector<uint8_t> root_path = { DHO_ROOT_PATH, 4, 'n', 'o', 'n', 'e' };
    vector<uint8_t> domain_name = { DHO_DOMAIN_NAME, 6, 'e', 'm', 0, 'b', 'e', 'd' };

    // Add the options to the onwire packet.
    vector<uint8_t> test_msg = base_msg;
    test_msg.insert(test_msg.end(), hostname.begin(), hostname.end());
    test_msg.insert(test_msg.end(), root_path.begin(), root_path.end());
    test_msg.insert(test_msg.end(), merit_dump.begin(), merit_dump.end());
    test_msg.insert(test_msg.end(), domain_name.begin(), domain_name.end());
    test_msg.push_back(DHO_END);

    boost::shared_ptr<Pkt4> pkt(new Pkt4(&test_msg[0], test_msg.size()));

    // Unpack the onwire packet.
    EXPECT_NO_THROW(
        pkt->unpack()
    );

    EXPECT_EQ(DHCPDISCOVER, pkt->getType());

    OptionPtr opt;
    OptionStringPtr opstr;

    // Now let's verify that each text option is as expected.
    ASSERT_TRUE(opt = pkt->getOption(DHO_HOST_NAME));
    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
    EXPECT_EQ(3, opstr->getValue().length());
    EXPECT_EQ("two", opstr->getValue());

    ASSERT_TRUE(opt = pkt->getOption(DHO_MERIT_DUMP));
    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
    EXPECT_EQ(3, opstr->getValue().length());
    EXPECT_EQ("one", opstr->getValue());

    ASSERT_TRUE(opt = pkt->getOption(DHO_ROOT_PATH));
    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
    EXPECT_EQ(4, opstr->getValue().length());
    EXPECT_EQ("none", opstr->getValue());

    ASSERT_TRUE(opt = pkt->getOption(DHO_DOMAIN_NAME));
    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
    EXPECT_EQ(6, opstr->getValue().length());
    std::string embed{"em\0bed", 6};
    EXPECT_EQ(embed, opstr->getValue());


    // Next we pack the packet, to make sure trailing NULLs have
    // been eliminated, embedded NULLs are intact.
    EXPECT_NO_THROW(
        pkt->pack()
    );

    // Create a vector of our expected packed option data.
    vector<uint8_t> packed_opts =
        {
          DHO_HOST_NAME, 3, 't', 'w', 'o',
          DHO_MERIT_DUMP, 3, 'o', 'n', 'e',
          DHO_DOMAIN_NAME, 6, 'e', 'm', 0, 'b', 'e', 'd',
          DHO_ROOT_PATH, 4, 'n', 'o', 'n', 'e',
        };

    const uint8_t* packed = pkt->getBuffer().getData();
    int packed_len = pkt->getBuffer().getLength();

    // Packed message options should be 3 bytes smaller than original onwire data.
    int dif = packed_len - test_msg.size();
    ASSERT_EQ(-3, dif);

    // Make sure the packed content is as expected.
    EXPECT_EQ(0, memcmp(&packed[base_size], &packed_opts[0], packed_opts.size()));
}

// Checks that unpacking correctly handles SkipThisOptionError by
// omitting the offending option from the unpacked options.
TEST_F(Pkt4Test, testSkipThisOptionError) {
    vector<uint8_t> orig = generateTestPacket2();

    orig.push_back(0x63);
    orig.push_back(0x82);
    orig.push_back(0x53);
    orig.push_back(0x63);

    orig.push_back(53);   // Message Type
    orig.push_back(1);    // length=1
    orig.push_back(2);    // type=2

    orig.push_back(14);   // merit-dump
    orig.push_back(3);    // length=3
    orig.push_back(0x61); // data="abc"
    orig.push_back(0x62);
    orig.push_back(0x63);

    orig.push_back(12);   // Hostname
    orig.push_back(3);    // length=3
    orig.push_back(0);    // data= all nulls
    orig.push_back(0);
    orig.push_back(0);

    orig.push_back(17);   // root-path
    orig.push_back(3);    // length=3
    orig.push_back(0x64); // data="def"
    orig.push_back(0x65);
    orig.push_back(0x66);

    // Unpacking should not throw.
    Pkt4Ptr pkt(new Pkt4(&orig[0], orig.size()));
    ASSERT_NO_THROW_LOG(pkt->unpack());

    // We should have option 14 = "abc".
    OptionPtr opt;
    OptionStringPtr opstr;
    ASSERT_TRUE(opt = pkt->getOption(14));
    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
    EXPECT_EQ(3, opstr->getValue().length());
    EXPECT_EQ("abc", opstr->getValue());

    // We should not have option 12.
    EXPECT_FALSE(opt = pkt->getOption(12));

    // We should have option 17 = "def".
    ASSERT_TRUE(opt = pkt->getOption(17));
    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
    EXPECT_EQ(3, opstr->getValue().length());
    EXPECT_EQ("def", opstr->getValue());
}

// Tests that getHWAddrLabel method produces the expected strings based on
// packet content.
TEST_F(Pkt4Test, getHWAddrLabel) {
    Pkt4 pkt(DHCPOFFER, 1234);

    // Verify getHWAddrLabel() handles empty values
    EXPECT_EQ ("hwaddr=", pkt.getHWAddrLabel());

    // Testing undefined hwaddr case is not possible
    EXPECT_THROW(pkt.setHWAddr(nullptr), BadValue);

    // Set that packet hardware address, then verify getLabel
    const uint8_t hw[] = { 2, 4, 6, 8, 10, 12 }; // MAC
    const uint8_t hw_type = 123; // hardware type
    HWAddrPtr dummy_hwaddr(new HWAddr(hw, sizeof(hw), hw_type));
    pkt.setHWAddr(dummy_hwaddr);

    EXPECT_EQ ("hwaddr=02:04:06:08:0a:0c", pkt.getHWAddrLabel());
}

// Exercises packet event stack and helper functions.
TEST_F(Pkt4Test, PktEvents) {
    // Get current time.
    auto start_time = PktEvent::now();

    // Verify that a set time is not equal to an EMPTY_TIME.
    ASSERT_NE(start_time, PktEvent::EMPTY_TIME());

    // Create a test packet.
    scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));

    // Upon creation, the events table should be empty.
    ASSERT_TRUE(pkt->getPktEvents().empty());

    // An non-existent event should return an empty time.
    auto event_time = pkt->getPktEventTime(PktEvent::BUFFER_READ);
    ASSERT_EQ(event_time, PktEvent::EMPTY_TIME());

    // Sleep for 200 microseconds to put some distance between now and start_time.
    usleep(200);

    // Should be able to add an event, defaulting the event time to current time.
    pkt->addPktEvent(PktEvent::BUFFER_READ);
    event_time = pkt->getPktEventTime(PktEvent::BUFFER_READ);
    ASSERT_GT(event_time, start_time);

    // Should be able to overwrite an existing event's time.
    pkt->setPktEvent(PktEvent::BUFFER_READ, start_time);
    event_time = pkt->getPktEventTime(PktEvent::BUFFER_READ);
    ASSERT_EQ(event_time, start_time);

    // Should be able to add an event with an explicit time.
    pkt->addPktEvent(PktEvent::RESPONSE_SENT, start_time);
    event_time = pkt->getPktEventTime(PktEvent::RESPONSE_SENT);
    ASSERT_EQ(event_time, start_time);

    // Should be able to fetch the list of events.
    auto const& events = pkt->getPktEvents();
    ASSERT_FALSE(events.empty());
    auto event = events.begin();
    ASSERT_EQ((*event).label_, PktEvent::BUFFER_READ);
    ++event;
    ASSERT_EQ((*event).label_, PktEvent::RESPONSE_SENT);

    // Discard the event stack contents.
    pkt->clearPktEvents();
    ASSERT_TRUE(pkt->getPktEvents().empty());

    // Verify dumpPktEvent terse output. Also serves to
    // verify adding events using struct timeval.
    struct timeval log_time = {1706802676, 100};
    struct timeval log_time_plus = {1706802676, 250};
    pkt->addPktEvent("first-event", log_time);
    pkt->addPktEvent("second-event", log_time_plus);
    std::string log = pkt->dumpPktEvents();
    EXPECT_EQ(log, "2024-Feb-01 15:51:16.000100 : first-event, 2024-Feb-01 15:51:16.000250 : second-event");

    // Verify dumpPktEvent verbose output.
    log = pkt->dumpPktEvents(true);
    EXPECT_EQ(log,
              "Event log: \n"
              "2024-Feb-01 15:51:16.000100 : first-event\n"
              "2024-Feb-01 15:51:16.000250 : second-event elapsed: 00:00:00.000150\n"
              "total elapsed: 00:00:00.000150");
}

} // end of anonymous namespace
