/* Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.

 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 also distributed 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 included with MySQL.
 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 <algorithm>
#include <functional>
#include "mysqlshdk/libs/db/connection_options.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "unittest/gtest_clean.h"

#define MY_EXPECT_THROW(e, m, c)         \
  EXPECT_THROW(                          \
      {                                  \
        try {                            \
          c;                             \
        } catch (const e &error) {       \
          EXPECT_STREQ(m, error.what()); \
          throw;                         \
        }                                \
      },                                 \
      e)

using mysqlshdk::db::Connection_options;
using mysqlshdk::db::Transport_type;
using mysqlshdk::db::uri_connection_attributes;
using mysqlshdk::utils::nullable_options::Comparison_mode;
namespace testing {

TEST(Connection_options, default_initialization) {
  Connection_options options;

  ASSERT_FALSE(options.has_scheme());
  ASSERT_FALSE(options.has_user());
  ASSERT_FALSE(options.has_password());
  ASSERT_FALSE(options.has_host());
  ASSERT_FALSE(options.has_port());
  ASSERT_FALSE(options.has_schema());
  ASSERT_FALSE(options.has_socket());
  ASSERT_FALSE(options.has_pipe());
  ASSERT_FALSE(options.has_transport_type());
  ASSERT_FALSE(options.get_ssl_options().has_data());
  ASSERT_FALSE(options.has_compression());
  ASSERT_FALSE(options.has_compression_algorithms());
  ASSERT_FALSE(options.has_compression_level());

  // has verifies the existence of the option
  EXPECT_TRUE(options.has(mysqlshdk::db::kScheme));
  EXPECT_TRUE(options.has(mysqlshdk::db::kUser));
  EXPECT_TRUE(options.has(mysqlshdk::db::kPassword));
  EXPECT_TRUE(options.has(mysqlshdk::db::kHost));
  EXPECT_TRUE(options.has(mysqlshdk::db::kPort));
  EXPECT_TRUE(options.has(mysqlshdk::db::kSchema));
  EXPECT_TRUE(options.has(mysqlshdk::db::kSocket));
  EXPECT_TRUE(options.has(mysqlshdk::db::kSocket));

  // has_value verifies the existence of a value for the option
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kSchema));
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kUser));
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kPassword));
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kHost));
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kPort));
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kSchema));
  EXPECT_FALSE(options.has_value(mysqlshdk::db::kSocket));
}

TEST(Connection_options, scheme_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_scheme("mysql"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kScheme);
  msg.append("' is already defined as 'mysql'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_scheme("mysqlx"));
  EXPECT_TRUE(options.has_scheme());
  EXPECT_STREQ("mysql", options.get_scheme().c_str());
  EXPECT_NO_THROW(options.clear_scheme());
  EXPECT_FALSE(options.has_scheme());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kScheme).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_scheme());
}

TEST(Connection_options, user_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_user("value"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kUser);
  msg.append("' is already defined as 'value'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_user("value1"));
  EXPECT_TRUE(options.has_user());
  EXPECT_STREQ("value", options.get_user().c_str());
  EXPECT_NO_THROW(options.clear_user());
  EXPECT_FALSE(options.has_user());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kUser).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_user());
}

TEST(Connection_options, password_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_password("value"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kPassword);
  msg.append("' is already defined as 'value'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_password("value1"));
  EXPECT_TRUE(options.has_password());
  EXPECT_STREQ("value", options.get_password().c_str());
  EXPECT_NO_THROW(options.clear_password());
  EXPECT_FALSE(options.has_password());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kPassword).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_password());
}

TEST(Connection_options, host_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_host("value"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kHost);
  msg.append("' is already defined as 'value'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_host("value1"));
  EXPECT_TRUE(options.has_host());
  EXPECT_STREQ("value", options.get_host().c_str());
  EXPECT_NO_THROW(options.clear_host());
  EXPECT_FALSE(options.has_host());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kHost).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_host());
}

TEST(Connection_options, port_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_port(3306));

  msg =
      "Unable to set a tcp connection to port '3307', a tcp connection to "
      "port '3306' is already defined.";
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.set_port(3307));
  EXPECT_TRUE(options.has_port());
  EXPECT_EQ(3306, options.get_port());
  EXPECT_NO_THROW(options.clear_port());
  EXPECT_FALSE(options.has_port());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kPort).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_port());
}

TEST(Connection_options, schema_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_schema("value"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kSchema);
  msg.append("' is already defined as 'value'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_schema("value1"));
  EXPECT_TRUE(options.has_schema());
  EXPECT_STREQ("value", options.get_schema().c_str());
  EXPECT_NO_THROW(options.clear_schema());
  EXPECT_FALSE(options.has_schema());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kSchema).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_schema());
}

TEST(Connection_options, socket_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_socket("value"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kSocket);
  msg.append("' is already defined as 'value'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_socket("value1"));
  EXPECT_TRUE(options.has_socket());
  EXPECT_STREQ("value", options.get_socket().c_str());
  EXPECT_NO_THROW(options.clear_socket());
  EXPECT_FALSE(options.has_socket());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kSocket).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_socket());
}

TEST(Connection_options, pipe_functions) {
  Connection_options options;
  std::string msg;

  EXPECT_NO_THROW(options.set_pipe("value"));
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kSocket);
  msg.append("' is already defined as 'value'.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                  options.set_pipe("value1"));
  EXPECT_TRUE(options.has_pipe());
  EXPECT_STREQ("value", options.get_pipe().c_str());
  EXPECT_NO_THROW(options.clear_pipe());
  EXPECT_FALSE(options.has_pipe());
  msg = "The connection option '";
  msg.append(mysqlshdk::db::kSocket).append("' has no value.");
  MY_EXPECT_THROW(std::invalid_argument, msg.c_str(), options.get_pipe());
}

TEST(Connection_options, uri_constructor) {
  const std::string hosts[][2] = {{"localhost", "localhost"},
                                  {"[::1]", "::1"},
                                  {"[fe80::850a:5a7c:6ab7:aec4%25enp0s3]",
                                   "fe80::850a:5a7c:6ab7:aec4%enp0s3"}};

  for (const auto &test : hosts) {
    std::string uri = "mysqlx://user:password@" + test[0] +
                      ":3300/"
                      "myschema?ssl-mode=REQUIRED";

    SCOPED_TRACE(uri);

    mysqlshdk::db::Connection_options data(uri);

    ASSERT_TRUE(data.has_scheme());
    ASSERT_STREQ("mysqlx", data.get_scheme().c_str());

    ASSERT_TRUE(data.has_user());
    ASSERT_STREQ("user", data.get_user().c_str());

    ASSERT_TRUE(data.has_password());
    ASSERT_STREQ("password", data.get_password().c_str());

    ASSERT_TRUE(data.has_host());
    ASSERT_EQ(test[1], data.get_host());

    ASSERT_TRUE(data.has_port());
    ASSERT_EQ(3300, data.get_port());

    ASSERT_TRUE(data.has_schema());
    ASSERT_STREQ("myschema", data.get_schema().c_str());

    ASSERT_FALSE(data.has_socket());
    ASSERT_FALSE(data.has_pipe());
    ASSERT_TRUE(data.get_ssl_options().has_data());

    auto ssl_options = data.get_ssl_options();
    ASSERT_TRUE(ssl_options.has_mode());
    ASSERT_EQ(mysqlshdk::db::Ssl_mode::Required, ssl_options.get_mode());
    ASSERT_FALSE(ssl_options.has_ca());
    ASSERT_FALSE(ssl_options.has_capath());
    ASSERT_FALSE(ssl_options.has_cert());
    ASSERT_FALSE(ssl_options.has_cipher());
    ASSERT_FALSE(ssl_options.has_crl());
    ASSERT_FALSE(ssl_options.has_crlpath());
    ASSERT_FALSE(ssl_options.has_key());
    ASSERT_FALSE(ssl_options.has_tls_version());

    ASSERT_EQ(uri, data.as_uri(mysqlshdk::db::uri::formats::full()));
  }
}

void combine(
    const std::string &input,
    std::function<void(const std::string &, const std::string)> callback) {
  std::string my_string(input);

  callback(input, my_string);

  // Turn first character of each word to uppercase
  bool convert = true;
  for (size_t index = 0; index < my_string.size(); index++) {
    if (convert) {
      my_string[index] = std::toupper(my_string[index]);
      convert = false;
      callback(input, my_string);
    } else {
      convert = my_string[index] == '-';
    }
  }

  // Now try with all characters uppercase
  my_string = shcore::str_upper(my_string);
  callback(input, my_string);
}

namespace case_insensitive {
auto callback = [](const std::string &property, const std::string &twisted,
                   const std::string &arg_value) {
  std::string uri = "mysql://root@host?" + twisted + "=" + arg_value;
  mysqlshdk::db::Connection_options data(uri);
  if (!data.has(property)) {
    std::string failed = "Failed Property: " + twisted;
    SCOPED_TRACE(failed);
    ADD_FAILURE();
  } else {
    std::string value = data.get(property);
    std::string failed = "Unexpected Value: " + value;
    SCOPED_TRACE(failed);
    EXPECT_STREQ(arg_value.c_str(), value.c_str());
  }
};
}  // namespace case_insensitive

TEST(Connection_options, case_insensitive_ssl_mode) {
  // This callback will get called with every combination of
  // uppercase/lowercase letters on ssl-mode
  auto callback = std::bind(case_insensitive::callback, std::placeholders::_1,
                            std::placeholders::_2, "REQUIRED");

  combine(mysqlshdk::db::kSslMode, callback);
}

TEST(Connection_options, case_insensitive_get_server_public_key) {
  // This callback will get called with every combination of
  // uppercase/lowercase letters on get-server-public-key
  auto callback = std::bind(case_insensitive::callback, std::placeholders::_1,
                            std::placeholders::_2, "true");

  combine(mysqlshdk::db::kGetServerPublicKey, callback);
}

TEST(Connection_options, case_insensitive_server_public_key_path) {
  // This callback will get called with every combination of
  // uppercase/lowercase letters on server-public-key-path
  auto callback = std::bind(case_insensitive::callback, std::placeholders::_1,
                            std::placeholders::_2, "pubkey.pem");

  combine(mysqlshdk::db::kServerPublicKeyPath, callback);
}

TEST(Connection_options, case_insensitive_options) {
  // This callback will get called with every combination of
  // uppercase/lowercase letters for each property
  auto callback = std::bind(case_insensitive::callback, std::placeholders::_1,
                            std::placeholders::_2, "whatever");

  std::set<std::string> attributes(uri_connection_attributes.begin(),
                                   uri_connection_attributes.end());

  // handled by other, more specific tests
  attributes.erase(mysqlshdk::db::kSslMode);
  attributes.erase(mysqlshdk::db::kGetServerPublicKey);
  attributes.erase(mysqlshdk::db::kServerPublicKeyPath);
  attributes.erase(mysqlshdk::db::kConnectTimeout);
  attributes.erase(mysqlshdk::db::kCompression);
  attributes.erase(mysqlshdk::db::kCompressionLevel);
  attributes.erase(mysqlshdk::db::kConnectionAttributes);

  for (auto property : attributes) {
    combine(property, callback);
  }
}

TEST(Connection_options, connect_timeout) {
  // Tests case insensitiveness of the connect-timeout option
  // This callback will get called with every combination of
  // uppercase/lowercase letters for connect-timeout
  auto callback = std::bind(case_insensitive::callback, std::placeholders::_1,
                            std::placeholders::_2, "1000");

  combine(mysqlshdk::db::kConnectTimeout, callback);

  // Test rejection of invalid values
  auto invalid_values = {"-1", "-100", "10.0", "whatever"};

  for (const auto &value : invalid_values) {
    std::string uri("root@host?connect-timeout=");
    uri.append(value);

    std::string msg("Invalid value '");
    msg.append(value);
    msg.append(
        "' for 'connect-timeout'. The connection timeout value must "
        "be a positive integer (including 0).");

    std::string uri_msg("Invalid URI: ");
    uri_msg.append(msg);
    MY_EXPECT_THROW(std::invalid_argument, uri_msg.c_str(),
                    { mysqlshdk::db::Connection_options data(uri); });

    mysqlshdk::db::Connection_options sample;
    MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                    sample.set(mysqlshdk::db::kConnectTimeout, value));
  }

  // Tests acceptance of valid values
  auto valid_values = {"0", "1000", "10000"};

  for (const auto &value : valid_values) {
    std::string uri("root@host?connect-timeout=");
    uri.append(value);

    EXPECT_NO_THROW({ mysqlshdk::db::Connection_options data(uri); });

    mysqlshdk::db::Connection_options sample;
    EXPECT_NO_THROW(sample.set(mysqlshdk::db::kConnectTimeout, value));
  }
}

TEST(Connection_options, compression) {
  // Tests case insensitiveness of the compression option
  // This callback will get called with every combination of
  // uppercase/lowercase letters for connect-timeout
  auto callback = std::bind(case_insensitive::callback, std::placeholders::_1,
                            std::placeholders::_2, "REQUIRED");

  combine(mysqlshdk::db::kCompression, callback);

  // Test rejection of invalid values
  auto invalid_values = {"-1", "-100", "10.0", "whatever"};

  for (const auto &value : invalid_values) {
    std::string uri("root@host?compression=");
    uri.append(value);

    std::string msg("Invalid value '");
    msg.append(value);
    msg.append(
        "' for 'compression'. Allowed values: 'REQUIRED', 'PREFERRED', "
        "'DISABLED', 'True', 'False', '1', and '0'.");

    std::string uri_msg("Invalid URI: ");
    uri_msg.append(msg);
    MY_EXPECT_THROW(std::invalid_argument, uri_msg.c_str(),
                    { mysqlshdk::db::Connection_options data(uri); });

    mysqlshdk::db::Connection_options sample;
    MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                    sample.set(mysqlshdk::db::kCompression, value));
  }

  // Tests acceptance of valid values
  std::vector<std::string> valid_values = {
      "1", "true", "0", "false", "required", "Disabled", "PREFERRED"};

  for (const auto &value : valid_values) {
    std::string uri("root@host?compression=");
    uri.append(value);

    EXPECT_NO_THROW({
      mysqlshdk::db::Connection_options data(uri);
      try {
        if (shcore::lexical_cast<bool>(value))
          EXPECT_EQ("REQUIRED", data.get_compression());
        else
          EXPECT_EQ("DISABLED", data.get_compression());
      } catch (...) {
        EXPECT_EQ(shcore::str_upper(value), data.get_compression());
      }
    });

    mysqlshdk::db::Connection_options sample;
    EXPECT_NO_THROW(sample.set(mysqlshdk::db::kCompression, {value}));
  }
}

TEST(Connection_options, compression_level) {
  auto invalid_values = {"-1e", "-0.5", "10.0", "whatever"};

  for (const auto &value : invalid_values) {
    std::string uri("root@host?compression-level=");
    uri.append(value);

    std::string msg = "The value of 'compression-level' must be an integer.";
    std::string uri_msg("Invalid URI: ");
    uri_msg.append(msg);
    MY_EXPECT_THROW(std::invalid_argument, uri_msg.c_str(),
                    { mysqlshdk::db::Connection_options data(uri); });

    mysqlshdk::db::Connection_options sample;
    MY_EXPECT_THROW(std::invalid_argument, msg.c_str(),
                    sample.set(mysqlshdk::db::kCompressionLevel, value));
  }

  // Tests acceptance of valid values
  std::vector<std::string> valid_values = {"-10", "1", "7", "22", "200"};

  for (const auto &value : valid_values) {
    std::string uri("root@host?compression-level=");
    uri.append(value);

    EXPECT_NO_THROW({
      mysqlshdk::db::Connection_options data(uri);
      EXPECT_EQ(shcore::lexical_cast<int>(value), data.get_compression_level());
    });

    mysqlshdk::db::Connection_options sample;
    EXPECT_NO_THROW(sample.set(mysqlshdk::db::kCompressionLevel, {value}));
  }
}

TEST(Connection_options, set_host) {
  // localhost does not determine the session type
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("localhost");

    EXPECT_TRUE(data.has_host());
    EXPECT_STREQ("localhost", data.get_host().c_str());
    EXPECT_FALSE(data.has_transport_type());
  }

  // Any other host would set the connection type to tcp
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("127.0.0.1");

    EXPECT_TRUE(data.has_host());
    EXPECT_TRUE(data.has_transport_type());
    EXPECT_STREQ("127.0.0.1", data.get_host().c_str());
    EXPECT_EQ(Transport_type::Tcp, data.get_transport_type());
  }

  // Setting host causes no error if i.e. port was set before
  {
    mysqlshdk::db::Connection_options data;
    data.set_port(3306);
    data.set_host("localhost");

    EXPECT_TRUE(data.has_host());
    EXPECT_STREQ("localhost", data.get_host().c_str());
    EXPECT_EQ(Transport_type::Tcp, data.get_transport_type());
  }

  // Setting host causes error if a socket connection is set
  {
    mysqlshdk::db::Connection_options data;
    data.set_socket("Some/Socket/Path");

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a connection to 'localhost', a socket "
                    "connection to 'Some/Socket/Path' is already defined.",
                    data.set_host("localhost"));
  }

  // Setting host causes error if a pipe connection is set
  {
    mysqlshdk::db::Connection_options data;
    data.set_pipe("PipeName");

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a connection to 'localhost', a pipe "
                    "connection to 'PipeName' is already defined.",
                    data.set_host("localhost"));
  }
}

TEST(Connection_options, set_socket) {
  {
    mysqlshdk::db::Connection_options data;
    data.set_socket("/Path/To/Socket");

    EXPECT_TRUE(data.has_socket());
    EXPECT_STREQ("/Path/To/Socket", data.get_socket().c_str());
    EXPECT_TRUE(data.has_transport_type());
    EXPECT_EQ(Transport_type::Socket, data.get_transport_type());
  }

  // Using socket is allowed for localhost
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("localhost");
    EXPECT_NO_THROW(data.set_socket("/Path/To/Socket"));

    EXPECT_TRUE(data.has_host());
    EXPECT_TRUE(data.has_socket());
    EXPECT_TRUE(data.has_transport_type());

    EXPECT_STREQ("localhost", data.get_host().c_str());
    EXPECT_STREQ("/Path/To/Socket", data.get_socket().c_str());
    EXPECT_EQ(Transport_type::Socket, data.get_transport_type());
  }

  // Using socket is not allowed for localhost is port was defined
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("localhost");
    data.set_port(3307);

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a socket connection to '/Path/To/Socket', "
                    "a tcp connection to 'localhost:3307' is already defined.",
                    data.set_socket("/Path/To/Socket"));
  }

  // Using socket is not allowed for 127.0.0.1
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("127.0.0.1");
    EXPECT_THROW(data.set_socket("/Path/To/Socket"), std::invalid_argument);

    EXPECT_TRUE(data.has_host());
    EXPECT_FALSE(data.has_socket());
    EXPECT_TRUE(data.has_transport_type());

    EXPECT_STREQ("127.0.0.1", data.get_host().c_str());
    EXPECT_EQ(Transport_type::Tcp, data.get_transport_type());
  }

  // Using socket is not allowed for 127.0.0.1 is port was defined
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("127.0.0.1");
    data.set_port(3307);

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a socket connection to '/Path/To/Socket', "
                    "a tcp connection to '127.0.0.1:3307' is already defined.",
                    data.set_socket("/Path/To/Socket"));
  }

  // Error trying to set a socket when a host was previously set
  {
    mysqlshdk::db::Connection_options data;
    data.set_host("192.168.1.110");

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a socket connection to '/Path/To/Socket', "
                    "a tcp connection to '192.168.1.110' is already defined.",
                    data.set_socket("/Path/To/Socket"));
  }

  // Error trying to set socket when a port was previously set
  {
    mysqlshdk::db::Connection_options data;
    data.set_port(3307);

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a socket connection to '/Path/To/Socket', "
                    "a tcp connection to port '3307' is already defined.",
                    data.set_socket("/Path/To/Socket"));
  }

  // Error trying to set a socket when a pipe was previously set
  {
    mysqlshdk::db::Connection_options data;
    data.set_pipe("PipeName");

    MY_EXPECT_THROW(std::invalid_argument,
                    "Unable to set a socket connection to '/Path/To/Socket', "
                    "a pipe connection to 'PipeName' is already defined.",
                    data.set_socket("/Path/To/Socket"));
  }
}

TEST(Connection_options, case_insensitive_duplicated_attribute) {
  // This callback will get called with every combination of
  // uppercase/lowercase letters on ssl-mode
  auto callback = [](const std::string & /* property */,
                     const std::string &twisted) {
    mysqlshdk::db::Connection_options data;
    data.set(mysqlshdk::db::kAuthMethod, "Value");

    std::string message = "The option '" + twisted +
                          "' is already defined as "
                          "'Value'.";

    MY_EXPECT_THROW(std::invalid_argument, message.c_str(),
                    data.set(twisted, "Whatever"));
  };

  combine(mysqlshdk::db::kAuthMethod, callback);
}

TEST(Connection_options, invalid_options_after_WL10912) {
  // This callback will get called with every combination of
  // uppercase/lowercase letters for each property
  auto callback = [](const std::string & /* property */,
                     const std::string &twisted) {
    std::string uri = "mysql://root@host?" + twisted + "=whatever";
    std::string message =
        "Invalid URI: Invalid connection option '" + twisted + "'.";

    MY_EXPECT_THROW(std::invalid_argument, message.c_str(),
                    mysqlshdk::db::Connection_options data(uri));
  };

  std::set<std::string> invalid_options = {
      "sslMode",   "sslCa",   "sslCaPath", "sslCrl",     "sslCrlPath",
      "sslCipher", "sslCert", "sslKey",    "authMethod", "sslTlsVersion"};

  for (auto property : invalid_options) combine(property, callback);
}

}  // namespace testing
