// Copyright (c) 2012-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <httpserver.h>
#include <rpc/protocol.h>
#include <test/util/common.h>
#include <test/util/setup_common.h>
#include <util/string.h>

#include <boost/test/unit_test.hpp>

using http_bitcoin::GetQueryParameterFromUri;
using http_bitcoin::HTTPHeaders;
using http_bitcoin::HTTPRemoteClient;
using http_bitcoin::HTTPRequest;
using http_bitcoin::HTTPResponse;
using http_bitcoin::HTTPServer;
using http_bitcoin::MAX_BODY_SIZE;
using http_bitcoin::MAX_HEADERS_SIZE;
using util::LineReader;

// HTTP request captured from bitcoin-cli
constexpr std::string_view full_request = "POST / HTTP/1.1\r\n"
                                          "Host: 127.0.0.1\r\n"
                                          "Connection: close\r\n"
                                          "Content-Type: application/json\r\n"
                                          "Authorization: Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl\r\n"
                                          "Content-Length: 46\r\n"
                                          "\r\n"
                                          R"({"method":"getblockcount","params":[],"id":1})""\n";

BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)

BOOST_AUTO_TEST_CASE(test_query_parameters)
{
    std::string uri {};

    // Tolerate a URI with invalid characters (% not followed by hex digits)
    uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%";
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2%");

    // No parameters
    uri = "localhost:8080/rest/headers/someresource.json";
    BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));

    // Single parameter
    uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
    BOOST_CHECK(!GetQueryParameterFromUri(uri, "p2"));

    // Multiple parameters
    uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "v2");

    // If the query string contains duplicate keys, the first value is returned
    uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1");

    // Invalid query string syntax is the same as not having parameters
    uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
    BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));

    // Multiple parameters, some characters encoded
    uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25";
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p1"), "v1 ");
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p2"), "100%");

    // Encoded query delimiters are part of the parameter value, not structure.
    uri = "/rest/endpoint/someresource.json?p=a%26b%3Dc%23frag&other=x";
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "p"), "a&b=c#frag");
    BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri, "other"), "x");

    // An encoded question mark in the path does not introduce a query section.
    uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25";
    BOOST_CHECK(!GetQueryParameterFromUri(uri, "p1"));
}

BOOST_AUTO_TEST_CASE(http_headers_tests)
{
    {
        // Writing response headers
        HTTPHeaders headers{};
        BOOST_CHECK(!headers.FindFirst("Cache-Control"));
        headers.Write("Cache-Control", "no-cache");
        // Check case-insensitive key matching
        BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
        BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
        // Additional values are appended, compared case-insensitive
        headers.Write("cache-control", "max-age=60");
        BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
        BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
        // Add a few more
        headers.Write("Pie", "apple");
        headers.Write("Sandwich", "ham");
        headers.Write("Coffee", "black");
        BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
        // Remove
        headers.RemoveAll("Pie");
        BOOST_CHECK(!headers.FindFirst("Pie"));
        // Combine for transmission
        std::string headers_string{headers.Stringify()};
        BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
                                          "cache-control: max-age=60\r\n"
                                          "Sandwich: ham\r\n"
                                          "Coffee: black\r\n"
                                          "\r\n");
    }
    {
        // Reading request headers captured from bitcoin-cli
        constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
                                                         "Connection: close\r\n"
                                                         "Content-Type: application/json\r\n"
                                                         "Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
                                                         "Content-Length: 46\r\n";
        util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
        HTTPHeaders headers{};
        headers.Read(reader);
        BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
        BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
        BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
        BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
        BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
        BOOST_CHECK(!headers.FindFirst("Pizza"));
    }
    // Ensure invalid headers are rejected
    {
        // missing a colon
        util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
    }
    {
        // missing a key
        util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
    }
    {
        // contains NUL
        util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
    }
    {
        // contains bare \r (not followed by \n)
        util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
    }
    {
        // contains odd \r preceding the expected CRLF
        util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
    }
    {
        // key contains whitespace
        util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
    }
    {
        // Individual lines are below MAX_HEADERS_SIZE but the total is excessive
        std::string lines;
        lines.reserve(820 * 10);
        for (int i = 0; i < 820; ++i) {
            lines.append("key:value\n");
        }
        std::string_view excessive_headers{lines};
        BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE);
        util::LineReader reader{excessive_headers, /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
    }
    {
        // Ok
        util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
        HTTPHeaders headers{};
        headers.Read(reader);
        BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value");
    }
}

BOOST_AUTO_TEST_CASE(http_response_tests)
{
    // Typical HTTP 1.1 response headers
    HTTPHeaders headers{};
    headers.Write("Content-Length", "41");

    // Response points to headers which already exist because some of them
    // are set before we even know what the response will be.
    HTTPResponse res;
    res.m_version = {.major = 1, .minor = 1};
    res.m_status = HTTP_OK;
    res.m_headers = std::move(headers);
    BOOST_CHECK_EQUAL(
        res.StringifyHeaders(),
        "HTTP/1.1 200 OK\r\n"
        "Content-Length: 41\r\n"
        "\r\n");
}

BOOST_AUTO_TEST_CASE(http_request_tests)
{
    {
        HTTPRequest req;
        LineReader reader(full_request, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
        BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::POST);
        BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::POST);
        BOOST_CHECK_EQUAL(req.m_target, "/");
        BOOST_CHECK_EQUAL(req.GetURI(), "/");
        BOOST_CHECK_EQUAL(req.m_version.major, 1);
        BOOST_CHECK_EQUAL(req.m_version.minor, 1);
        BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
        BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Connection"), "close");
        BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Type"), "application/json");
        BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
        BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Length"), "46");
        BOOST_CHECK_EQUAL(req.m_body.size(), 46);
        BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
    }
    {
        // Malformed: no spaces between data
        HTTPRequest req;
        LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
    }
    {
        // Malformed: too many spaces
        HTTPRequest req;
        LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
    }
    {
        // Malformed: slash missing before version
        HTTPRequest req;
        LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
    }
    {
        // Malformed: no decimal in version
        HTTPRequest req;
        LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
    }
    {
        // Malformed: version is not a number
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    }
    {
        // Malformed: version is out of range
        HTTPRequest req;
        LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    }
    {
        // Malformed: version is out of range
        HTTPRequest req;
        LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    }
    {
        // Malformed: version is out of range
        HTTPRequest req;
        LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    }
    {
        // Malformed: version is not exactly two integers and a dot
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    }
    {
        // Malformed: contains NUL
        HTTPRequest req;
        LineReader reader{std::string_view{"GET /safe\0/etc/passwd HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", 50}, MAX_HEADERS_SIZE};
        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
    }
    {
        // Malformed: differing Content-Length values, case insensitive
        constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
                                                      "Host: 127.0.0.1\n"
                                                      "Content-Length: 8\n"
                                                      "content-length: 9\n\n"
                                                      "12345678";
        HTTPRequest req;
        util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
    }
    {
        // Ok: multiple same Content-Length values
        constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
                                                      "Host: 127.0.0.1\n"
                                                      "Content-Length: 8\n"
                                                      "content-length: 8\n\n"
                                                      "12345678";
        HTTPRequest req;
        util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
    }
    {
        // Ok
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
        BOOST_CHECK_EQUAL(req.m_method, HTTPRequestMethod::GET);
        BOOST_CHECK_EQUAL(req.m_target, "/");
        BOOST_CHECK_EQUAL(req.m_version.major, 1);
        BOOST_CHECK_EQUAL(req.m_version.minor, 0);
        BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
        // no body is OK
        BOOST_CHECK_EQUAL(req.m_body.size(), 0);
    }
    {
        // Malformed: missing colon
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
    }
    {
        // We might not have received enough data from the client which is not
        // an error. We return false so the caller can try again later when the
        // buffer has more data.
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(!req.LoadHeaders(reader));
    }
    {
        // No Content-Length: body is not read
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
        // Don't try to read request body if Content-Length is missing
        BOOST_CHECK_EQUAL(req.m_body.size(), 0);
    }
    {
        // Malformed: Content-Length is not a number
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
    }
    {
        // Malformed: Content-Length is negative
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
    }
    {
        // Content-Length exceeds limit
        constexpr auto excessive_size{MAX_BODY_SIZE + 1};
        std::string huge_body(excessive_size, 'x');
        const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
        HTTPRequest req;
        LineReader reader(request, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Max body size exceeded"});
    }
    {
        // Content-Length exactly on the limit
        std::string max_body(MAX_BODY_SIZE, 'x');
        const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(MAX_BODY_SIZE) + "\r\n\r\n" + std::move(max_body)};
        HTTPRequest req;
        LineReader reader(request, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
    }
    {
        // Content-Length indicates more data than we have in the buffer.
        // Not an error; we wait for more data before completing the body.
        HTTPRequest req;
        LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(!req.LoadBody(reader));
    }
    {
        // Support "chunked" transfer. Chunk lengths are ascii-encoded hex integers, whitespace ignored
        HTTPRequest req;
        std::string_view ok_chunked = "GET / HTTP/1.0\n"
                                      "Transfer-Encoding: chunked\n"
                                      "\n"
                                      "10\n"
                                      R"({"method":"getbl)""\n"
                                      "   a    \n"
                                      R"(ockcount"})""\n"
                                      "0\n"
                                      "\n";
        LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
        BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
    }
    {
        // Prevent "chunked" transfer from exceeding size limit
        HTTPRequest req;
        std::string_view excessive_chunk_size = "GET / HTTP/1.0\n"
                                                "Transfer-Encoding: chunked\n"
                                                "\n"
                                                "10\n"
                                                R"({"method":"getbl)""\n"
                                                "20000000\n"
                                                R"(ockcount"})""\n"
                                                "0\n"
                                                "\n";
        LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
    }
    {
        // Allow (but ignore) Chunk Extensions
        HTTPRequest req;
        std::string_view ok_chunked = "GET / HTTP/1.0\n"
                                      "Transfer-Encoding: chunked\n"
                                      "\n"
                                      "10;sha256=715790e8a3b09d704ac9641f42d183a5ebc5fd939663de23da548519ac2165e5\n"
                                      R"({"method":"getbl)""\n"
                                      "   a   ;    compressed\n"
                                      R"(ockcount"})""\n"
                                      "0;why;would;anyone;do;this;\n"
                                      "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
                                      "\n";
        LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK(req.LoadBody(reader));
        BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount"})");
        // Chunk Trailer was cleared
        BOOST_CHECK_EQUAL(reader.Remaining(), 0);
    }
    {
        // Invalid "chunked" transfer, using roman numerals instead of hex for chunk length
        HTTPRequest req;
        std::string_view invalid_chunked = "GET / HTTP/1.0\n"
                                           "Transfer-Encoding: chunked\n"
                                           "\n"
                                           "XVI\n"
                                           R"({"method":"getbl)""\n"
                                           "X\n"
                                           R"(ockcount"})""\n"
                                           "0\n"
                                           "\n";
        LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
    }
    {
        // Invalid "chunked" transfer, missing chunk termination \n
        HTTPRequest req;
        std::string_view invalid_chunked = "GET / HTTP/1.0\n"
                                           "Transfer-Encoding: chunked\n"
                                           "\n"
                                           "10\n"
                                           R"({"method":"getbl)"
                                           "a\n" // interpreted as extra data at the end of `0x10`-sized chunk
                                           R"(ockcount"})"
                                           "0\n"
                                           "\n";
        LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader));
        BOOST_CHECK(req.LoadHeaders(reader));
        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
    }
    {
        // End of buffer reached without chunk termination, caller must wait for more data to arrive
        HTTPRequest req;
        std::string delayed_chunked = "GET / HTTP/1.0\n"
                                      "Transfer-Encoding: chunked\n"
                                      "\n"
                                      "10\n"
                                      R"({"method":"getbl)""\n"
                                      "a\n"
                                      R"(ockcount"})";
        LineReader reader1(delayed_chunked, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader1));
        BOOST_CHECK(req.LoadHeaders(reader1));
        BOOST_CHECK(!req.LoadBody(reader1));
        // more data arrives!
        delayed_chunked += "\n0\n\n";
        LineReader reader2(delayed_chunked, MAX_HEADERS_SIZE);
        BOOST_CHECK(req.LoadControlData(reader2));
        BOOST_CHECK(req.LoadHeaders(reader2));
        BOOST_CHECK(req.LoadBody(reader2));
    }
}

BOOST_AUTO_TEST_CASE(http_server_socket_tests)
{
    // Hard code the timestamp for the Date header in the HTTP response
    // Wed Dec 11 00:47:09 2024 UTC
    SetMockTime(1733878029);

    // Prepare a request handler that just stores received requests so we can examine them.
    // Mutex is required to prevent a race between this test's main thread and the server's I/O loop.
    Mutex requests_mutex;
    std::deque<std::unique_ptr<HTTPRequest>> requests;
    auto StoreRequest = [&](std::unique_ptr<HTTPRequest>&& req) {
        LOCK(requests_mutex);
        requests.push_back(std::move(req));
    };

    HTTPServer server{StoreRequest};

    {
        // We can only bind to NET_IPV4 and NET_IPV6
        CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
        auto result{server.BindAndStartListening(onion_address)};
        BOOST_REQUIRE(!result);
        BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
    }

    // This VALID address won't actually get used because we stubbed CreateSock()
    CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};

    // Init state
    BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
    // Bind to mock Listening Socket
    BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
    // We are bound and listening
    BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);

    // Start the I/O loop
    server.StartSocketsThreads();

    // No connections yet
    BOOST_CHECK_EQUAL(server.GetConnectionsCount(), 0);

    // Create a mock client with pre-loaded request data and add it to the local CreateSock queue.
    // Keep a handle for the mock client's send and receive pipes so we can examine
    // the data it "receives".
    std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(full_request)))};

    // Wait up to a minute to find and connect the client in the I/O loop
    int attempts{6000};
    while (server.GetConnectionsCount() < 1) {
        std::this_thread::sleep_for(10ms);
        BOOST_REQUIRE(--attempts > 0);
    }

    // Prepare a pointer to the client, we'll assign it from the request itself.
    std::shared_ptr<HTTPRemoteClient> client;

    // Wait up to a minute to read the request from the client.
    // Given that the mock client is itself a mock socket
    // with hard-coded data it should only take a fraction of that.
    attempts = 6000;
    while (true) {
        {
            LOCK(requests_mutex);
            // Connected client should have one request already from the static content.
            if (requests.size() == 1) {
                // Check the received request
                BOOST_CHECK_EQUAL(requests.front()->m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
                BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789");

                // Inspect the connection pointed to from the request
                client = requests.front()->m_client;
                BOOST_CHECK_EQUAL(client->m_origin, "5.5.5.5:6789");

                // Respond to request
                requests.front()->WriteReply(HTTP_OK, "874140\n");

                break;
            }
        }
        std::this_thread::sleep_for(10ms);
        BOOST_REQUIRE(--attempts > 0);
    }

    // Check the sent response from the mock client at the other end of the mock socket
    std::string actual;
    // Wait up to one minute for all the bytes to appear in the "send" pipe.
    char buf[0x10000] = {};
    attempts = 6000;
    while (attempts > 0)
    {
        ssize_t bytes_read = mock_client_socket_pipes->send.GetBytes(buf, sizeof(buf), 0);
        if (bytes_read > 0) {
            actual.append(buf, bytes_read);
            if (actual.length() == 146) {
                break;
            }
        }
        std::this_thread::sleep_for(10ms);
        --attempts;
    }
    BOOST_CHECK(actual.starts_with("HTTP/1.1 200 OK\r\n"));
    BOOST_CHECK(actual.ends_with("\r\n874140\n"));
    // Headers can be sorted in any order, and will be, since we use unordered_map
    BOOST_CHECK(actual.find("Connection: close\r\n") != std::string::npos);
    BOOST_CHECK(actual.find("Content-Length: 7\r\n") != std::string::npos);
    BOOST_CHECK(actual.find("Content-Type: text/html; charset=ISO-8859-1\r\n") != std::string::npos);
    BOOST_CHECK(actual.find("Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n") != std::string::npos);

    // Wait up to one minute for connection to be automatically closed, because
    // keep-alive was not set by the client and we are done responding to their request.
    attempts = 6000;
    while (server.GetConnectionsCount() != 0) {
        std::this_thread::sleep_for(10ms);
        BOOST_REQUIRE(--attempts > 0);
    }

    // Stop the I/O loop and shutdown
    server.InterruptNet();
    // Wait for I/O loop to finish, after all connected sockets are closed
    server.JoinSocketsThreads();
    // Close all listening sockets
    server.StopListening();
}

BOOST_AUTO_TEST_SUITE_END()
