/*
 * 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/common/utils/generic_lru_cache.h"

#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include <vector>

#include "gtest/gtest.h"
#include "paimon/testing/utils/testharness.h"

namespace paimon::test {

class GenericLruCacheTest : public ::testing::Test {
 public:
    using StringIntCache = GenericLruCache<std::string, int>;
    using StringStringCache = GenericLruCache<std::string, std::string>;
    using IntIntCache = GenericLruCache<int, int>;
    using StringSharedPtrCache = GenericLruCache<std::string, std::shared_ptr<int>>;
    using RemovalCause = StringIntCache::RemovalCause;

    struct RemovalRecord {
        std::string key;
        std::string value;
        RemovalCause cause;
    };
};

// ==================== Basic Operations ====================

TEST_F(GenericLruCacheTest, ConstructorAndDefaults) {
    {
        StringIntCache::Options options;
        StringIntCache cache(options);

        ASSERT_EQ(cache.Size(), 0);
        ASSERT_EQ(cache.GetCurrentWeight(), 0);
        ASSERT_EQ(cache.GetMaxWeight(), INT64_MAX);
    }
    {
        StringIntCache::Options options;
        options.max_weight = 42;
        StringIntCache cache(options);
        ASSERT_EQ(cache.GetMaxWeight(), 42);
    }
}

// ==================== GetIfPresent ====================

TEST_F(GenericLruCacheTest, GetIfPresentMissAndHit) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    auto result = cache.GetIfPresent("nonexistent");
    ASSERT_FALSE(result.has_value());

    ASSERT_OK(cache.Put("key1", 100));
    result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 100);
}

TEST_F(GenericLruCacheTest, GetIfPresentExpired) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.expire_after_access_ms = 50;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    ASSERT_EQ(cache.Size(), 1);

    std::this_thread::sleep_for(std::chrono::milliseconds(80));

    auto result = cache.GetIfPresent("key1");
    ASSERT_FALSE(result.has_value());
    ASSERT_EQ(cache.Size(), 0);

    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "key1");
    ASSERT_EQ(removals[0].cause, RemovalCause::EXPIRED);
}

TEST_F(GenericLruCacheTest, GetIfPresentPromotesEntry) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.max_weight = 2;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));

    // Access "a" to promote it
    auto result = cache.GetIfPresent("a");
    ASSERT_TRUE(result.has_value());

    // Insert "c": should evict "b" (LRU), not "a"
    ASSERT_OK(cache.Put("c", 3));
    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "b");
    ASSERT_EQ(removals[0].cause, RemovalCause::SIZE);
}

// ==================== Get with supplier ====================

TEST_F(GenericLruCacheTest, GetCacheMissLoadsViaSupplier) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    int32_t supplier_calls = 0;
    auto supplier = [&](const std::string& key) -> Result<int> {
        supplier_calls++;
        return 42;
    };

    ASSERT_OK_AND_ASSIGN(auto value, cache.Get("key1", supplier));
    ASSERT_EQ(value, 42);
    ASSERT_EQ(supplier_calls, 1);
    ASSERT_EQ(cache.Size(), 1);
}

TEST_F(GenericLruCacheTest, GetCacheHitSkipsSupplier) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));

    int32_t supplier_calls = 0;
    auto supplier = [&](const std::string& key) -> Result<int> {
        supplier_calls++;
        return 999;
    };

    ASSERT_OK_AND_ASSIGN(auto value, cache.Get("key1", supplier));
    ASSERT_EQ(value, 100);
    ASSERT_EQ(supplier_calls, 0);
}

TEST_F(GenericLruCacheTest, GetSupplierError) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    auto supplier = [](const std::string& key) -> Result<int> {
        return Status::IOError("load failed");
    };

    ASSERT_NOK_WITH_MSG(cache.Get("key1", supplier), "load failed");
    ASSERT_EQ(cache.Size(), 0);
}

TEST_F(GenericLruCacheTest, GetWeightExceedsMaxReturnsWithoutCaching) {
    StringStringCache::Options options;
    options.max_weight = 5;
    options.weigh_func = [](const std::string& key, const std::string& value) -> int64_t {
        return static_cast<int64_t>(value.size());
    };
    StringStringCache cache(options);

    auto supplier = [](const std::string& key) -> Result<std::string> {
        return std::string("this_is_a_very_long_value");
    };

    ASSERT_OK_AND_ASSIGN(auto value, cache.Get("key1", supplier));
    ASSERT_EQ(value, "this_is_a_very_long_value");
    ASSERT_EQ(cache.Size(), 0);
}

TEST_F(GenericLruCacheTest, GetTriggersEviction) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.max_weight = 2;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));
    ASSERT_EQ(cache.Size(), 2);

    auto supplier = [](const std::string& key) -> Result<int> { return 3; };
    ASSERT_OK_AND_ASSIGN(auto value, cache.Get("c", supplier));
    ASSERT_EQ(value, 3);
    ASSERT_EQ(cache.Size(), 2);

    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "a");
    ASSERT_EQ(removals[0].cause, RemovalCause::SIZE);
}

// ==================== Put ====================

TEST_F(GenericLruCacheTest, PutNewEntry) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    ASSERT_EQ(cache.Size(), 1);
    ASSERT_EQ(cache.GetCurrentWeight(), 1);

    auto result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 100);
}

TEST_F(GenericLruCacheTest, PutReplaceWithDifferentValue) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    ASSERT_OK(cache.Put("key1", 200));
    ASSERT_EQ(cache.Size(), 1);

    auto result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 200);

    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "key1");
    ASSERT_EQ(removals[0].value, "100");
    ASSERT_EQ(removals[0].cause, RemovalCause::REPLACED);
}

TEST_F(GenericLruCacheTest, PutReplaceWithSameValuePromotes) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.max_weight = 2;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));

    // Put same value for "a" — should promote without REPLACED callback
    ASSERT_OK(cache.Put("a", 1));
    ASSERT_TRUE(removals.empty());

    // Insert "c": should evict "b" (LRU after "a" was promoted)
    ASSERT_OK(cache.Put("c", 3));
    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "b");
    ASSERT_EQ(removals[0].cause, RemovalCause::SIZE);
}

TEST_F(GenericLruCacheTest, PutWeightExceedsMaxReturnsInvalid) {
    StringStringCache::Options options;
    options.max_weight = 5;
    options.weigh_func = [](const std::string& key, const std::string& value) -> int64_t {
        return static_cast<int64_t>(value.size());
    };
    StringStringCache cache(options);

    ASSERT_NOK_WITH_MSG(cache.Put("key1", "this_is_too_long"),
                        "Entry weight 16 exceeds cache max weight 5, entry will not be cached");
    ASSERT_EQ(cache.Size(), 0);
}

TEST_F(GenericLruCacheTest, PutTriggersWeightEviction) {
    std::vector<RemovalRecord> removals;
    StringStringCache::Options options;
    options.max_weight = 10;
    options.weigh_func = [](const std::string& key, const std::string& value) -> int64_t {
        return static_cast<int64_t>(value.size());
    };
    options.removal_callback = [&](const std::string& key, const std::string& value, auto cause) {
        removals.push_back({key, value, static_cast<RemovalCause>(cause)});
    };
    StringStringCache cache(options);

    ASSERT_OK(cache.Put("a", "aaaa"));   // weight 4
    ASSERT_OK(cache.Put("b", "bbbbb"));  // weight 5, total 9
    ASSERT_EQ(cache.GetCurrentWeight(), 9);

    // Insert "c" with weight 5: total would be 14 > 10, evict "a" (4), total becomes 10
    ASSERT_OK(cache.Put("c", "ccccc"));
    ASSERT_EQ(cache.Size(), 2);
    ASSERT_EQ(cache.GetCurrentWeight(), 10);

    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "a");
    ASSERT_EQ(removals[0].cause, RemovalCause::SIZE);
}

TEST_F(GenericLruCacheTest, PutMultipleEvictions) {
    std::vector<std::string> evicted_keys;
    StringStringCache::Options options;
    options.max_weight = 10;
    options.weigh_func = [](const std::string& key, const std::string& value) -> int64_t {
        return static_cast<int64_t>(value.size());
    };
    options.removal_callback = [&](const std::string& key, const std::string& value, auto cause) {
        evicted_keys.push_back(key);
    };
    StringStringCache cache(options);

    ASSERT_OK(cache.Put("a", "aaa"));  // weight 3
    ASSERT_OK(cache.Put("b", "bbb"));  // weight 3
    ASSERT_OK(cache.Put("c", "ccc"));  // weight 3, total 9

    // Insert "d" with weight 9: total would be 18 > 10, evict a(3), b(3), c(3) then add d(9)
    ASSERT_OK(cache.Put("d", "ddddddddd"));
    ASSERT_EQ(cache.Size(), 1);
    ASSERT_EQ(cache.GetCurrentWeight(), 9);

    ASSERT_EQ(evicted_keys.size(), 3);
    ASSERT_EQ(evicted_keys[0], "a");
    ASSERT_EQ(evicted_keys[1], "b");
    ASSERT_EQ(evicted_keys[2], "c");
}

// ==================== Invalidate ====================

TEST_F(GenericLruCacheTest, InvalidateExistingKey) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    ASSERT_EQ(cache.Size(), 1);

    cache.Invalidate("key1");
    ASSERT_EQ(cache.Size(), 0);
    ASSERT_EQ(cache.GetCurrentWeight(), 0);

    ASSERT_EQ(removals.size(), 1);
    ASSERT_EQ(removals[0].key, "key1");
    ASSERT_EQ(removals[0].cause, RemovalCause::EXPLICIT);
}

TEST_F(GenericLruCacheTest, InvalidateNonExistentKey) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    cache.Invalidate("nonexistent");
    ASSERT_EQ(cache.Size(), 1);
}

// ==================== InvalidateAll ====================

TEST_F(GenericLruCacheTest, InvalidateAllClearsEverything) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));
    ASSERT_OK(cache.Put("c", 3));
    ASSERT_EQ(cache.Size(), 3);

    cache.InvalidateAll();
    ASSERT_EQ(cache.Size(), 0);
    ASSERT_EQ(cache.GetCurrentWeight(), 0);

    ASSERT_EQ(removals.size(), 3);
    for (const auto& record : removals) {
        ASSERT_EQ(record.cause, RemovalCause::EXPLICIT);
    }
}

TEST_F(GenericLruCacheTest, InvalidateAllOnEmptyCache) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    cache.InvalidateAll();
    ASSERT_EQ(cache.Size(), 0);
    ASSERT_EQ(cache.GetCurrentWeight(), 0);
}

// ==================== Weight Function ====================

TEST_F(GenericLruCacheTest, DefaultWeightIsOne) {
    StringIntCache::Options options;
    options.max_weight = 3;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));
    ASSERT_OK(cache.Put("c", 3));
    ASSERT_EQ(cache.GetCurrentWeight(), 3);
    ASSERT_EQ(cache.Size(), 3);

    // Adding one more should evict the LRU entry
    ASSERT_OK(cache.Put("d", 4));
    ASSERT_EQ(cache.Size(), 3);
    ASSERT_EQ(cache.GetCurrentWeight(), 3);
    ASSERT_FALSE(cache.GetIfPresent("a").has_value());
}

TEST_F(GenericLruCacheTest, WeightUpdatedOnReplace) {
    StringStringCache::Options options;
    options.max_weight = 100;
    options.weigh_func = [](const std::string& key, const std::string& value) -> int64_t {
        return static_cast<int64_t>(value.size());
    };
    StringStringCache cache(options);

    ASSERT_OK(cache.Put("a", std::string(30, 'x')));
    ASSERT_EQ(cache.GetCurrentWeight(), 30);

    // Replace with larger value
    ASSERT_OK(cache.Put("a", std::string(70, 'y')));
    ASSERT_EQ(cache.GetCurrentWeight(), 70);
    ASSERT_EQ(cache.Size(), 1);
}

// ==================== Expiration ====================

TEST_F(GenericLruCacheTest, ExpirationDisabledByDefault) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    std::this_thread::sleep_for(std::chrono::milliseconds(10));

    auto result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 100);
}

TEST_F(GenericLruCacheTest, ExpirationOnGet) {
    StringIntCache::Options options;
    options.expire_after_access_ms = 50;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));

    // Access before expiration
    std::this_thread::sleep_for(std::chrono::milliseconds(2));
    auto result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());

    // Wait for expiration
    std::this_thread::sleep_for(std::chrono::milliseconds(80));
    result = cache.GetIfPresent("key1");
    ASSERT_FALSE(result.has_value());
    ASSERT_EQ(cache.Size(), 0);
}

TEST_F(GenericLruCacheTest, ExpirationOnGetWithSupplier) {
    StringIntCache::Options options;
    options.expire_after_access_ms = 50;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));

    std::this_thread::sleep_for(std::chrono::milliseconds(80));

    int32_t supplier_calls = 0;
    auto supplier = [&](const std::string& key) -> Result<int> {
        supplier_calls++;
        return 200;
    };

    ASSERT_OK_AND_ASSIGN(auto value, cache.Get("key1", supplier));
    ASSERT_EQ(value, 200);
    ASSERT_EQ(supplier_calls, 1);
}

TEST_F(GenericLruCacheTest, AccessResetsExpirationTimer) {
    StringIntCache::Options options;
    options.expire_after_access_ms = 100;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));

    // Access at 40ms to reset the timer
    std::this_thread::sleep_for(std::chrono::milliseconds(40));
    auto result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());

    // At 80ms from last access (40ms from the GetIfPresent), should still be valid
    std::this_thread::sleep_for(std::chrono::milliseconds(40));
    result = cache.GetIfPresent("key1");
    ASSERT_TRUE(result.has_value());

    // Wait for full expiration from last access
    std::this_thread::sleep_for(std::chrono::milliseconds(150));
    result = cache.GetIfPresent("key1");
    ASSERT_FALSE(result.has_value());
}

TEST_F(GenericLruCacheTest, ExpiredEntriesEvictedOnPut) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.expire_after_access_ms = 50;
    options.max_weight = 100;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));

    std::this_thread::sleep_for(std::chrono::milliseconds(80));

    // Put triggers EvictIfNeeded which calls EvictExpired
    ASSERT_OK(cache.Put("c", 3));

    // "a" and "b" should have been expired
    int32_t expired_count = 0;
    for (const auto& record : removals) {
        if (record.cause == RemovalCause::EXPIRED) {
            expired_count++;
        }
    }
    ASSERT_EQ(expired_count, 2);
    ASSERT_EQ(cache.Size(), 1);
}

// ==================== Removal Callback ====================

TEST_F(GenericLruCacheTest, NoCallbackConfigured) {
    StringIntCache::Options options;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("key1", 100));
    cache.Invalidate("key1");
    ASSERT_EQ(cache.Size(), 0);
}

TEST_F(GenericLruCacheTest, AllRemovalCauses) {
    std::vector<RemovalRecord> removals;
    StringIntCache::Options options;
    options.max_weight = 2;
    options.expire_after_access_ms = 50;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        removals.push_back({key, std::to_string(value), static_cast<RemovalCause>(cause)});
    };
    StringIntCache cache(options);

    // REPLACED: put same key with different value
    ASSERT_OK(cache.Put("r", 1));
    ASSERT_OK(cache.Put("r", 2));
    ASSERT_EQ(removals.back().cause, RemovalCause::REPLACED);

    // EXPLICIT: invalidate
    cache.Invalidate("r");
    ASSERT_EQ(removals.back().cause, RemovalCause::EXPLICIT);

    // SIZE: evict due to weight
    ASSERT_OK(cache.Put("s1", 10));
    ASSERT_OK(cache.Put("s2", 20));
    ASSERT_OK(cache.Put("s3", 30));
    ASSERT_EQ(removals.back().cause, RemovalCause::SIZE);

    // EXPIRED: wait and access
    cache.InvalidateAll();
    removals.clear();
    ASSERT_OK(cache.Put("e", 99));
    std::this_thread::sleep_for(std::chrono::milliseconds(80));
    ASSERT_FALSE(cache.GetIfPresent("e").has_value());
    ASSERT_EQ(removals.back().cause, RemovalCause::EXPIRED);
}

// ==================== valuesequal with shared_ptr ====================

TEST_F(GenericLruCacheTest, SharedPtrSamePointerNoReplace) {
    using Cause = StringSharedPtrCache::RemovalCause;
    std::vector<Cause> causes;
    StringSharedPtrCache::Options options;
    options.removal_callback = [&](const std::string& key, const std::shared_ptr<int>& value,
                                   auto cause) { causes.push_back(static_cast<Cause>(cause)); };
    StringSharedPtrCache cache(options);

    auto ptr = std::make_shared<int>(42);
    ASSERT_OK(cache.Put("key1", ptr));

    // Put same pointer — ValuesEqual returns true, should promote without REPLACED
    ASSERT_OK(cache.Put("key1", ptr));
    ASSERT_TRUE(causes.empty());
    ASSERT_EQ(cache.Size(), 1);
}

TEST_F(GenericLruCacheTest, SharedPtrDifferentPointerSameValueNoReplace) {
    using Cause = StringSharedPtrCache::RemovalCause;
    std::vector<Cause> causes;
    StringSharedPtrCache::Options options;
    options.removal_callback = [&](const std::string& key, const std::shared_ptr<int>& value,
                                   auto cause) { causes.push_back(static_cast<Cause>(cause)); };
    StringSharedPtrCache cache(options);

    auto ptr1 = std::make_shared<int>(42);
    auto ptr2 = std::make_shared<int>(42);
    ASSERT_NE(ptr1.get(), ptr2.get());

    ASSERT_OK(cache.Put("key1", ptr1));
    // Different pointer but same dereferenced value — ValuesEqual returns true
    ASSERT_OK(cache.Put("key1", ptr2));
    ASSERT_TRUE(causes.empty());
}

TEST_F(GenericLruCacheTest, SharedPtrDifferentValueReplaces) {
    using Cause = StringSharedPtrCache::RemovalCause;
    std::vector<Cause> causes;
    StringSharedPtrCache::Options options;
    options.removal_callback = [&](const std::string& key, const std::shared_ptr<int>& value,
                                   auto cause) { causes.push_back(static_cast<Cause>(cause)); };
    StringSharedPtrCache cache(options);

    ASSERT_OK(cache.Put("key1", std::make_shared<int>(1)));
    ASSERT_OK(cache.Put("key1", std::make_shared<int>(2)));

    ASSERT_EQ(causes.size(), 1);
    ASSERT_EQ(causes[0], Cause::REPLACED);
}

TEST_F(GenericLruCacheTest, SharedPtrNullptrComparison) {
    using Cause = StringSharedPtrCache::RemovalCause;
    std::vector<Cause> causes;
    StringSharedPtrCache::Options options;
    options.removal_callback = [&](const std::string& key, const std::shared_ptr<int>& value,
                                   auto cause) { causes.push_back(static_cast<Cause>(cause)); };
    StringSharedPtrCache cache(options);

    // Put nullptr
    ASSERT_OK(cache.Put("key1", nullptr));

    // Put nullptr again — same value, should not replace
    ASSERT_OK(cache.Put("key1", nullptr));
    ASSERT_TRUE(causes.empty());

    // Put non-null — different from nullptr, should replace
    ASSERT_OK(cache.Put("key1", std::make_shared<int>(1)));
    ASSERT_EQ(causes.size(), 1);
    ASSERT_EQ(causes[0], Cause::REPLACED);

    // Put nullptr again — different from non-null, should replace
    causes.clear();
    ASSERT_OK(cache.Put("key1", nullptr));
    ASSERT_EQ(causes.size(), 1);
    ASSERT_EQ(causes[0], Cause::REPLACED);
}

// ==================== Custom Hash and KeyEqual ====================

TEST_F(GenericLruCacheTest, CustomHashAndKeyEqual) {
    struct CaseInsensitiveHash {
        size_t operator()(const std::string& str) const {
            std::string lower = str;
            for (auto& ch : lower) {
                ch = static_cast<char>(std::tolower(ch));
            }
            return std::hash<std::string>{}(lower);
        }
    };
    struct CaseInsensitiveEqual {
        bool operator()(const std::string& lhs, const std::string& rhs) const {
            if (lhs.size() != rhs.size()) return false;
            for (size_t i = 0; i < lhs.size(); i++) {
                if (std::tolower(lhs[i]) != std::tolower(rhs[i])) return false;
            }
            return true;
        }
    };

    using CICache = GenericLruCache<std::string, int, CaseInsensitiveHash, CaseInsensitiveEqual>;
    CICache::Options options;
    CICache cache(options);

    ASSERT_OK(cache.Put("Hello", 1));
    ASSERT_EQ(cache.Size(), 1);

    // "hello" should match "Hello" with case-insensitive comparison
    auto result = cache.GetIfPresent("hello");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 1);

    // Put with different case should replace
    ASSERT_OK(cache.Put("HELLO", 2));
    ASSERT_EQ(cache.Size(), 1);

    result = cache.GetIfPresent("Hello");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 2);
}

// ==================== Thread Safety ====================

TEST_F(GenericLruCacheTest, ConcurrentPutAndGet) {
    IntIntCache::Options options;
    options.max_weight = 10000;
    IntIntCache cache(options);

    constexpr int32_t num_threads = 8;
    constexpr int32_t ops_per_thread = 200;

    std::vector<std::thread> threads;
    std::atomic<int32_t> errors{0};

    for (int32_t t = 0; t < num_threads; t++) {
        threads.emplace_back([&, t]() {
            for (int32_t i = 0; i < ops_per_thread; i++) {
                int32_t key = t * ops_per_thread + i;
                auto status = cache.Put(key, key * 10);
                if (!status.ok()) {
                    errors++;
                }
            }
        });
    }

    for (auto& thread : threads) {
        thread.join();
    }

    ASSERT_EQ(errors.load(), 0);
    ASSERT_EQ(static_cast<int32_t>(cache.Size()), num_threads * ops_per_thread);

    // Concurrent reads
    threads.clear();
    for (int32_t t = 0; t < num_threads; t++) {
        threads.emplace_back([&, t]() {
            for (int32_t i = 0; i < ops_per_thread; i++) {
                int32_t key = t * ops_per_thread + i;
                auto result = cache.GetIfPresent(key);
                if (!result.has_value() || result.value() != key * 10) {
                    errors++;
                }
            }
        });
    }

    for (auto& thread : threads) {
        thread.join();
    }

    ASSERT_EQ(errors.load(), 0);
}

TEST_F(GenericLruCacheTest, ConcurrentGetWithSupplier) {
    IntIntCache::Options options;
    options.max_weight = 10000;
    IntIntCache cache(options);

    constexpr int32_t num_threads = 8;
    constexpr int32_t ops_per_thread = 100;

    std::atomic<int> supplier_calls{0};
    std::vector<std::thread> threads;

    for (int32_t t = 0; t < num_threads; t++) {
        threads.emplace_back([&, t]() {
            for (int32_t i = 0; i < ops_per_thread; i++) {
                int32_t key = t * ops_per_thread + i;
                auto supplier = [&, key](const int&) -> Result<int> {
                    supplier_calls++;
                    return key * 10;
                };
                auto result = cache.Get(key, supplier);
                ASSERT_TRUE(result.ok());
                ASSERT_EQ(result.value(), key * 10);
            }
        });
    }

    for (auto& thread : threads) {
        thread.join();
    }

    ASSERT_EQ(static_cast<int>(cache.Size()), num_threads * ops_per_thread);
}

TEST_F(GenericLruCacheTest, ConcurrentInvalidate) {
    IntIntCache::Options options;
    IntIntCache cache(options);

    for (int32_t i = 0; i < 100; i++) {
        ASSERT_OK(cache.Put(i, i));
    }

    std::vector<std::thread> threads;
    for (int32_t t = 0; t < 4; t++) {
        threads.emplace_back([&, t]() {
            for (int32_t i = t * 25; i < (t + 1) * 25; i++) {
                cache.Invalidate(i);
            }
        });
    }

    for (auto& thread : threads) {
        thread.join();
    }

    ASSERT_EQ(cache.Size(), 0);
}

// ==================== Edge Cases ====================

TEST_F(GenericLruCacheTest, PutAndGetSingleEntry) {
    StringIntCache::Options options;
    options.max_weight = 1;
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("only", 42));
    ASSERT_EQ(cache.Size(), 1);

    auto result = cache.GetIfPresent("only");
    ASSERT_TRUE(result.has_value());
    ASSERT_EQ(result.value(), 42);

    // Adding another entry should evict the first
    ASSERT_OK(cache.Put("new", 99));
    ASSERT_EQ(cache.Size(), 1);
    ASSERT_FALSE(cache.GetIfPresent("only").has_value());
    ASSERT_TRUE(cache.GetIfPresent("new").has_value());
}

TEST_F(GenericLruCacheTest, ReplaceUpdatesWeight) {
    StringStringCache::Options options;
    options.max_weight = 100;
    options.weigh_func = [](const std::string& key, const std::string& value) -> int64_t {
        return static_cast<int64_t>(value.size());
    };
    StringStringCache cache(options);

    ASSERT_OK(cache.Put("a", std::string(50, 'x')));
    ASSERT_EQ(cache.GetCurrentWeight(), 50);

    // Replace with smaller value
    ASSERT_OK(cache.Put("a", std::string(20, 'y')));
    ASSERT_EQ(cache.GetCurrentWeight(), 20);

    // Replace with larger value
    ASSERT_OK(cache.Put("a", std::string(80, 'z')));
    ASSERT_EQ(cache.GetCurrentWeight(), 80);
}

TEST_F(GenericLruCacheTest, EvictionOrderIsLru) {
    std::vector<std::string> evicted_keys;
    StringIntCache::Options options;
    options.max_weight = 3;
    options.removal_callback = [&](const std::string& key, const int& value, auto cause) {
        if (static_cast<RemovalCause>(cause) == RemovalCause::SIZE) {
            evicted_keys.push_back(key);
        }
    };
    StringIntCache cache(options);

    ASSERT_OK(cache.Put("a", 1));
    ASSERT_OK(cache.Put("b", 2));
    ASSERT_OK(cache.Put("c", 3));

    // Access "a" and "b" to make "c" the LRU
    cache.GetIfPresent("a");
    cache.GetIfPresent("b");

    // Insert "d": should evict "c" (LRU)
    ASSERT_OK(cache.Put("d", 4));
    ASSERT_EQ(evicted_keys.size(), 1);
    ASSERT_EQ(evicted_keys[0], "c");
}

}  // namespace paimon::test
