// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/tests/test_transaction.c
 *
 *  Exercises the atomic transaction layer:
 *    - commit produces a new generation + current symlink
 *    - abort leaves no trace of a partial generation
 *    - one-version rule rejects duplicate stage
 *    - staging a hash not in the store is refused
 *    - recovery rolls back a journal without "ready"
 *    - recovery replays a journal with "ready"
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/transaction.h>
#include <ppm/store.h>
#include <ppm/lockfile.h>
#include <ppm/crypto.h>

#include <archive.h>
#include <archive_entry.h>
#include <errno.h>
#include <fcntl.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 build_tarball(const char *path, const char *entry, const char *body) {
    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, entry);
    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;
}

/* Ingest a freshly-built tarball into the store and return its hash. */
static int ingest_pkg(ppm_store_t *s, const char *base, const char *tag,
                      const char *body, ppm_sha512_t *out_hash) {
    char *tar;
    if (asprintf(&tar, "%s/%s.tar.gz", base, tag) < 0) return -1;
    if (build_tarball(tar, "hello.txt", body) != 0) { free(tar); return -1; }
    if (ppm_sha512_file(tar, out_hash) != PPM_OK) { free(tar); return -1; }
    if (ppm_store_ingest_tarball(s, tar, out_hash) != PPM_OK) { free(tar); return -1; }
    free(tar);
    return 0;
}

static int file_exists(const char *path) {
    struct stat st;
    return lstat(path, &st) == 0;
}

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

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

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

    /* --- happy path: stage two packages, commit, observe symlink ------- */
    ppm_sha512_t h1, h2;
    CHECK(ingest_pkg(s, base, "foo", "foo-body", &h1) == 0);
    CHECK(ingest_pkg(s, base, "bar", "bar-body", &h2) == 0);

    ppm_txn_t *t = NULL;
    CHECK(ppm_txn_begin(s, "default", &t) == PPM_OK);

    ppm_lock_entry_t e1 = {
        .name = "foo",
        .version = { .major = 1, .minor = 0, .patch = 0 },
        .hash = h1,
        .source_url = NULL, .deps = NULL,
    };
    ppm_lock_entry_t e2 = {
        .name = "bar",
        .version = { .major = 2, .minor = 3, .patch = 4 },
        .hash = h2,
        .source_url = NULL, .deps = NULL,
    };
    CHECK(ppm_txn_stage(t, &e1) == PPM_OK);
    CHECK(ppm_txn_stage(t, &e2) == PPM_OK);

    /* One-version rule: same name twice → CONFLICT. */
    CHECK(ppm_txn_stage(t, &e1) == PPM_ERR_CONFLICT);

    CHECK(ppm_txn_commit(t) == PPM_OK);
    ppm_txn_abort(t); /* now a no-op cleanup; abort after commit is safe */

    /* current symlink exists and resolves into gen-1 */
    char *current_link;
    CHECK(ppm_path_join(store_root, "profiles/default/current", &current_link) == PPM_OK);
    CHECK(file_exists(current_link));
    char target[256];
    ssize_t n = readlink(current_link, target, sizeof(target) - 1);
    CHECK(n > 0);
    target[n] = '\0';
    CHECK(strcmp(target, "gen-1") == 0);
    free(current_link);

    /* ppm_txn_current returns the resolved path. */
    char *resolved;
    CHECK(ppm_txn_current(s, "default", &resolved) == PPM_OK);
    CHECK(resolved && strstr(resolved, "gen-1") != NULL);
    /* manifest.toml is present and mentions both packages. */
    char *manifest_path;
    CHECK(ppm_path_join(resolved, "manifest.toml", &manifest_path) == PPM_OK);
    FILE *mf = fopen(manifest_path, "r");
    CHECK(mf != NULL);
    char buf[2048] = {0};
    size_t nread = fread(buf, 1, sizeof(buf) - 1, mf);
    fclose(mf);
    CHECK(nread > 0);
    CHECK(strstr(buf, "name    = \"foo\"") != NULL);
    CHECK(strstr(buf, "name    = \"bar\"") != NULL);
    free(manifest_path);
    free(resolved);

    /* --- second commit makes gen-2 ------------------------------------- */
    CHECK(ppm_txn_begin(s, "default", &t) == PPM_OK);
    CHECK(ppm_txn_stage(t, &e1) == PPM_OK);
    CHECK(ppm_txn_commit(t) == PPM_OK);
    ppm_txn_abort(t);

    CHECK(ppm_txn_current(s, "default", &resolved) == PPM_OK);
    CHECK(resolved && strstr(resolved, "gen-2") != NULL);
    free(resolved);

    /* --- abort path: pending generation must be removed ----------------- */
    CHECK(ppm_txn_begin(s, "default", &t) == PPM_OK);
    CHECK(ppm_txn_stage(t, &e2) == PPM_OK);
    /* Grab the gen path before we abort. */
    char *pending_gen;
    CHECK(ppm_path_join(store_root, "profiles/default/gen-3", &pending_gen) == PPM_OK);
    CHECK(file_exists(pending_gen)); /* exists during the txn */
    ppm_txn_abort(t);
    CHECK(!file_exists(pending_gen)); /* gone after abort */
    free(pending_gen);

    /* current still points at gen-2 (last successful commit). */
    CHECK(ppm_txn_current(s, "default", &resolved) == PPM_OK);
    CHECK(resolved && strstr(resolved, "gen-2") != NULL);
    free(resolved);

    /* --- unknown hash refused ------------------------------------------- */
    ppm_lock_entry_t bogus = e1;
    bogus.name = "missing";
    bogus.hash.bytes[0] ^= 0x55; /* perturb to a hash not in store */
    CHECK(ppm_txn_begin(s, "default", &t) == PPM_OK);
    CHECK(ppm_txn_stage(t, &bogus) == PPM_ERR_NOT_FOUND);
    ppm_txn_abort(t);

    /* --- crash recovery: hand-written journal without "ready" --------- */
    /* Create a fake half-written transaction: a gen-99 dir + a journal
     * that lacks the "ready" record. Recovery must remove gen-99 and
     * delete the journal. */
    char *fake_gen;
    CHECK(ppm_path_join(store_root, "profiles/default/gen-99", &fake_gen) == PPM_OK);
    CHECK(mkdir(fake_gen, 0755) == 0);

    char *journal_path;
    CHECK(ppm_path_join(store_root, "tmp/txn-deadbeef.journal", &journal_path) == PPM_OK);
    char *pdir;
    CHECK(ppm_path_join(store_root, "profiles/default", &pdir) == PPM_OK);

    FILE *jf = fopen(journal_path, "w");
    CHECK(jf != NULL);
    fprintf(jf, "v1\n");
    fprintf(jf, "profile\t%s\n", pdir);
    fprintf(jf, "gen\t%s\n", fake_gen);
    fprintf(jf, "prev\tgen-2\n");
    fclose(jf);

    CHECK(file_exists(fake_gen));
    CHECK(file_exists(journal_path));
    CHECK(ppm_txn_recover(s, "default") == PPM_OK);
    CHECK(!file_exists(fake_gen));      /* rolled back */
    CHECK(!file_exists(journal_path));  /* journal removed */

    /* --- crash recovery: forward replay (journal with "ready") -------- */
    char *fake_gen2;
    CHECK(ppm_path_join(store_root, "profiles/default/gen-100", &fake_gen2) == PPM_OK);
    CHECK(mkdir(fake_gen2, 0755) == 0);
    /* Drop a manifest so the generation looks plausible. */
    char *m2;
    CHECK(ppm_path_join(fake_gen2, "manifest.toml", &m2) == PPM_OK);
    FILE *mf2 = fopen(m2, "w"); fputs("schema = \"1\"\n", mf2); fclose(mf2);
    free(m2);

    char *journal_path2;
    CHECK(ppm_path_join(store_root, "tmp/txn-cafebabe.journal", &journal_path2) == PPM_OK);
    FILE *jf2 = fopen(journal_path2, "w");
    CHECK(jf2 != NULL);
    fprintf(jf2, "v1\n");
    fprintf(jf2, "profile\t%s\n", pdir);
    fprintf(jf2, "gen\t%s\n", fake_gen2);
    fprintf(jf2, "prev\tgen-2\n");
    fprintf(jf2, "ready\n");
    fclose(jf2);

    CHECK(ppm_txn_recover(s, "default") == PPM_OK);
    CHECK(file_exists(fake_gen2));         /* generation kept */
    CHECK(!file_exists(journal_path2));    /* journal cleaned up */
    /* current now points at gen-100 */
    CHECK(ppm_txn_current(s, "default", &resolved) == PPM_OK);
    CHECK(resolved && strstr(resolved, "gen-100") != NULL);
    free(resolved);

    free(fake_gen); free(fake_gen2);
    free(journal_path); free(journal_path2);
    free(pdir);

    ppm_store_close(s);
    ppm_path_rmrf(base);
    free(store_root);

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