/** @file

  Unit tests for a class that deals with remap plugins

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  @section details Details

  Implements code necessary for Reverse Proxy which mostly consists of
  general purpose hostname substitution in URLs.

 */

#define CATCH_CONFIG_MAIN /* include main function */
#include <catch.hpp>      /* catch unit-test framework */
#include <fstream>        /* ofstream */
#include <string>

#include "plugin_testing_common.h"
#include "proxy/http/remap/RemapPluginInfo.h"

static void    *INSTANCE_HANDLER = (void *)789;
std::error_code ec;

/* The following are paths that are used commonly in the unit-tests */
static fs::path sandboxDir     = getTemporaryDir();
static fs::path runtimeDir     = sandboxDir / "runtime";
static fs::path searchDir      = sandboxDir / "search";
static fs::path pluginBuildDir = fs::path{SRC_BUILD_DIR} / "src/proxy/http/remap/unit-tests/.libs";

void
clean()
{
  fs::remove_all(sandboxDir, ec);
}

/* Mock used only to make unit testing convenient to check if callbacks are really called and check errors */
class RemapPluginUnitTest : public RemapPluginInfo
{
public:
  using Ptr = std::unique_ptr<RemapPluginUnitTest>;

  RemapPluginUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
    : RemapPluginInfo(configPath, effectivePath, runtimePath)
  {
  }
  std::string
  getError(const char *required, const char *requiring = nullptr)
  {
    return missingRequiredSymbolError(_configPath.string(), required, requiring);
  }

  PluginDebugObject *
  getDebugObject()
  {
    std::string error; /* ignore the error, return nullptr if symbol not defined */
    void       *address = nullptr;
    getSymbol("getPluginDebugObjectTest", address, error);
    GetPluginDebugObjectFunction *getObject = reinterpret_cast<GetPluginDebugObjectFunction *>(address);
    if (getObject) {
      PluginDebugObject *object = reinterpret_cast<PluginDebugObject *>(getObject());
      return object;
    } else {
      return nullptr;
    }
  }
};

RemapPluginUnitTest *
setupSandBox(const fs::path &configPath)
{
  std::string error;
  clean();

  /* Create the directory structure and install plugins */
  CHECK(fs::create_directories(searchDir, ec));
  fs::copy(pluginBuildDir / configPath, searchDir, ec);
  CHECK(fs::create_directories(runtimeDir, ec));

  fs::path effectivePath   = searchDir / configPath;
  fs::path runtimePath     = runtimeDir / configPath;
  fs::path pluginBuildPath = pluginBuildDir / configPath;
  /* Instantiate and initialize a plugin DSO instance. */
  RemapPluginUnitTest *plugin = new RemapPluginUnitTest(configPath, effectivePath, runtimePath);

  return plugin;
}

bool
loadPlugin(RemapPluginUnitTest *plugin, std::string &error, PluginDebugObject *&debugObject)
{
  bool result = plugin->load(error, fs::path()); // Dummy compiler path.
  debugObject = plugin->getDebugObject();
  return result;
}

void
cleanupSandBox()
{
  clean();
}

SCENARIO("loading remap plugins", "[plugin][core]")
{
  REQUIRE_FALSE(sandboxDir.empty());

  std::string        error;
  PluginDebugObject *debugObject = nullptr;

  GIVEN("a plugin which has only minimum required call back functions")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_required_cb.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    WHEN("loading")
    {
      bool result = loadPlugin(plugin.get(), error, debugObject);

      THEN("expect it to successfully load")
      {
        CHECK(true == result);
        CHECK(error.empty());
      }
      cleanupSandBox();
    }
  }

  GIVEN("a plugin which is missing the plugin TSREMAP_FUNCNAME_INIT function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_missing_init.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    WHEN("loading")
    {
      bool result = loadPlugin(plugin.get(), error, debugObject);

      THEN("expect it to successfully load")
      {
        CHECK_FALSE(result);
        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_INIT));
      }
      cleanupSandBox();
    }
  }

  GIVEN("a plugin which is missing the TSREMAP_FUNCNAME_DO_REMAP function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_missing_doremap.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    WHEN("loading")
    {
      bool result = loadPlugin(plugin.get(), error, debugObject);

      THEN("expect it to fail")
      {
        CHECK_FALSE(result);
        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DO_REMAP));
      }
      cleanupSandBox();
    }
  }

  GIVEN("a plugin which has TSREMAP_FUNCNAME_NEW_INSTANCE but is missing the TSREMAP_FUNCNAME_DELETE_INSTANCE function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_missing_deleteinstance.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    WHEN("loading")
    {
      bool result = loadPlugin(plugin.get(), error, debugObject);

      THEN("expect it to fail")
      {
        CHECK_FALSE(result);
        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE));
      }
      cleanupSandBox();
    }
  }

  GIVEN("a plugin which has TSREMAP_FUNCNAME_DELETE_INSTANCE but is missing the TSREMAP_FUNCNAME_NEW_INSTANCE function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_missing_newinstance.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    WHEN("loading")
    {
      bool result = loadPlugin(plugin.get(), error, debugObject);

      THEN("expect it to fail")
      {
        CHECK_FALSE(result);
        CHECK(error == plugin->getError(TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE));
      }
      cleanupSandBox();
    }
  }
}

void
prepCallTest(bool toFail, PluginDebugObject *debugObject)
{
  debugObject->clear();
  debugObject->fail = toFail; // Tell the mock init to succeed or succeed.
}

void
checkCallTest(bool shouldHaveFailed, bool result, const std::string &error, std::string &expectedError, int &called)
{
  CHECK(1 == called); // Init was called.
  if (shouldHaveFailed) {
    CHECK(false == result);
    CHECK(error == expectedError); // Appropriate error was returned.
  } else {
    CHECK(true == result); // Init successful - returned TS_SUCCESS.
    CHECK(error.empty());  // No error was returned.
  }
}

SCENARIO("invoking plugin init", "[plugin][core]")
{
  REQUIRE_FALSE(sandboxDir.empty());

  std::string        error;
  PluginDebugObject *debugObject = nullptr;

  GIVEN("plugin init function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_testing_calls.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    bool result = loadPlugin(plugin.get(), error, debugObject);
    CHECK(true == result);

    WHEN("init succeeds")
    {
      prepCallTest(/* toFail */ false, debugObject);

      result = plugin->init(error);

      THEN("expect init to be called, success code and no error to be returned")
      {
        std::string expectedError;

        checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initCalled);
      }
      cleanupSandBox();
    }

    WHEN("init fails")
    {
      prepCallTest(/* toFail */ true, debugObject);

      result = plugin->init(error);

      THEN("expect init to be called, failure code and an error to be returned")
      {
        std::string expectedError;
        expectedError.assign("failed to initialize plugin ").append(pluginConfigPath.string()).append(": Init failed");

        checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initCalled);
      }
      cleanupSandBox();
    }
  }
}

SCENARIO("invoking plugin instance init", "[plugin][core]")
{
  REQUIRE_FALSE(sandboxDir.empty());

  std::string        error;
  PluginDebugObject *debugObject = nullptr;
  void              *ih          = nullptr; // Instance handler pointer.

  /* a sample test set of parameters */
  static const char *args[] = {"arg1", "arg2", "arg3"};
  static char      **ARGV   = const_cast<char **>(args);
  static char        ARGC   = sizeof ARGV;

  GIVEN("an instance init function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_testing_calls.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    bool result = loadPlugin(plugin.get(), error, debugObject);
    CHECK(true == result);

    WHEN("instance init succeeds")
    {
      prepCallTest(/* toFail */ false, debugObject);
      debugObject->input_ih = INSTANCE_HANDLER; /* this is what the plugin instance init will return */

      result = plugin->initInstance(ARGC, ARGV, &ih, error);

      THEN("expect init to be called successfully with no error and expected instance handler")
      {
        std::string expectedError;

        checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initInstanceCalled);

        /* Verify expected handler */
        CHECK(INSTANCE_HANDLER == ih);
        /* Plugin received the parameters that we passed */
        CHECK(ARGC == debugObject->argc);
        CHECK(ARGV == debugObject->argv);
        for (int i = 0; i < 3; i++) {
          CHECK(0 == strcmp(ARGV[i], debugObject->argv[i]));
        }
      }
      cleanupSandBox();
    }

    WHEN("instance init fails")
    {
      prepCallTest(/* toFail */ true, debugObject);

      result = plugin->initInstance(ARGC, ARGV, &ih, error);

      THEN("expect init to be called but failed with expected error and no instance handler")
      {
        std::string expectedError;
        expectedError.assign("failed to create instance for plugin ").append(pluginConfigPath.string()).append(": Init failed");

        checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initInstanceCalled);

        /* Ideally instance handler should not be touched in case of failure */
        CHECK(nullptr == ih);
        /* Plugin received the parameters that we passed */
        CHECK(ARGC == debugObject->argc);
        CHECK(ARGV == debugObject->argv);
        for (int i = 0; i < 3; i++) {
          CHECK(0 == strcmp(ARGV[i], debugObject->argv[i]));
        }
      }
      cleanupSandBox();
    }
  }
}

SCENARIO("unloading the plugin", "[plugin][core]")
{
  REQUIRE_FALSE(sandboxDir.empty());

  std::string        error;
  PluginDebugObject *debugObject = nullptr;

  GIVEN("a 'done' function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_testing_calls.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    bool result = loadPlugin(plugin.get(), error, debugObject);
    CHECK(true == result);

    WHEN("'done' is called")
    {
      debugObject->clear();

      plugin->done();

      THEN("expect it to run")
      {
        CHECK(1 == debugObject->doneCalled);
      }
      cleanupSandBox();
    }
  }

  GIVEN("a 'delete_instance' function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_testing_calls.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    bool result = loadPlugin(plugin.get(), error, debugObject);
    CHECK(true == result);

    WHEN("'delete_instance' is called")
    {
      debugObject->clear();

      plugin->doneInstance(INSTANCE_HANDLER);

      THEN("expect it to run and receive the right instance handler")
      {
        CHECK(1 == debugObject->deleteInstanceCalled);
        CHECK(INSTANCE_HANDLER == debugObject->ih);
      }
      cleanupSandBox();
    }
  }
}

SCENARIO("config reload", "[plugin][core]")
{
  REQUIRE_FALSE(sandboxDir.empty());

  std::string        error;
  PluginDebugObject *debugObject = nullptr;

  GIVEN("a 'config reload' callback function")
  {
    fs::path                 pluginConfigPath = fs::path("plugin_testing_calls.so");
    RemapPluginUnitTest::Ptr plugin{setupSandBox(pluginConfigPath)};

    bool result = loadPlugin(plugin.get(), error, debugObject);
    CHECK(true == result);

    WHEN("'config reload' failed")
    {
      debugObject->clear();

      plugin->indicatePreReload();
      plugin->indicatePostReload(TSREMAP_CONFIG_RELOAD_FAILURE);

      THEN("expect it to run")
      {
        CHECK(1 == debugObject->preReloadConfigCalled);
        CHECK(1 == debugObject->postReloadConfigCalled);
        CHECK(TSREMAP_CONFIG_RELOAD_FAILURE == debugObject->postReloadConfigStatus);
      }
      cleanupSandBox();
    }

    WHEN("'config reload' is successful and the plugin is part of the new configuration")
    {
      debugObject->clear();

      plugin->indicatePreReload();
      plugin->indicatePostReload(TSREMAP_CONFIG_RELOAD_SUCCESS_PLUGIN_USED);

      THEN("expect it to run")
      {
        CHECK(1 == debugObject->preReloadConfigCalled);
        CHECK(1 == debugObject->postReloadConfigCalled);
        CHECK(TSREMAP_CONFIG_RELOAD_SUCCESS_PLUGIN_USED == debugObject->postReloadConfigStatus);
      }
      cleanupSandBox();
    }

    WHEN("'config reload' is successful and the plugin is part of the new configuration")
    {
      debugObject->clear();

      plugin->indicatePreReload();
      plugin->indicatePostReload(TSREMAP_CONFIG_RELOAD_SUCCESS_PLUGIN_UNUSED);

      THEN("expect it to run")
      {
        CHECK(1 == debugObject->preReloadConfigCalled);
        CHECK(1 == debugObject->postReloadConfigCalled);
        CHECK(TSREMAP_CONFIG_RELOAD_SUCCESS_PLUGIN_UNUSED == debugObject->postReloadConfigStatus);
      }
      cleanupSandBox();
    }
  }
}
