/*
 * PreviewCache 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/preview.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 PreviewKey make_key(const std::string& id, int w = 0, int h = 0) {
    return PreviewKey{id, w, h, "webp", 0, 0};
}

static PreviewValue make_value(std::size_t size) {
    PreviewValue v;
    v.bytes.resize(size, 0xAB);
    v.mime = "image/webp";
    v.etag = "W/\"test\"";
    v.size = size;
    return v;
}

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

TEST(eviction_removes_oldest) {
    // Cache fits exactly 2 entries of 100 bytes each
    PreviewCache cache(200);

    auto k1 = make_key("a");
    auto k2 = make_key("b");
    auto k3 = make_key("c");

    cache.put(k1, make_value(100));
    cache.put(k2, make_value(100));

    // Both should be present
    ASSERT_TRUE(cache.get(k1) != nullptr);
    ASSERT_TRUE(cache.get(k2) != nullptr);

    // Adding k3 must evict the LRU entry (k1, since k2 was just accessed by get)
    // Actually after the two get() calls above, k2 was accessed last, k1 before that.
    // LRU order after puts: front=[k2, k1]=back
    // After get(k1): front=[k1, k2]=back
    // After get(k2): front=[k2, k1]=back
    // So inserting k3 evicts k1 (the back)
    cache.put(k3, make_value(100));

    ASSERT_TRUE(cache.get(k1) == nullptr);  // evicted
    ASSERT_TRUE(cache.get(k2) != nullptr);  // survived
    ASSERT_TRUE(cache.get(k3) != nullptr);  // just inserted
}

// =================================================================
// 2. LRU promotion: accessing an entry moves it to front, protecting
//    it from eviction
// =================================================================

TEST(lru_promotion_protects_entry) {
    PreviewCache cache(300);

    auto k1 = make_key("x");
    auto k2 = make_key("y");
    auto k3 = make_key("z");

    cache.put(k1, make_value(100));  // LRU: [k1]
    cache.put(k2, make_value(100));  // LRU: [k2, k1]
    cache.put(k3, make_value(100));  // LRU: [k3, k2, k1]

    // Access k1 to promote it to front
    auto hit = cache.get(k1);        // LRU: [k1, k3, k2]
    ASSERT_TRUE(hit != nullptr);

    // Now insert k4 (100 bytes) — must evict k2 (the back), NOT k1
    auto k4 = make_key("w");
    cache.put(k4, make_value(100));  // LRU: [k4, k1, k3] — k2 evicted

    ASSERT_TRUE(cache.get(k1) != nullptr);  // promoted, survived
    ASSERT_TRUE(cache.get(k2) == nullptr);  // was LRU tail, evicted
    ASSERT_TRUE(cache.get(k3) != nullptr);  // still fits
    ASSERT_TRUE(cache.get(k4) != nullptr);  // just inserted
}

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

TEST(overwrite_existing_key) {
    PreviewCache cache(500);

    auto k1 = make_key("dup");
    cache.put(k1, make_value(100));

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

    // Overwrite with larger value
    cache.put(k1, make_value(200));

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

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

TEST(reject_oversized_entry) {
    PreviewCache cache(100);

    auto k1 = make_key("huge");
    cache.put(k1, make_value(200));  // 200 > 100, should be rejected

    ASSERT_TRUE(cache.get(k1) == nullptr);
}

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

TEST(large_entry_evicts_multiple_small) {
    PreviewCache cache(400);

    auto k1 = make_key("s1");
    auto k2 = make_key("s2");
    auto k3 = make_key("s3");
    auto k4 = make_key("s4");

    cache.put(k1, make_value(100));
    cache.put(k2, make_value(100));
    cache.put(k3, make_value(100));
    cache.put(k4, make_value(100));  // full: 400/400

    // Insert a 300-byte entry — must evict 3 oldest entries
    auto k_big = make_key("big");
    cache.put(k_big, make_value(300));

    // k1, k2, k3 evicted (LRU tail first), only k4 + k_big remain
    ASSERT_TRUE(cache.get(k1) == nullptr);
    ASSERT_TRUE(cache.get(k2) == nullptr);
    ASSERT_TRUE(cache.get(k3) == nullptr);
    ASSERT_TRUE(cache.get(k4) != nullptr);
    ASSERT_TRUE(cache.get(k_big) != nullptr);
}

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

TEST(eviction_respects_access_pattern) {
    PreviewCache cache(300);

    auto k1 = make_key("p1");
    auto k2 = make_key("p2");
    auto k3 = make_key("p3");

    cache.put(k1, make_value(100));  // [k1]
    cache.put(k2, make_value(100));  // [k2, k1]
    cache.put(k3, make_value(100));  // [k3, k2, k1] — full

    // Access in reverse: k1, k2, k3 → LRU becomes [k3, k2, k1]
    cache.get(k1);  // [k1, k3, k2]
    cache.get(k2);  // [k2, k1, k3]
    cache.get(k3);  // [k3, k2, k1]

    // Insert k4 — evicts k1 (tail after the access pattern)
    auto k4 = make_key("p4");
    cache.put(k4, make_value(100));

    ASSERT_TRUE(cache.get(k1) == nullptr);  // evicted (was LRU)
    ASSERT_TRUE(cache.get(k2) != nullptr);
    ASSERT_TRUE(cache.get(k3) != nullptr);
    ASSERT_TRUE(cache.get(k4) != nullptr);
}

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

TEST(size_tracking_after_churn) {
    PreviewCache cache(500);

    // Fill with 5 × 100 = 500
    for (int i = 0; i < 5; ++i)
        cache.put(make_key("c" + std::to_string(i)), make_value(100));

    // Overwrite c0 with 50 bytes — frees 100, adds 50 → used=450
    cache.put(make_key("c0"), make_value(50));

    // Should fit one more 50-byte entry without evicting anything
    cache.put(make_key("extra"), make_value(50));  // used=500

    // All should still be present
    for (int i = 0; i < 5; ++i)
        ASSERT_TRUE(cache.get(make_key("c" + std::to_string(i))) != nullptr);
    ASSERT_TRUE(cache.get(make_key("extra")) != nullptr);
}

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

int main() {
    std::cout << "Running " << tests.size() << " PreviewCache 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;
}
