// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/tests/test_build.c
 *
 *  End-to-end derivation test:
 *    1. Build a source tarball containing a tiny Makefile/program.
 *    2. Hand it to ppm_build_run via a manifest with a [build] section.
 *    3. Verify the build output lands in the store under the
 *       deterministic derivation hash, with the expected file inside.
 *    4. Re-running is a cache hit, no second build.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/build.h>
#include <ppm/manifest.h>
#include <ppm/store.h>
#include <ppm/crypto.h>

#include <archive.h>
#include <archive_entry.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "util/path.h"

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

/* Build a source tarball with one file: "hello.txt" containing "world". */
static int build_source_tarball(const char *path) {
    struct archive *a = archive_write_new();
    if (!a) return -1;
    archive_write_add_filter_gzip(a);
    archive_write_set_format_pax_restricted(a);
    if (archive_write_open_filename(a, path) != ARCHIVE_OK) {
        archive_write_free(a); return -1;
    }
    const char *body = "world";
    struct archive_entry *e = archive_entry_new();
    archive_entry_set_pathname(e, "hello.txt");
    archive_entry_set_size(e, (la_int64_t)strlen(body));
    archive_entry_set_filetype(e, AE_IFREG);
    archive_entry_set_perm(e, 0644);
    archive_write_header(a, e);
    archive_write_data(a, body, strlen(body));
    archive_entry_free(e);
    archive_write_close(a);
    archive_write_free(a);
    return 0;
}

int main(void) {
    CHECK(ppm_crypto_init() == PPM_OK);

    char tpl[] = "/tmp/ppm-build-XXXXXX";
    char *base = mkdtemp(tpl);
    CHECK(base != NULL);

    char *store_root, *tar_path;
    CHECK(ppm_path_join(base, "store", &store_root) == PPM_OK);
    CHECK(ppm_path_join(base, "src.tar.gz", &tar_path) == PPM_OK);
    CHECK(build_source_tarball(tar_path) == 0);

    /* Hash the source tarball — the manifest will declare this value. */
    ppm_sha512_t src_hash;
    CHECK(ppm_sha512_file(tar_path, &src_hash) == PPM_OK);

    ppm_store_t *s = NULL;
    CHECK(ppm_store_open(store_root, &s) == PPM_OK);

    /* Fake packaged shell for the unit test. Production uses the real
     * bootstrap-tools package; this just exercises the same API shape. */
    char *boot, *boot_bin, *boot_sh;
    CHECK(ppm_path_join(base, "bootstrap", &boot) == PPM_OK);
    CHECK(ppm_path_join(boot, "bin", &boot_bin) == PPM_OK);
    CHECK(ppm_path_join(boot_bin, "sh", &boot_sh) == PPM_OK);
    CHECK(ppm_path_mkdirs(boot_bin, 0755) == PPM_OK);
    unlink(boot_sh);
    CHECK(symlink("/bin/sh", boot_sh) == 0);
    const char *dep_names[] = { "bootstrap-tools" };
    ppm_sha512_t dep_hashes[1] = {0};
    const char *dep_paths[] = { boot };

    /* Hand-build a manifest: derivation that verifies $src and
     * generates output files. */
    ppm_manifest_t *m = calloc(1, sizeof(*m));
    CHECK(m != NULL);
    m->name = strdup("demo");
    m->version = (ppm_version_t){ .major = 1, .minor = 0, .patch = 0 };
    m->source.url  = NULL;
    /* Use a file:// URL pointing at our local tarball so libcurl can fetch it. */
    char *src_url;
    CHECK(asprintf(&src_url, "file://%s", tar_path) >= 0);
    m->source.url  = src_url;
    m->source.hash = src_hash;
    m->build.has_build = 1;
    /* Hermetic builds have an empty PATH — no /bin/cp, no /bin/cat.
     * The script verifies $src is readable (via the parameter
     * expansion test below) and emits two output files using only
     * shell built-ins (printf). */
    m->build.script = strdup(
        "test -d \"$src\"\n"
        "test -f \"$src\"/hello.txt\n"
        "printf world > \"$out\"/hello.txt\n"
        "printf built > \"$out\"/stamp\n"
    );
    CHECK(m->name && m->build.script);

    /* Run the build. */
    ppm_sha512_t out_hash;
    CHECK(ppm_build_run(s, m, dep_names, dep_hashes, dep_paths, 1,
                        &out_hash) == PPM_OK);
    CHECK(ppm_store_has(s, &out_hash));

    /* Verify the produced files exist inside the store path. */
    char *obj;
    CHECK(ppm_store_path(s, &out_hash, &obj) == PPM_OK);
    char *stamp_path, *hello_path;
    CHECK(ppm_path_join(obj, "stamp", &stamp_path) == PPM_OK);
    CHECK(ppm_path_join(obj, "hello.txt", &hello_path) == PPM_OK);
    FILE *f = fopen(stamp_path, "r");
    CHECK(f != NULL);
    char buf[16] = {0};
    fread(buf, 1, sizeof(buf) - 1, f);
    fclose(f);
    CHECK(strncmp(buf, "built", 5) == 0);
    f = fopen(hello_path, "r");
    CHECK(f != NULL);
    char buf2[16] = {0};
    fread(buf2, 1, sizeof(buf2) - 1, f);
    fclose(f);
    CHECK(strncmp(buf2, "world", 5) == 0);

    /* Re-running yields the same hash and is a cache hit (no error,
     * idempotent). */
    ppm_sha512_t out_hash2;
    CHECK(ppm_build_run(s, m, dep_names, dep_hashes, dep_paths, 1,
                        &out_hash2) == PPM_OK);
    CHECK(ppm_sha512_equal(&out_hash, &out_hash2));

    /* Sanity: changing the script changes the hash. */
    free(m->build.script);
    m->build.script = strdup("echo different > \"$out\"/stamp\n");
    ppm_sha512_t alt;
    CHECK(ppm_build_output_hash(m, dep_names, dep_hashes, 1, &alt) == PPM_OK);
    CHECK(!ppm_sha512_equal(&alt, &out_hash));

    free(stamp_path); free(hello_path); free(obj);
    ppm_manifest_free(m);
    ppm_store_close(s);
    ppm_path_rmrf(base);
    free(store_root); free(tar_path);
    free(boot); free(boot_bin); free(boot_sh);

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