// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/tests/test_build_chain.c
 *
 *  Multi-package build chain test:
 *    libfoo (derivation, no deps) — writes "FOO" into $out/data
 *    libbar (derivation, deps = [libfoo]) — reads $dep_libfoo/data and
 *                                            writes "got=FOO" into $out/data
 *
 *  Asserts:
 *    - Both packages build successfully through the resolver + install
 *      topological walk.
 *    - libbar's build script saw $dep_libfoo pointing at libfoo's CAS
 *      path (verified by reading libbar's output).
 *    - Hashes are deterministic across re-runs.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/registry.h>
#include <ppm/resolver.h>
#include <ppm/store.h>
#include <ppm/transaction.h>
#include <ppm/manifest.h>
#include <ppm/lockfile.h>
#include <ppm/build.h>
#include <ppm/crypto.h>

#include <archive.h>
#include <archive_entry.h>
#include <git2.h>
#include <sodium.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)

static int write_file(const char *path, const char *body) {
    FILE *f = fopen(path, "w");
    if (!f) return -1;
    fputs(body, f);
    fclose(f);
    return 0;
}

/* One Ed25519 signer for the whole fixture (the registry trust anchor). */
static unsigned char g_pk[crypto_sign_ed25519_PUBLICKEYBYTES];
static unsigned char g_sk[crypto_sign_ed25519_SECRETKEYBYTES];

/* Publish signers/test.pub (hex public key) — the trusted key set. */
static int write_signers(const char *registry_root) {
    char dir[1024];
    snprintf(dir, sizeof(dir), "%s/signers", registry_root);
    if (ppm_path_mkdirs(dir, 0755) != PPM_OK) return -1;
    char pk_hex[sizeof(g_pk) * 2 + 1];
    sodium_bin2hex(pk_hex, sizeof(pk_hex), g_pk, sizeof(g_pk));
    char pub[1100];
    snprintf(pub, sizeof(pub), "%s/test.pub", dir);
    FILE *o = fopen(pub, "w");
    if (!o) return -1;
    fputs(pk_hex, o);
    fputc('\n', o);
    fclose(o);
    return 0;
}

/* Detached-sign manifest.toml -> manifest.toml.sig (hex), over raw bytes. */
static int sign_manifest(const char *manifest_path) {
    FILE *f = fopen(manifest_path, "rb");
    if (!f) return -1;
    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
    long sz = ftell(f);
    if (sz < 0) { fclose(f); return -1; }
    if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return -1; }
    unsigned char *buf = malloc((size_t)sz);
    if (!buf) { fclose(f); return -1; }
    if (fread(buf, 1, (size_t)sz, f) != (size_t)sz) { free(buf); fclose(f); return -1; }
    fclose(f);

    unsigned char sig[crypto_sign_ed25519_BYTES];
    if (crypto_sign_ed25519_detached(sig, NULL, buf, (size_t)sz, g_sk) != 0) {
        free(buf);
        return -1;
    }
    free(buf);

    char sig_hex[sizeof(sig) * 2 + 1];
    sodium_bin2hex(sig_hex, sizeof(sig_hex), sig, sizeof(sig));
    char sig_path[1100];
    snprintf(sig_path, sizeof(sig_path), "%s.sig", manifest_path);
    FILE *o = fopen(sig_path, "w");
    if (!o) return -1;
    fputs(sig_hex, o);
    fputc('\n', o);
    fclose(o);
    return 0;
}

/* Minimal source tarball: one empty file so the extracted tree is
 * not empty. The build script does not need any specific source. */
static int build_dummy_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;
    }
    struct archive_entry *e = archive_entry_new();
    archive_entry_set_pathname(e, "README");
    archive_entry_set_size(e, 0);
    archive_entry_set_filetype(e, AE_IFREG);
    archive_entry_set_perm(e, 0644);
    archive_write_header(a, e);
    archive_entry_free(e);
    archive_write_close(a);
    archive_write_free(a);
    return 0;
}

/* Hash the tarball so we know what to declare in the manifest. */
static int compute_hash(const char *tarball, char *hex_out) {
    ppm_sha512_t h;
    if (ppm_sha512_file(tarball, &h) != PPM_OK) return -1;
    ppm_sha512_to_hex(&h, hex_out);
    return 0;
}

static int put_pkg(const char *registry_root, const char *name,
                   const char *version, const char *src_url,
                   const char *src_hash_hex, const char *deps_block,
                   const char *script) {
    char shard[3] = { name[0], name[1] ? name[1] : '_', '\0' };
    char dir[1024];
    snprintf(dir, sizeof(dir), "%s/packages/%s/%s", registry_root, shard, name);
    ppm_path_mkdirs(dir, 0755);

    char vpath[1024];
    snprintf(vpath, sizeof(vpath), "%s/versions.toml", dir);
    FILE *f = fopen(vpath, "a");
    if (!f) return -1;
    if (ftell(f) == 0) fputs("[versions]\n", f);
    fprintf(f, "\"%s\" = \"ok\"\n", version);
    fclose(f);

    char ver_dir[1024];
    snprintf(ver_dir, sizeof(ver_dir), "%s/%s", dir, version);
    ppm_path_mkdirs(ver_dir, 0755);
    char mpath[1024];
    snprintf(mpath, sizeof(mpath), "%s/manifest.toml", ver_dir);

    char body[8192];
    snprintf(body, sizeof(body),
        "[package]\n"
        "name    = \"%s\"\n"
        "version = \"%s\"\n"
        "license = \"GPL-2.0-only\"\n"
        "\n"
        "[source]\n"
        "url  = \"%s\"\n"
        "hash = \"sha512:%s\"\n"
        "\n"
        "[build]\n"
        "script = \"%s\"\n"
        "%s\n",
        name, version, src_url, src_hash_hex, script,
        deps_block ? deps_block : "");
    if (write_file(mpath, body) != 0) return -1;
    return sign_manifest(mpath);
}

static int put_binary_pkg(const char *registry_root, const char *name,
                          const char *version, const char *src_url,
                          const char *src_hash_hex) {
    char shard[3] = { name[0], name[1] ? name[1] : '_', '\0' };
    char dir[1024];
    snprintf(dir, sizeof(dir), "%s/packages/%s/%s", registry_root, shard, name);
    ppm_path_mkdirs(dir, 0755);
    char vpath[1024];
    snprintf(vpath, sizeof(vpath), "%s/versions.toml", dir);
    FILE *f = fopen(vpath, "a");
    if (!f) return -1;
    if (ftell(f) == 0) fputs("[versions]\n", f);
    fprintf(f, "\"%s\" = \"ok\"\n", version);
    fclose(f);
    char ver_dir[1024];
    snprintf(ver_dir, sizeof(ver_dir), "%s/%s", dir, version);
    ppm_path_mkdirs(ver_dir, 0755);
    char mpath[1024];
    snprintf(mpath, sizeof(mpath), "%s/manifest.toml", ver_dir);
    char body[4096];
    snprintf(body, sizeof(body),
        "[package]\nname = \"%s\"\nversion = \"%s\"\nlicense = \"GPL-2.0-only\"\n\n"
        "[source]\nurl = \"%s\"\nhash = \"sha512:%s\"\n",
        name, version, src_url, src_hash_hex);
    if (write_file(mpath, body) != 0) return -1;
    return sign_manifest(mpath);
}

static int git_init_and_commit(const char *path) {
    git_libgit2_init();
    git_repository *repo = NULL;
    if (git_repository_init(&repo, path, 0) != 0) return -1;
    git_index *idx = NULL;
    if (git_repository_index(&idx, repo) != 0) goto fail;
    if (git_index_add_all(idx, NULL, 0, NULL, NULL) != 0) goto fail2;
    if (git_index_write(idx) != 0) goto fail2;
    git_oid tree_oid, commit_oid;
    if (git_index_write_tree(&tree_oid, idx) != 0) goto fail2;
    git_tree *tree = NULL;
    if (git_tree_lookup(&tree, repo, &tree_oid) != 0) goto fail2;
    git_signature *sig = NULL;
    if (git_signature_now(&sig, "ppm-test", "test@invalid") != 0) goto fail3;
    if (git_commit_create_v(&commit_oid, repo, "HEAD", sig, sig, NULL,
                            "initial", tree, 0) != 0) goto fail4;
    git_signature_free(sig);
    git_tree_free(tree);
    git_index_free(idx);
    git_repository_free(repo);
    return 0;
fail4: git_signature_free(sig);
fail3: git_tree_free(tree);
fail2: git_index_free(idx);
fail:  git_repository_free(repo);
    return -1;
}

int main(void) {
    CHECK(ppm_crypto_init() == PPM_OK);
    CHECK(crypto_sign_ed25519_keypair(g_pk, g_sk) == 0);

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

    /* Build a shared dummy source tarball; both packages reference it. */
    char *src_tar;
    CHECK(ppm_path_join(base, "src.tar.gz", &src_tar) == PPM_OK);
    CHECK(build_dummy_tarball(src_tar) == 0);
    char src_hash[PPM_SHA512_HEX_BUFSZ];
    CHECK(compute_hash(src_tar, src_hash) == 0);
    char src_url[1024];
    snprintf(src_url, sizeof(src_url), "file://%s", src_tar);

    /* Registry. */
    char *reg_root;
    CHECK(ppm_path_join(base, "registry", &reg_root) == PPM_OK);
    ppm_path_mkdirs(reg_root, 0755);

    /* Publish the fixture's signer public key so every signed manifest
     * passes the registry's hard signature enforcement (invariant #6). */
    CHECK(write_signers(reg_root) == 0);

    /*
     * Both scripts use only shell built-ins (printf, read, echo) so
     * the hermetic build (empty PATH) can run them. No /bin/cat or
     * /bin/printf — we cannot assume those exist.
     */

    /* bootstrap-tools is a binary seed in this fixture. Its source hash is
     * the dummy tarball hash, and we will provide a fake store path with
     * bin/sh for ppm_build_run. */
    CHECK(put_binary_pkg(reg_root, "bootstrap-tools", "0.1.0",
                         src_url, src_hash) == 0);

    /* libfoo: writes 'FOO' to $out/data. It needs bootstrap-tools for sh. */
    CHECK(put_pkg(reg_root, "libfoo", "1.0.0", src_url, src_hash,
                  "\n[build-dependencies]\nbootstrap-tools = \"*\"\n",
                  "printf FOO > \\\"$out\\\"/data") == 0);

    /* libbar: depends on libfoo; reads $dep_libfoo/data into $out/data
     * via a shell-builtin read loop instead of /bin/cat. */
    CHECK(put_pkg(reg_root, "libbar", "1.0.0", src_url, src_hash,
                  "\n[dependencies]\nlibfoo = \"*\"\n"
                  "\n[build-dependencies]\nbootstrap-tools = \"*\"\n",
                  "printf got= > \\\"$out\\\"/data; "
                  "while IFS= read -r l || [ -n \\\"$l\\\" ]; "
                  "do printf %s \\\"$l\\\"; done "
                  "< \\\"$dep_libfoo\\\"/data >> \\\"$out\\\"/data") == 0);

    CHECK(git_init_and_commit(reg_root) == 0);

    /* Resolve from a root that pulls in libbar. */
    char *root_toml;
    CHECK(ppm_path_join(base, "ppm.toml", &root_toml) == PPM_OK);
    write_file(root_toml,
        "[package]\nname = \"w\"\nversion = \"0.0.0\"\n"
        "[dependencies]\nlibbar = \"*\"\n");
    ppm_manifest_t *root = NULL;
    CHECK(ppm_manifest_load(root_toml, &root) == PPM_OK);

    ppm_registry_t *reg = NULL;
    CHECK(ppm_registry_open(reg_root, &reg) == PPM_OK);

    ppm_lockfile_t *lock = NULL;
    char *report = NULL;
    ppm_resolver_opts_t opts = {0};
    CHECK(ppm_resolve(root, reg, &opts, &lock, &report) == PPM_OK);
    CHECK(lock->entries_len == 3);

    /* Manually invoke ppm_build_run in topo order, mimicking install. */
    char *store_root;
    CHECK(ppm_path_join(base, "store", &store_root) == PPM_OK);
    ppm_store_t *s = NULL;
    CHECK(ppm_store_open(store_root, &s) == PPM_OK);

    /* Fake bootstrap-tools store path with bin/sh for the unit test. */
    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);

    ppm_sha512_t boot_hash;
    CHECK(ppm_sha512_from_hex(src_hash, &boot_hash) == PPM_OK);

    /* Find libfoo's manifest, build it. */
    ppm_version_t v100 = { .major = 1, .minor = 0, .patch = 0 };
    ppm_manifest_t *foo_mf = NULL;
    CHECK(ppm_registry_manifest(reg, "libfoo", &v100, &foo_mf) == PPM_OK);
    const char *foo_dep_names[] = { "bootstrap-tools" };
    ppm_sha512_t foo_dep_hashes[] = { boot_hash };
    const char *foo_dep_paths[] = { boot };
    ppm_sha512_t foo_out;
    CHECK(ppm_build_run(s, foo_mf, foo_dep_names, foo_dep_hashes,
                        foo_dep_paths, 1, &foo_out) == PPM_OK);
    CHECK(ppm_store_has(s, &foo_out));

    /* libbar with $dep_libfoo set. */
    ppm_manifest_t *bar_mf = NULL;
    CHECK(ppm_registry_manifest(reg, "libbar", &v100, &bar_mf) == PPM_OK);
    char *foo_path;
    CHECK(ppm_store_path(s, &foo_out, &foo_path) == PPM_OK);
    const char *dep_names[] = { "libfoo", "bootstrap-tools" };
    ppm_sha512_t dep_hashes[] = { foo_out, boot_hash };
    const char *dep_paths[] = { foo_path, boot };
    ppm_sha512_t bar_out;
    CHECK(ppm_build_run(s, bar_mf, dep_names, dep_hashes, dep_paths, 2,
                        &bar_out) == PPM_OK);
    CHECK(ppm_store_has(s, &bar_out));

    /* libbar's script should have written "got=FOO". */
    char *bar_path;
    CHECK(ppm_store_path(s, &bar_out, &bar_path) == PPM_OK);
    char *data_path;
    CHECK(ppm_path_join(bar_path, "data", &data_path) == PPM_OK);
    FILE *f = fopen(data_path, "r");
    CHECK(f != NULL);
    char buf[32] = {0};
    fread(buf, 1, sizeof(buf) - 1, f);
    fclose(f);
    CHECK(strcmp(buf, "got=FOO") == 0);

    /* The resolver's pre-computed lockfile hash must match the build's
     * actual output hash for libbar (deterministic chain). */
    for (size_t i = 0; i < lock->entries_len; i++) {
        if (strcmp(lock->entries[i].name, "libbar") == 0) {
            CHECK(ppm_sha512_equal(&lock->entries[i].hash, &bar_out));
        }
        if (strcmp(lock->entries[i].name, "libfoo") == 0) {
            CHECK(ppm_sha512_equal(&lock->entries[i].hash, &foo_out));
        }
    }

    free(foo_path); free(bar_path); free(data_path);
    ppm_manifest_free(foo_mf);
    ppm_manifest_free(bar_mf);
    ppm_lockfile_free(lock);
    ppm_manifest_free(root);
    ppm_registry_close(reg);
    ppm_store_close(s);
    ppm_path_rmrf(base);
    free(src_tar); free(reg_root); free(root_toml); free(store_root);
    free(boot); free(boot_bin); free(boot_sh);
    git_libgit2_shutdown();

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