// Copyright(c) 2018-2020, Intel Corporation
//
// Redistribution  and  use  in source  and  binary  forms,  with  or  without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of  source code  must retain the  above copyright notice,
//   this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// * Neither the name  of Intel Corporation  nor the names of its contributors
//   may be used to  endorse or promote  products derived  from this  software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,  BUT NOT LIMITED TO,  THE
// IMPLIED WARRANTIES OF  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT  SHALL THE COPYRIGHT OWNER  OR CONTRIBUTORS BE
// LIABLE  FOR  ANY  DIRECT,  INDIRECT,  INCIDENTAL,  SPECIAL,  EXEMPLARY,  OR
// CONSEQUENTIAL  DAMAGES  (INCLUDING,  BUT  NOT LIMITED  TO,  PROCUREMENT  OF
// SUBSTITUTE GOODS OR SERVICES;  LOSS OF USE,  DATA, OR PROFITS;  OR BUSINESS
// INTERRUPTION)  HOWEVER CAUSED  AND ON ANY THEORY  OF LIABILITY,  WHETHER IN
// CONTRACT,  STRICT LIABILITY,  OR TORT  (INCLUDING NEGLIGENCE  OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,  EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

extern "C" {
#include <json-c/json.h>
#include <uuid/uuid.h>
#include "opae_int.h"
#include <libgen.h>

char *find_ase_cfg();
void opae_init(void);
void opae_release(void);

#define HOME_CFG_PATHS 3
const char *_ase_home_configs[HOME_CFG_PATHS] = {
	"/.local/opae_ase.cfg",
	"/.local/opae/opae_ase.cfg",
	"/.config/opae/opae_ase.cfg",
};
}

#include <config.h>
#include <opae/fpga.h>

#include <array>
#include <cstdlib>
#include <fstream>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <stack>
#include "gtest/gtest.h"
#include "mock/test_system.h"

using namespace opae::testing;

/**
 * @test       opae_init
 * @brief      Test: opae_init, opae_release, opae_print
 */
TEST(init, opae_init_rel) {
  opae_init();
  opae_print(OPAE_LOG_ERROR, "OPAE_LOG_ERROR from test opae_init_rel\n");
  opae_print(OPAE_LOG_MESSAGE, "OPAE_LOG_MESSAGE from test opae_init_rel\n");
  opae_print(OPAE_LOG_DEBUG, "OPAE_LOG_DEBUG from test opae_init_rel\n");
  opae_release();
}

#ifdef LIBOPAE_DEBUG

/**
 * @test       log_debug
 *
 * @brief      When the log level is set to debug, then all errors,
 *             messages, and debug info are logged.
 */
TEST(init, log_debug) {
  ASSERT_EQ(0, putenv((char*)"LIBOPAE_LOG=2"));
  opae_init();
  testing::internal::CaptureStdout();
  testing::internal::CaptureStderr();

  OPAE_ERR("Error log.");
  OPAE_MSG("Message log.");
  OPAE_DBG("Debug log.");

  std::string log_stdout = testing::internal::GetCapturedStdout();
  std::string log_stderr = testing::internal::GetCapturedStderr();

  EXPECT_TRUE(log_stderr.find("Error log.") != std::string::npos);
  EXPECT_TRUE(log_stdout.find("Message log.") != std::string::npos);
  EXPECT_TRUE(log_stdout.find("Debug log.") != std::string::npos);

  opae_release();
  EXPECT_EQ(0, unsetenv("LIBOPAE_LOG"));
}

#endif

/**
 * @test       log_message
 *
 * @brief      When the log level is set to message, then all errors
 *             and messages are logged.
 */
TEST(init, log_message) {
  ASSERT_EQ(0, putenv((char*)"LIBOPAE_LOG=1"));
  opae_init();
  testing::internal::CaptureStdout();
  testing::internal::CaptureStderr();

  OPAE_ERR("Error log.");
  OPAE_MSG("Message log.");
  OPAE_DBG("Debug log.");

  std::string log_stdout = testing::internal::GetCapturedStdout();
  std::string log_stderr = testing::internal::GetCapturedStderr();

  EXPECT_TRUE(log_stderr.find("Error log.") != std::string::npos);
  EXPECT_TRUE(log_stdout.find("Message log.") != std::string::npos);
  EXPECT_FALSE(log_stdout.find("Debug log.") != std::string::npos);

  opae_release();
  EXPECT_EQ(0, unsetenv("LIBOPAE_LOG"));
}

/**
 * @test       log_error
 *
 * @brief      When the log level is set to error, then only errors
 *             are logged.
 */
TEST(init, log_error) {
  ASSERT_EQ(0, putenv((char*)"LIBOPAE_LOG=0"));
  opae_init();
  testing::internal::CaptureStdout();
  testing::internal::CaptureStderr();

  OPAE_ERR("Error log.");
  OPAE_MSG("Message log.");
  OPAE_DBG("Debug log.");

  std::string log_stdout = testing::internal::GetCapturedStdout();
  std::string log_stderr = testing::internal::GetCapturedStderr();

  EXPECT_TRUE(log_stderr.find("Error log.") != std::string::npos);
  EXPECT_FALSE(log_stdout.find("Message log.") != std::string::npos);
  EXPECT_FALSE(log_stdout.find("Debug log.") != std::string::npos);

  opae_release();
  EXPECT_EQ(0, unsetenv("LIBOPAE_LOG"));
}

/**
 * @test       log_file
 *
 * @brief      When LIBOPAE_LOGFILE is specified, then the logger
 *             will log to the specified file.
 */
TEST(init, log_file) {
  struct stat buf;

  EXPECT_NE(0, stat("opae_log.log", &buf));

  ASSERT_EQ(0, putenv((char*)"LIBOPAE_LOGFILE=opae_log.log"));
  opae_init();

  EXPECT_EQ(0, stat("opae_log.log", &buf));

  opae_release();
  EXPECT_EQ(0, unsetenv("LIBOPAE_LOGFILE"));
  unlink("opae_log.log");
}

/**
 * @test       find_ase_cfg
 *
 * @brief      When WITH_ASE is specified, opae_ase.cfg will
 *             be searched from OPAE source directory, OPAE
 *             installation directory or home/system config directory.
 *
 */
TEST(init, DISABLED_find_ase_cfg) {
  char *cfg_path = nullptr;

  ASSERT_EQ(0, putenv((char*)"WITH_ASE=1"));
  cfg_path = find_ase_cfg();
  EXPECT_NE(cfg_path, nullptr);

  EXPECT_EQ(0, unsetenv("WITH_ASE"));
  if (cfg_path)
    free(cfg_path);
}

const char *ase_cfg = R"plug(
{
    "configurations": {
        "ase": {
            "configuration": {
                "key1a": 10,
                "key1b": "hello"
            },
        "enabled": true,
        "plugin": "libase.so"
        },
    },
    "plugins": [
        "ase"
    ]
}
)plug";

class init_ase_cfg_p : public ::testing::TestWithParam<const char*> {
 protected:
  init_ase_cfg_p() : buffer_ {0}, rename_f(0) {}

  virtual void SetUp() override {
    // let's rename the opae_ase.cfg in OPAE_ASE_CFG_SRC_PATH and OPAE_ASE_CFG_INST_PATH

    // copy it to a temporary buffer that we can use dirname with
    std::string src_cfg_path = (OPAE_ASE_CFG_SRC_PATH? OPAE_ASE_CFG_SRC_PATH : "");
    std::copy(src_cfg_path.begin(), src_cfg_path.end(), &buffer_[0]);
    char *src_cfg_dir = dirname(buffer_);
    std::string cfg_dir = (src_cfg_dir? src_cfg_dir : "");

    // rename opae_ase.cfg under installation directory
    strcpy(tmpfile_, "opae_ase.cfg.XXXXXX");
    close(mkstemp(tmpfile_));
    src_cfg_file_ = cfg_dir + std::string("/") + std::string(tmpfile_);
    struct stat st;
    // check if the file exists or not
    if (!stat(OPAE_ASE_CFG_SRC_PATH, &st)) {
        rename_f = rename(OPAE_ASE_CFG_SRC_PATH, src_cfg_file_.c_str());
        EXPECT_EQ(rename_f, 0);
     }
    else
        rename_f = 1;

    // This parameterized test iterates over the possible config file paths
    // relative to a user's home directory

    // let's build the full path by prepending the parameter with $HOME
    char *home_cstr = getenv("HOME");
    ASSERT_NE(home_cstr, nullptr) << "No home environment found";
    std::string home = home_cstr;
    // the parameter paths start with a '/'
    cfg_file_ = home + std::string(GetParam());
    memset(buffer_, 0, sizeof(buffer_));
    // copy it to a temporary buffer that we can use dirname with
    std::copy(cfg_file_.begin(), cfg_file_.end(), &buffer_[0]);
    // get the directory name of the file
    cfg_dir_ = dirname(buffer_);
    // if the directory doesn't exist, create the entire path
    if (stat(cfg_dir_, &st)) {
      std::string dir = cfg_dir_;
      // find the first '/' after $HOME
      size_t pos = dir.find('/', home.size());
      while (pos != std::string::npos) {
        std::string sub = dir.substr(0, pos);
        // sub is $HOME/<dir1>, then $HOME/<dir1>/<dir2>, ...
        // if this directory doesn't exist, create it
        if (stat(sub.c_str(), &st) && sub != "") {
          ASSERT_EQ(mkdir(sub.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH),
                    0)
              << "Error creating subdirectory (" << sub
              << "}: " << strerror(errno);
          // keep track of directories created
          dirs_.push(sub);
        }
        pos = pos < dir.size() ? dir.find('/', pos + 1) : std::string::npos;
      }
      // finally, we know the entire path didn't exist, create the last
      // directory
      ASSERT_EQ(mkdir(cfg_dir_, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH), 0)
          << "Error creating subdirectory (" << cfg_dir_
          << "}: " << strerror(errno);
      dirs_.push(cfg_dir_);
    }

    if (stat(cfg_file_.c_str(), &st) == 0) {
      EXPECT_EQ(unlink(cfg_file_.c_str()), 0);
    }

    std::ofstream cfg_stream(cfg_file_);
    cfg_stream.write(ase_cfg, strlen(ase_cfg));
    cfg_stream.close();

    setenv("WITH_ASE", "1", 0);
  }

  virtual void TearDown() override {
    unsetenv("WITH_ASE");
    EXPECT_EQ(unlink(cfg_file_.c_str()), 0);
    // remove any directories we created in SetUp
    while (!dirs_.empty()) {
      EXPECT_EQ(rmdir(dirs_.top().c_str()), 0);
      dirs_.pop();
    }
    // restore the opae_ase.cfg file at OPAE_ASE_CFG_SRC_PATH
    if (!rename_f) {
        int ret = rename(src_cfg_file_.c_str(), (char *)OPAE_ASE_CFG_SRC_PATH);
        EXPECT_EQ(ret, 0);
    }
    struct stat st;
    if (stat(tmpfile_, &st) == 0) {
      EXPECT_EQ(unlink(tmpfile_), 0);
    }
  }

  char buffer_[PATH_MAX];
  std::string cfg_file_;
  char *cfg_dir_;
  std::stack<std::string> dirs_;
  std::string src_cfg_file_;
  char tmpfile_[32];
  int rename_f;
};


/**
 * @test       find_ase_cfg_2
 * @brief      Test: find_ase_cfg at OPAE_ASE_CFG_INST_PATH and OPAE_PLATFORM_ROOT
 *             release location <br>
 * @details    After renaming a configuration file located in the OPAE_ASE_CFG_SRC_PATH
 *             and OPAE_ASE_CFG_INST_PATH<br>
 *             When I call find_ase_cfg
 *             Then the call is successful<br>
 */
TEST_P(init_ase_cfg_p, find_ase_cfg_2) {
    char *cfg_file = nullptr;
    std::string inst_cfg_file_;
    char tmpfile2_[32];
    int rename_fail = 0;

    // find_ase_cfg at OPAE_ASE_CFG_INST_PATH
    cfg_file = find_ase_cfg();
    EXPECT_NE(cfg_file, nullptr);
    if (cfg_file)
        free(cfg_file);

    // copy it to a temporary buffer that we can use dirname with
    std::string inst_cfg_path = (OPAE_ASE_CFG_INST_PATH? OPAE_ASE_CFG_INST_PATH : "");
    std::copy(inst_cfg_path.begin(), inst_cfg_path.end(), &buffer_[0]);
    char *inst_cfg_dir = dirname(buffer_);
    std::string cfg_dir = (inst_cfg_dir? inst_cfg_dir : "");

    // rename opae_ase.cfg under installation directory
    strcpy(tmpfile2_, "opae_ase.cfg.XXXXXX");
    close(mkstemp(tmpfile2_));
    inst_cfg_file_ = cfg_dir + std::string("/") + std::string(tmpfile2_);
    struct stat st;
    // check if the file exists or not
    if (!stat(OPAE_ASE_CFG_INST_PATH, &st)) {
        rename_fail = rename(OPAE_ASE_CFG_INST_PATH, inst_cfg_file_.c_str());
        EXPECT_EQ(rename_fail, 0);
    }
    else
        rename_fail = 1;

    // find_ase_cfg at release directory
    cfg_file = find_ase_cfg();
    EXPECT_NE(cfg_file, nullptr);
    if (cfg_file)
        free(cfg_file);
    if (!rename_fail) {
        int ret = rename(inst_cfg_file_.c_str(), OPAE_ASE_CFG_INST_PATH);
        EXPECT_EQ(ret, 0);
    }
    if (stat(tmpfile2_, &st) == 0) {
      EXPECT_EQ(unlink(tmpfile2_), 0);
    }
}

/**
 * @test       find_ase_cfg_3
 * @brief      Test: find_ase_cfg at HOME location
 * @details    After renaming a configuration file located in the OPAE_ASE_CFG_SRC_PATH,
 *             OPAE_ASE_CFG_INST_PATH and OPAE release directory<br>
 *             When I call find_ase_cfg
 *             Then the call is successful<br>
 */
TEST_P(init_ase_cfg_p, find_ase_cfg_3) {
    char *cfg_file = nullptr;
    char *opae_path;
    std::string  inst_cfg_file_;
    std::string  rel_cfg_file_, rel_cfg_file2_;
    char tmpfile2_[32];
    char tmpfile3_[32];
    int rename_fail = 0;
    int rename_fail2 = 0;
    int ret;

    // copy it to a temporary buffer that we can use dirname with
    std::string inst_cfg_path = (OPAE_ASE_CFG_INST_PATH? OPAE_ASE_CFG_INST_PATH : "");
    std::copy(inst_cfg_path.begin(), inst_cfg_path.end(), &buffer_[0]);
    char *inst_cfg_dir = dirname(buffer_);
    std::string cfg_dir = (inst_cfg_dir? inst_cfg_dir : "");

    // rename opae_ase.cfg under installation directory
    strcpy(tmpfile2_, "opae_ase.cfg.XXXXXX");
    close(mkstemp(tmpfile2_));
    inst_cfg_file_ = cfg_dir + std::string("/") + std::string(tmpfile2_);
    struct stat st;
    // check if the file exists or not
    if (!stat(OPAE_ASE_CFG_INST_PATH, &st)) {
        rename_fail = rename(OPAE_ASE_CFG_INST_PATH, inst_cfg_file_.c_str());
        EXPECT_EQ(rename_fail, 0);
    }
    else
        rename_fail = 1;

    // rename opae_ase.cfg under releae directory
    opae_path = getenv("OPAE_PLATFORM_ROOT");
    if (opae_path) {
        std::string rel_cfg_path = (opae_path? opae_path: "");
        strcpy(tmpfile3_, "opae_ase.cfg.XXXXXX");
        close(mkstemp(tmpfile3_));        
        rel_cfg_file_ = rel_cfg_path + std::string("/share/opae/ase/opae_ase.cfg");
        rel_cfg_file2_ = rel_cfg_path + std::string("/share/opae/ase/") + std::string(tmpfile3_);
        // check if the file exists or not
        if (!stat(rel_cfg_file_.c_str(), &st)) {
            rename_fail2 = rename(rel_cfg_file_.c_str(), rel_cfg_file2_.c_str());
            EXPECT_EQ(rename_fail2, 0);
        }
        else
            rename_fail2 = 1;
    }
    // find_ase_cfg at HOME directory
    cfg_file = find_ase_cfg();
    EXPECT_NE(cfg_file, nullptr);
    if (cfg_file)
        free(cfg_file);

    if (opae_path && !rename_fail2) {
        ret = rename(rel_cfg_file2_.c_str(), rel_cfg_file_.c_str());
        EXPECT_EQ(ret, 0);
    }
    if (!rename_fail) {
        ret = rename(inst_cfg_file_.c_str(), OPAE_ASE_CFG_INST_PATH);
        EXPECT_EQ(ret, 0);
    }
    if (stat(tmpfile2_, &st) == 0)
      unlink(tmpfile2_);
    if (opae_path && stat(tmpfile3_, &st) == 0)
      unlink(tmpfile3_);
}

/**
 * @test       find_ase_cfg_4
 * @brief      Test: find_ase_cfg at OPAE_PLATFORM_ROOT release location
 * @details    After renaming a configuration file located in the OPAE_ASE_CFG_SRC_PATH,
 *             OPAE_ASE_CFG_INST_PATH, OPAE release and HOME directories<br>
 *             When I call find_ase_cfg
 *             Then the call is successful<br>
 */
TEST_P(init_ase_cfg_p, find_ase_cfg_4) {
    char *cfg_file = nullptr;
    char *opae_path;
    std::string  inst_cfg_file_;
    std::string  rel_cfg_file_, rel_cfg_file2_;
    char tmpfile2_[32];
    char tmpfile3_[32];
    int rename_fail = 0;
    int rename_fail2 = 0;
    int ret;

    // copy it to a temporary buffer that we can use dirname with
    std::string inst_cfg_path = (OPAE_ASE_CFG_INST_PATH? OPAE_ASE_CFG_INST_PATH : "");
    std::copy(inst_cfg_path.begin(), inst_cfg_path.end(), &buffer_[0]);
    char *inst_cfg_dir = dirname(buffer_);
    std::string cfg_dir = (inst_cfg_dir? inst_cfg_dir : "");

    // rename opae_ase.cfg under installation directory
    strcpy(tmpfile2_, "opae_ase.cfg.XXXXXX");
    close(mkstemp(tmpfile2_));
    inst_cfg_file_ = cfg_dir + std::string("/") + std::string(tmpfile2_);
    struct stat st;
    // check if the file exists or not
    if (!stat(OPAE_ASE_CFG_INST_PATH, &st)) {
        rename_fail = rename(OPAE_ASE_CFG_INST_PATH, inst_cfg_file_.c_str());
        EXPECT_EQ(rename_fail, 0);
    }
    else
        rename_fail = 1;

    // rename the opae_ase.cfg under release directory 
    opae_path = getenv("OPAE_PLATFORM_ROOT");
    if (opae_path) {
        std::string rel_cfg_path = (opae_path? opae_path: "");
        strcpy(tmpfile3_, "opae_ase.cfg.XXXXXX");
        close(mkstemp(tmpfile3_));        
        rel_cfg_file_ = rel_cfg_path + std::string("/share/opae/ase/opae_ase.cfg");
        rel_cfg_file2_ = rel_cfg_path + std::string("/share/opae/ase/") + std::string(tmpfile3_);
        // check if the file exists or not
        if (!stat(rel_cfg_file_.c_str(), &st)) {
            rename_fail2 = rename(rel_cfg_file_.c_str(), rel_cfg_file2_.c_str());
            EXPECT_EQ(rename_fail2, 0);
        }
        else
            rename_fail2 = 1;
    }

    // find_ase_cfg at HOME directory
    cfg_file = find_ase_cfg();
    EXPECT_NE(cfg_file, nullptr);
    if (cfg_file)
        free(cfg_file);

    opae_init();

    if (opae_path && !rename_fail2) {
        ret = rename(rel_cfg_file2_.c_str(), rel_cfg_file_.c_str());
        EXPECT_EQ(ret, 0);
    }
    if (!rename_fail) {
        ret = rename(inst_cfg_file_.c_str(), OPAE_ASE_CFG_INST_PATH);
        EXPECT_EQ(ret, 0);
    }
    if (stat(tmpfile2_, &st) == 0)
      unlink(tmpfile2_);
    if (opae_path && stat(tmpfile3_, &st) == 0)
      unlink(tmpfile3_);
}

INSTANTIATE_TEST_CASE_P(init_ase_cfg, init_ase_cfg_p,
                        ::testing::ValuesIn(_ase_home_configs));
