/*
 * Copyright 2026 Bloomberg 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_fastcdcutils.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_temporaryfile.h>

#include <cstdint>
#include <fstream>
#include <string>
#include <vector>

#include <fcntl.h>
#include <unistd.h>

#include <gtest/gtest.h>

using namespace buildboxcommon;

namespace {

const char *const TEST_IMAGE = "SekienAkashita.jpg";

// Parameters used to generate fastcdc2020_test_vectors.txt.
constexpr size_t VECTOR_AVG_CHUNK_SIZE = 16384;
constexpr int VECTOR_NORMALIZATION_LEVEL = 2;

struct ExpectedChunk {
    uint64_t offset = 0;
    uint64_t length = 0;
    std::string sha256;
};

struct SeedVectors {
    uint32_t seed = 0;
    std::vector<ExpectedChunk> chunks;
};

struct TestVectors {
    // Metadata for the input image the vectors were generated from, parsed
    // from the file's header comments.
    std::string imageSha256;
    uint64_t imageSize = 0;
    std::vector<SeedVectors> seeds;
};

// Parse the golden-vector file. Lines are either blank, a comment ('#'), or a
// tab-separated data row "offset length sha256 fingerprint". A "# Seed: N"
// comment starts a new group of chunks for seed N; "# SHA256: ..." and
// "# Size: ..." header comments describe the input image.
TestVectors parseTestVectors(const std::string &path)
{
    TestVectors result;

    std::ifstream file(path);
    std::string line;
    while (std::getline(file, line)) {
        if (line.empty()) {
            continue;
        }
        if (line.front() == '#') {
            // Strip the leading '#' and any following whitespace so the label
            // can be matched at the start of the comment. Matching at the
            // start distinguishes the image "Size:" from the parameter
            // "MinSize:"/"AvgSize:"/"MaxSize:" lines.
            size_t start = 1;
            while (start < line.size() &&
                   (line[start] == ' ' || line[start] == '\t')) {
                ++start;
            }
            const std::string comment = line.substr(start);

            if (comment.rfind("Seed:", 0) == 0) {
                const std::string number =
                    comment.substr(std::string("Seed:").size());
                result.seeds.push_back(SeedVectors{
                    static_cast<uint32_t>(std::stoul(number)), {}});
            }
            else if (comment.rfind("SHA256:", 0) == 0) {
                std::istringstream stream(
                    comment.substr(std::string("SHA256:").size()));
                stream >> result.imageSha256;
            }
            else if (comment.rfind("Size:", 0) == 0) {
                std::istringstream stream(
                    comment.substr(std::string("Size:").size()));
                stream >> result.imageSize;
            }
            continue;
        }
        if (result.seeds.empty()) {
            continue;
        }

        std::istringstream row(line);
        ExpectedChunk chunk;
        std::string fingerprint;
        if (row >> chunk.offset >> chunk.length >> chunk.sha256 >>
            fingerprint) {
            result.seeds.back().chunks.push_back(chunk);
        }
    }

    return result;
}

class FastCDCUtilsTest : public ::testing::Test {
  protected:
    void SetUp() override
    {
        // Force SHA-256 so the digests match the golden vectors regardless of
        // the build's default digest function.
        DigestGenerator::resetState();
        DigestGenerator::init(DigestFunction_Value_SHA256);
    }
};

} // namespace

// The definitive correctness test: reproduce the reference FastCDC 2020 chunk
// boundaries and digests for the standard SekienAkashita.jpg fixture, for both
// seeds in the golden-vector file.
TEST_F(FastCDCUtilsTest, GoldenVectors)
{
    const std::string content = FileUtils::getFileContents(TEST_IMAGE);

    const TestVectors vectors =
        parseTestVectors("fastcdc2020_test_vectors.txt");

    // The fixture image must be exactly the one the golden vectors were
    // generated from, otherwise the expected chunk boundaries below are
    // meaningless. Verify its hash and size against the file's header.
    ASSERT_FALSE(vectors.imageSha256.empty());
    ASSERT_NE(vectors.imageSize, 0u);
    const Digest imageDigest = DigestGenerator::hash(content);
    EXPECT_EQ(imageDigest.hash(), vectors.imageSha256);
    EXPECT_EQ(static_cast<uint64_t>(imageDigest.size_bytes()),
              vectors.imageSize);
    EXPECT_EQ(content.size(), vectors.imageSize);

    ASSERT_FALSE(vectors.seeds.empty());

    for (const SeedVectors &seedVectors : vectors.seeds) {
        ASSERT_FALSE(seedVectors.chunks.empty());

        FastCDCUtils::Config config;
        config.avgChunkSize = VECTOR_AVG_CHUNK_SIZE;
        config.normalizationLevel = VECTOR_NORMALIZATION_LEVEL;
        config.seed = seedVectors.seed;
        const FastCDCUtils chunker(config);

        const std::vector<Digest> digests = chunker.chunkToDigests(TEST_IMAGE);

        ASSERT_EQ(digests.size(), seedVectors.chunks.size())
            << "seed " << seedVectors.seed;

        uint64_t offset = 0;
        for (size_t i = 0; i < digests.size(); ++i) {
            const ExpectedChunk &expected = seedVectors.chunks[i];
            const Digest &actual = digests[i];

            EXPECT_EQ(offset, expected.offset)
                << "seed " << seedVectors.seed << " chunk " << i;
            EXPECT_EQ(static_cast<uint64_t>(actual.size_bytes()),
                      expected.length)
                << "seed " << seedVectors.seed << " chunk " << i;
            EXPECT_EQ(actual.hash(), expected.sha256)
                << "seed " << seedVectors.seed << " chunk " << i;

            offset += static_cast<uint64_t>(actual.size_bytes());
        }

        // The chunks tile the whole file, in order, with no gaps or overlaps.
        EXPECT_EQ(offset, content.size()) << "seed " << seedVectors.seed;
    }
}

// The path and dirfd overloads are thin wrappers that just open an fd and
// delegate to the fd overload, so it is enough to check all three agree.
TEST_F(FastCDCUtilsTest, OverloadsAgree)
{
    FastCDCUtils::Config config;
    config.avgChunkSize = VECTOR_AVG_CHUNK_SIZE;
    const FastCDCUtils chunker(config);

    // fd overload.
    const FileDescriptor fd(open(TEST_IMAGE, O_RDONLY | O_CLOEXEC));
    ASSERT_GE(fd.get(), 0);
    const std::vector<Digest> fromFd = chunker.chunkToDigests(fd.get());
    ASSERT_FALSE(fromFd.empty());

    // path overload.
    EXPECT_EQ(chunker.chunkToDigests(TEST_IMAGE), fromFd);

    // (dirfd, path) overload.
    const FileDescriptor dir(open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    ASSERT_GE(dir.get(), 0);
    EXPECT_EQ(chunker.chunkToDigests(dir.get(), TEST_IMAGE), fromFd);
}

TEST_F(FastCDCUtilsTest, EmptyInput)
{
    const FastCDCUtils chunker{FastCDCUtils::Config{}};
    const TemporaryFile empty;
    EXPECT_TRUE(chunker.chunkToDigests(empty.name()).empty());
}

TEST_F(FastCDCUtilsTest, InputSmallerThanMinSizeIsASingleChunk)
{
    // The default minimum chunk size is 128 KiB, so a short blob is emitted as
    // a single chunk whose digest is just the digest of the whole content.
    const FastCDCUtils chunker{FastCDCUtils::Config{}};

    const std::string data = "hello, content-defined chunking";
    TemporaryFile file;
    ASSERT_EQ(write(file.fd(), data.data(), data.size()),
              static_cast<ssize_t>(data.size()));

    const std::vector<Digest> digests = chunker.chunkToDigests(file.name());
    ASSERT_EQ(digests.size(), 1u);
    EXPECT_EQ(digests.front(), DigestGenerator::hash(data));
}

TEST_F(FastCDCUtilsTest, InvalidConfigThrows)
{
    FastCDCUtils::Config notPowerOfTwo;
    notPowerOfTwo.avgChunkSize = 1000;
    EXPECT_THROW(FastCDCUtils{notPowerOfTwo}, std::invalid_argument);

    FastCDCUtils::Config zeroAvg;
    zeroAvg.avgChunkSize = 0;
    EXPECT_THROW(FastCDCUtils{zeroAvg}, std::invalid_argument);

    FastCDCUtils::Config badNormalization;
    badNormalization.normalizationLevel = 5;
    EXPECT_THROW(FastCDCUtils{badNormalization}, std::invalid_argument);
}
