// test_http_request.cpp — Catch2 integration tests for fn_http::request.
// Issue 0110.
//
// Strategy: spin up a local Python HTTP server fixture (stdlib BaseHTTPServer)
// in a background process for the duration of the suite. Tests hit
// http://127.0.0.1:<PORT> instead of httpbin.org so the CI is deterministic
// and works offline. The server echoes method, headers, and body as JSON.
#include "catch_amalgamated.hpp"
#include "core/http_request.h"

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
#include <sys/types.h>
#include <thread>

#ifndef _WIN32
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#endif

namespace {

constexpr const char* kHost = "127.0.0.1";

// Random-ish port in the upper unprivileged range. Tests are independent —
// each suite picks one. Add jitter from PID to reduce collision risk.
int pick_port() {
    int base = 47200;
    int jitter = (int)(getpid() % 100);
    return base + jitter;
}

struct PythonServer {
    pid_t pid = 0;
    int   port = 0;

    bool start() {
        port = pick_port();
        // Write the server script to a tmp file.
        std::string script_path = std::tmpnam(nullptr);
        script_path += ".py";
        {
            std::ofstream f(script_path);
            f <<
                "import json, sys\n"
                "from http.server import BaseHTTPRequestHandler, HTTPServer\n"
                "PORT = int(sys.argv[1])\n"
                "class H(BaseHTTPRequestHandler):\n"
                "    def log_message(self, *a, **k): pass\n"
                "    def _echo(self):\n"
                "        n = int(self.headers.get('Content-Length','0') or '0')\n"
                "        body = self.rfile.read(n).decode('utf-8','replace') if n else ''\n"
                "        payload = {\n"
                "            'method': self.command,\n"
                "            'path':   self.path,\n"
                "            'headers':{k:v for k,v in self.headers.items()},\n"
                "            'body':   body,\n"
                "        }\n"
                "        out = json.dumps(payload).encode()\n"
                "        self.send_response(200); self.send_header('content-type','application/json')\n"
                "        self.send_header('x-fn-test','ok')\n"
                "        self.send_header('content-length', str(len(out)))\n"
                "        self.end_headers(); self.wfile.write(out)\n"
                "    def do_GET(self):\n"
                "        if self.path.startswith('/status/'):\n"
                "            code = int(self.path.rsplit('/',1)[1])\n"
                "            msg = ('error %d' % code).encode()\n"
                "            self.send_response(code); self.send_header('content-length', str(len(msg))); self.end_headers(); self.wfile.write(msg); return\n"
                "        if self.path == '/notjson':\n"
                "            self.send_response(200); self.send_header('content-type','text/plain'); self.end_headers(); self.wfile.write(b'<<not json>>'); return\n"
                "        if self.path.startswith('/slow/'):\n"
                "            import time; time.sleep(float(self.path.rsplit('/',1)[1]));\n"
                "        self._echo()\n"
                "    def do_POST(self):  self._echo()\n"
                "    def do_PUT(self):   self._echo()\n"
                "    def do_DELETE(self):self._echo()\n"
                "HTTPServer(('127.0.0.1', PORT), H).serve_forever()\n";
        }

#ifdef _WIN32
        return false; // tests skipped on Windows for now
#else
        pid_t p = fork();
        if (p < 0) return false;
        if (p == 0) {
            // child — redirect stdio to /dev/null and exec python3
            FILE* dn = std::freopen("/dev/null", "r", stdin);  (void)dn;
            FILE* d2 = std::freopen("/dev/null", "w", stdout); (void)d2;
            FILE* d3 = std::freopen("/dev/null", "w", stderr); (void)d3;
            execlp("python3", "python3", "-u", script_path.c_str(),
                   std::to_string(port).c_str(), (char*)nullptr);
            std::_Exit(127);
        }
        pid = p;
        // Poll for readiness — up to 3s.
        for (int i = 0; i < 60; ++i) {
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
            fn_http::Request r;
            r.url        = std::string("http://") + kHost + ":" + std::to_string(port) + "/ping";
            r.timeout_ms = 500;
            auto res = fn_http::request(r);
            if (res.status == 200) return true;
        }
        return false;
#endif
    }

    void stop() {
#ifndef _WIN32
        if (pid > 0) {
            kill(pid, SIGTERM);
            int st = 0;
            waitpid(pid, &st, 0);
            pid = 0;
        }
#endif
    }

    std::string base() const {
        return std::string("http://") + kHost + ":" + std::to_string(port);
    }
};

PythonServer& server() {
    static PythonServer s;
    static bool inited = false;
    if (!inited) {
        inited = true;
        s.start();
        // Best-effort cleanup at process exit (Catch2 has no global teardown
        // hook portable to v3 amalgamated — atexit is fine).
        std::atexit([] { server().stop(); });
    }
    return s;
}

bool server_alive() { return server().pid > 0; }

} // anon

TEST_CASE("request: GET 200 status + body", "[http_request]") {
    if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; }
    fn_http::Request r;
    r.method = "GET";
    r.url    = server().base() + "/get";
    auto res = fn_http::request(r);
    REQUIRE(res.error.empty());
    REQUIRE(res.status == 200);
    REQUIRE(res.body.find("\"method\": \"GET\"") != std::string::npos);
    REQUIRE(res.duration_ms >= 0);
}

TEST_CASE("request: GET 404 status (HTTP error not transport)", "[http_request]") {
    if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; }
    fn_http::Request r;
    r.method = "GET";
    r.url    = server().base() + "/status/404";
    auto res = fn_http::request(r);
    REQUIRE(res.error.empty());          // 404 is NOT a transport error
    REQUIRE(res.status == 404);
}

TEST_CASE("request: POST with body + content-type header", "[http_request]") {
    if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; }
    fn_http::Request r;
    r.method  = "POST";
    r.url     = server().base() + "/echo";
    r.headers = {{"Content-Type", "application/json"}};
    r.body    = R"({"hello":"world"})";
    auto res = fn_http::request(r);
    REQUIRE(res.error.empty());
    REQUIRE(res.status == 200);
    REQUIRE(res.body.find("\"method\": \"POST\"") != std::string::npos);
    REQUIRE(res.body.find("hello") != std::string::npos);
}

TEST_CASE("request: bearer_token shortcut adds Authorization header", "[http_request]") {
    if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; }
    fn_http::Request r;
    r.method       = "GET";
    r.url          = server().base() + "/whoami";
    r.bearer_token = "s3cret_xyz";
    auto res = fn_http::request(r);
    REQUIRE(res.error.empty());
    REQUIRE(res.status == 200);
    REQUIRE(res.body.find("Bearer s3cret_xyz") != std::string::npos);
}

TEST_CASE("request: basic_user/basic_pass shortcut authenticates", "[http_request]") {
    if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; }
    fn_http::Request r;
    r.method     = "GET";
    r.url        = server().base() + "/basic";
    r.basic_user = "alice";
    r.basic_pass = "wonderland";
    auto res = fn_http::request(r);
    REQUIRE(res.error.empty());
    REQUIRE(res.status == 200);
    // curl base64s "alice:wonderland" -> "YWxpY2U6d29uZGVybGFuZA=="
    REQUIRE(res.body.find("Basic YWxpY2U6d29uZGVybGFuZA==") != std::string::npos);
}

TEST_CASE("request: invalid URL surfaces transport error", "[http_request]") {
    // No server needed — this exercises curl's DNS/transport layer.
    fn_http::Request r;
    r.method     = "GET";
    r.url        = "http://127.0.0.1:1/never_listens_here";  // port 1 is reserved
    r.timeout_ms = 1500;
    auto res = fn_http::request(r);
    REQUIRE(res.status == 0);
    REQUIRE_FALSE(res.error.empty());
}

TEST_CASE("request: timeout_ms honored", "[http_request]") {
    if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; }
    fn_http::Request r;
    r.method     = "GET";
    r.url        = server().base() + "/slow/3";   // server sleeps 3s
    r.timeout_ms = 1000;                          // we want bail at 1s
    auto t0 = std::chrono::steady_clock::now();
    auto res = fn_http::request(r);
    auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
                          std::chrono::steady_clock::now() - t0).count();
    REQUIRE(res.status == 0);            // timeout = transport error
    REQUIRE_FALSE(res.error.empty());
    REQUIRE(elapsed_ms < 2500);          // we did NOT wait the full 3s
}
