/*
 * Import cluster unit tests for mediadb:
 * - Spin up a local 3-node cluster (shared across all tests)
 * - Test ClusterMediaBackend::begin_import() with incremental feed
 * - Verify import progress reporting
 * - Test import with multiple stores and albums
 * - Verify data integrity after import via cluster fetch
 * - Test import followed by sync_from_cluster on a fresh backend
 * - Test import with varied chunk sizes
 * - Test import of zero-length media
 * - Test double import (re-import over existing data)
 */
#include "../src/cluster.h"
#include "../src/backend.h"

#include <utils/certgen.h>

#include <cassert>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

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

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

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_import_cluster_test_" +
                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
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;
}

// Shared cluster infrastructure — one cluster for all tests
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 = 15433;
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> g_cluster;

static void ensureCluster() {
    if (g_cluster) return;
    g_cluster = std::make_unique<TestCluster>();
    g_cluster->certs = generateTestCerts(g_cluster->tmp.path());

    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 = g_cluster->certs.cert_path;
        cfg.key_file = g_cluster->certs.key_path;
        cfg.store_path = (g_cluster->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();
        g_cluster->nodes.push_back(std::move(node));
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    g_cluster->nodes[0]->waitForPeers(30);
}

// ═══════════════════════════════════════════════════════════
//  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. Basic incremental import through ClusterMediaBackend
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_basic) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    // Create test data
    auto store = src.create_store("photos");
    ASSERT_TRUE(!store.id.empty());
    auto album = src.create_album(store.id, "vacation");
    ASSERT_TRUE(album.has_value());

    auto data1 = makeTestData(2048, 0x11);
    auto data2 = makeTestData(4096, 0x22);
    auto m1 = src.add_media(album->id, "img1.jpg", "image/jpeg", data1);
    auto m2 = src.add_media(album->id, "img2.png", "image/png", data2);
    ASSERT_TRUE(m1.has_value());
    ASSERT_TRUE(m2.has_value());

    // Export
    auto exported = src.export_db_to_buffer();
    ASSERT_TRUE(!exported.empty());

    // Import into fresh backend via begin_import()
    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);

    auto session = dst.begin_import();
    ASSERT_TRUE(session != nullptr);

    // Feed in small chunks
    size_t offset = 0;
    while (offset < exported.size() && !session->done()) {
        size_t chunk = std::min<size_t>(256, exported.size() - offset);
        session->feed(reinterpret_cast<const char*>(exported.data() + offset), chunk);
        offset += chunk;
    }

    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());

    // Session destructor triggers sync_from_cluster, release it first
    session.reset();

    // Verify structure
    auto stores = dst.list_stores();
    ASSERT_EQ(stores.size(), 1u);
    ASSERT_EQ(stores[0].name, "photos");

    auto albums = dst.list_albums(store.id);
    ASSERT_EQ(albums.size(), 1u);

    auto media = dst.list_media(album->id);
    ASSERT_EQ(media.size(), 2u);

    // Verify data integrity via cluster fetch
    auto rd1 = dst.read_media_data(m1->id);
    ASSERT_EQ(rd1.size(), 2048u);
    ASSERT_TRUE(rd1 == data1);

    auto rd2 = dst.read_media_data(m2->id);
    ASSERT_EQ(rd2.size(), 4096u);
    ASSERT_TRUE(rd2 == data2);

    std::cout << "  basic import: 2 media items verified\n";
}

// ═══════════════════════════════════════════════════════════
//  2. Import with progress reporting
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_progress) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("progress_store");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    constexpr size_t NUM_MEDIA = 5;
    for (size_t i = 0; i < NUM_MEDIA; i++) {
        auto d = makeTestData(1024, static_cast<uint8_t>(i));
        auto m = src.add_media(album->id, "f" + std::to_string(i) + ".bin",
                               "application/octet-stream", std::move(d));
        ASSERT_TRUE(m.has_value());
    }

    auto exported = src.export_db_to_buffer();

    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);
    auto session = dst.begin_import();
    ASSERT_TRUE(session != nullptr);

    // Check initial progress
    auto p = session->progress();
    ASSERT_EQ(p.bytes_fed, 0u);

    // Feed and track progress
    size_t offset = 0;
    size_t last_bytes_fed = 0;
    while (offset < exported.size() && !session->done()) {
        size_t chunk = std::min<size_t>(512, exported.size() - offset);
        session->feed(reinterpret_cast<const char*>(exported.data() + offset), chunk);
        offset += chunk;

        auto prog = session->progress();
        // bytes_fed should be monotonically increasing
        ASSERT_TRUE(prog.bytes_fed >= last_bytes_fed);
        last_bytes_fed = prog.bytes_fed;
    }

    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());

    auto final_prog = session->progress();
    std::cout << "  progress: phase=" << final_prog.phase
              << " bytes_fed=" << final_prog.bytes_fed
              << " media_done=" << final_prog.media_done
              << " stores_done=" << final_prog.stores_done << "\n";

    // bytes_fed should equal total exported size
    ASSERT_EQ(final_prog.bytes_fed, exported.size());
}

// ═══════════════════════════════════════════════════════════
//  3. Import multiple stores with multiple albums
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_multi_store) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    // Create two stores with multiple albums
    auto s1 = src.create_store("multi_photos");
    auto s2 = src.create_store("multi_documents");
    auto a1 = src.create_album(s1.id, "vacation");
    auto a2 = src.create_album(s1.id, "family");
    auto a3 = src.create_album(s2.id, "work");
    ASSERT_TRUE(a1.has_value() && a2.has_value() && a3.has_value());

    auto d1 = makeTestData(512, 0x10);
    auto d2 = makeTestData(1024, 0x20);
    auto d3 = makeTestData(2048, 0x30);
    auto d4 = makeTestData(4096, 0x40);

    auto m1 = src.add_media(a1->id, "beach.jpg", "image/jpeg", d1);
    auto m2 = src.add_media(a1->id, "sunset.png", "image/png", d2);
    auto m3 = src.add_media(a2->id, "kids.mp4", "video/mp4", d3);
    auto m4 = src.add_media(a3->id, "report.pdf", "application/pdf", d4);
    ASSERT_TRUE(m1.has_value() && m2.has_value() && m3.has_value() && m4.has_value());

    auto exported = src.export_db_to_buffer();

    // Import into fresh backend
    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);
    auto session = dst.begin_import();

    // Feed entire buffer at once
    session->feed(reinterpret_cast<const char*>(exported.data()), exported.size());
    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());

    // Release session to trigger post-import sync
    session.reset();

    // Verify structure — shared cluster may have stores from prior tests,
    // so check that our stores are present rather than exact count.
    auto all_stores = dst.list_stores();
    bool found_s1 = false, found_s2 = false;
    for (const auto& s : all_stores) {
        if (s.id == s1.id) found_s1 = true;
        if (s.id == s2.id) found_s2 = true;
    }
    ASSERT_TRUE(found_s1);
    ASSERT_TRUE(found_s2);

    ASSERT_EQ(dst.list_albums(s1.id).size(), 2u);
    ASSERT_EQ(dst.list_albums(s2.id).size(), 1u);
    ASSERT_EQ(dst.list_media(a1->id).size(), 2u);
    ASSERT_EQ(dst.list_media(a2->id).size(), 1u);
    ASSERT_EQ(dst.list_media(a3->id).size(), 1u);

    // Verify data
    ASSERT_TRUE(dst.read_media_data(m1->id) == d1);
    ASSERT_TRUE(dst.read_media_data(m2->id) == d2);
    ASSERT_TRUE(dst.read_media_data(m3->id) == d3);
    ASSERT_TRUE(dst.read_media_data(m4->id) == d4);

    std::cout << "  multi-store import: 2 stores, 3 albums, 4 media verified\n";
}

// ═══════════════════════════════════════════════════════════
//  4. Import with zero-length media
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_zero_length_media) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("zerolen_store");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    // Mix zero-length and normal media
    auto data_normal = makeTestData(1024, 0xCC);
    auto m_empty = src.add_media(album->id, "empty.txt", "text/plain", {});
    auto m_normal = src.add_media(album->id, "photo.jpg", "image/jpeg", data_normal);
    ASSERT_TRUE(m_empty.has_value());
    ASSERT_TRUE(m_normal.has_value());

    auto exported = src.export_db_to_buffer();

    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);
    auto session = dst.begin_import();

    session->feed(reinterpret_cast<const char*>(exported.data()), exported.size());
    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());
    session.reset();

    auto media = dst.list_media(album->id);
    ASSERT_EQ(media.size(), 2u);

    ASSERT_EQ(dst.get_media_size(m_empty->id), 0u);
    ASSERT_EQ(dst.get_media_size(m_normal->id), 1024u);
    ASSERT_TRUE(dst.read_media_data(m_normal->id) == data_normal);

    std::cout << "  zero-length media import: verified\n";
}

// ═══════════════════════════════════════════════════════════
//  5. Import with varied chunk sizes
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_varied_chunks) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("varied_store");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    // Add several media of varied sizes
    struct TestItem { std::string id; std::vector<uint8_t> data; };
    std::vector<TestItem> items;
    for (int i = 0; i < 10; i++) {
        auto data = makeTestData(static_cast<size_t>(i * 500), static_cast<uint8_t>(i));
        auto m = src.add_media(album->id, "f" + std::to_string(i) + ".bin",
                               "application/octet-stream", data);
        ASSERT_TRUE(m.has_value());
        items.push_back({m->id, std::move(data)});
    }

    auto exported = src.export_db_to_buffer();

    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);
    auto session = dst.begin_import();

    // Feed in varying chunk sizes: 1, 7, 100, 1024, 4096, 13, 50000
    size_t offset = 0;
    size_t chunk_sizes[] = {1, 7, 100, 1024, 4096, 13, 50000};
    size_t ci = 0;
    while (offset < exported.size() && !session->done()) {
        size_t chunk = std::min(chunk_sizes[ci % 7], exported.size() - offset);
        session->feed(reinterpret_cast<const char*>(exported.data() + offset), chunk);
        offset += chunk;
        ci++;
    }

    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());
    session.reset();

    auto media = dst.list_media(album->id);
    ASSERT_EQ(media.size(), 10u);

    for (const auto& item : items) {
        auto m = dst.get_media(item.id);
        ASSERT_TRUE(m.has_value());
        ASSERT_EQ(m->size_bytes, item.data.size());
        if (!item.data.empty()) {
            auto rd = dst.read_media_data(item.id);
            ASSERT_EQ(rd.size(), item.data.size());
            ASSERT_TRUE(rd == item.data);
        }
    }

    std::cout << "  varied-chunk import: 10 items verified\n";
}

// ═══════════════════════════════════════════════════════════
//  6. Import then sync_from_cluster on a fresh backend
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_then_sync) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("synctest");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    constexpr size_t NUM_MEDIA = 5;
    std::vector<std::string> media_ids;
    std::vector<std::vector<uint8_t>> media_data;

    for (size_t i = 0; i < NUM_MEDIA; i++) {
        auto data = makeTestData(2048, static_cast<uint8_t>(i * 10));
        auto m = src.add_media(album->id, "file_" + std::to_string(i) + ".bin",
                               "application/octet-stream", data);
        ASSERT_TRUE(m.has_value());
        media_ids.push_back(m->id);
        media_data.push_back(std::move(data));
    }

    // Verify source data is in cluster
    for (const auto& mid : media_ids) {
        std::vector<uint8_t> out;
        ASSERT_TRUE(node.fetch("media:" + mid, out));
        ASSERT_TRUE(out.size() > 0);
    }

    // Fresh backend, sync from cluster
    BlobCache cache_sync(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend sync_dst(node, cache_sync);

    sync_dst.sync_from_cluster();

    // Verify synced data — find our store among any from prior tests
    bool found_store = false;
    for (const auto& s : sync_dst.list_stores()) {
        if (s.id == store.id) { found_store = true; break; }
    }
    ASSERT_TRUE(found_store);

    auto albums = sync_dst.list_albums(store.id);
    ASSERT_EQ(albums.size(), 1u);

    auto media = sync_dst.list_media(album->id);
    ASSERT_EQ(media.size(), NUM_MEDIA);

    for (size_t i = 0; i < NUM_MEDIA; i++) {
        auto rd = sync_dst.read_media_data(media_ids[i]);
        ASSERT_EQ(rd.size(), media_data[i].size());
        ASSERT_TRUE(rd == media_data[i]);
    }

    std::cout << "  import then sync: " << NUM_MEDIA << " media verified on fresh backend\n";
}

// ═══════════════════════════════════════════════════════════
//  7. Import from buffer (non-incremental) via ClusterMediaBackend
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_from_buffer) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("buftest");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    auto data = makeTestData(8192, 0xAA);
    auto m = src.add_media(album->id, "large.bin", "application/octet-stream", data);
    ASSERT_TRUE(m.has_value());

    auto exported = src.export_db_to_buffer();

    // import_db_from_buffer (non-incremental)
    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);

    bool ok = dst.import_db_from_buffer(exported);
    ASSERT_TRUE(ok);

    ASSERT_EQ(dst.list_media(album->id).size(), 1u);

    auto rd = dst.read_media_data(m->id);
    ASSERT_EQ(rd.size(), 8192u);
    ASSERT_TRUE(rd == data);

    std::cout << "  import_db_from_buffer: 8 KB media verified\n";
}

// ═══════════════════════════════════════════════════════════
//  8. Import large media (> stripe size)
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_large_media) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("large_store");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    // 256 KB media — likely crosses multiple parity stripes
    constexpr size_t LARGE_SIZE = 256 * 1024;
    auto data = makeTestData(LARGE_SIZE, 0x42);
    auto m = src.add_media(album->id, "bigfile.bin", "application/octet-stream", data);
    ASSERT_TRUE(m.has_value());

    auto exported = src.export_db_to_buffer();

    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);
    auto session = dst.begin_import();

    // Feed in 4 KB chunks
    size_t offset = 0;
    while (offset < exported.size() && !session->done()) {
        size_t chunk = std::min<size_t>(4096, exported.size() - offset);
        session->feed(reinterpret_cast<const char*>(exported.data() + offset), chunk);
        offset += chunk;
    }

    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());
    session.reset();

    auto rd = dst.read_media_data(m->id);
    ASSERT_EQ(rd.size(), LARGE_SIZE);
    ASSERT_TRUE(rd == data);

    // Also verify via range read
    auto range = dst.read_media_data_range(m->id, LARGE_SIZE / 2, 1024);
    ASSERT_EQ(range.size(), 1024u);
    for (size_t i = 0; i < 1024; i++) {
        ASSERT_EQ(range[i], data[LARGE_SIZE / 2 + i]);
    }

    std::cout << "  large media import: " << LARGE_SIZE / 1024 << " KB verified\n";
}

// ═══════════════════════════════════════════════════════════
//  9. Cross-node fetch after import
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_cross_node_fetch) {
    ensureCluster();

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(*g_cluster->nodes[0], cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("crossnode");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    auto data = makeTestData(4096, 0xDD);
    auto m = src.add_media(album->id, "photo.jpg", "image/jpeg", data);
    ASSERT_TRUE(m.has_value());

    // Fetch from a different node
    std::vector<uint8_t> out;
    bool fetch_ok = g_cluster->nodes[1]->fetch("media:" + m->id, out);
    ASSERT_TRUE(fetch_ok);
    ASSERT_EQ(out.size(), 4096u);
    ASSERT_TRUE(out == data);

    // Also verify from the third node
    std::vector<uint8_t> out2;
    bool fetch_ok2 = g_cluster->nodes[2]->fetch("media:" + m->id, out2);
    ASSERT_TRUE(fetch_ok2);
    ASSERT_EQ(out2.size(), 4096u);
    ASSERT_TRUE(out2 == data);

    std::cout << "  cross-node fetch: data accessible from all 3 nodes\n";
}

// ═══════════════════════════════════════════════════════════
//  10. Import with deferred cluster operations (BinDb level)
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_deferred_ops) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("deferred");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    for (int i = 0; i < 3; i++) {
        auto d = makeTestData(512, static_cast<uint8_t>(i));
        src.add_media(album->id, "f" + std::to_string(i) + ".bin",
                      "application/octet-stream", std::move(d));
    }

    auto exported = src.export_db_to_buffer();

    // Use BinDb directly with begin_import() to test take_deferred()
    BinDb dst_db;
    auto session = dst_db.begin_import();

    session->feed(reinterpret_cast<const char*>(exported.data()), exported.size());
    ASSERT_TRUE(session->done());
    ASSERT_TRUE(session->ok());

    // take_deferred() should return any pending ops (may be empty if no cluster callbacks)
    auto deferred = session->take_deferred();
    // Execute them
    for (auto& fn : deferred) {
        fn();
    }

    // Verify data was imported
    ASSERT_EQ(dst_db.list_media(album->id).size(), 3u);

    std::cout << "  deferred ops: " << deferred.size() << " ops dispatched\n";
}

// ═══════════════════════════════════════════════════════════
//  11. Re-import over existing data
// ═══════════════════════════════════════════════════════════

TEST(cluster_import_reimport) {
    ensureCluster();
    auto& node = *g_cluster->nodes[0];

    BlobCache cache_src(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend src(node, cache_src);
    src.sync_from_cluster();

    auto store = src.create_store("reimport");
    auto album = src.create_album(store.id, "a1");
    ASSERT_TRUE(album.has_value());

    auto data = makeTestData(2048, 0x55);
    auto m = src.add_media(album->id, "photo.jpg", "image/jpeg", data);
    ASSERT_TRUE(m.has_value());

    auto exported = src.export_db_to_buffer();

    // First import
    BlobCache cache_dst(64 * 1024 * 1024, blob_value_size);
    ClusterMediaBackend dst(node, cache_dst);
    ASSERT_TRUE(dst.import_db_from_buffer(exported));

    // Second import (re-import same data)
    ASSERT_TRUE(dst.import_db_from_buffer(exported));

    // Data should still be accessible
    auto rd = dst.read_media_data(m->id);
    ASSERT_EQ(rd.size(), 2048u);
    ASSERT_TRUE(rd == data);

    std::cout << "  re-import: data still intact\n";
}

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

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

    // Cleanup cluster before exit
    g_cluster.reset();

    return g_tests_failed > 0 ? 1 : 0;
}
