// Acceptance for P3.4a — the local `dist ci publish` SUCCESS pipeline.
//
// Drives the BUILT `build/dist` binary (via `process.run`, like
// cli_dispatch.sx) on `examples/dist.json` into a FRESH `.sx-tmp/` store and
// asserts the end-to-end publish:
//
//   1. exit 0; stdout in `--json` mode is a SINGLE valid JSON object
//      (parsed via std.json, no trailing junk).
//   2. the emitted release id / artifact ids / sha256 / local URLs MATCH the
//      store: each `<store>/objects/<sha256>` exists and re-hashes (std.hash)
//      to its own key, and each url is `file://<abs-store>/objects/<sha256>`.
//   3. `<store>/db.json` (re-parsed via std.json) records the release, both
//      artifacts (storage_key == sha256, validation_status valid), the
//      channel pointer (current_release_id == the release), and an audit
//      event per upload/publish/promotion.
//
// This FAILS against the pre-P3.4a stub (which rejects --manifest /
// --local-store as unknown flags, exiting 64, and writes no store) and
// PASSES against the real pipeline. 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";
hash :: #import "modules/std/hash.sx";

cstd     :: #library "c";
c_getcwd :: (buf: [*]u8, size: usize) -> *u8 #foreign cstd "getcwd";

STORE_REL :: ".sx-tmp/publish_happy";

// Process cwd, so the absolute store path / download URLs can be rebuilt
// exactly as the publish does.
cwd :: () -> string {
    buf : [4096]u8 = ---;
    r := c_getcwd(@buf[0], 4096);
    process.assert(cast(i64) r != 0, "getcwd must succeed");
    n := 0;
    while buf[n] != 0 { n += 1; }
    return substr(string.{ ptr = @buf[0], len = n }, 0, n);
}

// Fetch a member value by key, asserting presence (the publish 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; }

// True iff the object at `path` re-hashes (std.hash) to `want` (its key).
rehashes_to :: (path: string, want: string) -> bool {
    maybe := hash.sha256_file(path);
    if maybe == null { return false; }
    d := maybe!;
    view := string.{ ptr = @d[0], len = 64 };
    return view == want;
}

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

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

    base := cwd();
    store_abs := path_join(base, STORE_REL);

    // Fresh store, even after a crashed prior run.
    process.run(concat("rm -rf ", STORE_REL));

    // ── 1. Run the real publish; stdout must be one JSON object, exit 0 ──
    r := process.run("build/dist ci publish --manifest examples/dist.json --local-store .sx-tmp/publish_happy --json 2>/dev/null");
    process.assert(r != null, "spawn build/dist ci publish failed");
    res := r!;
    process.assert(res.exit_code == 0, "publish must exit 0 (EX_OK)");

    v, e := parse(res.stdout, xx arena);
    if e { process.assert(false, "stdout must be a single valid JSON object (parse failed / trailing junk)"); return 1; }
    root := v.object;

    process.assert(get_str(root, "status") == "published", "status must be published");

    rel := get_obj(root, "release");
    rel_id := get_str(rel, "id");
    process.assert(rel_id == "rel-acme-app-1.2.3", "release id derived from slug+version");
    process.assert(get_str(rel, "version") == "1.2.3", "release version");
    process.assert(get_str(rel, "channel") == "stable", "release channel");
    print("  release {} published\n", rel_id);

    // ── 2. Each artifact: object exists at objects/<sha256>, re-hashes to
    //       its key, and its url is file://<abs-store>/objects/<sha256> ──
    arts := get_arr(root, "artifacts");
    process.assert(arts.len == 2, "two artifacts published");
    i := 0;
    while i < arts.len {
        ao := arts.items[i].object;
        sha := get_str(ao, "sha256");
        url := get_str(ao, "url");
        process.assert(sha.len == 64, "sha256 is a 64-char digest");

        obj_path := path_join(STORE_REL, concat("objects/", sha));
        process.assert(fs.exists(obj_path), "object exists at objects/<sha256>");
        process.assert(rehashes_to(obj_path, sha), "stored object re-hashes to its key");

        want_url := concat(concat(concat("file://", store_abs), "/objects/"), sha);
        process.assert(url == want_url, "url is file://<abs-store>/objects/<sha256>");

        process.assert(get_str(ao, "id").len > 0, "artifact id present");
        i += 1;
    }
    print("  {} artifacts stored + re-hash to their keys\n", arts.len);

    // ── 3. db.json records the published aggregate ──────────────────────
    db_bytes := fs.read_file(path_join(STORE_REL, "db.json"));
    process.assert(db_bytes != null, "db.json must exist under the store");
    dv, de := parse(db_bytes!, xx arena);
    if de { process.assert(false, "db.json must be valid JSON"); return 1; }
    dbo := dv.object;

    db_rels := get_arr(dbo, "releases");
    process.assert(db_rels.len == 1, "db: one release");
    process.assert(get_str(db_rels.items[0].object, "id") == rel_id, "db: release id matches");

    db_arts := get_arr(dbo, "artifacts");
    process.assert(db_arts.len == 2, "db: two artifacts");
    j := 0;
    while j < db_arts.len {
        dao := db_arts.items[j].object;
        process.assert(get_str(dao, "storage_key") == get_str(dao, "sha256"),
                       "db: storage_key == sha256 (content-addressed)");
        process.assert(get_str(dao, "validation_status") == "valid",
                       "db: artifact validation passed");
        process.assert(get_str(dao, "release_id") == rel_id, "db: artifact belongs to the release");
        j += 1;
    }

    db_chans := get_arr(dbo, "channels");
    process.assert(db_chans.len == 1, "db: one channel");
    ch := db_chans.items[0].object;
    process.assert(get_str(ch, "name") == "stable", "db: channel name");
    process.assert(get_str(ch, "current_release_id") == rel_id, "db: channel points at the release");

    db_events := get_arr(dbo, "audit_events");
    process.assert(count_action(db_events, "artifact.upload") == 2, "db: one upload event per artifact");
    process.assert(count_action(db_events, "release.publish") == 1, "db: one publish event");
    process.assert(count_action(db_events, "channel.promote") == 1, "db: one promotion event");
    print("  db.json records release/artifacts/channel + {} audit events\n", db_events.len);

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