/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "paimon/utils/read_ahead_cache.h"

#include <fstream>
#include <vector>

#include "gtest/gtest.h"
#include "paimon/fs/file_system.h"
#include "paimon/fs/file_system_factory.h"
#include "paimon/testing/utils/testharness.h"

namespace paimon::test {

// Helper to create a test file, write content, and return a ready ReadAheadCache.
struct TestCacheEnv {
    std::string path;
    std::shared_ptr<paimon::ReadAheadCache> cache;
    std::shared_ptr<paimon::MemoryPool> pool;
};

TestCacheEnv CreateTestFileAndCache(const std::string& filename, const std::string& content,
                                    const paimon::CacheConfig& config,
                                    std::vector<paimon::ByteRange> ranges) {
    auto dir = UniqueTestDirectory::Create();
    EXPECT_TRUE(dir);
    std::string path = dir->Str() + "/" + filename;
    std::ofstream file(path, std::ios::binary);
    EXPECT_TRUE(file.is_open());
    file.write(content.data(), content.size());
    EXPECT_FALSE(file.fail());
    file.close();

    auto fs_result = FileSystemFactory::Get("local", path, {});
    EXPECT_TRUE(fs_result.ok());
    auto fs = std::move(fs_result).value();
    auto in_result = fs->Open(path);
    EXPECT_TRUE(in_result.ok());
    auto in = std::move(in_result).value();

    auto pool = GetDefaultPool();
    auto cache = std::make_shared<ReadAheadCache>(std::move(in), config, pool);
    EXPECT_OK(cache->Init(std::move(ranges)));
    return {path, cache, pool};
}

TEST(TestReadAheadCache, TestBasics) {
    CacheConfig config(/*buffer_size_limit=*/256 * 1024 * 1024, /*range_size_limit=*/10,
                       /*hole_size_limit=*/2, /*pre_buffer_limit=*/128 * 1024 * 1024);
    std::string content = "abcdefghijklmnopqrstuvwxyz";
    auto env = CreateTestFileAndCache(
        "data_file", content, config,
        {{1, 2}, {3, 2}, {8, 2}, {10, 4}, {14, 0}, {15, 4}, {20, 2}, {25, 0}});
    auto& cache = *env.cache;

    auto assert_slice_equal = [](const ByteSlice& slice, const std::string& expected) {
        ASSERT_TRUE(slice.buffer) << expected;
        EXPECT_EQ(expected, std::string_view(slice.buffer->data() + slice.offset, slice.length));
    };

    ByteSlice slice;

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({20, 2}));
    assert_slice_equal(slice, "uv");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({1, 2}));
    assert_slice_equal(slice, "bc");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({3, 2}));
    assert_slice_equal(slice, "de");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({8, 2}));
    assert_slice_equal(slice, "ij");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({10, 4}));
    assert_slice_equal(slice, "klmn");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({15, 4}));
    assert_slice_equal(slice, "pqrs");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({19, 3}));
    assert_slice_equal(slice, "tuv");

    // Zero-sized
    ASSERT_OK_AND_ASSIGN(slice, cache.Read({14, 0}));
    assert_slice_equal(slice, "");
    ASSERT_OK_AND_ASSIGN(slice, cache.Read({25, 0}));
    assert_slice_equal(slice, "");

    // Non-cached ranges

    ASSERT_FALSE(cache.Read({20, 3}).value().buffer);
    ASSERT_FALSE(cache.Read({0, 3}).value().buffer);
    ASSERT_FALSE(cache.Read({25, 2}).value().buffer);
}

// Test repeated reads to the same range to ensure cache reuse.
TEST(TestReadAheadCache, TestRepeatedReadCacheReuse) {
    CacheConfig config(/*buffer_size_limit=*/64, /*range_size_limit=*/10,
                       /*hole_size_limit=*/2, /*pre_buffer_limit=*/64);
    std::string content = "abcdefghijklmnopqrstuvwxyz";
    auto env = CreateTestFileAndCache("data_file", content, config, {{0, 5}, {7, 5}});
    auto& cache = *env.cache;

    ByteSlice slice;
    ASSERT_OK_AND_ASSIGN(slice, cache.Read({0, 5}));
    ASSERT_TRUE(slice.buffer);
    std::string first_read(slice.buffer->data() + slice.offset, slice.length);
    ASSERT_EQ(first_read, "abcde");

    ASSERT_OK_AND_ASSIGN(slice, cache.Read({0, 5}));
    ASSERT_TRUE(slice.buffer);
    std::string second_read(slice.buffer->data() + slice.offset, slice.length);
    ASSERT_EQ(second_read, "abcde");
}

// Test cache eviction when buffer size is limited.
TEST(TestReadAheadCache, TestCacheEviction) {
    CacheConfig config(/*buffer_size_limit=*/10, /*range_size_limit=*/5,
                       /*hole_size_limit=*/2, /*pre_buffer_limit=*/10);
    std::string content = "abcdefghijklmnopqrstuvwxyz";
    auto env = CreateTestFileAndCache("data_file", content, config, {{0, 5}, {8, 5}, {16, 5}});
    auto& cache = *env.cache;

    ByteSlice slice;
    ASSERT_OK_AND_ASSIGN(slice, cache.Read({0, 5}));
    ASSERT_TRUE(slice.buffer);
    std::string first_read(slice.buffer->data() + slice.offset, slice.length);
    ASSERT_EQ(first_read, "abcde");

    // Reading another range should evict the first one due to buffer size limit
    ASSERT_OK_AND_ASSIGN(slice, cache.Read({8, 5}));
    ASSERT_TRUE(slice.buffer);
    std::string second_read(slice.buffer->data() + slice.offset, slice.length);
    ASSERT_EQ(second_read, "ijklm");

    // The first range should now be a cache miss (buffer is nullptr)
    auto miss = cache.Read({0, 5});
    ASSERT_FALSE(miss.value().buffer);
}

}  // namespace paimon::test
