/*
 * Copyright (c) 2018, 2024, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <rapidjson/document.h>

#include <algorithm>
#include <iterator>
#include <memory>
#include <set>
#include <string>
#include <vector>

#include "mysql-secret-store/include/mysql-secret-store/api.h"
#include "mysqlshdk/libs/db/generic_uri.h"
#include "mysqlshdk/libs/db/uri_parser.h"
#include "mysqlshdk/libs/utils/process_launcher.h"
#include "mysqlshdk/libs/utils/utils_file.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_path.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/libs/utils/version.h"
#include "unittest/gtest_clean.h"
#include "unittest/test_utils/mocks/gmock_clean.h"
#include "unittest/test_utils/shell_test_wrapper.h"

using mysql::secret_store::api::get_available_helpers;
using mysql::secret_store::api::get_helper;
using mysql::secret_store::api::Helper_interface;
using mysql::secret_store::api::Helper_name;
using mysql::secret_store::api::Secret_spec;
using mysql::secret_store::api::Secret_type;

namespace tests {

namespace {

const auto g_format_parameter = [](const auto &info) {
  return shcore::str_replace(info.param, "-", "_");
};

template <typename T>
::testing::AssertionResult is_empty(const T &container) {
  if (container.empty()) {
    return ::testing::AssertionSuccess();
  } else {
    return ::testing::AssertionFailure()
           << "contains: " << shcore::str_join(container, ", ");
  }
}

constexpr auto k_secret_type_password = "password";

std::string to_string(Secret_type type) {
  switch (type) {
    case Secret_type::PASSWORD:
      return k_secret_type_password;
  }

  throw std::runtime_error{"Unknown secret type"};
}

#define SCOPED_TRACE_TYPE(msg) \
  SCOPED_TRACE(msg + std::string{", type: "} + to_string(type))

class Helper_tester {
 public:
  virtual ~Helper_tester() = default;

  virtual void select_helper(const std::string &name) = 0;
  virtual void clear_store() = 0;
  virtual bool has_get() = 0;

  virtual bool store(Secret_type type, const std::string &url,
                     const std::string &secret) = 0;
  virtual bool get(Secret_type type, const std::string &url,
                   std::string *secret) = 0;
  virtual bool erase(Secret_type type, const std::string &url) = 0;

  virtual bool list(std::vector<Secret_spec> *specs) = 0;

  virtual void expect_error(const std::string &msg) = 0;
  virtual void expect_no_error() = 0;

  virtual void expect_contains(Secret_type type, const std::string &url,
                               const std::string &secret) = 0;
  virtual void expect_not_contain(Secret_type type, const std::string &url) = 0;
};

class Mysql_secret_store_api_tester : public Helper_tester {
 public:
  static std::vector<std::string> list_helpers() {
    return call_get_available_helpers();
  }

  static std::vector<std::string> call_get_available_helpers(
      const std::string &path = "") {
    std::vector<std::string> names;
    auto helpers = get_available_helpers(path);

    std::transform(helpers.begin(), helpers.end(), std::back_inserter(names),
                   [](const Helper_name &name) { return name.get(); });

    return names;
  }

  void select_helper(const std::string &name) override {
    SCOPED_TRACE("Mysql_secret_store_api_tester::select_helper()");

    auto helpers = get_available_helpers();
    auto helper =
        std::find_if(helpers.begin(), helpers.end(),
                     [&name](const Helper_name &n) { return n.get() == name; });
    ASSERT_NE(helpers.end(), helper);
    m_helper = get_helper(*helper);
    ASSERT_NE(nullptr, m_helper);
  }

  void clear_store() override {
    SCOPED_TRACE("Mysql_secret_store_api_tester::clear_store()");

    std::vector<Secret_spec> specs;
    ASSERT_NE(nullptr, m_helper);
    EXPECT_TRUE(m_helper->list(&specs));
    expect_no_error();
    for (const auto &spec : specs) {
      EXPECT_TRUE(m_helper->erase(spec));
      expect_no_error();
    }
  }

  bool has_get() override { return true; }

  bool store(Secret_type type, const std::string &url,
             const std::string &secret) override {
    SCOPED_TRACE_TYPE("Mysql_secret_store_api_tester::store()");
    return m_helper->store({type, url}, secret);
  }

  bool get(Secret_type type, const std::string &url,
           std::string *secret) override {
    SCOPED_TRACE_TYPE("Mysql_secret_store_api_tester::get()");
    return m_helper->get({type, url}, secret);
  }

  bool erase(Secret_type type, const std::string &url) override {
    SCOPED_TRACE_TYPE("Mysql_secret_store_api_tester::erase()");
    return m_helper->erase({type, url});
  }

  bool list(std::vector<Secret_spec> *specs) override {
    SCOPED_TRACE("Mysql_secret_store_api_tester::list()");
    return m_helper->list(specs);
  }

  void expect_error(const std::string &msg) override {
    SCOPED_TRACE("Mysql_secret_store_api_tester::expect_error()");
    EXPECT_THAT(m_helper->get_last_error(), ::testing::HasSubstr(msg));
  }

  void expect_no_error() override {
    SCOPED_TRACE("Mysql_secret_store_api_tester::expect_no_error()");
    EXPECT_EQ("", m_helper->get_last_error());
  }

  void expect_contains(Secret_type type, const std::string &url,
                       const std::string &secret) override {
    SCOPED_TRACE_TYPE("Mysql_secret_store_api_tester::expect_contains()");
    std::string stored_secret;
    EXPECT_TRUE(get(type, url, &stored_secret));
    expect_no_error();
    EXPECT_EQ(secret, stored_secret);
  }

  void expect_not_contain(Secret_type type, const std::string &url) override {
    SCOPED_TRACE_TYPE("Mysql_secret_store_api_tester::expect_not_contain()");
    std::string stored_secret;
    EXPECT_FALSE(get(type, url, &stored_secret));
  }

  std::string name() const { return m_helper->name().get(); }

 private:
  std::unique_ptr<Helper_interface> m_helper;
};

class Shell_api_tester : public Helper_tester {
 public:
  Shell_api_tester() {}

  static std::vector<std::string> list_helpers() {
    // this is invoked too early to call execute(), use library function
    return Mysql_secret_store_api_tester::call_get_available_helpers();
  }

  std::vector<std::string> call_list_helpers() {
    execute("print(shell.listCredentialHelpers());");

    return parse_output();
  }

  void select_helper(const std::string &name) override {
    SCOPED_TRACE("Shell_api_tester::select_helper()");

    m_wrapper.reset(new Shell_test_wrapper{false});
    m_wrapper->execute("\\js");

    execute("shell.options[\"credentialStore.helper\"] = " +
            shcore::quote_string(name, '\"') + ";");

    ASSERT_EQ("", m_wrapper->get_output_handler().std_err);
  }

  void clear_store() override {
    SCOPED_TRACE("Shell_api_tester::clear_store()");

    expect_delete_all_credentials();
  }

  bool has_get() override { return false; }

  bool store(Secret_type type, const std::string &url,
             const std::string &secret) override {
    SCOPED_TRACE_TYPE("Shell_api_tester::store()");

    EXPECT_EQ(Secret_type::PASSWORD, type);

#ifdef HAVE_JS
    execute("shell.storeCredential(" + shcore::quote_string(url, '\"') + ", " +
            shcore::quote_string(secret, '\"') + ");");
#else
    execute("shell.store_credential(" + shcore::quote_string(url, '\"') + ", " +
            shcore::quote_string(secret, '\"') + ");");
#endif
    return m_wrapper->get_output_handler().std_err.empty();
  }

  bool get(Secret_type, const std::string &, std::string *) override {
    ADD_FAILURE() << "This should not be called";
    return false;
  }

  bool erase(Secret_type type, const std::string &url) override {
    SCOPED_TRACE_TYPE("Shell_api_tester::erase()");

    EXPECT_EQ(Secret_type::PASSWORD, type);

#ifdef HAVE_JS
    execute("shell.deleteCredential(" + shcore::quote_string(url, '\"') + ");");
#else
    execute("shell.delete_credential(" + shcore::quote_string(url, '\"') +
            ");");
#endif

    return m_wrapper->get_output_handler().std_err.empty();
  }

  bool list(std::vector<Secret_spec> *specs) override {
    SCOPED_TRACE("Shell_api_tester::list()");

#ifdef HAVE_JS
    execute("print(shell.listCredentials());");
#else
    execute("print(shell.list_credentials());");
#endif

    for (const auto &url : parse_output()) {
      specs->emplace_back(Secret_spec{Secret_type::PASSWORD, url});
    }

    return m_wrapper->get_output_handler().std_err.empty();
  }

  void expect_error(const std::string &msg) override {
    SCOPED_TRACE("Shell_api_tester::expect_error()");
    m_wrapper->get_output_handler().validate_stderr_content(msg, true);
  }

  void expect_no_error() override {
    SCOPED_TRACE("Shell_api_tester::expect_no_error()");
    EXPECT_EQ("", m_wrapper->get_output_handler().std_err);
  }

  void expect_contains(Secret_type type, const std::string &url,
                       const std::string &) override {
    SCOPED_TRACE_TYPE("Shell_api_tester::expect_contains()");

    EXPECT_EQ(Secret_type::PASSWORD, type);
    EXPECT_TRUE(has_url(url));
  }

  void expect_not_contain(Secret_type type, const std::string &url) override {
    SCOPED_TRACE_TYPE("Shell_api_tester::expect_not_contain()");

    EXPECT_EQ(Secret_type::PASSWORD, type);
    EXPECT_FALSE(has_url(url));
  }

  void expect_delete_all_credentials() {
    SCOPED_TRACE("Shell_api_tester::expect_delete_all_credentials()");

#ifdef HAVE_JS
    execute("shell.deleteAllCredentials();");
#else
    execute("shell.delete_all_credentials();");
#endif

    EXPECT_EQ("", m_wrapper->get_output_handler().std_err);
  }

 private:
  void execute(const std::string &code) {
    m_wrapper->get_output_handler().wipe_all();
    m_wrapper->execute(code);
  }

  std::vector<std::string> parse_output() {
    std::vector<std::string> output;

    rapidjson::Document doc;

    doc.Parse(m_wrapper->get_output_handler().std_out.c_str());

    if (!doc.HasParseError() && doc.IsArray()) {
      for (const auto &s : doc.GetArray()) {
        output.emplace_back(s.GetString());
      }
    }

    return output;
  }

  bool has_url(const std::string &url) {
    SCOPED_TRACE("Shell_api_tester::has_url()");

    std::vector<Secret_spec> specs;

    EXPECT_TRUE(list(&specs));
    expect_no_error();
    mysqlshdk::db::uri::Generic_uri copts;
    mysqlshdk::db::uri::Uri_parser parser(mysqlshdk::db::uri::Type::Generic);
    parser.parse(url, &copts);

    auto tokens = mysqlshdk::db::uri::formats::user_transport();
    if (copts.has_value(mysqlshdk::db::kScheme) &&
        (copts.get(mysqlshdk::db::kScheme) == "ssh" ||
         copts.get(mysqlshdk::db::kScheme) == "file"))
      tokens.set(mysqlshdk::db::uri::Tokens::Scheme);
    auto normalized = copts.as_uri(tokens);
    return specs.end() !=
           std::find(specs.begin(), specs.end(),
                     Secret_spec{Secret_type::PASSWORD, normalized});
  }

  std::unique_ptr<Shell_test_wrapper> m_wrapper;
};

class Helper_invoker {
 public:
  explicit Helper_invoker(const std::string &path) : m_path{path} {}

  bool store(const std::string &input, std::string *output) const {
    return invoke("store", input, output);
  }

  bool get(const std::string &input, std::string *output) const {
    return invoke("get", input, output);
  }

  bool erase(const std::string &input, std::string *output) const {
    return invoke("erase", input, output);
  }

  bool list(std::string *output) const { return invoke("list", {}, output); }

  bool version(std::string *output) const {
    return invoke("version", {}, output);
  }

  bool invoke(const char *command, const std::string &input,
              std::string *output) const {
    try {
      const char *const args[] = {m_path.c_str(), command, nullptr};
      shcore::Process_launcher app{args};

      app.start();

      if (!input.empty()) {
        app.write(input.c_str(), input.length());
        app.finish_writing();
      }

      *output = shcore::str_strip(app.read_all());

      return app.wait() == 0;
    } catch (const std::exception &ex) {
      *output = std::string{"Exception caught while running helper command '"} +
                command + "': " + ex.what();
      return false;
    }
  }

  const std::string m_path;
};

class Helper_executable_tester : public Helper_tester {
 public:
  static std::vector<std::string> list_helpers() {
    std::vector<std::string> output;
    auto paths = list_helper_paths();
    std::transform(paths.begin(), paths.end(), std::back_inserter(output),
                   get_helper_name);
    return output;
  }

  void select_helper(const std::string &name) override {
    SCOPED_TRACE("Helper_executable_tester::select_helper()");

    auto paths = list_helper_paths();
    auto helper = std::find_if(
        paths.begin(), paths.end(),
        [&name](const std::string &p) { return get_helper_name(p) == name; });
    ASSERT_NE(paths.end(), helper);
    m_invoker = std::make_unique<Helper_invoker>(*helper);
    ASSERT_NE(nullptr, m_invoker);
  }

  void clear_store() override {
    SCOPED_TRACE("Helper_executable_tester::clear_store()");

    ASSERT_NE(nullptr, m_invoker);

    std::string output;

    if (!m_invoker->list(&output)) {
      ::testing::AssertionFailure() << output;
      return;
    }

    rapidjson::Document doc;

    doc.Parse(output.c_str());

    if (doc.HasParseError()) {
      ::testing::AssertionFailure() << "Failed to parse JSON";
      return;
    }

    if (!doc.IsArray()) {
      ::testing::AssertionFailure() << "Expected array";
      return;
    }

    for (const auto &s : doc.GetArray()) {
      output.clear();
      if (!m_invoker->erase(to_json(s[k_secret_type].GetString(),
                                    s[k_server_url].GetString()),
                            &output)) {
        ::testing::AssertionFailure() << output << "\n";
      }
    }
  }

  bool has_get() override { return true; }

  bool store(Secret_type type, const std::string &url,
             const std::string &secret) override {
    SCOPED_TRACE_TYPE("Helper_executable_tester::store()");

    std::string output;
    bool ret = m_invoker->store(to_json(type, url, secret), &output);

    if (ret) {
      clear_last_error();
    } else {
      set_last_error(output);
    }

    return ret;
  }

  bool get(Secret_type type, const std::string &url,
           std::string *secret) override {
    SCOPED_TRACE_TYPE("Helper_executable_tester::get()");

    std::string output;
    bool ret = m_invoker->get(to_json(type, url), &output);

    if (ret) {
      clear_last_error();
      Secret_spec spec;
      ret = to_secret(output, &spec, secret);

      if (spec.type != type || spec.url != url) {
        ret = false;
        set_last_error("Helper returned mismatched secret");
      }
    } else {
      set_last_error(output);
    }

    return ret;
  }

  bool erase(Secret_type type, const std::string &url) override {
    SCOPED_TRACE_TYPE("Helper_executable_tester::erase()");

    std::string output;
    bool ret = m_invoker->erase(to_json(type, url), &output);

    if (ret) {
      clear_last_error();
    } else {
      set_last_error(output);
    }

    return ret;
  }

  bool list(std::vector<Secret_spec> *specs) override {
    SCOPED_TRACE("Helper_executable_tester::list()");

    std::string output;
    bool ret = m_invoker->list(&output);

    if (ret) {
      clear_last_error();
      ret = to_specs(output, specs);
    } else {
      set_last_error(output);
    }

    return ret;
  }

  void expect_error(const std::string &msg) override {
    SCOPED_TRACE("Helper_executable_tester::expect_error()");
    EXPECT_THAT(m_last_error, ::testing::HasSubstr(msg));
  }

  void expect_no_error() override {
    SCOPED_TRACE("Helper_executable_tester::expect_no_error()");
    EXPECT_EQ("", m_last_error);
  }

  void expect_contains(Secret_type type, const std::string &url,
                       const std::string &secret) override {
    SCOPED_TRACE_TYPE("Helper_executable_tester::expect_contains()");
    std::string stored_secret;
    EXPECT_TRUE(get(type, url, &stored_secret));
    expect_no_error();
    EXPECT_EQ(secret, stored_secret);
  }

  void expect_not_contain(Secret_type type, const std::string &url) override {
    SCOPED_TRACE_TYPE("Helper_executable_tester::expect_not_contain()");
    std::string stored_secret;
    EXPECT_FALSE(get(type, url, &stored_secret));
  }

  Helper_invoker &get_invoker() { return *m_invoker; }

 private:
  static std::string get_helper_name(const std::string &path) {
    std::string name =
        shcore::path::basename(path).substr(strlen(k_exe_prefix));
    name = name.substr(0, name.find_last_of("."));

    return name;
  }

  static std::vector<std::string> list_helper_paths() {
    std::vector<std::string> helpers;
    const auto folder = shcore::get_binary_folder();

    if (!shcore::is_folder(folder)) {
      return helpers;
    }

    for (const auto &entry : shcore::listdir(folder)) {
      const std::string path = shcore::path::join_path(folder, entry);
      std::string version;

      if (entry.compare(0, strlen(k_exe_prefix), k_exe_prefix) == 0 &&
          !shcore::is_folder(path) && Helper_invoker{path}.version(&version)) {
        helpers.emplace_back(path);
      }
    }

    return helpers;
  }

  static std::string to_json(Secret_type type, const std::string &url,
                             const std::string &secret) {
    auto json = to_json(type, url);
    json.pop_back();
    return json + ",\"" + k_secret +
           "\":" + shcore::quote_string(secret, '\"') + "}";
  }

  static std::string to_json(Secret_type type, const std::string &url) {
    return to_json(to_string(type), url);
  }

  static std::string to_json(const std::string &type, const std::string &url) {
    return std::string{"{\""} + k_server_url +
           "\":" + shcore::quote_string(url, '\"') + ",\"" + k_secret_type +
           "\":" + shcore::quote_string(type, '\"') + "}";
  }

  bool validate_object(const rapidjson::Value &object,
                       const std::set<std::string> &members) {
    if (!object.IsObject()) {
      set_last_error("Expected object");
      return false;
    }

    for (const auto &m : members) {
      if (!object.HasMember(m.c_str())) {
        set_last_error("Missing member: \"" + m + "\"");
      }

      if (!object[m.c_str()].IsString()) {
        set_last_error("Member \"" + m + "\" is not a string");
      }
    }

    return true;
  }

  bool to_secret_type(const std::string &type, Secret_type *output_type) {
    if (type == k_secret_type_password) {
      *output_type = Secret_type::PASSWORD;
    } else {
      set_last_error("Unknown secret type: \"" + type + "\"");
      return false;
    }

    return true;
  }

  bool to_spec(const rapidjson::Value &object, Secret_spec *spec) {
    if (!validate_object(object, {k_secret_type, k_server_url})) {
      return false;
    }

    if (!to_secret_type(object[k_secret_type].GetString(), &spec->type)) {
      return false;
    }

    spec->url = object[k_server_url].GetString();

    return true;
  }

  bool to_secret(const std::string &json, Secret_spec *spec,
                 std::string *secret) {
    rapidjson::Document doc;

    doc.Parse(json.c_str());

    if (doc.HasParseError()) {
      set_last_error("Failed to parse JSON");
      return false;
    }

    if (!to_spec(doc, spec)) {
      return false;
    }

    if (!validate_object(doc, {k_secret})) {
      return false;
    }

    *secret = doc[k_secret].GetString();

    return true;
  }

  bool to_specs(const std::string &json, std::vector<Secret_spec> *specs) {
    rapidjson::Document doc;

    doc.Parse(json.c_str());

    if (doc.HasParseError()) {
      set_last_error("Failed to parse JSON");
      return false;
    }

    if (!doc.IsArray()) {
      set_last_error("Expected array");
      return false;
    }

    for (const auto &s : doc.GetArray()) {
      Secret_spec spec;

      if (!to_spec(s, &spec)) {
        return false;
      } else {
        specs->emplace_back(spec);
      }
    }

    return true;
  }

  void set_last_error(const std::string &str) const { m_last_error = str; }

  void clear_last_error() const { m_last_error.clear(); }

  static const char k_exe_prefix[];
  static const char k_secret[];
  static const char k_secret_type[];
  static const char k_server_url[];

  std::unique_ptr<Helper_invoker> m_invoker;

  mutable std::string m_last_error;
};

const char Helper_executable_tester::k_exe_prefix[] = "mysql-secret-store-";
const char Helper_executable_tester::k_secret[] = "Secret";
const char Helper_executable_tester::k_secret_type[] = "SecretType";
const char Helper_executable_tester::k_server_url[] = "ServerURL";

template <typename T>
class Parametrized_helper_test : public ::testing::TestWithParam<std::string> {
 public:
  static std::set<std::string> list_helpers() {
    SCOPED_TRACE("Parametrized_helper_test::list_helpers()");
    auto helpers = T::list_helpers();
    std::set<std::string> ret{helpers.begin(), helpers.end()};
    EXPECT_TRUE(helpers.size() == ret.size())
        << "List of helpers contains duplicate values";
    return ret;
  }

 protected:
  void SetUp() override {
    tester.select_helper(GetParam());
    clear_store();
  }

  void TearDown() override { clear_store(); }

  bool store(const std::string &url, const std::string &secret) {
    return tester.store(Secret_type::PASSWORD, url, secret);
  }

  bool get(const std::string &url, std::string *secret) {
    return tester.get(Secret_type::PASSWORD, url, secret);
  }

  bool erase(const std::string &url) {
    return tester.erase(Secret_type::PASSWORD, url);
  }

  void expect_list(const std::set<std::string> &urls) {
    SCOPED_TRACE("Parametrized_helper_test::expect_list()");
    std::vector<Secret_spec> specs;
    EXPECT_TRUE(tester.list(&specs));
    expect_no_error();

    std::set<std::string> list_urls;

    for (const auto &s : specs) {
      SCOPED_TRACE(s.url + " -> " + to_string(s.type));
      EXPECT_EQ(Secret_type::PASSWORD, s.type);
      list_urls.emplace(s.url);
    }

    EXPECT_EQ(urls, list_urls);
  }

  void expect_contains(const std::string &url, const std::string &secret) {
    tester.expect_contains(Secret_type::PASSWORD, url, secret);
  }

  void expect_not_contain(const std::string &url) {
    tester.expect_not_contain(Secret_type::PASSWORD, url);
  }

  void expect_error(const std::string &msg) { tester.expect_error(msg); }

  void expect_no_error() { tester.expect_no_error(); }

  void clear_store() { tester.clear_store(); }

  T tester;
};

class Config_editor_invoker {
 public:
  void store(const std::string &name, const std::string &url,
             const std::string &secret) {
    std::vector<std::string> args = {"set", "--skip-warn", "--password"};
    args.emplace_back("--login-path=" + name);

    mysqlshdk::db::uri::Generic_uri options;
    mysqlshdk::db::uri::Uri_parser parser(mysqlshdk::db::uri::Type::Generic);
    parser.parse(url, &options);

    if (options.has_value(mysqlshdk::db::kUser)) {
      args.emplace_back("--user=" + options.get(mysqlshdk::db::kUser));
    }
    if (options.has_value(mysqlshdk::db::kHost)) {
      args.emplace_back("--host=" + options.get(mysqlshdk::db::kHost));
    }
    if (options.has_value(mysqlshdk::db::kPort)) {
      args.emplace_back("--port=" + std::to_string(options.get_numeric(
                                        mysqlshdk::db::kPort)));
    }
    if (options.has_value(mysqlshdk::db::kSocket)) {
      args.emplace_back("--socket=" + options.get(mysqlshdk::db::kSocket));
    }

    invoke(args, true, secret);
  }

  void clear() { invoke({"reset"}); }

  mysqlshdk::utils::Version version() const {
    return mysqlshdk::utils::Version{
        shcore::str_split(invoke({"--version"}), " ", -1, true)[2]};
  }

  std::string invoke(const std::vector<std::string> &args,
                     bool uses_terminal = false,
                     const std::string &password = "") const {
    std::vector<const char *> process_args;
    process_args.emplace_back("mysql_config_editor");

    for (const auto &arg : args) {
      process_args.emplace_back(arg.c_str());
    }

    process_args.emplace_back(nullptr);

    shcore::Process_launcher app{&process_args[0]};

    if (uses_terminal) {
      app.enable_child_terminal();
    }

    app.start();

    if (uses_terminal) {
      // wait for password prompt
      app.read_from_terminal();
      app.write_to_terminal(password.c_str(), password.length());
      app.write_to_terminal("\n", 1);
    }

    std::string output = shcore::str_strip(app.read_all());

    if (0 != app.wait()) {
      throw std::runtime_error{output};
    }

    return output;
  }
};

void verify_available_helpers(const std::set<std::string> &helpers) {
  SCOPED_TRACE("verify_available_helpers()");

  std::set<std::string> expected = {"plaintext"};

#ifdef _WIN32
  expected.emplace("windows-credential");
#else  // ! _WIN32
#ifdef __APPLE__
  expected.emplace("keychain");
#else   // ! __APPLE__
  expected.emplace("login-path");
#endif  // ! __APPLE__
#endif  // ! _WIN32

  std::set<std::string> missing;
  std::set_difference(expected.begin(), expected.end(), helpers.begin(),
                      helpers.end(), std::inserter(missing, missing.begin()));

  EXPECT_TRUE(is_empty(missing));
}

void verify_available_helpers(const std::vector<std::string> &helpers) {
  verify_available_helpers(
      std::set<std::string>{helpers.begin(), helpers.end()});
}

template <typename T>
void test_available_helpers() {
  SCOPED_TRACE("test_available_helpers()");

  verify_available_helpers(T::list_helpers());
}

#define IGNORE_TEST(test_case_name, test_name)                 \
  class test_case_name##_##test_name : public test_case_name { \
   private:                                                    \
    void test_body();                                          \
  };                                                           \
  void test_case_name##_##test_name::test_body()

#define MY_EXPECT_ERROR(expression, error) \
  do {                                     \
    EXPECT_FALSE(expression);              \
    expect_error(error);                   \
  } while (false)

#define MY_EXPECT_NO_ERROR(expression) \
  do {                                 \
    EXPECT_TRUE(expression);           \
    expect_no_error();                 \
  } while (false)

#define GET_EXPECT_ERROR(url, secret, error) \
  if (tester.has_get()) {                    \
    std::string s;                           \
    MY_EXPECT_ERROR(get(url, &s), error);    \
  }

#define INVALID_URL_ERROR "Invalid URL"

#define STORE_INVALID_URL(url, secret)                      \
  do {                                                      \
    SCOPED_TRACE(url + std::string{" -> "} + secret);       \
    MY_EXPECT_ERROR(store(url, secret), INVALID_URL_ERROR); \
  } while (false)

#define GET_INVALID_URL(url, secret)                  \
  do {                                                \
    SCOPED_TRACE(url + std::string{" -> "} + secret); \
    GET_EXPECT_ERROR(url, secret, INVALID_URL_ERROR)  \
  } while (false)

#define ERASE_INVALID_URL(url, secret)                \
  do {                                                \
    SCOPED_TRACE(url + std::string{" -> "} + secret); \
    MY_EXPECT_ERROR(erase(url), INVALID_URL_ERROR);   \
  } while (false)

#define OP_INVALID_URL_TESTS(op)                                    \
  do {                                                              \
    op("host", "password");                                         \
    op("user@", "password");                                        \
    op("user@@", "password");                                       \
    op("user:secret@host", "password");                             \
    op("http://user@host", "password");                             \
    op("http://user@host:3306", "password");                        \
    op("mysql://user@host", "password");                            \
    op("mysql://user@host:3306", "password");                       \
    op("mysqlx://user@host", "password");                           \
    op("mysqlx://user@host:33060", "password");                     \
    op("user@host/schema", "password");                             \
    op("user@host:3306/schema", "password");                        \
    op("user@host?tls-version=xyz", "password");                    \
    op("user@host:3306?tls-version=xyz", "password");               \
    op("user@host/schema?tls-version=xyz", "password");             \
    op("user@host:3306/schema?tls-version=xyz", "password");        \
    op("http://user@host/schema?tls-version=xyz", "password");      \
    op("http://user@host:3306/schema?tls-version=xyz", "password"); \
  } while (false)

#define STORE_AND_CHECK(url, secret)                  \
  do {                                                \
    SCOPED_TRACE(url + std::string{" -> "} + secret); \
    MY_EXPECT_NO_ERROR(store(url, secret));           \
    expect_contains(url, secret);                     \
  } while (false)

#define STORE_AND_ERASE(url, secret)                  \
  do {                                                \
    SCOPED_TRACE(url + std::string{" -> "} + secret); \
    MY_EXPECT_NO_ERROR(store(url, secret));           \
    expect_contains(url, secret);                     \
    MY_EXPECT_NO_ERROR(erase(url));                   \
    expect_not_contain(url);                          \
  } while (false)

#ifdef _WIN32
#define URL_WITH_SOCKET "user@\\\\.\\named.pipe"
#define URL_WITH_SOCKET_AND_PARENTHESES "user@\\\\.\\(named.pipe)"
#else  // !_WIN32
#define URL_WITH_SOCKET "user@/socket"
#define URL_WITH_SOCKET_AND_PARENTHESES "user@(/socket)"
#endif  // !_WIN32

#define STORE_AND_OP_TESTS(op)                                              \
  do {                                                                      \
    op("empty@host", "");                                                   \
    op("user@host", "one");                                                 \
    op("user@host:3306", "two");                                            \
    op(URL_WITH_SOCKET, "three");                                           \
    op("user@host:33060", "five");                                          \
    op("user@host:33060", "six");                                           \
    op("ssh://user@host.com:22", "test");                                   \
    op("file:/user/host/com", "test");                                      \
    if (GetParam() == "login-path") {                                       \
      op("user@host:55555",                                                 \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmn");                                                 \
    } else {                                                                \
      op("user@host:55555",                                                 \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"   \
         "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); \
    }                                                                       \
    op("user@example.com", "zażółć gęślą jaźń");                            \
    op("user@host", "\"pass\"'word'");                                      \
    op("user@host1", "\\");                                                 \
    op("user@host2", "\\\\");                                               \
    op("user@host3", "\\\"\\'");                                            \
    op("user@host4", "\"password\"");                                       \
    op("user@host5", "'password'");                                         \
    op("user@[::1]", "seven");                                              \
    op("user@[::1]:7777", "eight");                                         \
    op("user@[fe80::850a:5a7c:6ab7:aec4%25enp0s3]", "nine");                \
    op("user@[fe80::850a:5a7c:6ab7:aec4%25enp0s3]:8888", "ten");            \
    op("test%21%23%24%26%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"            \
       "@example.com:9999",                                                 \
       "eleven");                                                           \
  } while (false)

#define ADD_TESTS(test_case_name)                                             \
  VALIDATION_TEST(test_case_name, store_invalid_url) {                        \
    OP_INVALID_URL_TESTS(STORE_INVALID_URL);                                  \
  }                                                                           \
                                                                              \
  VALIDATION_TEST(test_case_name, get_invalid_url) {                          \
    if (!tester.has_get()) {                                                  \
      return;                                                                 \
    }                                                                         \
    OP_INVALID_URL_TESTS(GET_INVALID_URL);                                    \
  }                                                                           \
                                                                              \
  VALIDATION_TEST(test_case_name, erase_invalid_url) {                        \
    OP_INVALID_URL_TESTS(ERASE_INVALID_URL);                                  \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, store_and_check) {                                   \
    STORE_AND_OP_TESTS(STORE_AND_CHECK);                                      \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, store_and_erase) {                                   \
    STORE_AND_OP_TESTS(STORE_AND_ERASE);                                      \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, list) {                                              \
    std::set<std::string> urls{"user@host", "user@host:3306",                 \
                               "user@host:33060", URL_WITH_SOCKET};           \
    for (const auto &url : urls) {                                            \
      STORE_AND_CHECK(url, url);                                              \
    }                                                                         \
    expect_list(urls);                                                        \
    STORE_AND_CHECK(*urls.begin(), "new pass");                               \
    expect_list(urls);                                                        \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, get_nonexistent_url) {                               \
    constexpr auto error = "Could not find the secret";                       \
    GET_EXPECT_ERROR("user@nonexistent.host", "secret", error);               \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, erase_nonexistent_url) {                             \
    constexpr auto error = "Could not find the secret";                       \
    MY_EXPECT_ERROR(erase("user@nonexistent.host"), error);                   \
  }                                                                           \
                                                                              \
  NORMALIZATION_TEST(test_case_name, url_normalization) {                     \
    STORE_AND_CHECK(URL_WITH_SOCKET, "one");                                  \
    STORE_AND_CHECK(URL_WITH_SOCKET_AND_PARENTHESES, "two");                  \
    expect_contains(URL_WITH_SOCKET, "two");                                  \
  }                                                                           \
                                                                              \
  TEST(Helpers, test_case_name##_list_helpers) {                              \
    test_available_helpers<test_case_name>();                                 \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, login_path_external_entry) {                         \
    if (GetParam() != "login-path") {                                         \
      return;                                                                 \
    }                                                                         \
    Config_editor_invoker invoker;                                            \
    invoker.clear();                                                          \
    std::set<std::string> urls{"user@host", "user@host:3306",                 \
                               "user@host:33060", URL_WITH_SOCKET};           \
    for (const auto &url : urls) {                                            \
      invoker.store(url, url, url);                                           \
      expect_contains(url, url);                                              \
    }                                                                         \
    expect_list(urls);                                                        \
    invoker.clear();                                                          \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, login_path_external_and_helper_entries) {            \
    if (GetParam() != "login-path") {                                         \
      return;                                                                 \
    }                                                                         \
    Config_editor_invoker invoker;                                            \
    invoker.clear();                                                          \
    invoker.store("one", "user@host", "one");                                 \
    STORE_AND_CHECK("user@host", "two");                                      \
    expect_contains("user@host", "two");                                      \
    expect_list({"user@host"});                                               \
    erase("user@host");                                                       \
    expect_contains("user@host", "one");                                      \
    expect_list({"user@host"});                                               \
    erase("user@host");                                                       \
    expect_list({});                                                          \
    invoker.clear();                                                          \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, login_path_external_entry_with_port_and_socket) {    \
    if (GetParam() != "login-path") {                                         \
      return;                                                                 \
    }                                                                         \
    Config_editor_invoker invoker;                                            \
    invoker.clear();                                                          \
    auto initialize = [&invoker]() {                                          \
      std::vector<std::string> args = {"set",         "--skip-warn",          \
                                       "--password",  "--login-path=one",     \
                                       "--user=user", "--host=localhost",     \
                                       "--port=3306", "--socket=/socket"};    \
      invoker.invoke(args, true, "one");                                      \
    };                                                                        \
    initialize();                                                             \
    expect_list({"user@localhost:3306", URL_WITH_SOCKET});                    \
    STORE_AND_CHECK("user@localhost:3306", "two");                            \
    STORE_AND_CHECK(URL_WITH_SOCKET, "three");                                \
    expect_list({"user@localhost:3306", URL_WITH_SOCKET});                    \
    erase(URL_WITH_SOCKET);                                                   \
    expect_contains(URL_WITH_SOCKET, "one");                                  \
    expect_contains("user@localhost:3306", "two");                            \
    expect_list({"user@localhost:3306", URL_WITH_SOCKET});                    \
    erase("user@localhost:3306");                                             \
    expect_contains(URL_WITH_SOCKET, "one");                                  \
    expect_contains("user@localhost:3306", "one");                            \
    expect_list({"user@localhost:3306", URL_WITH_SOCKET});                    \
    erase(URL_WITH_SOCKET);                                                   \
    expect_contains("user@localhost:3306", "one");                            \
    expect_list({"user@localhost:3306"});                                     \
    erase("user@localhost:3306");                                             \
    expect_list({});                                                          \
    invoker.clear();                                                          \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, login_path_ipv6_stored_by_config_editor) {           \
    if (GetParam() != "login-path") {                                         \
      return;                                                                 \
    }                                                                         \
    Config_editor_invoker invoker;                                            \
    auto initialize = [&invoker](const std::string &host) {                   \
      std::vector<std::string> args = {                                       \
          "set",         "--skip-warn",    "--password", "--login-path=path", \
          "--user=user", "--host=" + host, "--port=3306"};                    \
      invoker.clear();                                                        \
      invoker.invoke(args, true, "pass");                                     \
    };                                                                        \
    initialize("::1");                                                        \
    expect_list({"user@[::1]:3306"});                                         \
    initialize("fe80::850a:5a7c:6ab7:aec4");                                  \
    expect_list({"user@[fe80::850a:5a7c:6ab7:aec4]:3306"});                   \
    initialize("fe80::850a:5a7c:6ab7:aec4%enp0s3");                           \
    expect_list({"user@[fe80::850a:5a7c:6ab7:aec4%25enp0s3]:3306"});          \
    invoker.clear();                                                          \
  }                                                                           \
                                                                              \
  TEST_P(test_case_name, login_path_ipv6_stored_by_helper) {                  \
    if (GetParam() != "login-path") {                                         \
      return;                                                                 \
    }                                                                         \
    Config_editor_invoker invoker;                                            \
    invoker.clear();                                                          \
    const auto quoted =                                                       \
        invoker.version() >= mysqlshdk::utils::Version(8, 0, 24);             \
    const auto host = [&quoted](const std::string &h) {                       \
      return "host = " + (quoted ? shcore::quote_string(h, '"') : h);         \
    };                                                                        \
    STORE_AND_CHECK("user@[::1]", "one");                                     \
    auto output = invoker.invoke({"print", "--all"});                         \
    EXPECT_THAT(output, ::testing::HasSubstr(host("::1")));                   \
    invoker.clear();                                                          \
    STORE_AND_CHECK("user@[fe80::850a:5a7c:6ab7:aec4]:4321", "two");          \
    output = invoker.invoke({"print", "--all"});                              \
    EXPECT_THAT(output,                                                       \
                ::testing::HasSubstr(host("fe80::850a:5a7c:6ab7:aec4")));     \
    EXPECT_THAT(output, ::testing::HasSubstr("port = 4321"));                 \
    invoker.clear();                                                          \
    STORE_AND_CHECK("user@[fe80::850a:5a7c:6ab7:aec4%25enp0s3]", "three");    \
    output = invoker.invoke({"print", "--all"});                              \
    EXPECT_THAT(output, ::testing::HasSubstr(                                 \
                            host("fe80::850a:5a7c:6ab7:aec4%enp0s3")));       \
    invoker.clear();                                                          \
  }

#define REGISTER_TESTS(test_case_name)                                 \
  namespace {                                                          \
  const auto test_case_name##_list = test_case_name::list_helpers();   \
  }                                                                    \
  INSTANTIATE_TEST_SUITE_P(Helpers, test_case_name,                    \
                           ::testing::ValuesIn(test_case_name##_list), \
                           g_format_parameter)

}  // namespace

#define VALIDATION_TEST TEST_P
#define NORMALIZATION_TEST TEST_P

class Mysql_secret_store_api_test
    : public Parametrized_helper_test<Mysql_secret_store_api_tester> {};

ADD_TESTS(Mysql_secret_store_api_test)

#undef VALIDATION_TEST
#undef NORMALIZATION_TEST

TEST_P(Mysql_secret_store_api_test, helper_name) {
  EXPECT_EQ(GetParam(), tester.name());
}

TEST_P(Mysql_secret_store_api_test, get_nullptr) {
  EXPECT_FALSE(get("url", nullptr));
  expect_error("Invalid pointer");
}

TEST_P(Mysql_secret_store_api_test, list_nullptr) {
  EXPECT_FALSE(tester.list(nullptr));
  expect_error("Invalid pointer");
}

TEST(Helpers, Mysql_secret_store_api_test_secret_spec_operators) {
  Secret_spec one{Secret_type::PASSWORD, "first URL"};
  Secret_spec two{Secret_type::PASSWORD, "second URL"};

  EXPECT_FALSE(one == two);
  EXPECT_TRUE(one != two);

  one.url = two.url;

  EXPECT_TRUE(one == two);
  EXPECT_FALSE(one != two);
}

TEST(Helpers, Mysql_secret_store_api_test_get_available_helpers_custom_dir) {
  verify_available_helpers(
      Mysql_secret_store_api_tester::call_get_available_helpers(
          shcore::get_binary_folder()));
}

TEST(Helpers, Mysql_secret_store_api_test_get_available_helpers_invalid_dir) {
  auto home = shcore::get_home_dir();

  do {
    home = shcore::path::join_path(home, "random_name");
  } while (shcore::is_folder(home));

  EXPECT_TRUE(is_empty(
      Mysql_secret_store_api_tester::call_get_available_helpers(home)));
}

REGISTER_TESTS(Mysql_secret_store_api_test);

#define VALIDATION_TEST TEST_P
#define NORMALIZATION_TEST TEST_P

class Shell_api_test : public Parametrized_helper_test<Shell_api_tester> {};

ADD_TESTS(Shell_api_test)

#undef VALIDATION_TEST
#undef NORMALIZATION_TEST

TEST_P(Shell_api_test, delete_all_credentials) {
  std::set<std::string> urls{"user@host", "user@host:3306", "user@host:33060",
                             URL_WITH_SOCKET};
  for (const auto &url : urls) {
    STORE_AND_CHECK(url, url);
  }
  tester.expect_delete_all_credentials();
  expect_list({});
}

REGISTER_TESTS(Shell_api_test);

#define VALIDATION_TEST IGNORE_TEST
#define NORMALIZATION_TEST IGNORE_TEST

class Helper_executable_test
    : public Parametrized_helper_test<Helper_executable_tester> {};

ADD_TESTS(Helper_executable_test)

#undef VALIDATION_TEST
#undef NORMALIZATION_TEST

TEST_P(Helper_executable_test, uri_should_not_be_validated) {
  std::string output;
  std::string spec = R"("ServerURL":"some+string","SecretType":"password")";
  auto &invoker = tester.get_invoker();

  EXPECT_TRUE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_EQ("", output);
  output.clear();

  EXPECT_TRUE(invoker.get("{" + spec + "}", &output));
  EXPECT_NE("", output);
  output.clear();

  EXPECT_TRUE(invoker.erase("{" + spec + "}", &output));
  EXPECT_EQ("", output);
  output.clear();
}

TEST_P(Helper_executable_test, secret_type_should_not_be_validated) {
  if (GetParam() == "login-path") {
    // login-path can only store passwords
    return;
  }
  std::string output;
  std::string spec = R"("ServerURL":"user@host","SecretType":"some_type")";
  auto &invoker = tester.get_invoker();

  EXPECT_TRUE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_EQ("", output);
  output.clear();

  EXPECT_TRUE(invoker.get("{" + spec + "}", &output));
  EXPECT_NE("", output);
  output.clear();

  EXPECT_TRUE(invoker.erase("{" + spec + "}", &output));
  EXPECT_EQ("", output);
  output.clear();
}

TEST_P(Helper_executable_test, invalid_command) {
  std::string output;
  std::string spec = R"({"ServerURL":"user@host","SecretType":"password"})";
  auto &invoker = tester.get_invoker();

  EXPECT_FALSE(invoker.invoke("unknown", spec, &output));
  EXPECT_THAT(output, ::testing::HasSubstr("Unknown command"));
}

TEST_P(Helper_executable_test, missing_command) {
  std::string output;
  std::string spec = R"({"ServerURL":"user@host","SecretType":"password"})";
  auto &invoker = tester.get_invoker();

  EXPECT_FALSE(invoker.invoke(nullptr, spec, &output));
  EXPECT_THAT(output, ::testing::HasSubstr("Missing command"));
}

TEST_P(Helper_executable_test, invalid_json_input_misspelled_server_url) {
  const std::string error_message = R"("ServerURL" is missing)";
  auto &invoker = tester.get_invoker();
  std::string output;

  for (const auto &s : {"ServerURl", "SErverURL", "ServerURLL", "SrverURL"}) {
    SCOPED_TRACE(s);

    std::string spec =
        R"(")" + std::string{s} + R"(":"user@host","SecretType":"password")";

    EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();

    EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();

    EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();
  }
}

TEST_P(Helper_executable_test, invalid_json_input_misspelled_secret_type) {
  const std::string error_message = R"("SecretType" is missing)";
  auto &invoker = tester.get_invoker();
  std::string output;

  for (const auto &s :
       {"Secrettype", "SEcretType", "SecretTypee", "ScretType"}) {
    SCOPED_TRACE(s);

    std::string spec =
        R"("ServerURL":"user@host",")" + std::string{s} + R"(":"password")";

    EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();

    EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();

    EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();
  }
}

TEST_P(Helper_executable_test, invalid_json_input_misspelled_secret) {
  const std::string error_message = R"("Secret" is missing)";
  auto &invoker = tester.get_invoker();
  std::string output;

  for (const auto &s : {"secret", "SEcret", "Secrett", "Scret"}) {
    SCOPED_TRACE(s);

    std::string spec = R"({"ServerURL":"user@host","SecretType":"password",")" +
                       std::string{s} + R"(":"pass"})";

    EXPECT_FALSE(invoker.store(spec, &output));
    EXPECT_THAT(output, ::testing::HasSubstr(error_message));
    output.clear();
  }
}

TEST_P(Helper_executable_test, invalid_json_input_unknown_member) {
  const std::string error_message = "JSON object contains unknown member";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec =
      R"("ServerURL":"user@host","SecretType":"password","InvalidMember":12345)";

  EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, invalid_json_input_missing_server_url) {
  const std::string error_message = R"("ServerURL" is missing)";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec = R"("SecretType":"password")";

  EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, invalid_json_input_missing_secret_type) {
  const std::string error_message = R"("SecretType" is missing)";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec = R"("ServerURL":"user@host")";

  EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, invalid_json_input_missing_secret) {
  const std::string error_message = R"("Secret" is missing)";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec = R"({"ServerURL":"user@host","SecretType":"password"})";

  EXPECT_FALSE(invoker.store(spec, &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, invalid_json_input_wrong_type_server_url) {
  const std::string error_message = R"("ServerURL" should be a string)";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec = R"("ServerURL":false,"SecretType":"password")";

  EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, invalid_json_input_wrong_type_secret_type) {
  const std::string error_message = R"("SecretType" should be a string)";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec = R"("ServerURL":"user@host","SecretType":false)";

  EXPECT_FALSE(invoker.store("{" + spec + R"(,"Secret":"pass"})", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.get("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();

  EXPECT_FALSE(invoker.erase("{" + spec + "}", &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, invalid_json_input_wrong_type_secret) {
  const std::string error_message = R"("Secret" should be a string)";
  auto &invoker = tester.get_invoker();
  std::string output;
  std::string spec =
      R"({"ServerURL":"user@host","SecretType":"password","Secret":false})";

  EXPECT_FALSE(invoker.store(spec, &output));
  EXPECT_THAT(output, ::testing::HasSubstr(error_message));
  output.clear();
}

TEST_P(Helper_executable_test, version_command) {
  auto &invoker = tester.get_invoker();
  std::string output;
  EXPECT_TRUE(invoker.version(&output));
  EXPECT_THAT(output, ::testing::HasSubstr(shcore::get_long_version()));
  output.clear();
}

REGISTER_TESTS(Helper_executable_test);

}  // namespace tests
