// Pinned acceptance for P3.5 — `dist release promote` / `dist release
// rollback` over the persisted store.
//
// Drives the BUILT `build/dist` binary (via `process.run`, like
// publish_persist.sx) through the slice plan's scripted scenario:
//
//   1. Publish release A (1.2.3, channel beta) then B (1.2.4, beta) into
//      one store → beta points at B.
//   2. `release rollback --app --channel beta` → exit 0, JSON
//      `rolled_back` from B to A; db.json's beta points at A; a
//      `channel.rollback` audit event (actor "cli") is recorded.
//   3. `release promote --release <B>` → exit 0, JSON `promoted` with
//      previous_release_id A; beta points at B again; a `channel.promote`
//      audit event by actor "cli" is recorded (distinct from the publish
//      pipeline's "ci" promote events).
//   4. Promoting an UNKNOWN release id → exit 1 + JSON error
//      (`promote.unknown_release`); db.json unchanged (beta still → B).
//   5. Rollback again (B → A), then rollback at the EARLIEST release →
//      exit 1 + `rollback.no_previous`; beta still → A. A failed op never
//      moves the pointer.
#import "modules/std.sx";
#import "modules/std/json.sx";
process :: #import "modules/std/process.sx";
fs   :: #import "modules/std/fs.sx";

STORE :: ".sx-tmp/release_ops";
MDIR  :: ".sx-tmp/release_ops_m";

MANIFEST_A :: "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"beta\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}";
MANIFEST_B :: "{\"app\":\"acme-app\",\"version\":\"1.2.4\",\"channel\":\"beta\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}";

REL_A :: "rel-acme-app-1.2.3";
REL_B :: "rel-acme-app-1.2.4";

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 matching (actor, action) — distinguishes the CLI's
// channel events from the publish pipeline's "ci" ones.
count_actor_action :: (events: Array, actor: string, action: string) -> i64 {
    c : i64 = 0;
    i := 0;
    while i < events.len {
        eo := events.items[i].object;
        if get_str(eo, "actor") == actor and get_str(eo, "action") == action { c += 1; }
        i += 1;
    }
    return c;
}

write_file :: (path: string, body: string) {
    cmd := concat(concat(concat("printf '%s' '", body), "' > "), path);
    process.run(cmd);
}

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

// The beta channel's current_release_id, read fresh from db.json.
beta_pointer :: (scratch: Allocator) -> string {
    dbo := load_db(scratch);
    chans := get_arr(dbo, "channels");
    process.assert(chans.len == 1, "store has exactly one channel");
    return get_str(chans.items[0].object, "current_release_id");
}

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

promote_cmd :: (release_id: string) -> string {
    c := concat("build/dist release promote --app acme-app --channel beta --release ", release_id);
    c = concat(c, concat(" --local-store ", STORE));
    return concat(c, " --json 2>/dev/null");
}

ROLLBACK_CMD :: "build/dist release rollback --app acme-app --channel beta --local-store .sx-tmp/release_ops --json 2>/dev/null";

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

    process.run(concat("rm -rf ", STORE));
    process.run(concat("rm -rf ", MDIR));
    process.run(concat("mkdir -p ", MDIR));
    write_file(path_join(MDIR, "a.json"), MANIFEST_A);
    write_file(path_join(MDIR, "b.json"), MANIFEST_B);

    // ── 1. Publish A then B → beta points at B ──────────────────────
    ra := process.run(publish_cmd(path_join(MDIR, "a.json")));
    process.assert(ra != null and ra!.exit_code == 0, "publish A must exit 0");
    rb := process.run(publish_cmd(path_join(MDIR, "b.json")));
    process.assert(rb != null and rb!.exit_code == 0, "publish B must exit 0");
    process.assert(beta_pointer(xx arena) == REL_B, "after publishes: beta -> B");
    print("  published A then B; beta -> B\n");

    // ── 2. Rollback: beta moves B -> A, audited ──────────────────────
    rr := process.run(ROLLBACK_CMD);
    process.assert(rr != null, "spawn rollback failed");
    process.assert(rr!.exit_code == 0, "rollback must exit 0");
    rv, re := parse(rr!.stdout, xx arena);
    if re { process.assert(false, "rollback stdout must be one JSON object"); return 1; }
    ro := rv.object;
    process.assert(get_str(ro, "status") == "rolled_back", "rollback json status");
    process.assert(get_str(ro, "from_release_id") == REL_B, "rollback json from B");
    process.assert(get_str(get_obj(ro, "to"), "id") == REL_A, "rollback json to A");
    process.assert(beta_pointer(xx arena) == REL_A, "after rollback: beta -> A");
    db2 := load_db(xx arena);
    process.assert(count_actor_action(get_arr(db2, "audit_events"), "cli", "channel.rollback") == 1,
                   "rollback recorded one cli channel.rollback audit event");
    print("  rollback: beta B -> A, audited\n");

    // ── 3. Promote B back: beta -> B, previous is A, audited ─────────
    rp := process.run(promote_cmd(REL_B));
    process.assert(rp != null, "spawn promote failed");
    process.assert(rp!.exit_code == 0, "promote must exit 0");
    pv, pe := parse(rp!.stdout, xx arena);
    if pe { process.assert(false, "promote stdout must be one JSON object"); return 1; }
    po := pv.object;
    process.assert(get_str(po, "status") == "promoted", "promote json status");
    process.assert(get_str(get_obj(po, "release"), "id") == REL_B, "promote json release B");
    process.assert(get_str(po, "previous_release_id") == REL_A, "promote json previous A");
    process.assert(beta_pointer(xx arena) == REL_B, "after promote: beta -> B");
    db3 := load_db(xx arena);
    process.assert(count_actor_action(get_arr(db3, "audit_events"), "cli", "channel.promote") == 1,
                   "promote recorded one cli channel.promote audit event");
    print("  promote: beta -> B (was A), audited\n");

    // ── 4. Promote an unknown release id → exit 1 + JSON error, db
    //       unchanged ─────────────────────────────────────────────────
    rn := process.run(promote_cmd("rel-nope"));
    process.assert(rn != null, "spawn promote-unknown failed");
    process.assert(rn!.exit_code == 1, "promote of unknown release must exit 1");
    nv, ne := parse(rn!.stdout, xx arena);
    if ne { process.assert(false, "promote-unknown stdout must be one JSON object"); return 1; }
    no := nv.object;
    process.assert(get_str(no, "status") == "error", "promote-unknown json status error");
    process.assert(get_str(get_obj(no, "error"), "code") == "promote.unknown_release",
                   "promote-unknown json names the code");
    process.assert(beta_pointer(xx arena) == REL_B, "after failed promote: beta unchanged (-> B)");
    print("  promote unknown release: exit 1 + JSON error, beta unchanged\n");

    // ── 5. Rollback to the earliest, then once more → no_previous ────
    r2 := process.run(ROLLBACK_CMD);
    process.assert(r2 != null and r2!.exit_code == 0, "second rollback must exit 0 (B -> A)");
    process.assert(beta_pointer(xx arena) == REL_A, "after second rollback: beta -> A");

    r3 := process.run(ROLLBACK_CMD);
    process.assert(r3 != null, "spawn third rollback failed");
    process.assert(r3!.exit_code == 1, "rollback at the earliest release must exit 1");
    v3, e3 := parse(r3!.stdout, xx arena);
    if e3 { process.assert(false, "no-previous stdout must be one JSON object"); return 1; }
    o3 := v3.object;
    process.assert(get_str(get_obj(o3, "error"), "code") == "rollback.no_previous",
                   "no-previous json names the code");
    process.assert(beta_pointer(xx arena) == REL_A, "after failed rollback: beta unchanged (-> A)");
    print("  rollback at earliest: exit 1 + no_previous, beta unchanged\n");

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