/*******************************************************************************
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.
*******************************************************************************/

// HTTP/1.1 checksum integrity test: drives a real HttpClient against a real
// HttpEvent-based server over plain TCP, sweeping payload sizes from empty
// to 1MB for both GET (server-generated, checksummed via X-Sha256) and POST
// (echoed back, checksummed both ways), plus:
//   - a raw hand-built "Transfer-Encoding: chunked" POST to exercise
//     HttpRequest::isChunkedRequest()/decodeChunkedBody() on the server, and
//   - a chunked GET response (HttpResponse::sendChunkedHeaders/sendChunk/
//     endChunked, irregular chunk sizes) read back transparently by
//     HttpClient's chunked-response reader, and
//   - HttpClient::GetStream()/readBodyChunk() incremental streaming reads
//     hashed as they arrive, matching the server's checksum.

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <thread>

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

#include <netplus/exception.h>
#include <netplus/socket.h>

#include "checksum_common.h"

using namespace checksumtest;

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

// Adds one H1-only path ("/chunked/<n>") that streams its response out via
// real chunked transfer-encoding with irregular chunk sizes, on top of the
// protocol-agnostic ChecksumEvent handler shared with the H2/H3 tests.
class H1ChecksumEvent : public ChecksumEvent {
public:
    using ChecksumEvent::ChecksumEvent;

    void RequestEvent(libhttppp::HttpRequest &req, const int tid, ULONG_PTR args) override {
        const std::string &url = req.getRequestURL();
        if (url.rfind("/chunked/", 0) == 0) {
            size_t len = 0;
            try { len = static_cast<size_t>(std::stoull(url.substr(9))); }
            catch (...) { len = 0; }

            std::vector<char> body = randomBuffer(len);
            std::string hash = sha256Hex(body);

            libhttppp::HttpResponse res;
            res.setContentType("application/octet-stream");
            res.setHeaderData("x-sha256")->push_back(hash);
            res.sendChunkedHeaders(req);

            static const size_t kChunkSizes[] = {1, 17, 4096, 65536};
            size_t off = 0, ci = 0;
            while (off < body.size()) {
                size_t take = std::min(kChunkSizes[ci % 4], body.size() - off);
                res.sendChunk(req, body.data() + off, take);
                off += take;
                ++ci;
            }
            res.endChunked(req);
            return;
        }
        ChecksumEvent::RequestEvent(req, tid, args);
    }
};

int startServer(std::unique_ptr<netplus::tcp> &sockOwner,
                 std::unique_ptr<H1ChecksumEvent> &serverOwner,
                 std::thread &serverThread) {
    sockOwner = std::make_unique<netplus::tcp>("127.0.0.1", 0, 16, -1);
    sockOwner->bind();

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

    sockOwner->listen();

    serverOwner = std::make_unique<H1ChecksumEvent>(
        std::vector<netplus::socket *>{sockOwner.get()});
    serverThread = std::thread([s = serverOwner.get()] { s->runEventloop(); });
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    return port;
}

std::string headerValue(const libhttppp::HttpResponse &res, const std::string &key) {
    auto *hd = res.getHeaderData(key);
    if (hd && hd->getfirstValue())
        return hd->getfirstValue()->getvalue();
    return {};
}

// ---- Raw-socket helpers for cases HttpClient doesn't cover directly ----

int rawConnect(int port) {
    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("raw connect failed");
    }
    return fd;
}

void sendAll(int fd, const std::string &data) {
    size_t sent = 0;
    while (sent < data.size()) {
        ssize_t n = ::send(fd, data.data() + sent, data.size() - sent, 0);
        if (n <= 0) throw std::runtime_error("raw send failed");
        sent += static_cast<size_t>(n);
    }
}

// Reads one non-chunked HTTP/1.1 response (headers + Content-Length body)
// off a raw fd, reusing the library's own HttpResponse::parse() so the
// header-parsing logic under test is the real thing, not a re-implementation.
std::vector<char> rawReadResponse(int fd, libhttppp::HttpResponse &res) {
    std::vector<char> raw;
    char tmp[8192];
    size_t header_end = std::string::npos;

    for (;;) {
        if (raw.size() >= 4) {
            for (size_t i = 0; i + 3 < raw.size(); ++i) {
                if (raw[i] == '\r' && raw[i + 1] == '\n' &&
                    raw[i + 2] == '\r' && raw[i + 3] == '\n') {
                    header_end = i + 4;
                    break;
                }
            }
        }
        if (header_end != std::string::npos) break;
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n <= 0) throw std::runtime_error("raw recv: EOF before headers complete");
        raw.insert(raw.end(), tmp, tmp + n);
    }

    size_t parsed = res.parse(raw.data(), raw.size());
    size_t contentLength = res.getContentLength();

    std::vector<char> body(raw.begin() + static_cast<ptrdiff_t>(parsed), raw.end());
    while (body.size() < contentLength) {
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n <= 0) throw std::runtime_error("raw recv: EOF before body complete");
        body.insert(body.end(), tmp, tmp + n);
    }
    body.resize(contentLength);
    return body;
}

void testRawChunkedUpload(int port) {
    std::vector<char> body = randomBuffer(150000);
    std::string expectedHash = sha256Hex(body);

    std::ostringstream req;
    req << "POST /echo HTTP/1.1\r\n"
        << "Host: 127.0.0.1:" << port << "\r\n"
        << "Transfer-Encoding: chunked\r\n"
        << "Connection: keep-alive\r\n"
        << "\r\n";

    // Deliberately irregular chunk sizes to stress decodeChunkedBody().
    static const size_t kChunkSizes[] = {3, 1, 4096, 65536};
    size_t off = 0, ci = 0;
    while (off < body.size()) {
        size_t take = std::min(kChunkSizes[ci % 4], body.size() - off);
        req << std::hex << take << std::dec << "\r\n";
        req.write(body.data() + off, static_cast<std::streamsize>(take));
        req << "\r\n";
        off += take;
        ++ci;
    }
    req << "0\r\n\r\n";

    int fd = rawConnect(port);
    sendAll(fd, req.str());

    libhttppp::HttpResponse res;
    std::vector<char> respBody = rawReadResponse(fd, res);
    ::close(fd);

    check(res.getStatusCode() == 200, "chunked upload: 200 response");
    check(headerValue(res, "x-sha256") == expectedHash,
          "chunked upload: server-reported sha256 matches client's");
    check(respBody.size() == body.size(),
          "chunked upload: echoed body size matches");
    check(respBody == body, "chunked upload: echoed bytes match exactly");
}

} // namespace

int main() {
    std::cout << "=== HTTP/1.1 Checksum Integrity Test ===" << std::endl;
    int rc = 0;

    try {
        std::unique_ptr<netplus::tcp> sock;
        std::unique_ptr<H1ChecksumEvent> server;
        std::thread serverThread;
        int port = startServer(sock, server, serverThread);
        ServerThreadGuard shutdownGuard{&serverThread};
        std::cout << "Server listening on 127.0.0.1:" << port << std::endl;

        libhttppp::HttpUrl url("http://127.0.0.1:" + std::to_string(port) + "/");
        libhttppp::HttpClient clt(url, /*vers=*/0, /*timeoutSec=*/5);

        // ---- GET/POST size sweep ----
        for (size_t size : sweepSizes()) {
            std::string label = "size=" + std::to_string(size);

            {
                libhttppp::HttpRequest req;
                req.setRequestURL("/gen/" + std::to_string(size));
                std::vector<char> body = clt.Get(req, /*maxTries=*/3);
                std::string want = headerValue(*clt.lastResponse(), "x-sha256");
                std::string got = sha256Hex(body);
                check(clt.lastStatusCode() == 200, "GET " + label + ": 200 response");
                check(body.size() == size, "GET " + label + ": body size matches");
                check(got == want, "GET " + label + ": sha256 matches X-Sha256 header");
            }

            {
                std::vector<char> payload = randomBuffer(size);
                std::string expectedHash = sha256Hex(payload);

                libhttppp::HttpRequest req;
                req.setRequestURL("/echo");
                std::vector<char> body = clt.Post(req, payload, /*maxTries=*/3);
                std::string reportedHash = headerValue(*clt.lastResponse(), "x-sha256");

                check(clt.lastStatusCode() == 200, "POST " + label + ": 200 response");
                check(body == payload, "POST " + label + ": echoed bytes match exactly");
                check(reportedHash == expectedHash,
                      "POST " + label + ": server-reported sha256 matches client's");
            }
        }

        // ---- Chunked transfer-encoding, both directions ----
        testRawChunkedUpload(port);

        {
            const size_t size = 262144;
            libhttppp::HttpRequest req;
            req.setRequestURL("/chunked/" + std::to_string(size));
            std::vector<char> body = clt.Get(req, /*maxTries=*/3);
            std::string want = headerValue(*clt.lastResponse(), "x-sha256");
            std::string got = sha256Hex(body);
            check(clt.lastStatusCode() == 200, "chunked download: 200 response");
            check(body.size() == size, "chunked download: body size matches");
            check(got == want, "chunked download: sha256 matches X-Sha256 header");
        }

        // ---- Streaming download via GetStream()/readBodyChunk() ----
        {
            const size_t size = 1048576;
            libhttppp::HttpRequest req;
            req.setRequestURL("/gen/" + std::to_string(size));
            libhttppp::HttpResponse res = clt.GetStream(req);
            std::string want = headerValue(res, "x-sha256");

            std::vector<char> collected;
            collected.reserve(size);
            char buf[4096];
            for (;;) {
                size_t n = clt.readBodyChunk(buf, sizeof(buf));
                if (n == 0) break;
                collected.insert(collected.end(), buf, buf + n);
            }
            std::string got = sha256Hex(collected);
            check(res.getStatusCode() == 200, "streaming GET: 200 response");
            check(collected.size() == size, "streaming GET: total bytes match");
            check(got == want, "streaming GET: incrementally-hashed sha256 matches header");
        }

        // shutdownGuard stops and joins the server thread here (its
        // destructor runs before sock/server as it's declared after them).

    } catch (libhttppp::HTTPException &e) {
        std::cerr << "HTTPException: " << e.what() << std::endl;
        rc = 1;
    } catch (netplus::NetException &e) {
        std::cerr << "NetException: " << e.what() << std::endl;
        rc = 1;
    } catch (std::exception &e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        rc = 1;
    }

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

    return (rc != 0 || g_failed > 0) ? 1 : 0;
}
