/*
 * Copyright (c) 2022, 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 "unittest/gprod_clean.h"

#include "mysqlshdk/libs/azure/signer.h"
#include "mysqlshdk/libs/utils/utils_file.h"

#include <set>
#include <string>

#include "mysqlshdk/libs/utils/utils_string.h"
#include "unittest/test_utils/mod_testutils.h"

#include "unittest/mysqlshdk/libs/azure/azure_tests.h"

namespace mysqlshdk {
namespace azure {

class Azure_signer_test : public testing::Test {
 public:
  static void SetUpTestCase() {
    shcore::create_file(
        k_config_file,
        "[storage]\nconnection_string="
        "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey="
        "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/"
        "K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/"
        "devstoreaccount1;",
        false);
  }

  static void TearDownTestCase() { shcore::delete_file(k_config_file); }

  void SetUp() override {
    const auto options = shcore::make_dict(
        Blob_storage_options::container_name_option(), "testcontainer",
        Blob_storage_options::storage_account_option(), "devstoreaccount1",
        Blob_storage_options::config_file_option(), k_config_file);

    m_options = std::make_shared<Blob_storage_options>(
        mysqlshdk::azure::Blob_storage_options::Operation::WRITE);
    Blob_storage_options::options().unpack(options, m_options.get());
    m_config = std::make_shared<Blob_storage_config>(*m_options.get(), false);
    m_signer = m_config->signer();
    m_azure_signer = dynamic_cast<mysqlshdk::azure::Signer *>(m_signer.get());
  }

 protected:
  std::shared_ptr<Blob_storage_options> m_options;
  std::shared_ptr<Blob_storage_config> m_config;
  std::unique_ptr<rest::Signer> m_signer;
  mysqlshdk::azure::Signer *m_azure_signer;

  // Friday, 24 May 2013 00:00:00
  static constexpr time_t k_now = 1369353600;
  static constexpr char k_authorization[] = "authorization";
  static constexpr char k_x_ms_date[] = "x-ms-date";
  static constexpr char k_x_ms_version[] = "x-ms-version";
  static constexpr char k_x_ms_date_value[] = "Fri, 24 May 2013 00:00:00 GMT";
  static constexpr char k_x_ms_version_value[] = "2021-08-06";
  static constexpr char k_config_file[] = "azure_signer.cfg";

  std::string missing_header(const std::string &name) {
    return shcore::str_format("Missing '%s header.", name.c_str());
  }

  std::string mismatched_header(const std::string &name,
                                const std::string &expected,
                                const std::string &actual) {
    return shcore::str_format(
        "Mismatched '%s header.\n-- Expecting: '%s'\n-- Actual: '%s'",
        name.c_str(), expected.c_str(), actual.c_str());
  }

  void test_sign_request(const std::string &context,
                         const rest::Signed_request *request,
                         const std::string &signature,
                         const std::string &expected_signature_prefix,
                         const std::string &canonical_resource,
                         const std::string &canonical_headers =
                             "x-ms-date:Fri, 24 May 2013 00:00:00 "
                             "GMT\nx-ms-version:2021-08-06\n") {
    // Verifies the signature headers
    SCOPED_TRACE(context.c_str());
    auto rheaders = m_azure_signer->get_required_headers(request, k_now);
    EXPECT_EQ(canonical_headers,
              m_azure_signer->get_canonical_headers(rheaders));

    // Verifies the signature result
    EXPECT_EQ(canonical_resource,
              m_azure_signer->get_canonical_resource(request));

    auto string_to_sign = m_azure_signer->get_string_to_sign(request, rheaders);
    auto actual_signature_prefix = string_to_sign.substr(
        0, string_to_sign.size() -
               (canonical_headers.size() + canonical_resource.size()));
    EXPECT_EQ(expected_signature_prefix, actual_signature_prefix);

    const auto signer_headers = m_azure_signer->sign_request(request, k_now);

    ASSERT_NE(signer_headers.end(), signer_headers.find(k_authorization));
    EXPECT_EQ(signature, signer_headers.at(k_authorization));

    ASSERT_NE(signer_headers.end(), signer_headers.find(k_x_ms_date));
    EXPECT_EQ(k_x_ms_date_value, signer_headers.at(k_x_ms_date));

    ASSERT_NE(signer_headers.end(), signer_headers.find(k_x_ms_version));
    EXPECT_EQ(k_x_ms_version_value, signer_headers.at(k_x_ms_version));

    for (const auto &header : request->headers()) {
      ASSERT_NE(signer_headers.end(), signer_headers.find(header.first));
      EXPECT_EQ(header.second, signer_headers.at(header.first));
    }
  }
};

TEST_F(Azure_signer_test, azure_requests) {
  Blob_container container(m_config);

  auto request = container.list_objects_request("", 0, true, {}, "");
  request.type = mysqlshdk::rest::Type::GET;
  test_sign_request(
      "LIST OBJECTS", &request,
      "SharedKey "
      "devstoreaccount1:EAGKNX/hpcvYJj5Zyoe8zNu/DtS7ChpE4STrNGkaDXA=",
      "GET\n\n\n\n\n\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/"
      "testcontainer\ncomp:list\nrestype:container");

  request = container.head_object_request("sample.txt");
  request.type = mysqlshdk::rest::Type::HEAD;
  test_sign_request(
      "HEAD OBJECT", &request,
      "SharedKey "
      "devstoreaccount1:Hdob74EiTY2Kkw19XLF2RTX8EnFpxgeG2xB3vwrKeyU=",
      "HEAD\n\n\n\n\n\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/sample.txt");

  std::string data = "sample.txt";
  request = container.put_object_request(
      "sample.txt", {{"x-ms-blob-type", "BlockBlob"},
                     {"content-type", "application/octet-stream"}});
  request.type = mysqlshdk::rest::Type::PUT;
  request.body = data.data();
  request.size = data.size();
  test_sign_request(
      "PUT OBJECT", &request,
      "SharedKey "
      "devstoreaccount1:g4WMGJTsSBzqltLGJTX7/iVF5YL0bqHhbmjDUkLFZy8=",
      "PUT\n\n\n10\n\napplication/octet-stream\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/sample.txt",
      "x-ms-blob-type:BlockBlob\nx-ms-date:Fri, 24 May 2013 00:00:00 "
      "GMT\nx-ms-version:2021-08-06\n");

  request = container.delete_object_request("sample.txt");
  request.type = mysqlshdk::rest::Type::DELETE;
  test_sign_request(
      "DELETE OBJECT", &request,
      "SharedKey "
      "devstoreaccount1:mT3CMI0wxLRWM6w/gXp85tzJKqjkQfsof045ht2OkMs=",
      "DELETE\n\n\n\n\napplication/x-www-form-urlencoded\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/sample.txt");

  request = container.get_object_request("sample.txt", {});
  request.type = mysqlshdk::rest::Type::GET;
  test_sign_request(
      "GET OBJECT", &request,
      "SharedKey "
      "devstoreaccount1:kZ3c3DgrUmJHuRDBnAJJPNvXjKR7Tqfkxk286v3E2D8=",
      "GET\n\n\n\n\n\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/sample.txt");

  request = container.get_object_request("sample.txt", {{"range", "bytes=5-"}});
  request.type = mysqlshdk::rest::Type::GET;
  test_sign_request(
      "GET OBJECT FROM BYTE", &request,
      "SharedKey "
      "devstoreaccount1:6QOWvxuB39OLiIZ9KqsXSns8Rx7T+KSXmA94JjbmnL4=",
      "GET\n\n\n\n\n\n\n\n\n\n\nbytes=5-\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/sample.txt");

  request =
      container.get_object_request("sample.txt", {{"range", "bytes=5-10"}});
  request.type = mysqlshdk::rest::Type::GET;
  test_sign_request(
      "GET OBJECT RANGE", &request,
      "SharedKey "
      "devstoreaccount1:S3rtRFTog4bFkOzIz5lN2q9ErnDCOCX8VphDdTVXekE=",
      "GET\n\n\n\n\n\n\n\n\n\n\nbytes=5-10\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/sample.txt");

  request = container.list_multipart_uploads_request(0);
  request.type = mysqlshdk::rest::Type::GET;
  test_sign_request(
      "LIST MULTIPART UPLOADS", &request,
      "SharedKey "
      "devstoreaccount1:nKXEe6KsgaLAwUBrWi01Xa17WFQAaLHFUCJLxGRivgs=",
      "GET\n\n\n\n\n\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/"
      "testcontainer\ncomp:list\ninclude:metadata,uncommittedblobs\nrestype:"
      "container");

  data = "-";
  request = container.create_multipart_upload_request("sample.txt", &data);
  request.type = mysqlshdk::rest::Type::PUT;
  test_sign_request(
      "CREATE MULTIPART UPLOAD", &request,
      "SharedKey "
      "devstoreaccount1:fNOEmX/yjNv36GVD23riZDiWFLYBAs+tSH6GJ3irj4Y=",
      "PUT\n\n\n1\n\napplication/octet-stream\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/"
      "sample.txt\nblockid:MDAwMDA=\ncomp:block");

  mysqlshdk::storage::backend::object_storage::Multipart_object object;
  object.name = "sample.txt";
  data = "sample.txt";
  request = container.upload_part_request(object, 1, data.size());
  request.body = data.data();
  request.size = data.size();
  request.type = mysqlshdk::rest::Type::PUT;
  test_sign_request(
      "UPLOAD PART", &request,
      "SharedKey "
      "devstoreaccount1:yOkmxDsgVv9JrjcFXDVB5ixPla63dWyhIMd55QTi1+0=",
      "PUT\n\n\n10\n\napplication/octet-stream\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/"
      "sample.txt\nblockid:MDAwMDE=\ncomp:block");

  request = container.list_multipart_uploaded_parts_request(object, 0);
  request.type = mysqlshdk::rest::Type::GET;
  test_sign_request(
      "LIST MULTIPART UPLOADED PARTS", &request,
      "SharedKey "
      "devstoreaccount1:I75Y7dw6PJ4M0i9AI53WSgFlC3eScA1QWP2WksxeYvE=",
      "GET\n\n\n\n\n\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/"
      "sample.txt\nblocklisttype:uncommitted\ncomp:blocklist");

  request = container.commit_multipart_upload_request(object, {}, &data);
  request.body = data.c_str();
  request.size = data.size();
  request.type = mysqlshdk::rest::Type::PUT;
  test_sign_request(
      "COMMIT MULTIPART UPLOAD", &request,
      "SharedKey "
      "devstoreaccount1:cPvaqQpFSdLcQ3sfZ3qKL198n6tanURxvOLtx/UZoL8=",
      "PUT\n\n\n50\n\ntext/plain\n\n\n\n\n\n\n",
      "/devstoreaccount1/devstoreaccount1/testcontainer/"
      "sample.txt\ncomp:blocklist");
}

}  // namespace azure
}  // namespace mysqlshdk
