/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

// Regression test for the HTTP/2 stream-dispatch staircase bug: multiple
// streams multiplexed on one connection must not serialize behind each
// other's RequestEvent. Runs the SAME synthetic slow server twice — once
// with h2OffloadThreads=0 (must reproduce the staircase, proving the
// synchronous fallback path is unchanged) and once with offload enabled
// (must show the streams complete with substantially overlapping wall
// clocks instead).

#include <algorithm>
#include <chrono>
#include <cstring>
#include <iostream>
#include <thread>
#include <vector>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

#include <netplus/socket.h>

#include "hpack.h"
#include "http.h"
#include "httpd.h"

namespace {

constexpr uint8_t FRAME_DATA = 0x00;
constexpr uint8_t FRAME_HEADERS = 0x01;
constexpr uint8_t FRAME_SETTINGS = 0x04;
constexpr uint8_t FLAG_END_STREAM = 0x01;
constexpr uint8_t FLAG_END_HEADERS = 0x04;
constexpr size_t FRAME_HEADER_LEN = 9;
constexpr char CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";

void appendFrameHeader(std::vector<char> &out, uint32_t len, uint8_t type,
                        uint8_t flags, uint32_t streamId) {
    out.push_back(static_cast<char>((len >> 16) & 0xFF));
    out.push_back(static_cast<char>((len >> 8) & 0xFF));
    out.push_back(static_cast<char>(len & 0xFF));
    out.push_back(static_cast<char>(type));
    out.push_back(static_cast<char>(flags));
    out.push_back(static_cast<char>((streamId >> 24) & 0x7F));
    out.push_back(static_cast<char>((streamId >> 16) & 0xFF));
    out.push_back(static_cast<char>((streamId >> 8) & 0xFF));
    out.push_back(static_cast<char>(streamId & 0xFF));
}

// A slow synthetic backend stand-in: sleeps for a caller-supplied number of
// milliseconds (parsed out of the request path, e.g. "/slow/50") before
// responding, standing in for a real blocking mediadb round-trip.
class SlowServer : public libhttppp::HttpEvent {
public:
    SlowServer(std::vector<netplus::socket*> serversocket, size_t h2OffloadThreads)
        : HttpEvent(serversocket, 1000, h2OffloadThreads) {}

    bool shouldOffloadH2Dispatch(libhttppp::HttpRequest&, uint32_t) const override {
        // Gated only by whether the pool exists (h2OffloadThreads), so one
        // class serves both the synchronous and offloaded test runs.
        return true;
    }

    void RequestEvent(libhttppp::HttpRequest &req, const int, ULONG_PTR) override {
        const std::string &path = req.getRequestURL();
        int delayMs = 0;
        auto pos = path.rfind('/');
        if (pos != std::string::npos) {
            try { delayMs = std::stoi(path.substr(pos + 1)); } catch (...) {}
        }
        if (delayMs > 0)
            std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));

        libhttppp::HttpResponse res;
        res.setContentType("text/plain");
        res.send(req, std::string("ok"), 2);
    }
};

// Builds one HEADERS(+END_HEADERS+END_STREAM) frame requesting
// "/slow/<delayMs>" on the given stream id.
std::vector<char> buildSlowRequest(uint32_t streamId, int delayMs) {
    std::string path = "/slow/" + std::to_string(delayMs);
    std::string hpack_out = libhttppp::hpack::Encoder::encodeRequestHeaders(
        "GET", path, "http", "127.0.0.1");
    std::vector<char> out;
    appendFrameHeader(out, static_cast<uint32_t>(hpack_out.size()), FRAME_HEADERS,
                       FLAG_END_HEADERS | FLAG_END_STREAM, streamId);
    out.insert(out.end(), hpack_out.begin(), hpack_out.end());
    return out;
}

// Connects, sends the H2 preface + an empty SETTINGS frame + one HEADERS
// frame per stream (all in one burst, before reading anything back — this
// is what makes it a genuine multiplexing test rather than N sequential
// requests), then reads frames until every stream has seen END_STREAM.
// Returns each stream's completion timestamp.
std::vector<std::chrono::steady_clock::time_point> runConcurrentRequest(
    int port, const std::vector<uint32_t> &streamIds, int delayMs) {

    int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(static_cast<uint16_t>(port));
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        ::close(fd);
        throw std::runtime_error("connect failed");
    }

    std::vector<char> req(CLIENT_PREFACE, CLIENT_PREFACE + sizeof(CLIENT_PREFACE) - 1);
    appendFrameHeader(req, 0, FRAME_SETTINGS, 0, 0); // empty client SETTINGS
    for (uint32_t sid : streamIds) {
        auto hdrs = buildSlowRequest(sid, delayMs);
        req.insert(req.end(), hdrs.begin(), hdrs.end());
    }
    size_t sent = 0;
    while (sent < req.size()) {
        ssize_t n = ::send(fd, req.data() + sent, req.size() - sent, 0);
        if (n <= 0) { ::close(fd); throw std::runtime_error("send failed"); }
        sent += static_cast<size_t>(n);
    }

    std::map<uint32_t, std::chrono::steady_clock::time_point> completed;
    std::vector<char> buf;
    char tmp[4096];
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
    while (completed.size() < streamIds.size() &&
           std::chrono::steady_clock::now() < deadline) {
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n <= 0) break;
        buf.insert(buf.end(), tmp, tmp + n);

        size_t off = 0;
        while (off + FRAME_HEADER_LEN <= buf.size()) {
            uint32_t len = (static_cast<uint8_t>(buf[off]) << 16) |
                           (static_cast<uint8_t>(buf[off + 1]) << 8) |
                           static_cast<uint8_t>(buf[off + 2]);
            uint8_t type = static_cast<uint8_t>(buf[off + 3]);
            uint8_t flags = static_cast<uint8_t>(buf[off + 4]);
            uint32_t sid = ((static_cast<uint8_t>(buf[off + 5]) & 0x7F) << 24) |
                           (static_cast<uint8_t>(buf[off + 6]) << 16) |
                           (static_cast<uint8_t>(buf[off + 7]) << 8) |
                           static_cast<uint8_t>(buf[off + 8]);
            if (off + FRAME_HEADER_LEN + len > buf.size())
                break; // incomplete frame, wait for more bytes
            if ((type == FRAME_HEADERS || type == FRAME_DATA) &&
                (flags & FLAG_END_STREAM) && sid != 0) {
                completed.emplace(sid, std::chrono::steady_clock::now());
            }
            off += FRAME_HEADER_LEN + len;
        }
        buf.erase(buf.begin(), buf.begin() + off);
    }
    ::close(fd);

    if (completed.size() != streamIds.size())
        throw std::runtime_error("not all streams completed in time");

    std::vector<std::chrono::steady_clock::time_point> times;
    for (uint32_t sid : streamIds) times.push_back(completed[sid]);
    return times;
}

// A running server plus everything needed to tear it down deterministically.
// netplus::event's connection registry, wakeup eventfd and Running/Restart
// flags are process-wide statics (needed elsewhere so the H2-offload dispatch
// can look up/rearm a connection from any thread by fd). That means a
// SlowServer's background threads keep touching that shared static state for
// as long as they're alive — they must be fully stopped and joined before the
// next server starts (or before the process exits), or a worker thread can be
// mid-access when those statics get destroyed, which segfaults.
struct RunningServer {
    std::unique_ptr<netplus::tcp> sock;
    std::unique_ptr<SlowServer> server;
    std::thread thread;
};

// Binds an ephemeral-port TCP listener and starts a SlowServer on it in a
// background thread.
RunningServer startServer(int &port, size_t h2OffloadThreads) {
    RunningServer rs;
    rs.sock = std::make_unique<netplus::tcp>("127.0.0.1", 0, 16, -1);
    rs.sock->bind();

    sockaddr_in bound{};
    socklen_t blen = sizeof(bound);
    ::getsockname(rs.sock->fd(), reinterpret_cast<sockaddr*>(&bound), &blen);
    port = ntohs(bound.sin_port);

    rs.sock->listen();

    rs.server = std::make_unique<SlowServer>(
        std::vector<netplus::socket*>{rs.sock.get()}, h2OffloadThreads);
    // requestStop() (see stopServer()) leaves this false; a fresh
    // runEventloop() call needs it true again to not exit its worker loop
    // immediately.
    netplus::event::Running = true;
    SlowServer *serverPtr = rs.server.get();
    rs.thread = std::thread([serverPtr] { serverPtr->runEventloop(); });
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    return rs;
}

// Signals the server's event loop to stop and blocks until its worker
// threads have actually joined and torn down (see RunningServer above for
// why this must happen before the next server starts or the process exits).
void stopServer(RunningServer &rs) {
    netplus::event::requestStop();
    if (rs.thread.joinable())
        rs.thread.join();
}

} // namespace

int main() {
    const std::vector<uint32_t> streamIds = {1, 3, 5, 7, 9};
    const int delayMs = 50;

    int failures = 0;

    // Run 1: h2OffloadThreads = 0 — must reproduce the staircase, proving
    // the untouched synchronous fallback path still behaves as before.
    {
        int port = 0;
        RunningServer rs = startServer(port, /*h2OffloadThreads=*/0);
        auto times = runConcurrentRequest(port, streamIds, delayMs);
        stopServer(rs);
        auto totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(
            times.back() - times.front()).count();
        std::cout << "[sync]    spread=" << totalMs << "ms (expect >= ~"
                  << (streamIds.size() - 1) * delayMs / 2 << "ms, staircase)"
                  << std::endl;
        // Staircase signature: each stream finishes roughly delayMs after
        // the previous one, so the spread across all of them should be a
        // sizeable fraction of (N-1)*delayMs, not near zero.
        if (totalMs < static_cast<long long>((streamIds.size() - 1) * delayMs) / 2) {
            std::cerr << "FAIL: synchronous path did not show the expected "
                          "staircase (spread=" << totalMs << "ms) — either the "
                          "fallback path regressed, or this environment "
                          "schedules unrealistically fast" << std::endl;
            ++failures;
        }
    }

    // Run 2: h2OffloadThreads > 0 with shouldOffloadH2Dispatch() true —
    // must show the streams completing with overlapping wall clocks.
    {
        int port = 0;
        RunningServer rs = startServer(port, /*h2OffloadThreads=*/4);
        auto times = runConcurrentRequest(port, streamIds, delayMs);
        stopServer(rs);
        auto totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(
            times.back() - times.front()).count();
        std::cout << "[offload] spread=" << totalMs << "ms (expect < ~"
                  << 3 * delayMs << "ms, overlap)" << std::endl;
        if (totalMs >= 3 * delayMs) {
            std::cerr << "FAIL: offloaded streams did not overlap "
                          "(spread=" << totalMs << "ms, expected < "
                       << 3 * delayMs << "ms)" << std::endl;
            ++failures;
        }
    }

    std::cout << (failures == 0 ? "PASS" : "FAIL") << std::endl;
    return failures == 0 ? 0 : 1;
}
