/*
 * BlobCache unit tests:
 *   - LRU eviction when cache is full
 *   - LRU promotion: recently accessed entries survive eviction
 *   - Overwrite existing key
 *   - Entry too large for cache is rejected
 */
#include "../src/lru_cache.h"

#include <cassert>
#include <iostream>
#include <string>
#include <vector>

using namespace mediadb;

// ---------- minimal test framework ----------

static int g_tests_run    = 0;
static int g_tests_passed = 0;
static int g_tests_failed = 0;

struct TestEntry { const char* name; void (*fn)(); };
static std::vector<TestEntry>& tests_vec() {
    static std::vector<TestEntry> v;
    return v;
}
#define tests tests_vec()

#define TEST(name)                                                      \
    static void test_##name();                                          \
    struct test_reg_##name {                                            \
        test_reg_##name() { tests.push_back({#name, test_##name}); }   \
    } test_reg_inst_##name;                                             \
    static void test_##name()

#define ASSERT_TRUE(cond)                                               \
    do { if (!(cond)) {                                                 \
        std::cerr << "  FAIL: " << #cond << " at line " << __LINE__    \
                  << "\n"; throw std::runtime_error("assertion");       \
    } } while(0)

#define ASSERT_EQ(a, b)                                                 \
    do { if ((a) != (b)) {                                              \
        std::cerr << "  FAIL: " << #a << " == " << #b << " at line "   \
                  << __LINE__ << "  (" << (a) << " vs " << (b) << ")\n"; \
        throw std::runtime_error("assertion");                          \
    } } while(0)

// ---------- helpers ----------

static BlobValue make_value(std::size_t size) {
    BlobValue v;
    v.data.resize(size, 0xAB);
    return v;
}

// =================================================================
// 1. Basic eviction: oldest entry is evicted when cache is full
// =================================================================

TEST(eviction_removes_oldest) {
    BlobCache cache(200, blob_value_size);

    cache.put("a", make_value(100));
    cache.put("b", make_value(100));

    ASSERT_TRUE(cache.get("a") != nullptr);
    ASSERT_TRUE(cache.get("b") != nullptr);

    // Adding "c" must evict LRU entry ("a", since "b" was just accessed by get)
    cache.put("c", make_value(100));

    ASSERT_TRUE(cache.get("a") == nullptr);  // evicted
    ASSERT_TRUE(cache.get("b") != nullptr);  // survived
    ASSERT_TRUE(cache.get("c") != nullptr);  // just inserted
}

// =================================================================
// 2. LRU promotion: accessing an entry moves it to front
// =================================================================

TEST(lru_promotion_protects_entry) {
    BlobCache cache(300, blob_value_size);

    cache.put("x", make_value(100));
    cache.put("y", make_value(100));
    cache.put("z", make_value(100));

    // Access "x" to promote it
    auto hit = cache.get("x");
    ASSERT_TRUE(hit != nullptr);

    // Insert "w" — must evict "y" (the back), NOT "x"
    cache.put("w", make_value(100));

    ASSERT_TRUE(cache.get("x") != nullptr);
    ASSERT_TRUE(cache.get("y") == nullptr);
    ASSERT_TRUE(cache.get("z") != nullptr);
    ASSERT_TRUE(cache.get("w") != nullptr);
}

// =================================================================
// 3. Overwrite: putting same key again updates value and size
// =================================================================

TEST(overwrite_existing_key) {
    BlobCache cache(500, blob_value_size);

    cache.put("dup", make_value(100));

    auto v1 = cache.get("dup");
    ASSERT_TRUE(v1 != nullptr);
    ASSERT_EQ(v1->data.size(), 100u);

    cache.put("dup", make_value(200));

    auto v2 = cache.get("dup");
    ASSERT_TRUE(v2 != nullptr);
    ASSERT_EQ(v2->data.size(), 200u);
}

// =================================================================
// 4. Entry larger than max_bytes is rejected
// =================================================================

TEST(reject_oversized_entry) {
    BlobCache cache(100, blob_value_size);

    cache.put("huge", make_value(200));
    ASSERT_TRUE(cache.get("huge") == nullptr);
}

// =================================================================
// 5. Multiple evictions: inserting one large entry evicts several
// =================================================================

TEST(large_entry_evicts_multiple_small) {
    BlobCache cache(400, blob_value_size);

    cache.put("s1", make_value(100));
    cache.put("s2", make_value(100));
    cache.put("s3", make_value(100));
    cache.put("s4", make_value(100));

    cache.put("big", make_value(300));

    ASSERT_TRUE(cache.get("s1") == nullptr);
    ASSERT_TRUE(cache.get("s2") == nullptr);
    ASSERT_TRUE(cache.get("s3") == nullptr);
    ASSERT_TRUE(cache.get("s4") != nullptr);
    ASSERT_TRUE(cache.get("big") != nullptr);
}

// =================================================================
// 6. Eviction order respects access pattern, not insertion order
// =================================================================

TEST(eviction_respects_access_pattern) {
    BlobCache cache(300, blob_value_size);

    cache.put("p1", make_value(100));
    cache.put("p2", make_value(100));
    cache.put("p3", make_value(100));

    cache.get("p1");
    cache.get("p2");
    cache.get("p3");

    cache.put("p4", make_value(100));

    ASSERT_TRUE(cache.get("p1") == nullptr);
    ASSERT_TRUE(cache.get("p2") != nullptr);
    ASSERT_TRUE(cache.get("p3") != nullptr);
    ASSERT_TRUE(cache.get("p4") != nullptr);
}

// =================================================================
// 7. Cache correctly tracks size after mixed put/evict/overwrite
// =================================================================

TEST(size_tracking_after_churn) {
    BlobCache cache(500, blob_value_size);

    for (int i = 0; i < 5; ++i)
        cache.put("c" + std::to_string(i), make_value(100));

    cache.put("c0", make_value(50));
    cache.put("extra", make_value(50));

    for (int i = 0; i < 5; ++i)
        ASSERT_TRUE(cache.get("c" + std::to_string(i)) != nullptr);
    ASSERT_TRUE(cache.get("extra") != nullptr);
}

// =================================================================
// main
// =================================================================

int main() {
    std::cout << "Running " << tests.size() << " BlobCache tests\n"
              << std::string(50, '=') << "\n";

    for (auto& t : tests) {
        ++g_tests_run;
        std::cout << "[RUN ] " << t.name << "\n";
        try {
            t.fn();
            ++g_tests_passed;
            std::cout << "[PASS] " << t.name << "\n";
        } catch (const std::exception& e) {
            ++g_tests_failed;
            std::cout << "[FAIL] " << t.name << " (" << e.what() << ")\n";
        }
    }

    std::cout << std::string(50, '=') << "\n"
              << g_tests_passed << "/" << g_tests_run << " passed";
    if (g_tests_failed > 0)
        std::cout << ", " << g_tests_failed << " FAILED";
    std::cout << "\n";

    return g_tests_failed > 0 ? 1 : 0;
}
