// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/tests/test_resolver.c
 *
 *  End-to-end resolver test against a real git-backed registry built
 *  on the fly in a tmpdir. Verifies:
 *    - simple resolution picks the highest version
 *    - SemVer constraints are honoured
 *    - transitive deps land in the lockfile
 *    - unsatisfiable constraints produce a human-readable report
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/registry.h>
#include <ppm/resolver.h>
#include <ppm/manifest.h>
#include <ppm/lockfile.h>
#include <ppm/crypto.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];

/* Write 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;
}

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

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

    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);

    /* A valid SHA-512 (of empty string) so PUBLISHED validation passes. */
    char body[4096];
    snprintf(body, sizeof(body),
        "[package]\n"
        "name    = \"%s\"\n"
        "version = \"%s\"\n"
        "license = \"GPL-2.0-only\"\n"
        "\n"
        "[source]\n"
        "url  = \"https://example.invalid/%s-%s.tar.zst\"\n"
        "hash = \"sha512:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
                       "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e\"\n"
        "%s\n",
        name, version, name, version, deps_block ? deps_block : "");
    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 fail_idx;
    if (git_index_write(idx) != 0) goto fail_idx;

    git_oid tree_oid, commit_oid;
    if (git_index_write_tree(&tree_oid, idx) != 0) goto fail_idx;
    git_tree *tree = NULL;
    if (git_tree_lookup(&tree, repo, &tree_oid) != 0) goto fail_idx;

    git_signature *sig = NULL;
    if (git_signature_now(&sig, "ppm-test", "test@invalid") != 0) goto fail_tree;
    if (git_commit_create_v(&commit_oid, repo, "HEAD", sig, sig, NULL,
                            "initial", tree, 0) != 0) goto fail_sig;

    git_signature_free(sig);
    git_tree_free(tree);
    git_index_free(idx);
    git_repository_free(repo);
    return 0;

fail_sig:  git_signature_free(sig);
fail_tree: git_tree_free(tree);
fail_idx:  git_index_free(idx);
fail:      git_repository_free(repo);
    return -1;
}

static ppm_manifest_t *load_root(const char *path) {
    ppm_manifest_t *m = NULL;
    if (ppm_manifest_load(path, &m) != PPM_OK) return NULL;
    return m;
}

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

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

    /* Generate the fixture's signing key and publish its public key so the
     * registry's hard signature enforcement (invariant #6) is satisfied. */
    CHECK(ppm_crypto_init() == PPM_OK);
    CHECK(crypto_sign_ed25519_keypair(g_pk, g_sk) == 0);
    CHECK(write_signers(reg_root) == 0);

    /* Build a fixture:
     *   foo 1.0.0  (no deps)
     *   foo 1.2.0  (no deps)
     *   foo 2.0.0  (no deps)
     *   bar 0.5.0  depends on foo ^1.0
     *   baz 0.1.0  depends on bar ^0.5 and foo >=1.2
     */
    CHECK(put_package(reg_root, "foo", "1.0.0", NULL) == 0);
    CHECK(put_package(reg_root, "foo", "1.2.0", NULL) == 0);
    CHECK(put_package(reg_root, "foo", "2.0.0", NULL) == 0);
    CHECK(put_package(reg_root, "bar", "0.5.0",
                      "\n[dependencies]\nfoo = \"^1.0\"\n") == 0);
    CHECK(put_package(reg_root, "baz", "0.1.0",
                      "\n[dependencies]\nbar = \"^0.5\"\nfoo = \">=1.2\"\n") == 0);

    CHECK(git_init_and_commit(reg_root) == 0);

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

    /* --- simple: install foo, expect 2.0.0 (newest matching '*') ------ */
    char *root1_path;
    CHECK(ppm_path_join(base, "ppm.toml", &root1_path) == PPM_OK);
    write_file(root1_path,
        "[package]\nname = \"w\"\nversion = \"0.0.0\"\n"
        "[dependencies]\nfoo = \"*\"\n");
    ppm_manifest_t *root = load_root(root1_path);
    CHECK(root != NULL);

    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 == 1);
    CHECK(strcmp(lock->entries[0].name, "foo") == 0);
    CHECK(lock->entries[0].version.major == 2);
    ppm_lockfile_free(lock);
    ppm_manifest_free(root);

    /* --- constrained: foo ^1.0 → expect 1.2.0 -------------------------- */
    write_file(root1_path,
        "[package]\nname = \"w\"\nversion = \"0.0.0\"\n"
        "[dependencies]\nfoo = \"^1.0\"\n");
    root = load_root(root1_path);
    CHECK(ppm_resolve(root, reg, &opts, &lock, &report) == PPM_OK);
    CHECK(lock->entries_len == 1);
    CHECK(lock->entries[0].version.major == 1);
    CHECK(lock->entries[0].version.minor == 2);
    ppm_lockfile_free(lock);
    ppm_manifest_free(root);

    /* --- transitive: baz pulls bar + foo, with mutual constraints ----- */
    write_file(root1_path,
        "[package]\nname = \"w\"\nversion = \"0.0.0\"\n"
        "[dependencies]\nbaz = \"^0.1\"\n");
    root = load_root(root1_path);
    CHECK(ppm_resolve(root, reg, &opts, &lock, &report) == PPM_OK);
    CHECK(lock->entries_len == 3);
    /* Lockfile is sorted by name: bar, baz, foo */
    CHECK(strcmp(lock->entries[0].name, "bar") == 0);
    CHECK(strcmp(lock->entries[1].name, "baz") == 0);
    CHECK(strcmp(lock->entries[2].name, "foo") == 0);
    /* foo must satisfy both ^1.0 (from bar) and >=1.2 (from baz) → 1.2.0 */
    CHECK(lock->entries[2].version.major == 1);
    CHECK(lock->entries[2].version.minor == 2);
    ppm_lockfile_free(lock);
    ppm_manifest_free(root);

    /* --- unsatisfiable: foo ^3 → no such version, expect report ------- */
    write_file(root1_path,
        "[package]\nname = \"w\"\nversion = \"0.0.0\"\n"
        "[dependencies]\nfoo = \"^3.0\"\n");
    root = load_root(root1_path);
    CHECK(ppm_resolve(root, reg, &opts, &lock, &report) == PPM_ERR_UNSAT);
    CHECK(report != NULL);
    CHECK(strstr(report, "foo") != NULL);
    free(report); report = NULL;
    ppm_manifest_free(root);

    /* --- negative: a TAMPERED manifest must be refused (sig no longer
     *     covers the bytes) → PPM_ERR_SIG_INVALID. ---------------------- */
    char foo_mpath[1024];
    snprintf(foo_mpath, sizeof(foo_mpath),
             "%s/packages/fo/foo/2.0.0/manifest.toml", reg_root);
    FILE *tf = fopen(foo_mpath, "a");
    CHECK(tf != NULL);
    fputs("# tampered after signing\n", tf);
    fclose(tf);
    ppm_version_t v200 = { .major = 2, .minor = 0, .patch = 0 };
    ppm_manifest_t *tampered = NULL;
    CHECK(ppm_registry_manifest(reg, "foo", &v200, &tampered)
          == PPM_ERR_SIG_INVALID);
    CHECK(tampered == NULL);

    /* --- negative: a MISSING signature must be refused too. ----------- */
    char bar_sig[1024];
    snprintf(bar_sig, sizeof(bar_sig),
             "%s/packages/ba/bar/0.5.0/manifest.toml.sig", reg_root);
    CHECK(unlink(bar_sig) == 0);
    ppm_version_t v050 = { .major = 0, .minor = 5, .patch = 0 };
    ppm_manifest_t *unsigned_mf = NULL;
    CHECK(ppm_registry_manifest(reg, "bar", &v050, &unsigned_mf)
          == PPM_ERR_SIG_INVALID);
    CHECK(unsigned_mf == NULL);

    ppm_registry_close(reg);
    ppm_path_rmrf(base);
    free(reg_root); free(root1_path);
    git_libgit2_shutdown();

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