/*******************************************************************************
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 HttpClient::readBodyChunkNonBlocking()'s contract on
// STREAM_CHUNKED and STREAM_EOF bodies: "returns 0 immediately if no data
// available yet" (see http.h). Historically this fell back to the *blocking*
// readBodyChunk() for exactly these two modes, silently violating the
// contract for any event-loop-driven caller. There was previously no test
// exercising the non-blocking path at all for chunked/EOF bodies -- this
// file drives a raw hand-built HTTP/1.1 server (not the HttpEvent framework,
// so response timing is fully controlled) that sends a body in two halves
// with a deliberate delay in between, then asserts every
// readBodyChunkNonBlocking() call that finds no data returns in well under
// that delay, and that the fully-assembled body is still byte-for-byte
// correct once all data has arrived.

#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

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

#include <netplus/crypto/sha.h>
#include <netplus/random.h>

#include "http.h"

using namespace libhttppp;

namespace {

int g_passed = 0, g_failed = 0;

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; }
}

std::vector<char> randomBuffer(size_t len) {
    std::vector<char> buf(len);
    if (len) netplus::fillRandomBytes(reinterpret_cast<uint8_t *>(buf.data()), len);
    return buf;
}

int listenOnEphemeralPort() {
    int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    int one = 1;
    ::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port = 0;
    if (::bind(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0)
        throw std::runtime_error("bind failed");
    ::listen(fd, 1);
    return fd;
}

int portOf(int listenFd) {
    sockaddr_in addr{};
    socklen_t len = sizeof(addr);
    ::getsockname(listenFd, reinterpret_cast<sockaddr *>(&addr), &len);
    return ntohs(addr.sin_port);
}

void sendAll(int fd, const char *data, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t n = ::send(fd, data + sent, len - sent, 0);
        if (n <= 0) return; // client may have already gotten everything it needed
        sent += static_cast<size_t>(n);
    }
}
void sendAll(int fd, const std::string &s) { sendAll(fd, s.data(), s.size()); }

// Accepts exactly one connection, discards the request, then streams `body`
// in two halves with `delay` in between. `chunked` selects
// Transfer-Encoding: chunked framing vs. a bare Connection: close/EOF body.
void rawStreamingServer(int listenFd, std::vector<char> body,
                         std::chrono::milliseconds delay, bool chunked) {
    sockaddr_in peer{};
    socklen_t plen = sizeof(peer);
    int fd = ::accept(listenFd, reinterpret_cast<sockaddr *>(&peer), &plen);
    if (fd < 0) return;

    // Read (and discard) the request up through the blank line.
    std::string req;
    char tmp[4096];
    for (;;) {
        if (req.find("\r\n\r\n") != std::string::npos) break;
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n <= 0) { ::close(fd); return; }
        req.append(tmp, static_cast<size_t>(n));
    }

    if (chunked) {
        sendAll(fd, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n");
    } else {
        sendAll(fd, "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n");
    }

    size_t half = body.size() / 2;
    auto writePart = [&](const char *data, size_t len) {
        if (len == 0) return;
        if (chunked) {
            std::ostringstream sz;
            sz << std::hex << len << "\r\n";
            sendAll(fd, sz.str());
            sendAll(fd, data, len);
            sendAll(fd, "\r\n");
        } else {
            sendAll(fd, data, len);
        }
    };

    writePart(body.data(), half);
    std::this_thread::sleep_for(delay);
    writePart(body.data() + half, body.size() - half);

    if (chunked) sendAll(fd, "0\r\n\r\n");
    ::close(fd);
}

// Drives GetStream() + a readBodyChunkNonBlocking() poll loop against a
// rawStreamingServer, asserting every "no data yet" (0) return happens well
// under interChunkDelay, and that the fully-collected body is correct.
void testNonBlockingStreaming(bool chunked, std::chrono::milliseconds interChunkDelay) {
    const std::string label = chunked ? "chunked" : "eof";
    const size_t size = 65536 + 777; // odd size, spans multiple TCP segments
    std::vector<char> body = randomBuffer(size);

    int listenFd = listenOnEphemeralPort();
    int port = portOf(listenFd);
    std::thread server(rawStreamingServer, listenFd, body, interChunkDelay, chunked);

    bool timingOk = true;
    std::chrono::milliseconds worstBusyWait{0};
    std::vector<char> collected;
    bool completed = false;

    try {
        HttpUrl url("http://127.0.0.1:" + std::to_string(port) + "/x");
        HttpClient clt(url, /*vers=*/0, /*timeoutSec=*/5);
        HttpRequest req;
        req.setRequestURL("/x");
        HttpResponse res = clt.GetStream(req);
        (void)res;

        collected.reserve(size);
        char buf[8192];
        auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
        for (;;) {
            if (std::chrono::steady_clock::now() > deadline) break;

            auto t0 = std::chrono::steady_clock::now();
            size_t n = clt.readBodyChunkNonBlocking(buf, sizeof(buf));
            auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::steady_clock::now() - t0);

            // The whole point of the *non-blocking* variant: a single call
            // must never itself block for the server's inter-chunk delay --
            // regardless of whether it ends up returning 0 (no data yet),
            // real bytes, or -1 (done). A call that silently falls back to
            // blocking I/O still returns a "valid-looking" byte count or
            // completion signal, just late -- so timing must be checked on
            // every call, not only on 0-returns.
            if (elapsed >= interChunkDelay / 2) {
                timingOk = false;
                if (elapsed > worstBusyWait) worstBusyWait = elapsed;
            }

            if (n == static_cast<size_t>(-1)) {
                completed = true;
                break;
            }
            if (n == 0) {
                std::this_thread::sleep_for(std::chrono::milliseconds(5));
                continue;
            }
            collected.insert(collected.end(), buf, buf + n);
        }
    } catch (std::exception &e) {
        std::cerr << "[" << label << "] exception: " << e.what() << std::endl;
    }

    if (server.joinable()) server.join();
    ::close(listenFd);

    check(completed, label + ": stream reached completion (-1) within deadline");
    check(collected == body, label + ": collected bytes match the original body exactly");
    check(timingOk, label + ": readBodyChunkNonBlocking() never blocked for the "
                     "server's inter-chunk delay (worst observed: " +
                     std::to_string(worstBusyWait.count()) + "ms, limit " +
                     std::to_string(interChunkDelay.count() / 2) + "ms)");
}

// Drives the *blocking* readBodyChunk() against the same rawStreamingServer
// (chunked and EOF modes). No existing test exercised readBodyChunk()
// against STREAM_CHUNKED specifically (the existing checksum tests only
// stream STREAM_CONTENT_LENGTH via readBodyChunk(), and exercise chunked
// bodies only through the buffered Get() path, which uses an entirely
// different parser in _h1ReadResponse) -- this closes that gap now that
// _streamReadChunked()/_streamReadEof() are shared between both the
// blocking and non-blocking callers, so a bug affecting one could easily
// affect the other.
void testBlockingStreaming(bool chunked) {
    const std::string label = chunked ? "chunked-blocking" : "eof-blocking";
    const size_t size = 65536 + 1234;
    std::vector<char> body = randomBuffer(size);

    int listenFd = listenOnEphemeralPort();
    int port = portOf(listenFd);
    std::thread server(rawStreamingServer, listenFd, body, std::chrono::milliseconds(20), chunked);

    std::vector<char> collected;
    bool ok = true;
    try {
        HttpUrl url("http://127.0.0.1:" + std::to_string(port) + "/x");
        HttpClient clt(url, /*vers=*/0, /*timeoutSec=*/5);
        HttpRequest req;
        req.setRequestURL("/x");
        HttpResponse res = clt.GetStream(req);
        (void)res;

        collected.reserve(size);
        char buf[8192];
        for (;;) {
            size_t n = clt.readBodyChunk(buf, sizeof(buf));
            if (n == 0) break;
            collected.insert(collected.end(), buf, buf + n);
        }
    } catch (std::exception &e) {
        std::cerr << "[" << label << "] exception: " << e.what() << std::endl;
        ok = false;
    }

    if (server.joinable()) server.join();
    ::close(listenFd);

    check(ok, label + ": no exception");
    check(collected == body, label + ": collected bytes match the original body exactly");
}

} // namespace

int main() {
    // The raw test server may write to a socket the client has already
    // stopped reading from (e.g. after the client hit its own deadline);
    // without this, that write raises SIGPIPE and kills the whole process
    // instead of send() just returning -1/EPIPE.
    std::signal(SIGPIPE, SIG_IGN);

    std::cout << "=== HTTP/1.1 Non-Blocking Streaming Test ===" << std::endl;

    const auto delay = std::chrono::milliseconds(400);
    testNonBlockingStreaming(/*chunked=*/true, delay);
    testNonBlockingStreaming(/*chunked=*/false, delay);
    testBlockingStreaming(/*chunked=*/true);
    testBlockingStreaming(/*chunked=*/false);

    std::cout << "==============================" << std::endl;
    std::cout << "Results: " << g_passed << " passed, " << g_failed << " failed" << std::endl;
    std::cout << "==============================" << std::endl;
    return g_failed > 0 ? 1 : 0;
}
