/*
 * Copyright (c) 2017, 2026, 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 "mysqlshdk/libs/db/mysql/session.h"
#include "mysqlshdk/libs/db/mysqlx/session.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "unittest/mysqlshdk/libs/db/db_common.h"
#include "unittest/test_utils.h"

namespace mysqlshdk {
namespace db {

std::shared_ptr<mysqlshdk::db::ISession> Db_tests::make_session() {
  if (is_classic)
    return mysqlshdk::db::mysql::Session::create();
  else
    return mysqlshdk::db::mysqlx::Session::create();
}

void Db_tests::TearDownTestCase() { run_script_classic({"drop schema xtest"}); }

void Db_tests::SetUp() {
  is_classic = true;
  Shell_test_env::SetUp();
  std::cout << "Testing classic\n";
  session = mysqlshdk::db::mysql::Session::create();
}

bool Db_tests::switch_proto() {
  if (is_classic) {
    is_classic = false;
    std::cout << "Testing X proto\n";
    session = mysqlshdk::db::mysqlx::Session::create();
    return true;
  }
  return false;
}

TEST_F(Db_tests, connect_uri) {
  do {
    SCOPED_TRACE(is_classic ? "mysql" : "mysqlx");

    // Connection failure
    {
      auto connection_options = mysqlshdk::db::Connection_options(uri_nopass());
      connection_options.set_password("fake_pwd");

      EXPECT_THROW(session->connect(connection_options), std::exception);
    }

    // Success connection, no schema selected
    {
      auto connection_options = mysqlshdk::db::Connection_options(uri());

      ASSERT_NO_THROW(session->connect(connection_options));
      auto result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query("select database()"));
      auto row = result->fetch_one();
      EXPECT_TRUE(row->is_null(0));
      session->close();
    }

    // Success connection, default schema
    {
      auto connection_options =
          mysqlshdk::db::Connection_options(uri() + "/mysql");
      ASSERT_NO_THROW(session->connect(connection_options));
      auto result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query("select database()"));
      auto row = result->fetch_one();
      EXPECT_FALSE(row->is_null(0));
      EXPECT_EQ("mysql", row->get_string(0));
      session->close();
    }
  } while (switch_proto());
}

TEST_F(Db_tests, execute) {
  do {
    SCOPED_TRACE(is_classic ? "mysql" : "mysqlx");
    auto connection_options = mysqlshdk::db::Connection_options(uri());
    EXPECT_NO_THROW(session->connect(connection_options));

    // Execute error, trying to use an unexisting schema
    EXPECT_THROW(session->execute("use some_weird_schema"), std::exception);

    // Execute success, use a valid schema
    EXPECT_NO_THROW(session->execute("use mysql"));

    session->close();
  } while (switch_proto());
}

TEST_F(Db_tests, query) {
  do {
    SCOPED_TRACE(is_classic ? "mysql" : "mysqlx");
    auto connection_options = mysqlshdk::db::Connection_options(uri());

    EXPECT_NO_THROW(session->connect(connection_options));

    // Query error, trying to retrieve an unexisting system variable
    EXPECT_THROW(session->query("select @@some_weird_variable"),
                 std::exception);

    // Query success, retrieving a valid system variable
    {
      auto result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query("select @@server_id"));
    }

    // Query success, retrieving a valid system variable, again
    {
      auto result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query("select @@server_id"));
    }

    // Query success, using buffered result
    {
      auto result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query(std::string("select @@server_id"), true));
    }

    // Required test database for the rest of this test
    EXPECT_NO_THROW(session->execute("drop schema if exists Db_sessions"));
    EXPECT_NO_THROW(session->execute("create schema Db_sessions"));
    EXPECT_NO_THROW(session->execute("use Db_sessions"));

    // Uses a stored procedure to test multi result behavior
    {
      std::string procedure =
          "create procedure test_query() begin select @@server_id; select "
          "@@server_uuid; end";
      EXPECT_NO_THROW(session->execute(procedure));
      auto result = session->query("call test_query()");
      EXPECT_TRUE(result->next_resultset());
      EXPECT_TRUE(result->next_resultset());
      EXPECT_FALSE(result->next_resultset());
    }

    // Pulls multiple results and ensures they are discarded
    // if a new execution occurs
    if (is_classic) {
      // TODO(alfredo) ensure common behaviour
      // at this layer, the pending resultset should not be discarded (or
      // cached) but rather throw an exception
      auto result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query("call test_query()"));
      result = std::shared_ptr<mysqlshdk::db::IResult>(
          session->query("select current_user()"));
      auto row = result->fetch_one();
      std::string user = _user + "@";
      EXPECT_EQ(row->get_string(0).find(user), 0);
    } else {
      PENDING_BUG_TEST(
          "Need to ensure proper behavior for non-consumed results");
    }

    EXPECT_NO_THROW(session->execute("drop schema Db_sessions"));

    session->close();
  } while (switch_proto());
}

TEST_F(Db_tests, auto_close) {
  do {
    SCOPED_TRACE(is_classic ? "mysql" : "mysqlx");
    ASSERT_NO_THROW(session->connect(Connection_options(uri())));

    auto result = session->query(
        "select count(*) from performance_schema.threads where "
        "PROCESSLIST_USER IS NOT NULL");
    int ocount = result->fetch_one()->get_int(0);

    {
      auto connection_options = mysqlshdk::db::Connection_options(uri());
      auto mysql_session = make_session();
      EXPECT_NO_THROW(mysql_session->connect(connection_options));

      EXPECT_BECOMES_TRUE(
          3,
          (ocount + 1) ==
              session
                  ->query(
                      "select count(*) from performance_schema.threads where "
                      "PROCESSLIST_USER IS NOT NULL")
                  ->fetch_one_or_throw()
                  ->get_int(0));
    }

    bool ok = false;
    int count = 0;
    for (int i = 0; i < 10; i++) {
      count = session
                  ->query(
                      "select count(*) from performance_schema.threads where "
                      "PROCESSLIST_USER IS NOT NULL")
                  ->fetch_one()
                  ->get_int(0);
      if (count == ocount) {
        ok = true;
        break;
      }
      shcore::sleep_ms(1000);
    }
    if (!ok) {
      EXPECT_EQ(ocount, count);
    }
  } while (switch_proto());
}

TEST_F(Db_tests, connect_read_timeout) {
  auto connection_options = mysqlshdk::db::Connection_options(uri());

  connection_options.set_unchecked("net-read-timeout", "1000");

  EXPECT_NO_THROW(session->connect(connection_options));

  EXPECT_NO_THROW(session->execute("SELECT SLEEP(0)"));

  EXPECT_THROW_LIKE(session->execute("SELECT SLEEP(2)"), mysqlshdk::db::Error,
                    "Lost connection to MySQL server during query");
}

TEST_F(Db_tests, connect_write_timeout) {
  auto connection_options = mysqlshdk::db::Connection_options(uri());

  connection_options.set_unchecked("net-write-timeout", "500");

  EXPECT_NO_THROW(session->connect(connection_options));

  // no way to easily test this
}

TEST_F(Db_tests, connect_local_infile_reject_policy_is_per_connection) {
  const auto mysql_session =
      std::dynamic_pointer_cast<mysqlshdk::db::mysql::Session>(session);
  ASSERT_NE(nullptr, mysql_session);

  auto reject_options = mysqlshdk::db::Connection_options(uri());
  reject_options.set(mysqlshdk::db::kLocalInfile, "true");
  reject_options.set_reject_local_infile_requests(true);

  ASSERT_NO_THROW(mysql_session->connect(reject_options));
  auto *handle = mysql_session->get_handle();
  ASSERT_NE(nullptr, handle);
  EXPECT_NE(nullptr, handle->options.local_infile_init);
  EXPECT_NE(nullptr, handle->options.local_infile_read);
  EXPECT_NE(nullptr, handle->options.local_infile_end);
  EXPECT_NE(nullptr, handle->options.local_infile_error);
  EXPECT_EQ(nullptr, handle->options.local_infile_userdata);

  mysql_session->close();

  auto regular_options = mysqlshdk::db::Connection_options(uri());
  regular_options.set(mysqlshdk::db::kLocalInfile, "true");

  ASSERT_NO_THROW(mysql_session->connect(regular_options));
  handle = mysql_session->get_handle();
  ASSERT_NE(nullptr, handle);
  EXPECT_EQ(nullptr, handle->options.local_infile_init);
  EXPECT_EQ(nullptr, handle->options.local_infile_read);
  EXPECT_EQ(nullptr, handle->options.local_infile_end);
  EXPECT_EQ(nullptr, handle->options.local_infile_error);
  EXPECT_EQ(nullptr, handle->options.local_infile_userdata);
}

TEST_F(Db_tests, connect_max_allowed_packet) {
  auto connection_options = mysqlshdk::db::Connection_options(uri());

  connection_options.set_unchecked("max-allowed-packet", "4096");
  connection_options.set_unchecked("net-buffer-length", "2048");

  EXPECT_NO_THROW(session->connect(connection_options));

  session->execute("use mysql");
  session->execute(
      "create temporary table big (a int primary key, b longtext)");
  session->execute("insert into big values (1, repeat('x', 10000))");

  auto result = session->query("select * from big");
  EXPECT_THROW_LIKE(result->fetch_one(), mysqlshdk::db::Error,
                    "Got packet bigger than 'max_allowed_packet' bytes");
}

}  // namespace db
}  // namespace mysqlshdk
