// Copyright (C) 2015-2024 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 <dhcpsrv/testutils/alloc_engine_utils.h>
#include <dhcpsrv/testutils/test_utils.h>

#include <hooks/server_hooks.h>
#include <hooks/callout_manager.h>
#include <hooks/hooks_manager.h>

#include <iostream>

using namespace std;
using namespace isc::hooks;
using namespace isc::asiolink;

namespace isc {
namespace dhcp {
namespace test {

/// @brief helper class used in Hooks testing in AllocEngine6
///
/// It features a couple of callout functions and buffers to store
/// the data that is accessible via callouts.
class HookAllocEngine6Test : public AllocEngine6Test {
public:
    HookAllocEngine6Test() {
        resetCalloutBuffers();
    }

    virtual ~HookAllocEngine6Test() {
        resetCalloutBuffers();
        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
            "lease6_select");
        bool status = HooksManager::unloadLibraries();
        if (!status) {
            cerr << "(fixture dtor) unloadLibraries failed" << endl;
        }
    }

    /// @brief clears out buffers, so callouts can store received arguments
    void resetCalloutBuffers() {
        callback_name_ = string("");
        callback_subnet6_.reset();
        callback_fake_allocation_ = false;
        callback_lease6_.reset();
        callback_argument_names_.clear();
        callback_addr_original_ = IOAddress("::");
        callback_addr_updated_ = IOAddress("::");
        callback_qry_pkt6_.reset();
        callback_qry_options_copy_ = false;
        callback_skip_ = 0;
    }

    /// @brief Checks if the state of the callout handle associated with a query
    /// was reset after the callout invocation.
    ///
    /// The check includes verification if the status was set to 'continue' and
    /// that all arguments were deleted.
    ///
    /// @param query pointer to the query which callout handle is associated
    /// with.
    void checkCalloutHandleReset(const Pkt6Ptr& query) {
        CalloutHandlePtr callout_handle = query->getCalloutHandle();
        ASSERT_TRUE(callout_handle);
        EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
        EXPECT_TRUE(callout_handle->getArgumentNames().empty());
    }

    /// callback that stores received callout name and received values
    static int
    lease6_select_callout(CalloutHandle& callout_handle) {

        callback_name_ = string("lease6_select");

        callout_handle.getArgument("query6", callback_qry_pkt6_);
        callout_handle.getArgument("subnet6", callback_subnet6_);
        callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
        callout_handle.getArgument("lease6", callback_lease6_);

        callback_addr_original_ = callback_lease6_->addr_;

        callback_argument_names_ = callout_handle.getArgumentNames();

        if (callback_qry_pkt6_) {
            callback_qry_options_copy_ =
                callback_qry_pkt6_->isCopyRetrievedOptions();
        }

        return (0);
    }

    /// callback that overrides the lease with different values
    static int
    lease6_select_different_callout(CalloutHandle& callout_handle) {

        // Let's call the basic callout, so it can record all parameters
        lease6_select_callout(callout_handle);

        // Now we need to tweak the least a bit
        Lease6Ptr lease;
        callout_handle.getArgument("lease6", lease);
        callback_addr_updated_ = addr_override_;
        lease->addr_ = callback_addr_updated_;
        lease->preferred_lft_ = pref_override_;
        lease->valid_lft_ = valid_override_;

        return (0);
    }

    /// callback that return next step skip status
    static int
    lease6_select_skip_callout(CalloutHandle& callout_handle) {

        // Let's call the basic callout, so it can record all parameters
        lease6_select_callout(callout_handle);

        // Count the call
        callback_skip_++;

        callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);

        return (0);
    }

    // Values to be used in callout to override lease6 content
    static const IOAddress addr_override_;
    static const uint32_t pref_override_;
    static const uint32_t valid_override_;

    // Callback will store original and overridden values here
    static IOAddress callback_addr_original_;
    static IOAddress callback_addr_updated_;

    // Buffers (callback will store received values here)
    static string callback_name_;
    static ConstSubnet6Ptr callback_subnet6_;
    static Lease6Ptr callback_lease6_;
    static bool callback_fake_allocation_;
    static vector<string> callback_argument_names_;
    static Pkt6Ptr callback_qry_pkt6_;
    static bool callback_qry_options_copy_;

    // Counter for next step skip (should be not retried)
    static unsigned callback_skip_;
};

// For some reason initialization within a class makes the linker confused.
// linker complains about undefined references if they are defined within
// the class declaration.
const IOAddress HookAllocEngine6Test::addr_override_("2001:db8::abcd");
const uint32_t HookAllocEngine6Test::pref_override_ = 8000;
const uint32_t HookAllocEngine6Test::valid_override_ = 9000;

IOAddress HookAllocEngine6Test::callback_addr_original_("::");
IOAddress HookAllocEngine6Test::callback_addr_updated_("::");

string HookAllocEngine6Test::callback_name_;
ConstSubnet6Ptr HookAllocEngine6Test::callback_subnet6_;
Lease6Ptr HookAllocEngine6Test::callback_lease6_;
bool HookAllocEngine6Test::callback_fake_allocation_;
vector<string> HookAllocEngine6Test::callback_argument_names_;
Pkt6Ptr HookAllocEngine6Test::callback_qry_pkt6_;
bool HookAllocEngine6Test::callback_qry_options_copy_;

unsigned HookAllocEngine6Test::callback_skip_;

// This test checks if the lease6_select callout is executed and expected
// parameters as passed.
TEST_F(HookAllocEngine6Test, lease6_select) {

    // Note: The following order is working as expected:
    // 1. create AllocEngine (that register hook points)
    // 2. call loadLibraries()
    //
    // This order, however, causes segfault in HooksManager
    // 1. call loadLibraries()
    // 2. create AllocEngine (that register hook points)

    // Create allocation engine (hook names are registered in its ctor)
    boost::scoped_ptr<AllocEngine> engine;
    ASSERT_NO_THROW(engine.reset(new AllocEngine(100)));
    ASSERT_TRUE(engine);

    // Initialize Hooks Manager
    HookLibsCollection libraries; // no libraries at this time
    ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));

    // Install lease6_select
    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
                        "lease6_select", lease6_select_callout));

    Lease6Ptr lease;
    AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
                                    Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)),
                                    HooksManager::createCalloutHandle());
    ctx.currentIA().iaid_ = iaid_;

    EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
    // Check that we got a lease
    ASSERT_TRUE(lease);

    // Do all checks on the lease
    checkLease6(duid_, lease, Lease::TYPE_NA, 128);

    // Check that the lease is indeed in LeaseMgr
    Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
                                                               lease->addr_);
    ASSERT_TRUE(from_mgr);

    // Check that callouts were indeed called
    EXPECT_EQ("lease6_select", callback_name_);

    // Check that query6 argument was set correctly
    ASSERT_TRUE(callback_qry_pkt6_);
    EXPECT_EQ(callback_qry_pkt6_.get(), ctx.query_.get());

    // Now check that the lease in LeaseMgr has the same parameters
    ASSERT_TRUE(callback_lease6_);
    detailCompareLease(callback_lease6_, from_mgr);

    ASSERT_TRUE(callback_subnet6_);
    EXPECT_EQ(subnet_->toText(), callback_subnet6_->toText());

    EXPECT_FALSE(callback_fake_allocation_);

    // Check if all expected parameters are reported.  The order needs to be
    // alphabetical to match the order returned by
    // CallbackHandle::getArgumentNames()
    vector<string> expected_argument_names;
    expected_argument_names.push_back("fake_allocation");
    expected_argument_names.push_back("lease6");
    expected_argument_names.push_back("query6");
    expected_argument_names.push_back("subnet6");

    sort(callback_argument_names_.begin(), callback_argument_names_.end());
    sort(expected_argument_names.begin(), expected_argument_names.end());

    EXPECT_TRUE(callback_argument_names_ == expected_argument_names);

    EXPECT_TRUE(callback_qry_options_copy_);

    // Check if the callout handle state was reset after the callout.
    checkCalloutHandleReset(ctx.query_);
}

// This test checks if lease6_select callout is able to override the values
// in a lease6.
TEST_F(HookAllocEngine6Test, change_lease6_select) {

    // Make sure that the overridden values are different than the ones from
    // subnet originally used to create the lease
    ASSERT_NE(pref_override_, subnet_->getPreferred());
    ASSERT_NE(valid_override_, subnet_->getValid());
    ASSERT_FALSE(subnet_->inRange(addr_override_));

    // Create allocation engine (hook names are registered in its ctor)
    boost::scoped_ptr<AllocEngine> engine;
    ASSERT_NO_THROW(engine.reset(new AllocEngine(100)));
    ASSERT_TRUE(engine);

    // Initialize Hooks Manager
    HookLibsCollection libraries; // no libraries at this time
    ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));

    // Install a callout
    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
                        "lease6_select", lease6_select_different_callout));

    // Call allocateLeases6. Callouts should be triggered here.
    Lease6Ptr lease;
    AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
                                    Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)),
                                    HooksManager::createCalloutHandle());
    ctx.currentIA().iaid_ = iaid_;
    EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
    // Check that we got a lease
    ASSERT_TRUE(lease);

    // See if the values overridden by callout are there
    EXPECT_TRUE(lease->addr_.equals(addr_override_));
    EXPECT_EQ(pref_override_, lease->preferred_lft_);
    EXPECT_EQ(valid_override_, lease->valid_lft_);

    // Now check if the lease is in the database
    Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
                                                               lease->addr_);
    ASSERT_TRUE(from_mgr);

    // Check if values in the database are overridden
    EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
    EXPECT_EQ(pref_override_, from_mgr->preferred_lft_);
    EXPECT_EQ(valid_override_, from_mgr->valid_lft_);

    // Check if the callout handle state was reset after the callout.
    checkCalloutHandleReset(ctx.query_);
}

// This test checks if lease6_select callout can set the status to next
// step skip without the engine to retry.
TEST_F(HookAllocEngine6Test, skip_lease6_select) {

    // Create allocation engine (hook names are registered in its ctor)
    boost::scoped_ptr<AllocEngine> engine;
    ASSERT_NO_THROW(engine.reset(new AllocEngine(100)));
    ASSERT_TRUE(engine);

    // Initialize Hooks Manager
    HookLibsCollection libraries; // no libraries at this time
    ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));

    // Install a callout
    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
                        "lease6_select", lease6_select_skip_callout));

    // Call allocateLeases6. Callouts should be triggered here.
    Lease6Ptr lease;
    AllocEngine::ClientContext6 ctx(subnet_, duid_, false, false, "", false,
                                    Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234)),
                                    HooksManager::createCalloutHandle());
    ctx.currentIA().iaid_ = iaid_;
    EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(ctx)));
    // Check that we got no lease
    EXPECT_FALSE(lease);

    // Check no retry was attempted
    EXPECT_EQ(1, callback_skip_);

    // Check if the callout handle state was reset after the callout.
    checkCalloutHandleReset(ctx.query_);
}

/// @brief helper class used in Hooks testing in AllocEngine4
///
/// It features a couple of callout functions and buffers to store
/// the data that is accessible via callouts.
///
/// Note: lease4_renew callout is tested from DHCPv4 server.
/// See HooksDhcpv4SrvTest.basic_lease4_renew in
/// src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
class HookAllocEngine4Test : public AllocEngine4Test {
public:
    HookAllocEngine4Test() {
        // The default context is not used in these tests.
        ctx_.callout_handle_.reset();
        resetCalloutBuffers();
    }

    virtual ~HookAllocEngine4Test() {
        resetCalloutBuffers();
        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
            "lease4_select");
        bool status = HooksManager::unloadLibraries();
        if (!status) {
            cerr << "(fixture dtor) unloadLibraries failed" << endl;
        }
    }

    /// @brief clears out buffers, so callouts can store received arguments
    void resetCalloutBuffers() {
        callback_name_ = string("");
        callback_subnet4_.reset();
        callback_fake_allocation_ = false;
        callback_lease4_.reset();
        callback_argument_names_.clear();
        callback_addr_original_ = IOAddress("::");
        callback_addr_updated_ = IOAddress("::");
        callback_qry_pkt4_.reset();
        callback_qry_options_copy_ = false;
        callback_skip_ = 0;
    }

    /// @brief Checks if the state of the callout handle associated with a query
    /// was reset after the callout invocation.
    ///
    /// The check includes verification if the status was set to 'continue' and
    /// that all arguments were deleted.
    ///
    /// @param query pointer to the query which callout handle is associated
    /// with.
    void checkCalloutHandleReset(const Pkt4Ptr& query) {
        CalloutHandlePtr callout_handle = query->getCalloutHandle();
        ASSERT_TRUE(callout_handle);
        EXPECT_EQ(CalloutHandle::NEXT_STEP_CONTINUE, callout_handle->getStatus());
        EXPECT_TRUE(callout_handle->getArgumentNames().empty());
    }

    /// callback that stores received callout name and received values
    static int
    lease4_select_callout(CalloutHandle& callout_handle) {

        callback_name_ = string("lease4_select");

        callout_handle.getArgument("query4", callback_qry_pkt4_);
        callout_handle.getArgument("subnet4", callback_subnet4_);
        callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
        callout_handle.getArgument("lease4", callback_lease4_);

        callback_addr_original_ = callback_lease4_->addr_;

        callback_argument_names_ = callout_handle.getArgumentNames();

        if (callback_qry_pkt4_) {
            callback_qry_options_copy_ =
                callback_qry_pkt4_->isCopyRetrievedOptions();
        }

        return (0);
    }

    /// callback that overrides the lease with different values
    static int
    lease4_select_different_callout(CalloutHandle& callout_handle) {

        // Let's call the basic callout, so it can record all parameters
        lease4_select_callout(callout_handle);

        // Now we need to tweak the least a bit
        Lease4Ptr lease;
        callout_handle.getArgument("lease4", lease);
        callback_addr_updated_ = addr_override_;
        lease->addr_ = callback_addr_updated_;
        lease->valid_lft_ = valid_override_;

        return (0);
    }

    /// callback that return next step skip status
    static int
    lease4_select_skip_callout(CalloutHandle& callout_handle) {

        // Let's call the basic callout, so it can record all parameters
        lease4_select_callout(callout_handle);

        // Count the call
        callback_skip_++;

        callout_handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);

        return (0);
    }

    // Values to be used in callout to override lease4 content
    static const IOAddress addr_override_;
    static const uint32_t valid_override_;

    // Callback will store original and overridden values here
    static IOAddress callback_addr_original_;
    static IOAddress callback_addr_updated_;

    // Buffers (callback will store received values here)
    static string callback_name_;
    static ConstSubnet4Ptr callback_subnet4_;
    static Lease4Ptr callback_lease4_;
    static bool callback_fake_allocation_;
    static vector<string> callback_argument_names_;
    static Pkt4Ptr callback_qry_pkt4_;
    static bool callback_qry_options_copy_;

    // Counter for next step skip (should be not retried)
    static unsigned callback_skip_;
};

// For some reason initialization within a class makes the linker confused.
// linker complains about undefined references if they are defined within
// the class declaration.
const IOAddress HookAllocEngine4Test::addr_override_("192.0.3.1");
const uint32_t HookAllocEngine4Test::valid_override_ = 9000;

IOAddress HookAllocEngine4Test::callback_addr_original_("::");
IOAddress HookAllocEngine4Test::callback_addr_updated_("::");

string HookAllocEngine4Test::callback_name_;
ConstSubnet4Ptr HookAllocEngine4Test::callback_subnet4_;
Lease4Ptr HookAllocEngine4Test::callback_lease4_;
bool HookAllocEngine4Test::callback_fake_allocation_;
vector<string> HookAllocEngine4Test::callback_argument_names_;
Pkt4Ptr HookAllocEngine4Test::callback_qry_pkt4_;
bool HookAllocEngine4Test::callback_qry_options_copy_;

unsigned HookAllocEngine4Test::callback_skip_;

// This test checks if the lease4_select callout is executed and expected
// parameters as passed.
TEST_F(HookAllocEngine4Test, lease4_select) {

    // Note: The following order is working as expected:
    // 1. create AllocEngine (that register hook points)
    // 2. call loadLibraries()
    //
    // This order, however, causes segfault in HooksManager
    // 1. call loadLibraries()
    // 2. create AllocEngine (that register hook points)

    // Create allocation engine (hook names are registered in its ctor)
    boost::scoped_ptr<AllocEngine> engine;
    ASSERT_NO_THROW(engine.reset(new AllocEngine(100)));
    ASSERT_TRUE(engine);

    // Initialize Hooks Manager
    HookLibsCollection libraries; // no libraries at this time
    ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));

    // Install lease4_select
    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
                        "lease4_select", lease4_select_callout));

    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();

    AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
                                    IOAddress("0.0.0.0"),
                                    false, false, "", false);
    ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
    ctx.callout_handle_ = callout_handle;

    Lease4Ptr lease = engine->allocateLease4(ctx);

    // Check that we got a lease
    ASSERT_TRUE(lease);

    // Do all checks on the lease
    checkLease4(lease);

    // Check that the lease is indeed in LeaseMgr
    Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
    ASSERT_TRUE(from_mgr);

    // Check that callouts were indeed called
    EXPECT_EQ("lease4_select", callback_name_);

    // Check that query4 argument was set correctly
    ASSERT_TRUE(callback_qry_pkt4_);
    EXPECT_EQ(callback_qry_pkt4_.get(), ctx.query_.get());

    // Now check that the lease in LeaseMgr has the same parameters
    ASSERT_TRUE(callback_lease4_);
    detailCompareLease(callback_lease4_, from_mgr);

    ASSERT_TRUE(callback_subnet4_);
    EXPECT_EQ(subnet_->toText(), callback_subnet4_->toText());

    EXPECT_EQ(callback_fake_allocation_, false);

    // Check if all expected parameters are reported.  The order needs to be
    // alphabetical to match the order returned by
    // CallbackHandle::getArgumentNames()
    vector<string> expected_argument_names;
    expected_argument_names.push_back("fake_allocation");
    expected_argument_names.push_back("lease4");
    expected_argument_names.push_back("offer_lft");
    expected_argument_names.push_back("query4");
    expected_argument_names.push_back("subnet4");
    EXPECT_TRUE(callback_argument_names_ == expected_argument_names);

    EXPECT_TRUE(callback_qry_options_copy_);

    // Check if the callout handle state was reset after the callout.
    checkCalloutHandleReset(ctx.query_);
}

// This test checks if lease4_select callout is able to override the values
// in a lease4.
TEST_F(HookAllocEngine4Test, change_lease4_select) {

    // Make sure that the overridden values are different than the ones from
    // subnet originally used to create the lease
    ASSERT_NE(valid_override_, subnet_->getValid());
    ASSERT_FALSE(subnet_->inRange(addr_override_));

    // Create allocation engine (hook names are registered in its ctor)
    boost::scoped_ptr<AllocEngine> engine;
    ASSERT_NO_THROW(engine.reset(new AllocEngine(100)));
    ASSERT_TRUE(engine);

    // Initialize Hooks Manager
    HookLibsCollection libraries; // no libraries at this time
    ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));

    // Install a callout
    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
                        "lease4_select", lease4_select_different_callout));

    // Normally, dhcpv4_srv would passed the handle when calling allocateLease4,
    // but in tests we need to create it on our own.
    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();


    AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
                                    false, true, "somehost.example.com.", false);
    ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
    ctx.callout_handle_ = callout_handle;

    // Call allocateLease4. Callouts should be triggered here.
    Lease4Ptr lease = engine->allocateLease4(ctx);

    // Check that we got a lease
    ASSERT_TRUE(lease);

    // See if the values overridden by callout are there
    EXPECT_TRUE(lease->addr_.equals(addr_override_));
    EXPECT_EQ(valid_override_, lease->valid_lft_);

    // Now check if the lease is in the database
    Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
    ASSERT_TRUE(from_mgr);

    // Check if values in the database are overridden
    EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
    EXPECT_EQ(valid_override_, from_mgr->valid_lft_);

    // Check if the callout handle state was reset after the callout.
    checkCalloutHandleReset(ctx.query_);
}

// This test checks if lease4_select callout can set the status to next
// step skip without the engine to retry.
TEST_F(HookAllocEngine4Test, skip_lease4_select) {

    // Create allocation engine (hook names are registered in its ctor)
    boost::scoped_ptr<AllocEngine> engine;
    ASSERT_NO_THROW(engine.reset(new AllocEngine(100)));
    ASSERT_TRUE(engine);

    // Initialize Hooks Manager
    HookLibsCollection libraries; // no libraries at this time
    ASSERT_NO_THROW(HooksManager::loadLibraries(libraries));

    // Install a callout
    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
                        "lease4_select", lease4_select_skip_callout));

    CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();

    AllocEngine::ClientContext4 ctx(subnet_, clientid_, hwaddr_,
                                    IOAddress("0.0.0.0"),
                                    false, false, "", false);
    ctx.query_.reset(new Pkt4(DHCPREQUEST, 1234));
    ctx.callout_handle_ = callout_handle;

    Lease4Ptr lease = engine->allocateLease4(ctx);

    // Check that we got no lease
    EXPECT_FALSE(lease);

    // Check no retry was attempted
    EXPECT_EQ(1, callback_skip_);

    // Check if the callout handle state was reset after the callout.
    checkCalloutHandleReset(ctx.query_);
}

} // namespace test
} // namespace dhcp
} // namespace isc
