// Copyright (C) 2013-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 <hooks/callout_handle.h>
#include <hooks/hooks_manager.h>
#include <hooks/hooks_parser.h>
#include <hooks/server_hooks.h>
#include <util/filesystem.h>
#include <testutils/gtest_utils.h>
#include <testutils/log_utils.h>

#include <hooks/tests/common_test_class.h>
#define TEST_ASYNC_CALLOUT
#include <hooks/tests/test_libraries.h>
#include <cc/data.h>

#include <boost/shared_ptr.hpp>
#include <gtest/gtest.h>

#include <algorithm>
#include <fstream>
#include <string>

#include <unistd.h>
#include <cstdlib>

using namespace isc;
using namespace isc::hooks;
using namespace isc::data;
using namespace isc::util::file;
using namespace isc::dhcp::test;
using namespace std;

namespace {

/// @brief Hooks manager collection test class

class HooksManagerTest : public ::testing::Test,
                         public HooksCommonTestClass {
public:
    /// @brief Constructor
    ///
    /// Reset the hooks manager.  The hooks manager is a singleton, so needs
    /// to be reset for each test.
    HooksManagerTest() {
        HooksManager::setTestMode(false);
        HooksManager::prepareUnloadLibraries();
        bool status = HooksManager::unloadLibraries();
        if (!status) {
            cerr << "(fixture ctor) unloadLibraries failed" << endl;
        }
        // Ensure the marker file is not present at the start of a test.
        static_cast<void>(remove(MARKER_FILE));
    }

    /// @brief Destructor
    ///
    /// Unload all libraries.
    ~HooksManagerTest() {
        static_cast<void>(remove(MARKER_FILE));
        HooksManager::setTestMode(false);
        HooksManager::prepareUnloadLibraries();
        bool status = HooksManager::unloadLibraries();
        if (!status) {
            cerr << "(fixture dtor) unloadLibraries failed" << endl;
        }
    }

    /// @brief Marker file present
    ///
    /// Convenience function to check whether a marker file is present.  It
    /// does this by opening the file.
    ///
    /// @return true if the marker file is present.
    bool markerFilePresent() const {

        // Try to open it.
        std::fstream marker;
        marker.open(MARKER_FILE, std::fstream::in);

        // Check if it is open and close it if so.
        bool exists = marker.is_open();
        if (exists) {
            marker.close();
        }

        return (exists);
    }

    /// @brief Call callouts test
    ///
    /// See the header for HooksCommonTestClass::execute for details.
    ///
    /// @param r0...r3, d1..d3 Values and intermediate values expected.  They
    ///        are ordered so that the variables appear in the argument list in
    ///        the order they are used.
    void executeCallCallouts(int r0, int d1, int r1, int d2, int r2, int d3,
                             int r3) {
        static const char* COMMON_TEXT = " callout returned the wrong value";
        static const char* RESULT = "result";

        // Get a CalloutHandle for the calculation.
        CalloutHandlePtr handle = HooksManager::createCalloutHandle();

        // Initialize the argument RESULT.  This simplifies testing by
        // eliminating the generation of an exception when we try the unload
        // test.  In that case, RESULT is unchanged.
        int result = -1;
        handle->setArgument(RESULT, result);

        // Seed the calculation.
        HooksManager::callCallouts(isc::hooks::ServerHooks::CONTEXT_CREATE,
                                   *handle);
        handle->getArgument(RESULT, result);
        EXPECT_EQ(r0, result) << "context_create" << COMMON_TEXT;

        // Perform the first calculation.
        handle->setArgument("data_1", d1);
        HooksManager::callCallouts(hookpt_one_index_, *handle);
        handle->getArgument(RESULT, result);
        EXPECT_EQ(r1, result) << "hookpt_one" << COMMON_TEXT;

        // ... the second ...
        handle->setArgument("data_2", d2);
        HooksManager::callCallouts(hookpt_two_index_, *handle);
        handle->getArgument(RESULT, result);
        EXPECT_EQ(r2, result) << "hookpt_two" << COMMON_TEXT;

        // ... and the third.
        handle->setArgument("data_3", d3);
        HooksManager::callCallouts(hookpt_three_index_, *handle);
        handle->getArgument(RESULT, result);
        EXPECT_EQ(r3, result) << "hookpt_three" << COMMON_TEXT;
    }

    /// @brief Call command handlers test.
    ///
    /// This test is similar to @c executeCallCallouts but it uses
    /// @ref HooksManager::callCommandHandlers to execute the command
    /// handlers for the following commands: 'command-one' and 'command-two'.
    ///
    /// @param r1..r2, d1..d2 Data (dN) and expected results (rN).
    void executeCallCommandHandlers(int d1, int r1, int d2, int r2) {
        static const char* COMMON_TEXT = " command handler returned the wrong value";
        static const char* RESULT = "result";

        int result;

        // Set up a callout handle for the calls.
        CalloutHandlePtr handle = HooksManager::createCalloutHandle();

        // Initialize the argument RESULT.  This simplifies testing by
        // eliminating the generation of an exception when we try the unload
        // test.  In that case, RESULT is unchanged.
        handle->setArgument(RESULT, -1);

        // Perform the first calculation: it should assign the data to the
        // result.
        handle->setArgument("data_1", d1);
        HooksManager::callCommandHandlers("command-one", *handle);
        handle->getArgument(RESULT, result);
        EXPECT_EQ(r1, result) << "command-one" << COMMON_TEXT;

        // Perform the second calculation: it should multiply the data by 10
        // and return in the result.
        handle->setArgument("data_2", d2);
        HooksManager::callCommandHandlers("command-two", *handle);
        handle->getArgument(RESULT, result);
        EXPECT_EQ(r2, result) << "command-two" << COMMON_TEXT;
    }

private:
    /// To avoid unused variable errors
    std::string dummy(int i) {
        if (i == 0) {
            return (LOAD_CALLOUT_LIBRARY);
        } else {
            return (LOAD_ERROR_CALLOUT_LIBRARY);
        }
    }
};

// This is effectively the same test as for LibraryManager, but using the
// HooksManager object.

TEST_F(HooksManagerTest, LoadLibraries) {

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Execute the callouts.  The first library implements the calculation.
    //
    // r3 = (7 * d1 - d2) * d3
    //
    // The last-loaded library implements the calculation
    //
    // r3 = (10 + d1) * d2 - d3
    //
    // Putting the processing for each library together in the appropriate
    // order, we get:
    //
    // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
    {
        SCOPED_TRACE("Calculation with libraries loaded");
        executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
    }

    // r2 = 5 * 7 * 10
    {
        SCOPED_TRACE("Calculation using command handlers");
        executeCallCommandHandlers(5, 5, 7, 350);
    }

    // Try unloading the libraries.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);

    // Re-execute the calculation - callouts can be called but as nothing
    // happens, the result should always be -1.
    {
        SCOPED_TRACE("Calculation with libraries not loaded");
        executeCallCallouts(-1, 3, -1, 22, -1, 83, -1);
    }
}

// This is effectively the same test as above, but with a library generating
// an error when loaded. It is expected that the failing library will not be
// loaded, but others will be.

TEST_F(HooksManagerTest, LoadLibrariesWithError) {

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(INCORRECT_VERSION_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the libraries.  We expect a failure return because one of the
    // libraries fails to load.
    EXPECT_FALSE(HooksManager::loadLibraries(library_names));
}

// Test that we can unload a set of libraries while we have a CalloutHandle
// created on them in existence, and can delete the handle afterwards.

TEST_F(HooksManagerTest, CalloutHandleUnloadLibrary) {

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Execute the callouts.  This library implements:
    //
    // r3 = (7 * d1 - d2) * d3
    {
        SCOPED_TRACE("Calculation with full callout library loaded");
        executeCallCallouts(7, 4, 28, 8, 20, 2, 40);
    }

    // Get an outstanding callout handle on this library.
    CalloutHandlePtr handle = HooksManager::createCalloutHandle();

    // Execute once of the callouts again to ensure that the handle contains
    // memory allocated by the library.
    HooksManager::callCallouts(ServerHooks::CONTEXT_CREATE, *handle);

    // Unload the libraries.
    HooksManager::prepareUnloadLibraries();
    EXPECT_FALSE(HooksManager::unloadLibraries());

    // Deleting the callout handle should not cause a segmentation fault.
    handle.reset();

    // And allows unload.
    EXPECT_TRUE(HooksManager::unloadLibraries());
}

// Test that we can load a new set of libraries while we have a CalloutHandle
// created on them in existence, and can delete the handle afterwards.

TEST_F(HooksManagerTest, CalloutHandleLoadLibrary) {

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Execute the callouts.  This library implements:
    //
    // r3 = (7 * d1 - d2) * d3
    {
        SCOPED_TRACE("Calculation with full callout library loaded");
        executeCallCallouts(7, 4, 28, 8, 20, 2, 40);
    }

    // Get an outstanding callout handle on this library and execute one of
    // the callouts again to ensure that the handle contains memory allocated
    // by the library.
    CalloutHandlePtr handle = HooksManager::createCalloutHandle();
    HooksManager::callCallouts(ServerHooks::CONTEXT_CREATE, *handle);

    // Load a new library that implements the calculation
    //
    // r3 = (10 + d1) * d2 - d3
    HookLibsCollection new_library_names;
    new_library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                            data::ConstElementPtr()));

    // Load the libraries.
    EXPECT_THROW(HooksManager::loadLibraries(new_library_names),
                 LibrariesStillOpened);

    // Deleting the old callout handle should not cause a segmentation fault.
    handle.reset();

    // But it allows the load of the new library.
    EXPECT_TRUE(HooksManager::loadLibraries(new_library_names));

    // Execute the calculation.
    {
        SCOPED_TRACE("Calculation with basic callout library loaded");
        executeCallCallouts(10, 7, 17, 3, 51, 16, 35);
    }
}

// This is effectively the same test as the LoadLibraries test.

TEST_F(HooksManagerTest, ReloadSameLibraries) {

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Execute the callouts.  See the LoadLibraries test for an explanation of
    // the calculation.
    {
        SCOPED_TRACE("Calculation with libraries loaded");
        executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
    }

    // Try reloading the libraries and re-execute the calculation - we should
    // get the same results.
    EXPECT_NO_THROW(HooksManager::loadLibraries(library_names));
    {
        SCOPED_TRACE("Calculation with libraries reloaded");
        executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
    }
}

TEST_F(HooksManagerTest, ReloadLibrariesReverseOrder) {

    // Set up the list of libraries to be loaded and load them.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                       data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Execute the callouts.  The first library implements the calculation.
    //
    // r3 = (7 * d1 - d2) * d3
    //
    // The last-loaded library implements the calculation
    //
    // r3 = (10 + d1) * d2 - d3
    //
    // Putting the processing for each library together in the given order
    // gives.
    //
    // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
    {
        SCOPED_TRACE("Calculation with libraries loaded");
        executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
    }

    // Reload the libraries in the reverse order.
    std::reverse(library_names.begin(), library_names.end());
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // The calculation in the reverse order gives:
    //
    // r3 = ((((7 + d1) * d1) * d2 - d2) - d3) * d3
    {
        SCOPED_TRACE("Calculation with libraries loaded in reverse order");
        executeCallCallouts(7, 3, 30, 3, 87, 7, 560);
    }
}

// Local callouts for the test of server-registered callouts.

namespace {

    int
testPreCallout(CalloutHandle& handle) {
    handle.setArgument("result", static_cast<int>(1027));
    return (0);
}

int
testPostCallout(CalloutHandle& handle) {
    int result;
    handle.getArgument("result", result);
    result *= 2;
    handle.setArgument("result", result);
    return (0);
}

}

// The next test registers the pre and post- callouts above for hook hookpt_two,
// and checks they are called.

TEST_F(HooksManagerTest, PrePostCalloutTest) {

    // Load a single library.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Load the pre- and post- callouts.
    HooksManager::preCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                             testPreCallout);
    HooksManager::postCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                              testPostCallout);

    // Execute the callouts.  hookpt_two implements the calculation:
    //
    //  "result - data_2"
    //
    // With the pre- and post- callouts above, the result expected is
    //
    // (1027 - data_2) * 2
    CalloutHandlePtr handle = HooksManager::createCalloutHandle();
    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    int result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(2024, result);

    // Reset the handle to allow a reload.
    handle.reset();

    // ... and check that the pre- and post- callout functions don't survive a
    // reload.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));
    handle = HooksManager::createCalloutHandle();

    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(-15, result);
}

// Test with test mode enabled and the pre- and post- callout functions survive
// a reload

TEST_F(HooksManagerTest, TestModeEnabledPrePostSurviveLoad) {

    // Load a single library.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the pre- and post- callouts.
    HooksManager::preCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                             testPreCallout);
    HooksManager::postCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                              testPostCallout);

    HooksManager::setTestMode(true);

    // With the pre- and post- callouts above, the result expected is
    //
    // 1027 * 2
    CalloutHandlePtr handle = HooksManager::createCalloutHandle();
    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    int result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(2054, result);

    // Reset the handle to allow a reload.
    handle.reset();

    // ... and check that the pre- and post- callout functions survive a
    // reload.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));
    handle = HooksManager::createCalloutHandle();

    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    // Expect same value i.e. 1027 * 2
    result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(2054, result);
}

// Test with test mode disabled and the pre- and post- callout functions do not
// survive a reload

TEST_F(HooksManagerTest, TestModeDisabledPrePostDoNotSurviveLoad) {

    // Load a single library.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the pre- and post- callouts.
    HooksManager::preCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                             testPreCallout);
    HooksManager::postCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                              testPostCallout);

    HooksManager::setTestMode(false);

    // With the pre- and post- callouts above, the result expected is
    //
    // 1027 * 2
    CalloutHandlePtr handle = HooksManager::createCalloutHandle();
    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    int result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(2054, result);

    // Reset the handle to allow a reload.
    handle.reset();

    // ... and check that the pre- and post- callout functions don't survive a
    // reload.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));
    handle = HooksManager::createCalloutHandle();

    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(-15, result);
}

// Test with test mode enabled and the pre- and post- callout functions do not
// survive a reload if the test mode is set too late.

TEST_F(HooksManagerTest, TestModeEnabledTooLatePrePostDoNotSurvive) {

    // Load a single library.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the pre- and post- callouts.
    HooksManager::preCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                             testPreCallout);
    HooksManager::postCalloutsLibraryHandle().registerCallout("hookpt_two",
                                                              testPostCallout);

    // With the pre- and post- callouts above, the result expected is
    //
    // 1027 * 2
    CalloutHandlePtr handle = HooksManager::createCalloutHandle();
    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    int result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(2054, result);

    // Reset the handle to allow a reload.
    handle.reset();

    // ... and check that the pre- and post- callout functions don't survive a
    // reload.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));
    handle = HooksManager::createCalloutHandle();

    HooksManager::setTestMode(true);

    handle->setArgument("result", static_cast<int>(0));
    handle->setArgument("data_2", static_cast<int>(15));

    HooksManager::callCallouts(hookpt_two_index_, *handle);

    result = 0;
    handle->getArgument("result", result);
    EXPECT_EQ(-15, result);
}

// Check that everything works even with no libraries loaded.  First that
// calloutsPresent() always returns false.

TEST_F(HooksManagerTest, NoLibrariesCalloutsPresent) {
    // No callouts should be present on any hooks.
    EXPECT_FALSE(HooksManager::calloutsPresent(hookpt_one_index_));
    EXPECT_FALSE(HooksManager::calloutsPresent(hookpt_two_index_));
    EXPECT_FALSE(HooksManager::calloutsPresent(hookpt_three_index_));
    EXPECT_FALSE(HooksManager::commandHandlersPresent("command-one"));
    EXPECT_FALSE(HooksManager::commandHandlersPresent("command-two"));
}

TEST_F(HooksManagerTest, NoLibrariesCallCallouts) {
    executeCallCallouts(-1, 3, -1, 22, -1, 83, -1);
}

// Test the encapsulation of the ServerHooks::registerHook() method.

TEST_F(HooksManagerTest, RegisterHooks) {
    ServerHooks::getServerHooks().reset();
    EXPECT_EQ(2, ServerHooks::getServerHooks().getCount());

    // Check that the hook indexes are as expected. (Use temporary variables
    // as it appears that Google test can't access the constants.)
    int sh_cc = ServerHooks::CONTEXT_CREATE;
    int hm_cc = HooksManager::CONTEXT_CREATE;
    EXPECT_EQ(sh_cc, hm_cc);

    int sh_cd = ServerHooks::CONTEXT_DESTROY;
    int hm_cd = HooksManager::CONTEXT_DESTROY;
    EXPECT_EQ(sh_cd, hm_cd);

    // Register a few hooks and check we have the indexes as expected.
    EXPECT_EQ(2, HooksManager::registerHook(string("alpha")));
    EXPECT_EQ(3, HooksManager::registerHook(string("beta")));
    EXPECT_EQ(4, HooksManager::registerHook(string("gamma")));


    // The code used to throw, but it now allows to register the same
    // hook several times. It simply returns existing index.
    //EXPECT_THROW(static_cast<void>(HooksManager::registerHook(string("alpha"))),
    //             DuplicateHook);
    EXPECT_EQ(2, HooksManager::registerHook(string("alpha")));

    // ... an check the hooks are as we expect.
    EXPECT_EQ(5, ServerHooks::getServerHooks().getCount());
    vector<string> names = ServerHooks::getServerHooks().getHookNames();
    sort(names.begin(), names.end());

    EXPECT_EQ(string("alpha"), names[0]);
    EXPECT_EQ(string("beta"), names[1]);
    EXPECT_EQ(string("context_create"), names[2]);
    EXPECT_EQ(string("context_destroy"), names[3]);
    EXPECT_EQ(string("gamma"), names[4]);
}

// Check that we can get the names of the libraries.

TEST_F(HooksManagerTest, LibraryNames) {

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(FULL_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Check the names before the libraries are loaded.
    std::vector<std::string> loaded_names = HooksManager::getLibraryNames();
    EXPECT_TRUE(loaded_names.empty());

    // Load the libraries and check the names again.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));
    loaded_names = HooksManager::getLibraryNames();
    EXPECT_TRUE(extractNames(library_names) == loaded_names);

    // Unload the libraries and check again.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
    loaded_names = HooksManager::getLibraryNames();
    EXPECT_TRUE(loaded_names.empty());
}

// Test the library validation function.

TEST_F(HooksManagerTest, validateLibraries) {
    // Vector of libraries that failed validation
    std::vector<std::string> failed;

    // Test different vectors of libraries.

    // No libraries should return a success.
    std::vector<std::string> libraries;

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed.empty());

    // Single valid library should validate.
    libraries.clear();
    libraries.push_back(BASIC_CALLOUT_LIBRARY);

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed.empty());

    // Multiple valid libraries should succeed.
    libraries.clear();
    libraries.push_back(BASIC_CALLOUT_LIBRARY);
    libraries.push_back(FULL_CALLOUT_LIBRARY);
    libraries.push_back(UNLOAD_CALLOUT_LIBRARY);

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed.empty());

    // Single invalid library should fail.
    libraries.clear();
    libraries.push_back(NOT_PRESENT_LIBRARY);

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed == libraries);

    // Multiple invalid libraries should fail.
    libraries.clear();
    libraries.push_back(INCORRECT_VERSION_LIBRARY);
    libraries.push_back(NO_VERSION_LIBRARY);
    libraries.push_back(FRAMEWORK_EXCEPTION_LIBRARY);

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed == libraries);

    // Combination of valid and invalid (first one valid) should fail.
    libraries.clear();
    libraries.push_back(FULL_CALLOUT_LIBRARY);
    libraries.push_back(INCORRECT_VERSION_LIBRARY);
    libraries.push_back(NO_VERSION_LIBRARY);

    std::vector<std::string> expected_failures;
    expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
    expected_failures.push_back(NO_VERSION_LIBRARY);

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed == expected_failures);

    // Combination of valid and invalid (first one invalid) should fail.
    libraries.clear();
    libraries.push_back(NO_VERSION_LIBRARY);
    libraries.push_back(FULL_CALLOUT_LIBRARY);
    libraries.push_back(INCORRECT_VERSION_LIBRARY);

    expected_failures.clear();
    expected_failures.push_back(NO_VERSION_LIBRARY);
    expected_failures.push_back(INCORRECT_VERSION_LIBRARY);

    failed = HooksManager::validateLibraries(libraries);
    EXPECT_TRUE(failed == expected_failures);
}

// This test verifies that unload is called by the prepare method.
TEST_F(HooksManagerTest, prepareUnload) {

    // Set up the list of libraries to be loaded and load them.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(UNLOAD_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Check that the marker file is not present.
    EXPECT_FALSE(markerFilePresent());

    // Prepare unload libraries runs unload functions.
    HooksManager::prepareUnloadLibraries();

    // Now the marker file is present.
    EXPECT_TRUE(markerFilePresent());
}

// This test verifies that the specified parameters are accessed properly.
TEST_F(HooksManagerTest, LibraryParameters) {

    // Prepare parameters for the callout parameters library.
    ElementPtr params = Element::createMap();
    params->set("svalue", Element::create("string value"));
    params->set("ivalue", Element::create(42));
    params->set("bvalue", Element::create(true));

    // Set up the list of libraries to be loaded.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(BASIC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(CALLOUT_PARAMS_LIBRARY),
                                        params));

    // Load the libraries. Note that callout params library checks if
    // all mandatory parameters are there, so if anything is missing, its
    // load() function will return error, thus causing the library to not
    // load.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Try unloading the libraries.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
}

// This test verifies that an object can be parked in two different
// callouts and that it is unparked when the last callout calls the
// unpark function.
TEST_F(HooksManagerTest, Parking) {
    // Load the same library twice. Both installed callouts will trigger
    // asynchronous operation.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));

    // Load the libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    CalloutHandlePtr handle = HooksManager::createCalloutHandle();

    // We could be parked any object. Typically it will be a pointer to the
    // packet. In this case, however, it is simpler to just use a string.
    std::string parked_object = "foo";
    handle->setArgument("parked_object", parked_object);

    // This boolean value will be set to true when the packet gets unparked.
    bool unparked = false;

    // The callouts instruct us to park the object. We associated the callback
    // function with the parked object, which sets "unparked" flag to true. We
    // can later test the value of this flag to verify when exactly the packet
    // got unparked.
    ASSERT_NO_THROW(
        HooksManager::park<std::string>("hookpt_one", "foo",
        [&unparked] {
            unparked = true;
        })
    );

    // Call both installed callouts.
    HooksManager::callCallouts(hookpt_one_index_, *handle);

    // We have two callouts which should have returned pointers to the
    // functions which we can call to simulate completion of asynchronous
    // tasks.
    std::function<void()> unpark_trigger_func1;
    handle->getArgument("unpark_trigger1", unpark_trigger_func1);
    // Call the first function. It should cause the hook library to call the
    // "unpark" function. However, the object should not be unparked yet,
    // because the other callout hasn't completed its scheduled asynchronous
    // operation (keeps a reference on the parked object).
    unpark_trigger_func1();
    EXPECT_FALSE(unparked);

    // Call the second function. This should decrease the reference count to
    // 0 and the packet should be unparked.
    std::function<void()> unpark_trigger_func2;
    handle->getArgument("unpark_trigger2", unpark_trigger_func2);
    unpark_trigger_func2();
    EXPECT_TRUE(unparked);

    // Resetting the handle makes return from test body to crash.

    // Try unloading the libraries.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    // Handle is still active.
    EXPECT_FALSE(status);
}

// This test verifies that the server can also unpark the packet.
TEST_F(HooksManagerTest, ServerUnpark) {
    // Load the same library twice. Both installed callouts will trigger
    // asynchronous operation.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    // Load libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    CalloutHandlePtr handle = HooksManager::createCalloutHandle();

    // We could be parked any object. Typically it will be a pointer to the
    // packet. In this case, however, it is simpler to just use a string.
    std::string parked_object = "foo";
    handle->setArgument("parked_object", parked_object);

    // This boolean value will be set to true when the packet gets unparked.
    bool unparked = false;

    // The callouts instruct us to park the object. We associated the callback
    // function with the parked object, which sets "unparked" flag to true. We
    // can later test the value of this flag to verify when exactly the packet
    // got unparked.
    HooksManager::park<std::string>("hookpt_one", "foo",
    [&unparked] {
        unparked = true;
    });

    // It should be possible for the server to increase reference counter.
    ASSERT_NO_THROW(HooksManager::reference<std::string>("hookpt_one", "foo"));

    // Call installed callout.
    HooksManager::callCallouts(hookpt_one_index_, *handle);

    // Server can force unparking the object.
    EXPECT_TRUE(HooksManager::unpark<std::string>("hookpt_one", "foo"));

    EXPECT_TRUE(unparked);

    // Reset the handle to allow unload.
    handle.reset();

    // Try unloading the libraries.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
}

// This test verifies that the server can drop parked packet.
TEST_F(HooksManagerTest, ServerDropParked) {
    // Load library.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    CalloutHandlePtr handle = HooksManager::createCalloutHandle();

    // We could be parked any object. Typically it will be a pointer to the
    // packet. In this case, however, it is simpler to just use a string.
    std::string parked_object = "foo";
    handle->setArgument("parked_object", parked_object);

    // This boolean value will be set to true when the packet gets unparked.
    bool unparked = false;

    // The callouts instruct us to park the object. We associated the callback
    // function with the parked object, which sets "unparked" flag to true. We
    // can later test the value of this flag to verify when exactly the packet
    // got unparked.
    HooksManager::park<std::string>("hookpt_one", "foo",
    [&unparked] {
        unparked = true;
    });

    // It should be possible for the server to increase reference counter.
    ASSERT_NO_THROW(HooksManager::reference<std::string>("hookpt_one", "foo"));

    // Call installed callout.
    HooksManager::callCallouts(hookpt_one_index_, *handle);

    // Drop the parked packet. The callback should not be called.
    EXPECT_TRUE(HooksManager::drop<std::string>("hookpt_one", "foo"));

    EXPECT_FALSE(unparked);

    // An attempt to unpark the packet should return false, as this packet
    // is not parked anymore.
    EXPECT_FALSE(HooksManager::unpark<std::string>("hookpt_one", "foo"));

    // Reset the handle to allow unload.
    handle.reset();

    // Try unloading the libraries.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
}

// This test verifies that parked objects are removed when libraries are
// unloaded.
TEST_F(HooksManagerTest, UnloadBeforeUnpark) {
    // Load the same library twice. Both installed callouts will trigger
    // asynchronous operation.
    HookLibsCollection library_names;
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    library_names.push_back(HookLibInfo(std::string(ASYNC_CALLOUT_LIBRARY),
                                        data::ConstElementPtr()));
    // Load libraries.
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    CalloutHandlePtr handle = HooksManager::createCalloutHandle();

    // We could be parked any object. Typically it will be a pointer to the
    // packet. In this case, however, it is simpler to just use a string.
    std::string parked_object = "foo";
    handle->setArgument("parked_object", parked_object);

    // This boolean value will be set to true when the packet gets unparked.
    bool unparked = false;

    // The callouts instruct us to park the object. We associated the callback
    // function with the parked object, which sets "unparked" flag to true. We
    // can later test the value of this flag to verify when exactly the packet
    // got unparked.
    HooksManager::park<std::string>("hookpt_one", "foo",
    [&unparked] {
        unparked = true;
    });

    // Call installed callout.
    HooksManager::callCallouts(hookpt_one_index_, *handle);

    // Reset the handle to allow a reload.
    handle.reset();

    // Try reloading the libraries.
    EXPECT_NO_THROW(HooksManager::prepareUnloadLibraries());
    bool status = false;
    EXPECT_NO_THROW(status = HooksManager::unloadLibraries());
    EXPECT_TRUE(status);
    EXPECT_TRUE(HooksManager::loadLibraries(library_names));

    // Parked object should have been removed.
    EXPECT_FALSE(HooksManager::unpark<std::string>("hookpt_one", "foo"));

    // Callback should not be called.
    EXPECT_FALSE(unparked);
}

/// @brief Test fixture for hooks parsing.
class HooksParserTest : public LogContentTest {
public:
    /// @brief Constructor
    HooksParserTest() {
        // Save current value of the environment path.
        char* env_path = std::getenv("KEA_HOOKS_PATH");
        if (env_path) {
            original_path_ = std::string(env_path);
        }

        // Clear the environment path.
        unsetenv("KEA_HOOKS_PATH");
    }

    /// @brief Destructor
    virtual ~HooksParserTest() {
        // Restore the original environment path.
        if (!original_path_.empty()) {
            setenv("KEA_HOOKS_PATH", original_path_.c_str(), 1);
        } else {
            unsetenv("KEA_HOOKS_PATH");
        }
    }

    /// @brief Retains the environment variable's original value.
    std::string original_path_;
};

TEST_F(HooksParserTest, getHooksPath) {
    ASSERT_FALSE(std::getenv("KEA_HOOKS_PATH"));
    auto hooks_path = HooksLibrariesParser::getHooksPath(true);
    EXPECT_EQ(hooks_path, DEFAULT_HOOKS_PATH);
}

TEST_F(HooksParserTest, getHooksPathWithEnv) {
    std::string evar("KEA_HOOKS_PATH=/tmp");
    putenv(const_cast<char*>(evar.c_str()));
    ASSERT_TRUE(std::getenv("KEA_HOOKS_PATH"));
    auto hooks_path = HooksLibrariesParser::getHooksPath(true);
    EXPECT_EQ(hooks_path, "/tmp");
}

TEST_F(HooksParserTest, getHooksPathExplicit) {
    auto hooks_path = HooksLibrariesParser::getHooksPath(true, "/explicit/path");
    EXPECT_EQ(hooks_path, "/explicit/path");
}

// Verifies HooksParser::validatePath() when enforce_path is true.
TEST_F(HooksParserTest, validatePathEnforcePath) {
    HooksLibrariesParser::getHooksPath(true);
    std::string def_path(HooksLibrariesParser::getHooksPath());
    struct Scenario {
        int line_;
        std::string lib_path_;
        std::string exp_path_;
        std::string exp_error_;
        bool exp_security_err_;
    };

    std::list<Scenario> scenarios = {
    {
        // Invalid parent path.
        __LINE__,
        "/var/lib/bs/mylib.so",
        "",
        string("invalid path specified: '/var/lib/bs', supported path is '" + def_path + "'"),
        true
    },
    {
        // No file name.
        __LINE__,
        def_path + "/",
        "",
        string ("path: '" + def_path + "/' has no filename"),
        false
    },
    {
        // File name only is valid.
        __LINE__,
        "mylib.so",
        def_path + "/mylib.so",
        "",
        false
    },
    {
        // Valid full path.
        __LINE__,
        def_path + "/mylib.so",
        def_path + "/mylib.so",
        "",
        false
    },
    {
        // Invalid relative path.
        __LINE__,
        "../kea/mylib.so",
        "",
        string("invalid path specified: '../kea', supported path is '" +
               def_path + "'"),
        true
    }
    };

    for (auto scenario : scenarios) {
        std::ostringstream oss;
        oss << " Scenario at line: " << scenario.line_;
        SCOPED_TRACE(oss.str());
        std::string validated_path;
        if (scenario.exp_error_.empty()) {
            ASSERT_NO_THROW_LOG(validated_path =
                                HooksLibrariesParser::validatePath(scenario.lib_path_));
            EXPECT_EQ(validated_path, scenario.exp_path_);
        } else {
            if (scenario.exp_security_err_)  {
                ASSERT_THROW_MSG(validated_path =
                                 HooksLibrariesParser::validatePath(scenario.lib_path_),
                                 SecurityError, scenario.exp_error_);
            } else {
                ASSERT_THROW_MSG(validated_path =
                                 HooksLibrariesParser::validatePath(scenario.lib_path_),
                                 BadValue, scenario.exp_error_);
            }
        }
    }
}

// Verifies HooksParser::validatePath() when enforce_path is false.
TEST_F(HooksParserTest, validatePathEnforcePathFalse) {
    HooksLibrariesParser::getHooksPath(true);
    std::string def_path(HooksLibrariesParser::getHooksPath());
    struct Scenario {
        int line_;
        std::string lib_path_;
        std::string exp_path_;
        std::string exp_error_;
    };

    std::list<Scenario> scenarios = {
    {
        // Invalid parent path will fly.
        __LINE__,
        "/var/lib/bs/mylib.so",
        "/var/lib/bs/mylib.so",
        "",
    },
    {
        // No file name.
        __LINE__,
        def_path + "/",
        "",
        string ("path: '" + def_path + "/' has no filename")
    },
    {
        // File name only is valid.
        __LINE__,
        "mylib.so",
        def_path + "/mylib.so",
        ""
    },
    {
        // Valid full path.
        __LINE__,
        def_path + "/mylib.so",
        def_path + "/mylib.so",
        ""
    }
    };

    PathChecker::enableEnforcement(false);
    for (auto scenario : scenarios) {
        std::ostringstream oss;
        oss << " Scenario at line: " << scenario.line_;
        SCOPED_TRACE(oss.str());
        std::string validated_path;
        if (scenario.exp_error_.empty()) {
            ASSERT_NO_THROW_LOG(validated_path =
                                HooksLibrariesParser::validatePath(scenario.lib_path_));
            EXPECT_EQ(validated_path, scenario.exp_path_);
        } else {
            ASSERT_THROW_MSG(validated_path =
                             HooksLibrariesParser::validatePath(scenario.lib_path_),
                             BadValue, scenario.exp_error_);
        }
    }

    std::ostringstream oss;
    oss  << "HOOKS_LIBPATH_SECURITY_WARNING Library path specified is NOT SECURE:"
         << " invalid path specified: '/var/lib/bs', supported path is '"
         << def_path << "'";
    EXPECT_EQ(1, countFile(oss.str()));
}

// Verifies output of HooksConfig::toElement().
TEST(HooksConfig, toElementTest) {
    HooksConfig cfg;

    // Prepare parameters for the callout parameters library.
    ElementPtr params = Element::createMap();
    params->set("svalue", Element::create("string value"));
    params->set("ivalue", Element::create(42));
    params->set("bvalue", Element::create(true));

    cfg.add(BASIC_CALLOUT_LIBRARY, ConstElementPtr(), "libbc1.so");
    cfg.add(FULL_CALLOUT_LIBRARY, params, "libfc1.so");
    auto to_elem = cfg.toElement();

    std::string exp_cfg = R"([
  {
    "library": "libbc1.so"
  },
  {
    "library": "libfc1.so",
    "parameters": {
      "bvalue": true,
      "ivalue": 42,
      "svalue": "string value"
    }
  }
])";

    EXPECT_EQ(data::prettyPrint(cfg.toElement()), exp_cfg);
}

} // Anonymous namespace
