// =====================================================================
// cli_dispatch.sx — acceptance for the `dist` CLI entrypoint (P3.1).
//
// Drives the BUILT `build/dist` binary through `process.run` (the binary,
// not `sx run src/dist.sx` — only a real executable sees its own argv;
// under `sx run` the process argv is the interpreter's). Asserts the
// std.cli exit-code contract and the `--json` stdout-purity contract:
//
//   1. no args            → human help/usage on STDERR + EX_USAGE (64).
//   2. unknown command    → human error on STDERR + EX_USAGE (64).
//   3. a fully-flagged `release promote --json` against a store that was
//      never published → exit 1 (command failed, NOT usage), and STDOUT is
//      a SINGLE valid JSON error object (parses via std.json with no
//      trailing junk; status "error", code "store.load"); the human
//      sentence is on STDERR, never stdout. (Success-path json output is
//      exercised by publish_happy.sx / release_ops.sx.)
//   4. `--help`           → lists the `ci` / `release` groups, exits 0.
//   5. `ci publish --json` / `release promote --json` with NO required
//      flags → EX_USAGE (64), error on stderr (the required-flag contract).
//
// `make test` depends on `build`, so `build/dist` exists before this runs;
// the relative path resolves from the repo root (the `make test` cwd).
// =====================================================================

#import "modules/std.sx";
proc :: #import "modules/std/process.sx";
json :: #import "modules/std/json.sx";

// True iff `needle` occurs in `hay`. Plain scan — the captured streams are
// small, and the test only needs presence, not position.
contains :: (hay: string, needle: string) -> bool {
    if needle.len == 0 { return true; }
    if needle.len > hay.len { return false; }
    i := 0;
    while i + needle.len <= hay.len {
        j := 0;
        ok := true;
        while j < needle.len {
            if hay[i + j] != needle[j] { ok = false; break; }
            j += 1;
        }
        if ok { return true; }
        i += 1;
    }
    return false;
}

main :: () -> i32 {
    gpa := GPA.init();

    // ── 1. No command → readable usage on stderr, EX_USAGE (64) ───────
    // `2>&1 1>/dev/null` routes the command's stderr into the captured
    // pipe and discards its stdout, so `r.stdout` here IS the stderr text.
    if r := proc.run("build/dist 2>&1 1>/dev/null") {
        proc.assert(r.exit_code == 64, "no-args must exit EX_USAGE (64)");
        proc.assert(r.stdout.len > 0, "no-args must print human help/usage to stderr");
        proc.assert(contains(r.stdout, "Usage"), "no-args help must carry a usage line");
    } else {
        proc.assert(false, "spawn build/dist (no args) failed");
    }

    // ── 2. Unknown command → readable error on stderr, EX_USAGE (64) ──
    if r := proc.run("build/dist bogus 2>&1 1>/dev/null") {
        proc.assert(r.exit_code == 64, "unknown command must exit EX_USAGE (64)");
        proc.assert(contains(r.stdout, "unknown"), "unknown command must name the failure on stderr");
    } else {
        proc.assert(false, "spawn build/dist bogus failed");
    }

    // ── 3a. `--json` stdout purity on a FAILURE path: a single valid
    //        JSON error object, nothing else. The store dir was never
    //        published into, so the command fails with `store.load` and
    //        exit 1 (command failed — distinct from usage's 64).
    //        `2>/dev/null` drops the human note so the pipe carries ONLY
    //        stdout; std.json.parse rejects trailing junk. ──────────────
    PROMOTE :: "build/dist release promote --app x --channel beta --release rel-x --local-store .sx-tmp/cli_dispatch_nostore --json";
    if r := proc.run(concat(PROMOTE, " 2>/dev/null")) {
        proc.assert(r.exit_code == 1, "failed command must exit 1 (not EX_USAGE)");
        v, e := json.parse(r.stdout, xx gpa);
        proc.assert(!e, "stdout in --json mode must be a single valid JSON object (parse failed / trailing junk)");
        if !e {
            o := v.object;
            proc.assert(o.items[0].key == "status" and o.items[0].val.str == "error",
                        "failure json reports status error");
            eo := o.items[1].val.object;
            proc.assert(eo.items[0].key == "code" and eo.items[0].val.str == "store.load",
                        "failure json names the store.load code");
        }
    } else {
        proc.assert(false, "spawn build/dist release promote --json failed");
    }

    // ── 3b. `--json` mode keeps human text on STDERR (not stdout) ──────
    if r := proc.run(concat(PROMOTE, " 2>&1 1>/dev/null")) {
        proc.assert(r.stdout.len > 0, "--json mode must still emit human text to stderr");
    } else {
        proc.assert(false, "spawn build/dist release promote --json (stderr) failed");
    }

    // ── 4. `--help` lists the ci / release groups, exits 0 ────────────
    if r := proc.run("build/dist --help 2>/dev/null") {
        proc.assert(r.exit_code == 0, "--help exits 0");
        proc.assert(contains(r.stdout, "ci"), "--help lists the ci group");
        proc.assert(contains(r.stdout, "release"), "--help lists the release group");
    } else {
        proc.assert(false, "spawn build/dist --help failed");
    }

    // ── 5. required flags: ci publish AND release promote ─────────────
    // Missing a required flag is a usage error: EX_USAGE (64), human
    // diagnostic on stderr (`2>&1 1>/dev/null` captures the stderr text).
    if r := proc.run("build/dist ci publish --json 2>&1 1>/dev/null") {
        proc.assert(r.exit_code == 64, "ci publish without required flags must exit EX_USAGE (64)");
        proc.assert(contains(r.stdout, "missing required flag"), "missing-flag error names the failure on stderr");
    } else {
        proc.assert(false, "spawn build/dist ci publish (no flags) failed");
    }
    if r := proc.run("build/dist release promote --json 2>&1 1>/dev/null") {
        proc.assert(r.exit_code == 64, "release promote without required flags must exit EX_USAGE (64)");
        proc.assert(contains(r.stdout, "missing required flag"), "missing-flag error names the failure on stderr");
    } else {
        proc.assert(false, "spawn build/dist release promote (no flags) failed");
    }

    print("cli_dispatch: ok\n");
    return 0;
}
