// Regression for P3.4a-001 — `dist ci publish` must LOAD an existing
// `<store>/db.json` before publishing, so separate CLI invocations SHARE
// state through the store (not start from an empty Repo and clobber it).
//
// Drives the BUILT `build/dist` binary (via `process.run`, like
// publish_happy.sx) twice into ONE store and asserts cross-invocation
// persistence:
//
//   1. Publish version A (1.2.3) into a fresh store → db.json has the release.
//   2. Publish a DIFFERENT version B (1.2.4) of the SAME app into the SAME
//      store → exit 0, and db.json now records BOTH releases under ONE app
//      (the app is FOUND, not duplicated); the channel points at the latest
//      release B; both content-addressed objects exist.
//   3. Re-publishing the SAME release id (A again) into the same store FAILS
//      (exit != 0 — the P2.3 integrity transaction rejects the duplicate
//      release id) and leaves db.json UNCHANGED (still two releases).
//
// FAIL-BEFORE / PASS-AFTER: against the pre-fix publish (which never reads
// db.json and so begins every invocation from an EMPTY Repo) step 2 CLOBBERS
// A — db.json ends with one release, not two — and step 3 "succeeds" (exit 0)
// and overwrites. Both assertions fail. After the load-then-merge fix they
// pass. Fresh store per run.
#import "modules/std.sx";
#import "modules/std/json.sx";
process :: #import "modules/std/process.sx";
fs   :: #import "modules/std/fs.sx";

STORE  :: ".sx-tmp/publish_persist";
MDIR   :: ".sx-tmp/publish_persist_m";
A_PATH :: ".sx-tmp/publish_persist_m/a.json";
B_PATH :: ".sx-tmp/publish_persist_m/b.json";

// Two manifests for the SAME app/channel/fixtures, differing only in version.
// Artifact paths resolve relative to the manifest's own directory (MDIR), so
// `../../examples/fixtures/...` reaches the repo's committed fixtures.
MANIFEST_A :: "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"},{\"platform\":\"ios\",\"path\":\"../../examples/fixtures/acme-1.2.3-ios.ipa\"}]}";
MANIFEST_B :: "{\"app\":\"acme-app\",\"version\":\"1.2.4\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"},{\"platform\":\"ios\",\"path\":\"../../examples/fixtures/acme-1.2.3-ios.ipa\"}]}";

// Fetch a member value by key, asserting presence (the publish/db output is a
// fixed shape, so an absent key is a hard failure).
get :: (o: Object, key: string) -> Value {
    i := 0;
    while i < o.len {
        if o.items[i].key == key { return o.items[i].val; }
        i += 1;
    }
    process.assert(false, concat("missing json key: ", key));
    dummy : Value = .null_;
    return dummy;
}
get_str :: (o: Object, key: string) -> string { return get(o, key).str; }
get_obj :: (o: Object, key: string) -> Object { return get(o, key).object; }
get_arr :: (o: Object, key: string) -> Array  { return get(o, key).array; }

// Count audit events whose "action" equals `action`.
count_action :: (events: Array, action: string) -> i64 {
    c : i64 = 0;
    i := 0;
    while i < events.len {
        eo := events.items[i].object;
        if get_str(eo, "action") == action { c += 1; }
        i += 1;
    }
    return c;
}

// True iff `releases` (a db.json array) contains a release with id `id`.
has_release :: (releases: Array, id: string) -> bool {
    i := 0;
    while i < releases.len {
        if get_str(releases.items[i].object, "id") == id { return true; }
        i += 1;
    }
    return false;
}

// `build/dist ci publish` for `mpath` into the shared store, JSON mode,
// stderr suppressed so stdout stays pure for the parser.
publish_cmd :: (mpath: string) -> string {
    c := concat("build/dist ci publish --manifest ", mpath);
    c = concat(c, concat(" --local-store ", STORE));
    return concat(c, " --json 2>/dev/null");
}

// Write `body` to `path` via the shell (single-quoted, so the JSON's double
// quotes pass through literally).
write_file :: (path: string, body: string) {
    cmd := concat(concat(concat("printf '%s' '", body), "' > "), path);
    process.run(cmd);
}

// Parse `<STORE>/db.json` into its root object (re-read fresh each call).
load_db :: (scratch: Allocator) -> Object {
    db_bytes := fs.read_file(path_join(STORE, "db.json"));
    process.assert(db_bytes != null, "db.json must exist under the store");
    dv, de := parse(db_bytes!, scratch);
    if de { process.assert(false, "db.json must be valid JSON"); dummy : Object = .{}; return dummy; }
    return dv.object;
}

main :: () -> i32 {
    gpa := GPA.init();
    arena := Arena.init(xx gpa, 1 << 20);
    defer arena.deinit();

    // Fresh store + manifest dir, even after a crashed prior run.
    process.run(concat("rm -rf ", STORE));
    process.run(concat("rm -rf ", MDIR));
    process.run(concat("mkdir -p ", MDIR));
    write_file(A_PATH, MANIFEST_A);
    write_file(B_PATH, MANIFEST_B);

    rel_a := "rel-acme-app-1.2.3";
    rel_b := "rel-acme-app-1.2.4";

    // ── 1. Publish version A into the fresh store ───────────────────────
    ra := process.run(publish_cmd(A_PATH));
    process.assert(ra != null, "spawn publish A failed");
    res_a := ra!;
    process.assert(res_a.exit_code == 0, "publish A must exit 0");
    va, ea := parse(res_a.stdout, xx arena);
    if ea { process.assert(false, "publish A stdout must be one JSON object"); return 1; }
    process.assert(get_str(get_obj(va.object, "release"), "id") == rel_a, "A release id");

    db1 := load_db(xx arena);
    process.assert(get_arr(db1, "releases").len == 1, "after A: db has one release");
    process.assert(get_arr(db1, "apps").len == 1, "after A: db has one app");
    print("  A published; db has 1 release\n");

    // ── 2. Publish a DIFFERENT version B into the SAME store ────────────
    rb := process.run(publish_cmd(B_PATH));
    process.assert(rb != null, "spawn publish B failed");
    res_b := rb!;
    process.assert(res_b.exit_code == 0, "publish B must exit 0 (accumulates)");
    vb, eb := parse(res_b.stdout, xx arena);
    if eb { process.assert(false, "publish B stdout must be one JSON object"); return 1; }
    process.assert(get_str(get_obj(vb.object, "release"), "id") == rel_b, "B release id");

    db2 := load_db(xx arena);

    // The crux: BOTH releases under ONE app (app found, not duplicated).
    db2_rels := get_arr(db2, "releases");
    process.assert(db2_rels.len == 2, "after B: db records BOTH releases (no clobber)");
    process.assert(has_release(db2_rels, rel_a), "after B: release A still present");
    process.assert(has_release(db2_rels, rel_b), "after B: release B present");
    process.assert(get_arr(db2, "apps").len == 1, "after B: still ONE app (found, not duplicated)");

    // Four artifacts (two per release); channel promoted to the latest (B).
    process.assert(get_arr(db2, "artifacts").len == 4, "after B: four artifacts (two per release)");
    db2_chans := get_arr(db2, "channels");
    process.assert(db2_chans.len == 1, "after B: one channel");
    process.assert(get_str(db2_chans.items[0].object, "current_release_id") == rel_b,
                   "after B: channel promoted to the latest release");

    // Each artifact's content-addressed object exists on disk.
    db2_arts := get_arr(db2, "artifacts");
    k := 0;
    while k < db2_arts.len {
        sha := get_str(db2_arts.items[k].object, "sha256");
        process.assert(fs.exists(path_join(STORE, concat("objects/", sha))),
                       "after B: object exists at objects/<sha256>");
        k += 1;
    }

    // Audit accumulated across both publishes (>= 2 publish events).
    process.assert(count_action(get_arr(db2, "audit_events"), "release.publish") == 2,
                   "after B: one publish event per release");
    print("  B accumulated; db has 2 releases under 1 app\n");

    // ── 3. Re-publish the SAME release id (A) → duplicate is rejected ───
    rdup := process.run(publish_cmd(A_PATH));
    process.assert(rdup != null, "spawn duplicate publish failed");
    res_dup := rdup!;
    process.assert(res_dup.exit_code != 0, "re-publishing the same release id must FAIL (duplicate)");

    db3 := load_db(xx arena);
    process.assert(get_arr(db3, "releases").len == 2, "after duplicate: db UNCHANGED (still two releases)");
    process.assert(get_arr(db3, "apps").len == 1, "after duplicate: still one app");
    print("  duplicate release id rejected; db unchanged\n");

    process.run(concat("rm -rf ", STORE));
    process.run(concat("rm -rf ", MDIR));
    print("publish_persist: ALL CASES PASS\n");
    return 0;
}
