// auth_recursion_test.cpp
//
// Regression test for an unbounded-recursion hang found live on a production
// mediadb cluster node (2026-08-01). A gdb backtrace on the stuck process
// showed hundreds of stacked frames of:
//
//   send_recv -> authenticate_node -> connect_to_node -> send_recv -> ...
//
// Root cause: authenticate_node() (called only from inside connect_to_node(),
// right after a fresh handshake completes) sends AUTH_REQUEST/AUTH_RESPONSE
// via the generic send_recv(). When a peer's response to that auth message
// came back empty — for whatever reason, e.g. the message never reaching the
// peer's application-level dispatch — send_recv()'s existing retry logic
// ("empty response — stale connection, retry once with fresh connection")
// reset the connection and, on its next attempt, called ensure_connected(),
// which (since the connection was just marked disconnected) called
// connect_to_node() again — a *nested* call, not a loop iteration, since
// authenticate_node() only exists inside connect_to_node()'s call chain.
// That nested connect_to_node() completes a fresh handshake, calls
// authenticate_node() again, which calls send_recv() again — and if the
// peer never answers auth, this repeats with no base case, growing the
// stack without bound instead of failing cleanly. The client-facing HTTP
// request that needed this cluster read simply hung forever.
//
// Fixed by giving send_recv() an allow_reconnect=false mode (used only by
// authenticate_node()'s two calls) that skips ensure_connected() and fails
// after a single attempt instead of resetting the connection and retrying —
// connect_to_node() already owns the connection's fate on auth failure
// (mark_connection_failed() + throw), so authenticate_node() must never be
// able to re-enter it.
//
// This test reproduces the exact precondition: a real QUIC peer that
// completes the handshake normally (so connect_to_node() gets far enough to
// call authenticate_node()) but never answers any application message —
// simulating "empty response" regardless of what causes it on a real peer.
// Before the fix, warmup() against such a peer (with credentials configured,
// so authenticate_node() actually runs) never returns. After the fix, it
// must return within a small bounded time.

#include <atomic>
#include <chrono>
#include <future>
#include <iostream>
#include <map>
#include <string>
#include <thread>
#include <vector>

#include <paritypp/client.h>

#include "connection.h"
#include "eventapi.h"
#include "exception.h"
#include "random.h"
#include "socket.h"

#include "https_ca_cert.h"
#include "https_certs.h"

using namespace netplus;

static int g_passed = 0, g_failed = 0;

static void check(bool ok, const std::string& name) {
    if (ok) { std::cout << "  PASS: " << name << std::endl; g_passed++; }
    else    { std::cout << "  FAIL: " << name << std::endl; g_failed++; }
}

// A QUIC peer that completes the handshake but answers nothing at the
// application level — the stream callback deliberately never calls
// sendStreamData(), so any message (including AUTH_REQUEST) gets a
// genuinely empty response, exactly like the production incident.
static std::atomic<bool> g_quic_ready(false);

class SilentServer : public event {
public:
    SilentServer(std::vector<netplus::socket*> socks, int timeout = 500)
        : event(socks, timeout) {}
    void RequestEvent(con&, const int, ULONG_PTR) override {}
    void ResponseEvent(con&, const int, ULONG_PTR) override {}
    void ConnectEvent(con&, const int, ULONG_PTR) override {}
    void DisconnectEvent(con&, const int, ULONG_PTR) override {}
    void CreateConnection(std::shared_ptr<con>& res) override {
        res = std::make_shared<con>(this);
    }
};

static void runSilentServer(std::map<std::string, ssl::CertificateBundle>& certs, int port) {
    try {
        quic serverSock(certs, "127.0.0.1", port, 256, -1);
        serverSock.setStreamCallback([](netplus::socket*, uint64_t,
                                        const std::vector<uint8_t>&, bool) {
            // Deliberately drop everything — never respond.
        });

        SilentServer srv({&serverSock});
        g_quic_ready.store(true);
        srv.runEventloop();
    } catch (NetException& e) {
        std::cerr << "[silent server] NetException: " << e.what() << std::endl;
        g_quic_ready.store(true);
    } catch (std::exception& e) {
        std::cerr << "[silent server] Exception: " << e.what() << std::endl;
        g_quic_ready.store(true);
    }
}

int main() {
    std::cout << "=== paritypp auth-recursion regression test ===" << std::endl;

    const int kPort = 19621;
    // Generous but bounded: a single connect_to_node() attempt is bounded by
    // its own ~5s handshake wait plus authenticate_node()'s two ~5s send/recv
    // rounds — comfortably under 20s total for one clean attempt. Before the
    // fix this never returned at all; this bound only needs to distinguish
    // "returns" from "hangs forever", not pin down an exact duration.
    const auto kMaxWait = std::chrono::seconds(20);

    x509cert cert;
    if (!cert.loadFromBuffer(test_cert_der)) {
        std::cerr << "Failed to load certificate" << std::endl;
        return 1;
    }
    std::map<std::string, ssl::CertificateBundle> certs;
    ssl::CertificateBundle bundle;
    bundle.cert = cert;
    bundle.privateKeyDer = std::vector<uint8_t>(test_key_der.begin(), test_key_der.end());
    bundle.rsa_key = rsa(bundle.privateKeyDer);
    bundle.chain.push_back(std::vector<uint8_t>(MKCERT_ROOT_CA_DER,
        MKCERT_ROOT_CA_DER + MKCERT_ROOT_CA_DER_LEN));
    certs["127.0.0.1"] = bundle;

    std::thread serverThread(runSilentServer, std::ref(certs), kPort);
    while (!g_quic_ready.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    // Credentials configured — this is what makes connect_to_node() call
    // authenticate_node() at all; without them this test wouldn't exercise
    // the bug.
    paritypp::client::credentials creds;
    creds.client_name = "test-client";
    creds.key = {0x01, 0x02, 0x03, 0x04};

    std::vector<paritypp::client::node_info> nodes = {{"127.0.0.1", kPort}};
    paritypp::client c(1, 1, nodes, creds);

    auto fut = std::async(std::launch::async, [&c] { c.warmup(); });
    bool returned = (fut.wait_for(kMaxWait) == std::future_status::ready);

    check(returned,
          "warmup() against a peer that never answers auth returns within "
          + std::to_string(kMaxWait.count()) + "s instead of recursing forever");

    if (!returned) {
        // Don't join a thread that may be recursing/growing its stack
        // forever — let the process exit (and the OS reclaim it) rather
        // than hang the test suite itself waiting for a future that will
        // never become ready.
        std::cout << "\n==============================" << std::endl;
        std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl;
        std::cout << "==============================" << std::endl;
        std::_Exit(1);
    }

    event::Running = false;
    serverThread.join();

    std::cout << "\n==============================" << std::endl;
    std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl;
    std::cout << "==============================" << std::endl;

    return (g_failed > 0) ? 1 : 0;
}
