// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/tests/test_http.c
 *
 *  Unit tests for the libcurl wrapper ppm_http_download():
 *    - file:// happy path: bytes match and the `.part` staging file is
 *      removed once the rename(2) succeeds.
 *    - protocol restriction: ftp:// / gopher:// are refused
 *      (CURLOPT_PROTOCOLS_STR allows only file,http,https).
 *    - `.part` atomicity: a failed fetch leaves no destination file
 *      (nor a stray `.part`) behind.
 *    - connection failure: a refused TCP connect maps to PPM_ERR_NETWORK.
 *
 *  TODO: a true HTTP 404 test needs a local listener — none exists in
 *  this suite yet. With CURLOPT_FAILONERROR a 4xx becomes
 *  CURLE_HTTP_RETURNED_ERROR -> PPM_ERR_NETWORK; add this once a tiny
 *  one-shot test server fixture is available.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include "util/http.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>

#define CHECK(expr) do {                                            \
    if (!(expr)) {                                                  \
        fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \
        return 1;                                                   \
    }                                                               \
} while (0)

static int path_exists(const char *p) {
    struct stat st;
    return stat(p, &st) == 0;
}

/*
 * Bind a TCP socket to an ephemeral loopback port, read the assigned
 * port number, then close it. The returned port is (almost certainly)
 * refusing connections, which lets us exercise the connect-failure path
 * deterministically without a real server.
 */
static int find_closed_port(int *out_port) {
    int s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) return -1;
    struct sockaddr_in sa = {0};
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    sa.sin_port = 0;
    if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) != 0) { close(s); return -1; }
    socklen_t len = sizeof(sa);
    if (getsockname(s, (struct sockaddr *)&sa, &len) != 0) { close(s); return -1; }
    *out_port = ntohs(sa.sin_port);
    close(s);
    return 0;
}

int main(void) {
    char tpl[] = "/tmp/ppm-http-XXXXXX";
    char *base = mkdtemp(tpl);
    CHECK(base != NULL);

    char *src = NULL, *url = NULL, *dest = NULL, *part = NULL;
    char *ftp_dest = NULL, *ftp_part = NULL, *goph_dest = NULL;
    char *miss_dest = NULL, *miss_part = NULL;
    char *conn_url = NULL, *conn_dest = NULL;

    /* --- file:// happy path --------------------------------------------- */
    static const char PAYLOAD[] = "ppm-http-payload-0123456789";
    const size_t PAYLOAD_LEN = sizeof(PAYLOAD) - 1;

    CHECK(asprintf(&src, "%s/src.bin", base) >= 0);
    FILE *f = fopen(src, "wb");
    CHECK(f != NULL);
    CHECK(fwrite(PAYLOAD, 1, PAYLOAD_LEN, f) == PAYLOAD_LEN);
    fclose(f);

    CHECK(asprintf(&url, "file://%s", src) >= 0);
    CHECK(asprintf(&dest, "%s/dest.bin", base) >= 0);
    CHECK(asprintf(&part, "%s.part", dest) >= 0);

    CHECK(ppm_http_download(url, dest) == PPM_OK);

    /* Downloaded bytes match the source exactly. */
    f = fopen(dest, "rb");
    CHECK(f != NULL);
    char buf[64] = {0};
    size_t n = fread(buf, 1, sizeof(buf) - 1, f);
    fclose(f);
    CHECK(n == PAYLOAD_LEN);
    CHECK(memcmp(buf, PAYLOAD, n) == 0);

    /* The `.part` staging file is gone once the rename succeeded. */
    CHECK(!path_exists(part));

    /* --- protocol restriction: ftp:// refused -------------------------- */
    /* Refused by the file,http,https allowlist; even if it slipped past,
     * there is no ftp server here, so nothing is ever written. */
    CHECK(asprintf(&ftp_dest, "%s/ftp.bin", base) >= 0);
    CHECK(asprintf(&ftp_part, "%s.part", ftp_dest) >= 0);
    CHECK(ppm_http_download("ftp://127.0.0.1/whatever", ftp_dest) == PPM_ERR_NETWORK);
    CHECK(!path_exists(ftp_dest));
    CHECK(!path_exists(ftp_part));

    /* gopher:// is likewise outside the allowlist. */
    CHECK(asprintf(&goph_dest, "%s/goph.bin", base) >= 0);
    CHECK(ppm_http_download("gopher://127.0.0.1/x", goph_dest) == PPM_ERR_NETWORK);
    CHECK(!path_exists(goph_dest));

    /* --- .part atomicity: a failed fetch leaves no destination --------- */
    CHECK(asprintf(&miss_dest, "%s/missing.bin", base) >= 0);
    CHECK(asprintf(&miss_part, "%s.part", miss_dest) >= 0);
    /* file:// to a path that does not exist: the read fails, the staging
     * file is unlinked, and the destination is never created. */
    CHECK(ppm_http_download("file:///nonexistent/ppm/xyz.bin", miss_dest) != PPM_OK);
    CHECK(!path_exists(miss_dest));
    CHECK(!path_exists(miss_part));

    /* --- connection failure: refused TCP connect ----------------------- */
    int port = 0;
    CHECK(find_closed_port(&port) == 0);
    CHECK(asprintf(&conn_url, "http://127.0.0.1:%d/x", port) >= 0);
    CHECK(asprintf(&conn_dest, "%s/conn.bin", base) >= 0);
    CHECK(ppm_http_download(conn_url, conn_dest) == PPM_ERR_NETWORK);
    CHECK(!path_exists(conn_dest));

    /* --- cleanup ------------------------------------------------------- */
    unlink(src);
    unlink(dest);
    free(src); free(url); free(dest); free(part);
    free(ftp_dest); free(ftp_part); free(goph_dest);
    free(miss_dest); free(miss_part);
    free(conn_url); free(conn_dest);
    rmdir(base);

    printf("ok\n");
    return 0;
}
