// Copyright (C) 2018-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/testutils/timed_signal.h>
#include <cc/data.h>
#include <config/unix_command_config.h>
#include <netconf/netconf_controller.h>
#include <netconf/netconf_process.h>
#include <process/testutils/d_test_stubs.h>
#include <testutils/gtest_utils.h>
#include <util/filesystem.h>

#include <gtest/gtest.h>

using namespace isc::asiolink::test;
using namespace isc::config;
using namespace isc::netconf;
using namespace isc::data;
using namespace isc::http;
using namespace isc::process;
using namespace isc::util::file;
using namespace std;

namespace {

/// @brief Valid Netconf Config used in tests.
const char* valid_netconf_config =
    "{"
    "  \"boot-update\": false,"
    "  \"subscribe-changes\": false,"
    "  \"managed-servers\": {"
    "    \"dhcp4\": {"
    "      \"control-socket\": {"
    "        \"socket-type\": \"unix\","
    "        \"socket-name\": \"dhcp4-socket\""
    "      }"
    "    },"
    "    \"dhcp6\": {"
    "      \"control-socket\": {"
    "        \"socket-type\": \"unix\","
    "        \"socket-name\": \"dhcp6-socket\""
    "      }"
    "    }"
    "  }"
    "}";

string const bad_socket_name_config(R"(
{
  "managed-servers": {
    "dhcp6": {
      "control-socket": {
        "socket-name": "/tmp/kea-dhcp6-ctrl.sock",
        "socket-type": "unix"
      },
      "model": "kea-dhcp6-server"
    }
  }
}
)");

/// @brief test fixture class for testing NetconfController class.
///
/// This class derives from DControllerTest and wraps NetconfController. Much
/// of the underlying functionality is in the DControllerBase class which
/// has extensive set of unit tests that are independent from Netconf.
class NetconfControllerTest : public DControllerTest {
public:
    /// @brief Constructor.
    NetconfControllerTest()
        : DControllerTest(NetconfController::instance) {
        setSocketTestPath();
    }

    void TearDown() override {
        resetSocketPath();
    }

    /// @brief Returns pointer to NetconfProcess instance.
    NetconfProcessPtr getNetconfProcess() {
        return (boost::dynamic_pointer_cast<NetconfProcess>(getProcess()));
    }

    /// @brief Returns pointer to NetconfCfgMgr instance for a process.
    NetconfCfgMgrPtr getNetconfCfgMgr() {
        NetconfCfgMgrPtr p;
        if (getNetconfProcess()) {
            p = getNetconfProcess()->getNetconfCfgMgr();
        }
        return (p);
    }

    /// @brief Returns a pointer to the configuration context.
    NetconfConfigPtr getNetconfConfig() {
        NetconfConfigPtr p;
        if (getNetconfCfgMgr()) {
            p = getNetconfCfgMgr()->getNetconfConfig();
        }
        return (p);
    }

    /// @brief Sets the path in which the socket can be created.
    ///
    /// @param explicit_path path to use as the socket path.
    void setSocketTestPath(const std::string explicit_path = string()) {
        string path(UnixCommandConfig::getSocketPath(
            true, (!explicit_path.empty() ? explicit_path : TEST_DATA_BUILDDIR)));
        UnixCommandConfig::setSocketPathPerms(getPermissions(path));
    }

    /// @brief Resets the socket path to the default.
    void resetSocketPath() {
        UnixCommandConfig::getSocketPath(true);
        UnixCommandConfig::setSocketPathPerms();
    }
};  // NetconfControllerTest

// Basic Controller instantiation testing.
// Verifies that the controller singleton gets created and that the
// basic derivation from the base class is intact.
TEST_F(NetconfControllerTest, basicInstanceTesting) {
    // Verify the singleton instance can be fetched and that
    // it has the correct type.
    DControllerBasePtr& controller = DControllerTest::getController();
    ASSERT_TRUE(controller);
    ASSERT_NO_THROW_LOG(boost::dynamic_pointer_cast<NetconfController>(controller));

    // Verify that controller's app name is correct.
    EXPECT_TRUE(checkAppName(NetconfController::netconf_app_name_));

    // Verify that controller's bin name is correct.
    EXPECT_TRUE(checkBinName(NetconfController::netconf_bin_name_));

    // Verify that controller's IOService exists.
    EXPECT_TRUE(checkIOService());

    // Verify that the Process does NOT exist.
    EXPECT_FALSE(checkProcess());
}

// Tests basic command line processing.
// Verifies that:
// 1. Standard command line options are supported.
// 2. Invalid options are detected.
TEST_F(NetconfControllerTest, commandLineArgs) {
    char* argv[] = { const_cast<char*>("progName"),
                     const_cast<char*>("-c"),
                     const_cast<char*>(DControllerTest::CFG_TEST_FILE),
                     const_cast<char*>("-d") };
    int argc = 4;

    // Verify that verbose flag is false initially.
    EXPECT_TRUE(checkVerbose(false));

    // Verify that standard options can be parsed without error.
    EXPECT_NO_THROW_LOG(parseArgs(argc, argv));

    // Verify that verbose flag is true.
    EXPECT_TRUE(checkVerbose(true));

    // Verify configuration file name is correct.
    EXPECT_TRUE(checkConfigFileName(DControllerTest::CFG_TEST_FILE));

    // Verify that an unknown option is detected.
    char* argv2[] = { const_cast<char*>("progName"),
                      const_cast<char*>("-x") };
    argc = 2;
    EXPECT_THROW_MSG(parseArgs(argc, argv2), InvalidUsage, "unsupported option: -x");
}

// Tests application process creation and initialization.
// Verifies that the process can be successfully created and initialized.
TEST_F(NetconfControllerTest, initProcessTesting) {
    ASSERT_NO_THROW_LOG(initProcess());
    EXPECT_TRUE(checkProcess());
}

// Tests launch and normal shutdown (stand alone mode).
// This creates an interval timer to generate a normal shutdown and then
// launches with a valid, stand-alone command line and no simulated errors.
TEST_F(NetconfControllerTest, launchNormalShutdown) {
    // Write valid_netconf_config and then run launch() for 200 ms.
    time_duration elapsed_time;
    runWithConfig(valid_netconf_config, 200, elapsed_time);

    // Give a generous margin to accommodate slower test environs.
    EXPECT_TRUE(elapsed_time.total_milliseconds() >= 100 &&
                elapsed_time.total_milliseconds() <= 500);
}

// Tests that the SIGINT triggers a normal shutdown.
TEST_F(NetconfControllerTest, sigintShutdown) {
    // Setup to raise SIGINT in 1 ms.
    TimedSignal sighup(getIOService(), SIGINT, 1);

    // Write valid_netconf_config and then run launch() for a maximum
    // of 500 ms.
    time_duration elapsed_time;
    runWithConfig(valid_netconf_config, 500, elapsed_time);

    // Signaled shutdown should make our elapsed time much smaller than
    // the maximum run time.  Give generous margin to accommodate slow
    // test environs.
    EXPECT_TRUE(elapsed_time.total_milliseconds() < 300);
}

// Tests that the SIGTERM triggers a normal shutdown.
TEST_F(NetconfControllerTest, sigtermShutdown) {
    // Setup to raise SIGTERM in 1 ms.
    TimedSignal sighup(getIOService(), SIGTERM, 1);

    // Write valid_netconf_config and then run launch() for a maximum
    // of 500 ms.
    time_duration elapsed_time;
    runWithConfig(valid_netconf_config, 500, elapsed_time);

    // Signaled shutdown should make our elapsed time much smaller than
    // the maximum run time.  Give generous margin to accommodate slow
    // test environs.
    EXPECT_TRUE(elapsed_time.total_milliseconds() < 300);
}

// Check that a bad socket path is refused.
TEST_F(NetconfControllerTest, badSocketPath) {
    time_duration elapsed_time;
    EXPECT_THROW_MSG(runWithConfig(bad_socket_name_config, 200, elapsed_time), ProcessInitError,
                     string("Could Not load configuration file: invalid path specified: '/tmp', "
                            "supported path is '") +
                         UnixCommandConfig::getSocketPath() + "'");
}

}  // namespace
