// Pinned acceptance for P4.5 — `dist ci publish --server --token`, the
// PLAN.md CI contract in remote mode.
//
// Mints a token in a fresh store, starts the BUILT `build/dist server
// run` over it, then drives the BUILT CLI against the live server:
//
//   * happy path: exit 0; stdout is the publish JSON (status published,
//     release id, per-artifact sha256 equal to an independent digest of
//     the fixture); the SERVER's store gained objects/<sha>, the release,
//     the channel pointer, and `token:<name>` audit actors.
//   * duplicate version: exit 1, the server's transaction.integrity code
//     passed through verbatim.
//   * wrong secret: exit 1, auth.unknown_token.
//   * https:// target: exit 1, server.tls_unsupported (TLS terminates at
//     a reverse proxy by design).
//   * malformed --server: exit 1, server.bad_url.
//   * unreachable server: exit 1, server.unreachable.
//   * --local-store together with --server: usage error (exit 64).
#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";

STORE   :: ".sx-tmp/remote_publish";
MDIR    :: ".sx-tmp/remote_publish_m";
PORT    :: "18794";
SERVER  :: "http://127.0.0.1:18794";
FIXTURE :: "examples/fixtures/acme-1.2.3-android.apk";

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

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

parse_body :: (body: string, what: string, scratch: Allocator) -> Object {
    v, e := parse(body, scratch);
    if e {
        process.assert(false, concat("must be valid JSON: ", what));
        dummy : Object = .{};
        return dummy;
    }
    return v.object;
}

// Run a remote publish with `server` and `token`; returns the run result.
remote_publish :: (server: string, token: string) -> ?process.ProcessResult {
    cmd := concat("build/dist ci publish --manifest ", path_join(MDIR, "m.json"));
    cmd = concat(cmd, concat(" --server ", server));
    cmd = concat(cmd, concat(" --token ", token));
    return process.run(concat(cmd, " --json 2>/dev/null"));
}

// Assert `r` failed (exit 1) with the given JSON error code.
assert_fails_with :: (r: ?process.ProcessResult, code: string, what: string, scratch: Allocator) {
    process.assert(r != null, concat("spawn failed: ", what));
    process.assert(r!.exit_code == 1, concat("must exit 1: ", what));
    o := parse_body(r!.stdout, what, scratch);
    process.assert(get_str(o, "status") == "error", concat("json status error: ", what));
    process.assert(get_str(get_obj(o, "error"), "code") == code,
                   concat(concat("error code must be ", code), concat(": ", what)));
}

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

    process.run("pkill -f 'dist server run --local-store .sx-tmp/remote_publish' 2>/dev/null");
    process.run(concat("rm -rf ", STORE));
    process.run(concat("rm -rf ", MDIR));
    process.run(concat("mkdir -p ", MDIR));
    process.run(concat(concat(concat("printf '%s' '", MANIFEST), "' > "), path_join(MDIR, "m.json")));

    // ── token + server ─────────────────────────────────────────────────
    tc := process.run("build/dist token create --name ci-remote --local-store .sx-tmp/remote_publish --json 2>/dev/null");
    process.assert(tc != null and tc!.exit_code == 0, "token create must exit 0");
    secret := get_str(get_obj(parse_body(tc!.stdout, "token create", xx arena), "token"), "secret");

    sp := process.run(concat(concat(concat("sh -c 'build/dist server run --local-store ", STORE), concat(concat(" --port ", PORT), " >/dev/null 2>&1 & echo $!'")), ""));
    process.assert(sp != null, "server spawn failed");
    pid := sp!.stdout;

    ready := false;
    tries := 0;
    while tries < 50 {
        c := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' ", SERVER), "/healthz"));
        if c != null {
            if c!.stdout == "200" { ready = true; break; }
        }
        process.run("sleep 0.2");
        tries += 1;
    }
    process.assert(ready, "server must answer /healthz within 10s");
    print("  server up\n");

    // ── happy path ─────────────────────────────────────────────────────
    fixture_bytes := fs.read_file(FIXTURE);
    process.assert(fixture_bytes != null, "fixture must be readable");
    d := hash.sha256_hex(fixture_bytes!);
    expect_sha := string.{ ptr = @d[0], len = 64 };

    hp := remote_publish(SERVER, secret);
    process.assert(hp != null, "remote publish spawn failed");
    process.assert(hp!.exit_code == 0, "remote publish must exit 0");
    o := parse_body(hp!.stdout, "remote publish stdout", xx arena);
    process.assert(get_str(o, "status") == "published", "json status published");
    process.assert(get_str(get_obj(o, "release"), "id") == "rel-acme-app-2.0.0", "json names the release");
    arts := get_arr(o, "artifacts");
    process.assert(arts.len == 1, "json lists one artifact");
    process.assert(get_str(arts.items[0].object, "sha256") == expect_sha,
                   "artifact sha256 equals an independent digest of the fixture");

    process.assert(fs.exists(path_join(STORE, concat("objects/", expect_sha))),
                   "the SERVER's store holds the uploaded object");
    dbo := parse_body(fs.read_file(path_join(STORE, "db.json"))!, "db.json", xx arena);
    rels := get_arr(dbo, "releases");
    process.assert(rels.len == 1, "server db records the release");
    process.assert(get_str(rels.items[0].object, "created_by") == "token:ci-remote",
                   "release created_by carries the token actor");
    chans := get_arr(dbo, "channels");
    process.assert(chans.len == 1, "server db records the channel");
    process.assert(get_str(chans.items[0].object, "current_release_id") == "rel-acme-app-2.0.0",
                   "beta points at the published release");
    print("  remote publish: CI contract round trip ok\n");

    // ── failure paths ──────────────────────────────────────────────────
    assert_fails_with(remote_publish(SERVER, secret), "transaction.integrity",
                      "duplicate version", xx arena);
    assert_fails_with(remote_publish(SERVER, "dist_wrong"), "auth.unknown_token",
                      "wrong secret", xx arena);
    assert_fails_with(remote_publish("https://127.0.0.1:18794", secret), "server.tls_unsupported",
                      "https target", xx arena);
    assert_fails_with(remote_publish("http://127.0.0.1", secret), "server.bad_url",
                      "url without port", xx arena);
    assert_fails_with(remote_publish("http://127.0.0.1:18790", secret), "server.unreachable",
                      "unreachable server", xx arena);

    both := process.run(concat(concat("build/dist ci publish --manifest ", path_join(MDIR, "m.json")), concat(concat(" --server ", SERVER), " --token x --local-store .sx-tmp/remote_publish --json 2>/dev/null")));
    process.assert(both != null and both!.exit_code == 64, "--server with --local-store is a usage error");
    print("  failure paths: server codes passed through, usage gated\n");

    // ── teardown ───────────────────────────────────────────────────────
    process.run(concat("kill ", pid));
    process.run(concat("rm -rf ", STORE));
    process.run(concat("rm -rf ", MDIR));
    print("remote_publish: ALL CASES PASS\n");
    return 0;
}
