// Regression test for the node-cooldown-vs-recovery-time bug found while
// investigating recurring "stripe N unreachable" failures on a real 3-node
// authdb/paritypp cluster (2026-07-28): every connection failure — however
// brief — used to blacklist that node for a flat 10 seconds
// (client.cpp's old `dead_until = now + 10s`), regardless of whether the
// underlying problem had already cleared. With m=1 parity (tolerates only
// one simultaneous node failure), two independent, momentary blips landing
// close together — plausible during any cluster-wide restart or network
// hiccup — used to cost a guaranteed ~10s full outage for every group that
// needed those nodes, even though both nodes were actually reachable again
// within a second.
//
// This test spins up 3 real paritypp servers on loopback, stores real data,
// then simulates two independent nodes each taking one hit (the same
// `mark_node_dead()` call real failure paths funnel through) and verifies
// the client recovers well before the old flat 10s cooldown would have
// expired.

#include <paritypp/server.h>
#include <paritypp/client.h>
#include "https_certs.h"

#include <iostream>
#include <cassert>
#include <cstring>
#include <thread>
#include <chrono>
#include <fstream>
#include <filesystem>

using namespace paritypp;

namespace {

std::string write_temp_der(const std::vector<uint8_t>& der, const std::string& name) {
    auto path = (std::filesystem::temp_directory_path() / name).string();
    std::ofstream f(path, std::ios::binary | std::ios::trunc);
    f.write(reinterpret_cast<const char*>(der.data()),
            static_cast<std::streamsize>(der.size()));
    return path;
}

struct test_node {
    std::shared_ptr<memory_block_store> store;
    std::unique_ptr<server> srv;
    std::thread thread;

    void start(const server::config& cfg) {
        store = std::make_shared<memory_block_store>();
        srv = std::make_unique<server>(cfg, store);
        thread = std::thread([this] { srv->run(); });
    }

    ~test_node() {
        if (srv) srv->stop();
        if (thread.joinable()) thread.join();
    }
};

} // namespace

static void test_node_cooldown_recovers_quickly() {
    std::cout << "=== Node cooldown recovers quickly after a single failure ===" << std::endl;

    auto cert_path = write_temp_der(test_cert_der, "paritypp_reconnect_test_cert.der");
    auto key_path  = write_temp_der(test_key_der,  "paritypp_reconnect_test_key.der");

    constexpr int base_port = 15201;
    std::vector<test_node> nodes(3);
    std::vector<client::node_info> node_infos;
    for (int i = 0; i < 3; ++i) {
        server::config cfg;
        cfg.bind_address = "127.0.0.1";
        cfg.port = base_port + i;
        cfg.cert_file = cert_path;
        cfg.key_file = key_path;
        nodes[static_cast<size_t>(i)].start(cfg);
        node_infos.push_back({"127.0.0.1", base_port + i});
    }
    // Give the accept loops a moment to bind and start listening.
    std::this_thread::sleep_for(std::chrono::milliseconds(300));

    client c(2, 1, node_infos);
    c.warmup();

    const uint64_t gid = 0xC00110FF00D1234ULL;
    const std::string payload = "cooldown regression test payload";
    c.store(gid, reinterpret_cast<const uint8_t*>(payload.data()), payload.size());

    // Baseline: retrieve must succeed with all 3 nodes healthy.
    {
        auto data = c.retrieve(gid);
        assert(data.size() == payload.size());
        assert(std::memcmp(data.data(), payload.data(), payload.size()) == 0);
    }

    // Simulate two independent nodes each taking one connection failure —
    // the same call real send/recv/connect failure paths make internally.
    // With k=2, m=1 (tolerates exactly one failure), losing two of three
    // nodes at once means the group is genuinely unreachable until at
    // least one of them is usable again.
    c.mark_node_dead(1);
    c.mark_node_dead(2);

    // Immediately after: only node 0 is usable, below k=2 — must fail.
    {
        bool threw = false;
        try {
            auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
            c.retrieve(gid, deadline);
        } catch (const std::exception&) {
            threw = true;
        }
        assert(threw);
    }

    // Wait past the new adaptive cooldown (1s after a single failure) but
    // well short of the old flat 10s blackout.
    std::this_thread::sleep_for(std::chrono::milliseconds(1300));

    {
        auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
        auto data = c.retrieve(gid, deadline);
        assert(data.size() == payload.size());
        assert(std::memcmp(data.data(), payload.data(), payload.size()) == 0);
    }

    std::cout << "  Node cooldown recovery test passed." << std::endl;
}

int main() {
    std::cout << "libparitypp Reconnect/Cooldown Test Suite" << std::endl;
    std::cout << "=========================================" << std::endl << std::endl;

    test_node_cooldown_recovers_quickly();

    std::cout << std::endl << "All reconnect tests passed!" << std::endl;
    return 0;
}
