/*
 * Copyright 2018-2019 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_digestgenerator.h>

#include <buildboxcommon_temporaryfile.h>
#include <fcntl.h>
#include <gtest/gtest.h>
#include <string>

using namespace buildboxcommon;

using namespace std::literals::string_literals;

namespace {
const auto digestFunctionInitializer = []() {
    buildboxcommon::DigestGenerator::init();
    return 0;
}();
} // namespace

const std::string TEST_STRING =
    "This is a sample blob to hash. \0 It contains some NUL characters "
    "\0."s;

Digest hashTestString(DigestFunction_Value digest_function_value)
{
    auto context = DigestContext::generate(digest_function_value);
    context.update(TEST_STRING.c_str(), TEST_STRING.size());
    return context.finalizeDigest();
}

TEST(DigestGeneratorTest, EmptyString)
{
    const Digest d = DigestGenerator::hash("");
    EXPECT_EQ(d.size_bytes(), 0);
}

TEST(DigestGeneratorTest, FileDescriptor)
{
    // Hashing the file:
    int fd = open("test.txt", O_RDONLY);
    const Digest digest_from_fd = DigestGenerator::hash(fd);
    close(fd);

    // Reading and hashing the contents directly:
    const std::string file_contents =
        buildboxcommon::FileUtils::getFileContents("test.txt");
    const Digest digest_from_string = DigestGenerator::hash(file_contents);

    EXPECT_EQ(digest_from_fd, digest_from_string);
}

TEST(DigestGeneratorTest, PathToFile)
{
    const Digest digest_from_path = DigestGenerator::hashFile("test.txt");

    // Reading and hashing the contents directly:
    const std::string file_contents =
        buildboxcommon::FileUtils::getFileContents("test.txt");
    const Digest digest_from_string = DigestGenerator::hash(file_contents);

    ASSERT_EQ(digest_from_path, digest_from_string);
}

TEST(DigestGeneratorTest, InvalidFileDescriptorFileThrows)
{
    const int invalid_fd = -2;
    ASSERT_THROW(DigestGenerator::hash(invalid_fd), std::runtime_error);
}

TEST(DigestGeneratorTest, stringToDigestFunction)
{
    ASSERT_EQ(DigestGenerator::stringToDigestFunction("SHA256"),
              DigestFunction_Value_SHA256);
    ASSERT_EQ(DigestGenerator::stringToDigestFunction("MD5"),
              DigestFunction_Value_MD5);
    ASSERT_EQ(DigestGenerator::stringToDigestFunction("SHA1"),
              DigestFunction_Value_SHA1);
    ASSERT_THROW(DigestGenerator::stringToDigestFunction("BLAKE3"),
                 std::runtime_error);
    ASSERT_THROW(DigestGenerator::stringToDigestFunction("SHAAA"),
                 std::runtime_error);
}

TEST(DigestGeneratorTest, PathToNonExistingFileThrows)
{
    const auto non_existent_path = "this-does-not-exist.txt";
    ASSERT_FALSE(FileUtils::isRegularFile(non_existent_path));
    ASSERT_THROW(DigestGenerator::hashFile(non_existent_path),
                 std::runtime_error);
}

TEST(DigestGeneratorTest, TestResetStateAndInit)
{
    ASSERT_TRUE(DigestGenerator::initialized());

    DigestGenerator::resetState();
    ASSERT_FALSE(DigestGenerator::initialized());
    ASSERT_THROW(DigestGenerator::digestFunction(), std::runtime_error);

    DigestGenerator::init(DigestFunction_Value_SHA1);
    ASSERT_TRUE(DigestGenerator::initialized());
    EXPECT_EQ(DigestGenerator::digestFunction(), DigestFunction_Value_SHA1);

    DigestGenerator::resetState();
    DigestGenerator::init();
    EXPECT_EQ(DigestGenerator::digestFunction(),
              static_cast<DigestFunction_Value>(
                  BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE));
}

TEST(DigestContextTest, TestStringMD5)
{
    const Digest d =
        hashTestString(DigestFunction::Value::DigestFunction_Value_MD5);

    const std::string expected_md5_string = "c1ad80398f865c700449c073bd0a8358";

    EXPECT_EQ(d.hash(), expected_md5_string);
    EXPECT_EQ(d.size_bytes(), TEST_STRING.size());
}

TEST(DigestContextTest, TestStringSha1)
{
    const Digest d = hashTestString(DigestFunction_Value_SHA1);

    const std::string expected_sha1_hash =
        "716e65700ad0e969cca29ec2259fa526e4bdb129";

    EXPECT_EQ(d.hash(), expected_sha1_hash);
    EXPECT_EQ(d.size_bytes(), TEST_STRING.size());
}

TEST(DigestContextTest, TestStringSha256)
{
    const Digest d = hashTestString(DigestFunction_Value_SHA256);

    const std::string expected_sha256_hash =
        "b1c4daf6e3812505064c07f1ad0b1d6693d93b1b28c452e55ad17e38c30e89aa";

    EXPECT_EQ(d.hash(), expected_sha256_hash);
    EXPECT_EQ(d.size_bytes(), TEST_STRING.size());
}

TEST(DigestContextTest, TestStringSha384)
{
    const Digest d = hashTestString(DigestFunction_Value_SHA384);

    const std::string expected_sha384_hash =
        "614589fe6e8bfd0e5a78e6819e439965364ec3af3a7482b69dd62e4ba47d82b5e305c"
        "b609d529164c794ba2b98e0279b";

    EXPECT_EQ(d.hash(), expected_sha384_hash);
    EXPECT_EQ(d.size_bytes(), TEST_STRING.size());
}

TEST(DigestContextTest, TestStringSha512)
{
    const Digest d = hashTestString(DigestFunction_Value_SHA512);

    const std::string expected_sha512_hash =
        "0e2c5c04c391ca0b8ca5fd9f6707bcddd53e8b7245c59331590d1c5490ffab7d505db"
        "0ba9b70a0f48e0f26ab6afeb84f600a7501a5fb1958f82f8623a7a1f692";

    EXPECT_EQ(d.hash(), expected_sha512_hash);
    EXPECT_EQ(d.size_bytes(), TEST_STRING.size());
}

TEST(DigestGeneratorTest, File)
{
    int fd = open("test.txt", O_RDONLY);
    const Digest d = DigestGenerator::hash(fd);
    EXPECT_EQ(
        d.hash(),
        "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
    EXPECT_EQ(d.size_bytes(), 4);
    close(fd);
}

TEST(DigestGeneratorTest, SupportedDigestFunctionList)
{
    std::string supported = DigestGenerator::supportedDigestFunctionsList();
    auto present = [supported](const std::string &fun) {
        return supported.find(fun) != std::string::npos;
    };
    EXPECT_TRUE(present("SHA512") && present("SHA384") && present("SHA256") &&
                present("SHA1") && present("MD5") && !present("BLAKE3"));
}

TEST(DigestGeneratorTest, InvalidDigestFunction)
{
    ASSERT_EQ(DigestGenerator::supportedDigestFunctions().count(
                  DigestFunction::Value::DigestFunction_Value_UNKNOWN),
              0);

    ASSERT_THROW(DigestContext::generate(
                     DigestFunction::Value::DigestFunction_Value_UNKNOWN),
                 std::runtime_error);
}

TEST(DigestGeneratorTest, VSONotImplemented)
{
    ASSERT_EQ(DigestGenerator::supportedDigestFunctions().count(
                  DigestFunction::Value::DigestFunction_Value_VSO),
              0);

    ASSERT_THROW(DigestContext::generate(
                     DigestFunction::Value::DigestFunction_Value_VSO),
                 std::runtime_error);
}

TEST(DigestContextTest, TestUpdateFinalized)
{
    DigestContext context = DigestGenerator::createDigestContext();
    context.finalizeDigest();

    ASSERT_THROW(context.update(TEST_STRING.c_str(), TEST_STRING.size()),
                 std::runtime_error);
}

TEST(DigestContextTest, TestFinalizeFinalized)
{
    DigestContext context = DigestGenerator::createDigestContext();
    context.finalizeDigest();

    ASSERT_THROW(context.finalizeDigest(), std::runtime_error);
}
