/*
 * Cluster performance tests for mediadb:
 * - Spin up a local 3-node cluster (each node on a different port)
 * - Measure replicate / fetch / fetch_range / remove throughput
 * - Measure scrub + vacuum latency
 * - Verify data integrity after scrub
 * - Measure concurrent read vs. write contention (scrub_client_ / read_client_)
 * - ClusterMediaBackend: store/album/media CRUD through the full stack
 * - Preview generation + cache performance
 */
#include "../src/cluster.h"
#include "../src/backend.h"
#include "../src/preview.h"

#include <utils/certgen.h>

#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <numeric>
#include <string>
#include <thread>
#include <atomic>
#include <vector>

namespace fs = std::filesystem;
using namespace mediadb;

// ═══════════════════════════════════════════════════════════
//  Helpers
// ═══════════════════════════════════════════════════════════

using Clock = std::chrono::high_resolution_clock;

struct TimingStats {
    double min_us;
    double max_us;
    double avg_us;
    double total_ms;
};

static TimingStats computeStats(const std::vector<double>& samples_us) {
    TimingStats s{};
    if (samples_us.empty()) return s;
    s.min_us = *std::min_element(samples_us.begin(), samples_us.end());
    s.max_us = *std::max_element(samples_us.begin(), samples_us.end());
    s.total_ms = std::accumulate(samples_us.begin(), samples_us.end(), 0.0) / 1000.0;
    s.avg_us = (s.total_ms * 1000.0) / static_cast<double>(samples_us.size());
    return s;
}

static void printStats(const char* label, const TimingStats& s, size_t n) {
    std::cout << "  " << label << " (" << n << " ops): "
              << "min=" << s.min_us << " us  "
              << "avg=" << s.avg_us << " us  "
              << "max=" << s.max_us << " us  "
              << "total=" << s.total_ms << " ms\n";
}

static std::vector<uint8_t> makeTestData(size_t size, uint8_t seed = 0xAB) {
    std::vector<uint8_t> d(size);
    for (size_t i = 0; i < size; i++)
        d[i] = static_cast<uint8_t>((seed + i) & 0xFF);
    return d;
}

// RAII temp directory
class TmpDir {
    fs::path path_;
public:
    explicit TmpDir(const std::string& suffix) {
        path_ = fs::path("/tmp") / ("mediadb_cluster_perf_" +
                std::to_string(getpid()) + "_" + suffix);
        fs::create_directories(path_);
    }
    ~TmpDir() { std::error_code ec; fs::remove_all(path_, ec); }
    const fs::path& path() const { return path_; }
};

// Generate self-signed cert + key files in a directory, return paths
struct CertFiles {
    std::string cert_path;
    std::string key_path;
};

static CertFiles generateTestCerts(const fs::path& dir) {
    netplus::CertGenConfig cfg;
    cfg.commonName = "localhost";
    cfg.rsaBits = 2048;
    cfg.validDays = 1;

    netplus::CertKeyPair pair;
    if (!netplus::generateSelfSignedCert(cfg, pair)) {
        throw std::runtime_error("Failed to generate self-signed certificate");
    }

    CertFiles files;
    files.cert_path = (dir / "cert.pem").string();
    files.key_path = (dir / "key.der").string();

    if (!netplus::writePemFile(files.cert_path, pair.certPem))
        throw std::runtime_error("Failed to write cert PEM");
    if (!netplus::writeDerFile(files.key_path, pair.keyDer))
        throw std::runtime_error("Failed to write key DER");

    return files;
}

// Spin up a 3-node local cluster. All nodes share the same cert.
// Returns the clusters (caller must stop() them).
struct TestCluster {
    std::vector<std::unique_ptr<Cluster>> nodes;
    TmpDir tmp;
    CertFiles certs;

    TestCluster() : tmp("cluster") {}
    ~TestCluster() {
        for (auto& n : nodes) n->stop();
    }
};

static constexpr int BASE_PORT = 14433;
static constexpr size_t DATA_BLOCKS = 2;
static constexpr size_t PARITY_BLOCKS = 1;
static constexpr size_t NUM_NODES = DATA_BLOCKS + PARITY_BLOCKS;

static std::unique_ptr<TestCluster> makeCluster() {
    auto tc = std::make_unique<TestCluster>();
    tc->certs = generateTestCerts(tc->tmp.path());

    // Build peer list (all nodes on localhost, different ports)
    std::vector<ClusterNode> peers;
    for (size_t i = 0; i < NUM_NODES; i++) {
        peers.push_back({"127.0.0.1", BASE_PORT + static_cast<int>(i)});
    }

    for (size_t i = 0; i < NUM_NODES; i++) {
        auto node = std::make_unique<Cluster>();

        ClusterConfig cfg;
        cfg.enabled = true;
        cfg.peers = peers;
        cfg.bind_address = "0.0.0.0";
        cfg.port = BASE_PORT + static_cast<int>(i);
        cfg.cert_file = tc->certs.cert_path;
        cfg.key_file = tc->certs.key_path;
        cfg.store_path = (tc->tmp.path() / ("node_" + std::to_string(i))).string();
        cfg.data_blocks = DATA_BLOCKS;
        cfg.parity_blocks = PARITY_BLOCKS;

        fs::create_directories(cfg.store_path);

        node->init(cfg);
        node->start();
        tc->nodes.push_back(std::move(node));
    }

    // Wait for nodes to come up
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    return tc;
}

// ═══════════════════════════════════════════════════════════
//  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)

// ═══════════════════════════════════════════════════════════
//  1. Replicate throughput
// ═══════════════════════════════════════════════════════════

TEST(replicate_throughput) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 100;
    constexpr size_t DATA_SIZE = 4096;
    auto data = makeTestData(DATA_SIZE);

    std::vector<double> times;
    std::vector<std::string> keys;

    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "replicate_perf_" + std::to_string(i);
        keys.push_back(key);

        auto t0 = Clock::now();
        bool ok = node.replicate(key, data.data(), data.size());
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        times.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    auto s = computeStats(times);
    double mbps = (double)(NUM_KEYS * DATA_SIZE) / (1024.0 * 1024.0) / (s.total_ms / 1000.0);

    std::cout << "\n=== Replicate Throughput (" << NUM_KEYS << " x "
              << DATA_SIZE << " B) ===\n";
    printStats("REPLICATE", s, NUM_KEYS);
    std::cout << "  Throughput = " << mbps << " MB/s\n";
}

// ═══════════════════════════════════════════════════════════
//  2. Fetch throughput
// ═══════════════════════════════════════════════════════════

TEST(fetch_throughput) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 100;
    constexpr size_t DATA_SIZE = 4096;
    auto data = makeTestData(DATA_SIZE);

    // Populate
    std::vector<std::string> keys;
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "fetch_perf_" + std::to_string(i);
        keys.push_back(key);
        ASSERT_TRUE(node.replicate(key, data.data(), data.size()));
    }

    // Fetch from same node
    std::vector<double> times;
    for (auto& key : keys) {
        std::vector<uint8_t> out;
        auto t0 = Clock::now();
        bool ok = node.fetch(key, out);
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        ASSERT_EQ(out.size(), DATA_SIZE);
        times.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    auto s = computeStats(times);
    double mbps = (double)(NUM_KEYS * DATA_SIZE) / (1024.0 * 1024.0) / (s.total_ms / 1000.0);

    std::cout << "\n=== Fetch Throughput (" << NUM_KEYS << " x " << DATA_SIZE << " B) ===\n";
    printStats("FETCH", s, NUM_KEYS);
    std::cout << "  Throughput = " << mbps << " MB/s\n";

    // Fetch from different node (cross-node)
    std::vector<double> cross_times;
    auto& node2 = *tc->nodes[1];
    for (auto& key : keys) {
        std::vector<uint8_t> out;
        auto t0 = Clock::now();
        bool ok = node2.fetch(key, out);
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        ASSERT_EQ(out.size(), DATA_SIZE);
        cross_times.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    auto cs = computeStats(cross_times);
    double cmbps = (double)(NUM_KEYS * DATA_SIZE) / (1024.0 * 1024.0) / (cs.total_ms / 1000.0);

    printStats("FETCH (cross-node)", cs, NUM_KEYS);
    std::cout << "  Cross-node throughput = " << cmbps << " MB/s\n";
}

// ═══════════════════════════════════════════════════════════
//  3. Fetch range throughput
// ═══════════════════════════════════════════════════════════

TEST(fetch_range_throughput) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 50;
    constexpr size_t DATA_SIZE = 64 * 1024; // 64 KB each
    constexpr size_t RANGE_SIZE = 4096;
    auto data = makeTestData(DATA_SIZE, 0x42);

    std::vector<std::string> keys;
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "range_perf_" + std::to_string(i);
        keys.push_back(key);
        ASSERT_TRUE(node.replicate(key, data.data(), data.size()));
    }

    std::vector<double> times;
    for (auto& key : keys) {
        std::vector<uint8_t> out;
        auto t0 = Clock::now();
        bool ok = node.fetch_range(key, DATA_SIZE / 2, RANGE_SIZE, out);
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        ASSERT_EQ(out.size(), RANGE_SIZE);
        times.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    auto s = computeStats(times);
    std::cout << "\n=== Fetch Range Throughput (" << NUM_KEYS << " x "
              << RANGE_SIZE << " B range from " << DATA_SIZE / 1024 << " KB blobs) ===\n";
    printStats("FETCH_RANGE", s, NUM_KEYS);
}

// ═══════════════════════════════════════════════════════════
//  4. Remove throughput
// ═══════════════════════════════════════════════════════════

TEST(remove_throughput) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 100;
    constexpr size_t DATA_SIZE = 2048;
    auto data = makeTestData(DATA_SIZE);

    std::vector<std::string> keys;
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "remove_perf_" + std::to_string(i);
        keys.push_back(key);
        ASSERT_TRUE(node.replicate(key, data.data(), data.size()));
    }

    std::vector<double> times;
    for (auto& key : keys) {
        auto t0 = Clock::now();
        node.remove(key);
        auto t1 = Clock::now();
        times.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    auto s = computeStats(times);
    std::cout << "\n=== Remove Throughput (" << NUM_KEYS << " ops) ===\n";
    printStats("REMOVE", s, NUM_KEYS);

    // Verify removed
    for (auto& key : keys) {
        std::vector<uint8_t> out;
        bool ok = node.fetch(key, out);
        ASSERT_TRUE(!ok || out.empty());
    }
}

// ═══════════════════════════════════════════════════════════
//  5. Scrub + vacuum latency
// ═══════════════════════════════════════════════════════════

TEST(scrub_and_vacuum) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 50;
    constexpr size_t DATA_SIZE = 4096;
    auto data = makeTestData(DATA_SIZE);

    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "scrub_perf_" + std::to_string(i);
        ASSERT_TRUE(node.replicate(key, data.data(), data.size()));
    }

    // Scrub
    auto t0 = Clock::now();
    auto result = node.scrub();
    auto t1 = Clock::now();
    double scrubMs = std::chrono::duration<double, std::milli>(t1 - t0).count();

    std::cout << "\n=== Scrub + Vacuum (" << NUM_KEYS << " groups) ===\n"
              << "  scrub time     = " << scrubMs << " ms\n"
              << "  groups checked = " << result.groups_checked << "\n"
              << "  groups repaired= " << result.groups_repaired << "\n"
              << "  groups failed  = " << result.groups_failed << "\n";

    ASSERT_EQ(result.groups_failed, 0u);

    // Vacuum all nodes
    auto t2 = Clock::now();
    size_t vacuumed = node.vacuum_all_nodes();
    auto t3 = Clock::now();
    double vacuumMs = std::chrono::duration<double, std::milli>(t3 - t2).count();

    std::cout << "  vacuum time    = " << vacuumMs << " ms\n"
              << "  nodes vacuumed = " << vacuumed << "\n";

    // Verify data intact after scrub
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "scrub_perf_" + std::to_string(i);
        std::vector<uint8_t> out;
        ASSERT_TRUE(node.fetch(key, out));
        ASSERT_EQ(out.size(), DATA_SIZE);
        ASSERT_EQ(out[0], data[0]);
    }
}

// ═══════════════════════════════════════════════════════════
//  6. Concurrent read vs. write (contention test)
// ═══════════════════════════════════════════════════════════

TEST(concurrent_read_write) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 80;
    constexpr size_t DATA_SIZE = 8192;
    auto data = makeTestData(DATA_SIZE);

    // Pre-populate
    std::vector<std::string> keys;
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "conc_perf_" + std::to_string(i);
        keys.push_back(key);
        ASSERT_TRUE(node.replicate(key, data.data(), data.size()));
    }

    std::atomic<size_t> reads_ok{0};
    std::atomic<size_t> writes_ok{0};
    std::atomic<bool> stop_flag{false};

    // Reader thread: fetch existing keys in a loop
    std::thread reader([&]() {
        size_t idx = 0;
        while (!stop_flag.load()) {
            std::vector<uint8_t> out;
            if (node.fetch(keys[idx % keys.size()], out) && out.size() == DATA_SIZE)
                reads_ok++;
            idx++;
        }
    });

    // Writer thread: replicate new keys
    auto t0 = Clock::now();
    constexpr size_t WRITE_COUNT = 50;
    for (size_t i = 0; i < WRITE_COUNT; i++) {
        std::string key = "conc_write_" + std::to_string(i);
        if (node.replicate(key, data.data(), data.size()))
            writes_ok++;
    }
    auto t1 = Clock::now();

    stop_flag = true;
    reader.join();

    double writeMs = std::chrono::duration<double, std::milli>(t1 - t0).count();

    std::cout << "\n=== Concurrent Read vs. Write ===\n"
              << "  writes completed = " << writes_ok.load() << "/" << WRITE_COUNT << "\n"
              << "  write time       = " << writeMs << " ms\n"
              << "  reads during writes = " << reads_ok.load() << "\n"
              << "  write throughput = "
              << ((double)(writes_ok.load() * DATA_SIZE) / (1024.0 * 1024.0) / (writeMs / 1000.0))
              << " MB/s\n";

    ASSERT_TRUE(writes_ok.load() == WRITE_COUNT);
    ASSERT_TRUE(reads_ok.load() > 0);
}

// ═══════════════════════════════════════════════════════════
//  7. Scrub under concurrent reads (read_client_ isolation)
// ═══════════════════════════════════════════════════════════

TEST(scrub_under_concurrent_reads) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 60;
    constexpr size_t DATA_SIZE = 4096;
    auto data = makeTestData(DATA_SIZE);

    std::vector<std::string> keys;
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "scrub_conc_" + std::to_string(i);
        keys.push_back(key);
        ASSERT_TRUE(node.replicate(key, data.data(), data.size()));
    }

    std::atomic<size_t> reads_ok{0};
    std::atomic<size_t> reads_fail{0};
    std::atomic<bool> stop_flag{false};

    // Continuous reader — should NOT be blocked by scrub thanks to read_client_
    std::thread reader([&]() {
        size_t idx = 0;
        while (!stop_flag.load()) {
            std::vector<uint8_t> out;
            if (node.fetch(keys[idx % keys.size()], out) && out.size() == DATA_SIZE)
                reads_ok++;
            else
                reads_fail++;
            idx++;
        }
    });

    // Run scrub on same node
    auto t0 = Clock::now();
    auto result = node.scrub();
    auto t1 = Clock::now();

    stop_flag = true;
    reader.join();

    double scrubMs = std::chrono::duration<double, std::milli>(t1 - t0).count();

    std::cout << "\n=== Scrub Under Concurrent Reads ===\n"
              << "  scrub time       = " << scrubMs << " ms\n"
              << "  groups checked   = " << result.groups_checked << "\n"
              << "  reads during scrub = " << reads_ok.load() << "\n"
              << "  read failures    = " << reads_fail.load() << "\n";

    ASSERT_EQ(result.groups_failed, 0u);
    ASSERT_TRUE(reads_ok.load() > 0);
}

// ═══════════════════════════════════════════════════════════
//  8. Large file (4 MB) replicate + fetch + concurrent
// ═══════════════════════════════════════════════════════════

TEST(large_file_4mb) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    constexpr size_t NUM_KEYS = 10;
    constexpr size_t FILE_SIZE = 4 * 1024 * 1024; // 4 MB — exercises multi-stripe path
    auto data = makeTestData(FILE_SIZE, 0x42);

    // Replicate
    std::vector<std::string> keys;
    std::vector<double> writeTimes;
    for (size_t i = 0; i < NUM_KEYS; i++) {
        std::string key = "large4m_" + std::to_string(i);
        keys.push_back(key);
        auto t0 = Clock::now();
        bool ok = node.replicate(key, data.data(), data.size());
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        writeTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // Fetch
    std::vector<double> readTimes;
    for (auto& key : keys) {
        std::vector<uint8_t> out;
        auto t0 = Clock::now();
        bool ok = node.fetch(key, out);
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        ASSERT_EQ(out.size(), FILE_SIZE);
        // Verify first + last byte
        ASSERT_EQ(out[0], data[0]);
        ASSERT_EQ(out[FILE_SIZE - 1], data[FILE_SIZE - 1]);
        readTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // Range read (64 KB from middle)
    constexpr size_t RANGE_SIZE = 64 * 1024;
    std::vector<double> rangeTimes;
    for (auto& key : keys) {
        std::vector<uint8_t> out;
        auto t0 = Clock::now();
        bool ok = node.fetch_range(key, FILE_SIZE / 2, RANGE_SIZE, out);
        auto t1 = Clock::now();
        ASSERT_TRUE(ok);
        ASSERT_EQ(out.size(), RANGE_SIZE);
        rangeTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // Concurrent: write 4 MB while reading 4 MB on same node
    std::atomic<size_t> reads_ok{0};
    std::atomic<bool> stop_flag{false};

    std::thread reader([&]() {
        size_t idx = 0;
        while (!stop_flag.load()) {
            std::vector<uint8_t> out;
            if (node.fetch(keys[idx % keys.size()], out) && out.size() == FILE_SIZE)
                reads_ok++;
            idx++;
        }
    });

    constexpr size_t CONC_WRITES = 5;
    std::vector<double> concWriteTimes;
    for (size_t i = 0; i < CONC_WRITES; i++) {
        std::string key = "large4m_conc_" + std::to_string(i);
        auto t0 = Clock::now();
        node.replicate(key, data.data(), data.size());
        auto t1 = Clock::now();
        concWriteTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    stop_flag = true;
    reader.join();

    auto ws = computeStats(writeTimes);
    auto rs = computeStats(readTimes);
    auto rng = computeStats(rangeTimes);
    auto cws = computeStats(concWriteTimes);

    double writeMBs = (double)(NUM_KEYS * FILE_SIZE) / (1024.0 * 1024.0) / (ws.total_ms / 1000.0);
    double readMBs = (double)(NUM_KEYS * FILE_SIZE) / (1024.0 * 1024.0) / (rs.total_ms / 1000.0);

    std::cout << "\n=== Large File 4 MB (" << NUM_KEYS << " files) ===\n";
    printStats("REPLICATE  ", ws, NUM_KEYS);
    std::cout << "  Write throughput = " << writeMBs << " MB/s\n";
    printStats("FETCH      ", rs, NUM_KEYS);
    std::cout << "  Read throughput  = " << readMBs << " MB/s\n";
    printStats("RANGE 64KB ", rng, NUM_KEYS);
    printStats("CONC WRITE ", cws, CONC_WRITES);
    std::cout << "  Reads during " << CONC_WRITES << " x 4MB writes = " << reads_ok.load() << "\n";
}

// ═══════════════════════════════════════════════════════════
//  9. ClusterMediaBackend CRUD (store/album/media lifecycle)
// ═══════════════════════════════════════════════════════════

// Generate a minimal valid JPEG (2x2 red pixel, ~600 bytes)
static std::vector<uint8_t> makeTestJpeg() {
    // Encode a tiny image via FFmpeg to get a valid JPEG
    AVFrame* frame = av_frame_alloc();
    frame->format = AV_PIX_FMT_YUV420P;
    frame->width = 2;
    frame->height = 2;
    av_frame_get_buffer(frame, 0);
    // Fill with red in YUV: Y=82, U=90, V=240
    memset(frame->data[0], 82, frame->linesize[0] * 2);
    memset(frame->data[1], 90, frame->linesize[1]);
    memset(frame->data[2], 240, frame->linesize[2]);

    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
    AVCodecContext* ctx = avcodec_alloc_context3(codec);
    ctx->width = 2;
    ctx->height = 2;
    ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;
    ctx->time_base = {1, 25};
    avcodec_open2(ctx, codec, nullptr);

    AVPacket* pkt = av_packet_alloc();
    avcodec_send_frame(ctx, frame);
    avcodec_receive_packet(ctx, pkt);

    std::vector<uint8_t> jpeg(pkt->data, pkt->data + pkt->size);

    av_packet_free(&pkt);
    avcodec_free_context(&ctx);
    av_frame_free(&frame);
    return jpeg;
}

TEST(cluster_backend_crud) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    BlobCache data_cache(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend backend(node, data_cache);
    // Don't start sync_loop — we test single-node operations

    constexpr size_t NUM_MEDIA = 30;
    constexpr size_t MEDIA_SIZE = 8192;
    auto data = makeTestData(MEDIA_SIZE);

    // Create store
    auto t0 = Clock::now();
    auto store = backend.create_store("perf_store");
    auto t1 = Clock::now();
    double createStoreMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
    ASSERT_TRUE(!store.id.empty());

    // Create album
    t0 = Clock::now();
    auto album = backend.create_album(store.id, "perf_album");
    t1 = Clock::now();
    double createAlbumMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
    ASSERT_TRUE(album.has_value());

    // Add media
    std::vector<std::string> mediaIds;
    std::vector<double> addTimes;
    for (size_t i = 0; i < NUM_MEDIA; i++) {
        std::vector<uint8_t> mediaData(data);
        t0 = Clock::now();
        auto m = backend.add_media(album->id,
            "test_" + std::to_string(i) + ".bin",
            "application/octet-stream", std::move(mediaData));
        t1 = Clock::now();
        ASSERT_TRUE(m.has_value());
        mediaIds.push_back(m->id);
        addTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // Read media data
    std::vector<double> readTimes;
    for (auto& id : mediaIds) {
        t0 = Clock::now();
        auto raw = backend.read_media_data(id);
        t1 = Clock::now();
        ASSERT_EQ(raw.size(), MEDIA_SIZE);
        readTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // Read range (1 KB from middle)
    std::vector<double> rangeTimes;
    for (auto& id : mediaIds) {
        t0 = Clock::now();
        auto range = backend.read_media_data_range(id, MEDIA_SIZE / 2, 1024);
        t1 = Clock::now();
        ASSERT_EQ(range.size(), 1024u);
        rangeTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // List media
    t0 = Clock::now();
    auto list = backend.list_media(album->id);
    t1 = Clock::now();
    double listMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
    ASSERT_EQ(list.size(), NUM_MEDIA);

    // Delete media (first half)
    std::vector<double> delTimes;
    for (size_t i = 0; i < NUM_MEDIA / 2; i++) {
        t0 = Clock::now();
        bool ok = backend.delete_media(mediaIds[i]);
        t1 = Clock::now();
        ASSERT_TRUE(ok);
        delTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    // Verify deleted
    auto remaining = backend.list_media(album->id);
    ASSERT_EQ(remaining.size(), NUM_MEDIA - NUM_MEDIA / 2);

    // Delete store (cascade)
    t0 = Clock::now();
    bool storeDeleted = backend.delete_store(store.id);
    t1 = Clock::now();
    double deleteStoreMs = std::chrono::duration<double, std::milli>(t1 - t0).count();
    ASSERT_TRUE(storeDeleted);

    auto addS = computeStats(addTimes);
    auto readS = computeStats(readTimes);
    auto rangeS = computeStats(rangeTimes);
    auto delS = computeStats(delTimes);

    std::cout << "\n=== ClusterMediaBackend CRUD (" << NUM_MEDIA << " media, "
              << MEDIA_SIZE << " B) ===\n"
              << "  create store  = " << createStoreMs << " ms\n"
              << "  create album  = " << createAlbumMs << " ms\n";
    printStats("ADD MEDIA  ", addS, NUM_MEDIA);
    printStats("READ DATA  ", readS, NUM_MEDIA);
    printStats("READ RANGE ", rangeS, NUM_MEDIA);
    std::cout << "  list media    = " << listMs << " ms (" << NUM_MEDIA << " items)\n";
    printStats("DELETE MEDIA", delS, NUM_MEDIA / 2);
    std::cout << "  delete store  = " << deleteStoreMs << " ms (cascade)\n";
}

// ═══════════════════════════════════════════════════════════
//  10. Preview generation + cache (FFmpeg → cluster round-trip)
// ═══════════════════════════════════════════════════════════

TEST(preview_generate_and_cache) {
    auto tc = makeCluster();
    auto& node = *tc->nodes[0];

    BlobCache data_cache(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend backend(node, data_cache);

    auto store = backend.create_store("preview_store");
    auto album = backend.create_album(store.id, "preview_album");
    ASSERT_TRUE(album.has_value());

    // Upload a valid JPEG image
    auto jpeg = makeTestJpeg();
    ASSERT_TRUE(jpeg.size() > 0);

    auto media = backend.add_media(album->id, "test.jpg", "image/jpeg",
                                   std::vector<uint8_t>(jpeg));
    ASSERT_TRUE(media.has_value());

    // Verify round-trip
    auto fetched = backend.read_media_data(media->id);
    ASSERT_EQ(fetched.size(), jpeg.size());

    // Setup preview service
    BlobCache cache(64 * 1024 * 1024, blob_value_size); // 64 MB cache
    PreviewService svc(cache, backend);

    constexpr int PREVIEW_W = 64;
    constexpr int PREVIEW_H = 64;
    constexpr size_t NUM_REQUESTS = 20;

    // First request: generate preview (cold cache)
    std::string err;
    auto t0 = Clock::now();
    auto pv = svc.get_or_create_streaming(*media, PREVIEW_W, PREVIEW_H, "jpeg", 0, 0, err);
    auto t1 = Clock::now();
    double generateMs = std::chrono::duration<double, std::milli>(t1 - t0).count();

    ASSERT_TRUE(pv != nullptr);
    ASSERT_TRUE(pv->data.size() > 0);

    std::cout << "\n=== Preview Generate + Cache ===\n"
              << "  source JPEG    = " << jpeg.size() << " bytes\n"
              << "  preview size   = " << pv->data.size() << " bytes ("
              << PREVIEW_W << "x" << PREVIEW_H << " jpeg)\n"
              << "  generate (cold)= " << generateMs << " ms\n";

    // Subsequent requests: cache hits
    std::vector<double> cacheTimes;
    for (size_t i = 0; i < NUM_REQUESTS; i++) {
        t0 = Clock::now();
        auto cached = svc.get_or_create_streaming(*media, PREVIEW_W, PREVIEW_H, "jpeg", 0, 0, err);
        t1 = Clock::now();
        ASSERT_TRUE(cached != nullptr);
        ASSERT_EQ(cached->data.size(), pv->data.size());
        cacheTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    auto cacheS = computeStats(cacheTimes);
    printStats("CACHE HIT  ", cacheS, NUM_REQUESTS);

    // Different sizes: each triggers a new generate
    std::vector<double> variantTimes;
    int sizes[] = {32, 48, 96, 128, 256};
    for (int sz : sizes) {
        t0 = Clock::now();
        auto variant = svc.get_or_create_streaming(*media, sz, sz, "jpeg", 0, 0, err);
        t1 = Clock::now();
        ASSERT_TRUE(variant != nullptr);
        variantTimes.push_back(std::chrono::duration<double, std::milli>(t1 - t0).count());
    }

    auto varS = computeStats(variantTimes);
    std::cout << "  variant gen    = avg " << varS.avg_us / 1000.0
              << " ms (" << sizeof(sizes) / sizeof(sizes[0]) << " sizes)\n";

    // WebP format
    t0 = Clock::now();
    auto webp = svc.get_or_create_streaming(*media, PREVIEW_W, PREVIEW_H, "webp", 0, 0, err);
    t1 = Clock::now();

    if (webp) {
        std::cout << "  webp generate  = "
                  << std::chrono::duration<double, std::milli>(t1 - t0).count()
                  << " ms (" << webp->data.size() << " bytes)\n";
    } else {
        std::cout << "  webp generate  = skipped (codec unavailable)\n";
    }
}

// ═══════════════════════════════════════════════════════════
//  main
// ═══════════════════════════════════════════════════════════

int main() {
    std::cout << "Running " << tests.size() << " mediadb cluster performance tests\n"
              << std::string(60, '=') << "\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(60, '=') << "\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;
}
