// Pinned acceptance for P4.1 — `dist server run` serves the local store
// read-only over HTTP.
//
// Publishes the fixture manifest into a fresh store, starts the BUILT
// `build/dist server run` on a test port in the background, waits for
// /healthz, and asserts every route against curl:
//
//   * /                       → HTML index naming the app, with a
//                               /download/<sha> link
//   * /healthz                → {"status":"ok"}
//   * /api/apps               → the published app is listed
//   * /api/apps/<slug>        → app + its releases + channels
//   * /api/apps/<unknown>     → JSON error api.unknown_app
//   * /download/<sha256>      → bytes identical to the source fixture
//   * /download/<unknown sha> → JSON error download.unknown_object
//   * any other path          → JSON error http.not_found
//
// FRESHNESS is asserted by publishing a SECOND version while the server
// is running: the next /api/apps/<slug> must list both releases (db.json
// is reloaded per request — no stale cache).
#import "modules/std.sx";
#import "modules/std/json.sx";
process :: #import "modules/std/process.sx";
fs   :: #import "modules/std/fs.sx";

STORE :: ".sx-tmp/server_http";
MDIR  :: ".sx-tmp/server_http_m";
PORT  :: "18792";
BASE  :: "http://127.0.0.1:18792";

MANIFEST_A :: "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}";
MANIFEST_B :: "{\"app\":\"acme-app\",\"version\":\"1.2.4\",\"channel\":\"stable\",\"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; }

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

// True iff `needle` occurs in `hay` (plain scan; bodies are small).
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;
}

// GET `path` and return the body (curl; 2s timeout so a dead server fails
// the test instead of hanging it).
fetch :: (path: string) -> string {
    r := process.run(concat(concat("curl -s -m 2 ", BASE), path));
    process.assert(r != null, concat("curl spawn failed: ", path));
    return r!.stdout;
}

// GET `path` and return the HTTP status code as text.
fetch_code :: (path: string) -> string {
    r := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' ", BASE), path));
    process.assert(r != null, concat("curl spawn failed: ", path));
    return r!.stdout;
}

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

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

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

    // Fresh state; clear any stale server from a crashed prior run.
    process.run("pkill -f 'dist server run --local-store .sx-tmp/server_http' 2>/dev/null");
    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);

    ra := process.run(publish_cmd(path_join(MDIR, "a.json")));
    process.assert(ra != null and ra!.exit_code == 0, "publish A must exit 0");

    // ── start the server, hold its pid, poll /healthz until ready ─────
    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 := fetch_code("/healthz");
        if c == "200" { ready = true; break; }
        process.run("sleep 0.2");
        tries += 1;
    }
    process.assert(ready, "server must answer /healthz within 10s");
    print("  server up\n");

    // ── routes ────────────────────────────────────────────────────────
    process.assert(fetch_code("/") == "200", "/ serves the HTML index");
    idx := fetch("/");
    process.assert(contains(idx, "<title>dist</title>"), "index is the dist HTML page");
    process.assert(contains(idx, "acme-app"), "index names the published app");
    process.assert(contains(idx, "/download/"), "index links artifact downloads");

    hz := parse_body(fetch("/healthz"), "/healthz", xx arena);
    process.assert(get_str(hz, "status") == "ok", "/healthz status ok");

    apps := parse_body(fetch("/api/apps"), "/api/apps", xx arena);
    apps_arr := get_arr(apps, "apps");
    process.assert(apps_arr.len == 1, "/api/apps lists one app");
    process.assert(get_str(apps_arr.items[0].object, "slug") == "acme-app", "/api/apps lists acme-app");

    det := parse_body(fetch("/api/apps/acme-app"), "/api/apps/acme-app", xx arena);
    process.assert(get_str(get_obj(det, "app"), "slug") == "acme-app", "detail names the app");
    process.assert(get_arr(det, "releases").len == 1, "detail lists one release");
    process.assert(get_arr(det, "channels").len == 1, "detail lists one channel");

    process.assert(fetch_code("/api/apps/nope") == "404", "unknown slug is 404");
    nf := parse_body(fetch("/api/apps/nope"), "unknown slug body", xx arena);
    process.assert(get_str(get_obj(nf, "error"), "code") == "api.unknown_app", "unknown slug names api.unknown_app");

    process.assert(fetch_code("/bogus") == "404", "unknown route is 404");
    nr := parse_body(fetch("/bogus"), "unknown route body", xx arena);
    process.assert(get_str(get_obj(nr, "error"), "code") == "http.not_found", "unknown route names http.not_found");
    print("  api routes ok\n");

    // ── download: bytes identical to the source fixture ───────────────
    db_bytes := fs.read_file(path_join(STORE, "db.json"));
    process.assert(db_bytes != null, "db.json must exist");
    dbo := parse_body(db_bytes!, "db.json", xx arena);
    sha := get_str(get_arr(dbo, "artifacts").items[0].object, "sha256");

    dl := process.run(concat(concat(concat(concat("curl -s -m 2 -o .sx-tmp/server_http_dl.bin ", BASE), "/download/"), sha), " && cmp -s .sx-tmp/server_http_dl.bin examples/fixtures/acme-1.2.3-android.apk && echo SAME"));
    process.assert(dl != null, "download curl spawn failed");
    process.assert(dl!.stdout == "SAME\n", "downloaded bytes must equal the source fixture");

    bad := parse_body(fetch("/download/0000000000000000000000000000000000000000000000000000000000000000"), "unknown object body", xx arena);
    process.assert(get_str(get_obj(bad, "error"), "code") == "download.unknown_object", "unknown digest names download.unknown_object");
    print("  download ok\n");

    // ── idle preconnect must not wedge the accept loop ────────────────
    // Hold a connection open that never sends bytes (what a browser's
    // speculative preconnect does) and require a real request to still be
    // answered: the 2s read timeout must free the loop well inside curl's
    // 5s budget. Pre-fix (no SO_RCVTIMEO) this curl times out with 000.
    process.run("sh -c '(sleep 6 | nc 127.0.0.1 18792 > /dev/null 2>&1) &'");
    process.run("sleep 0.3");
    wc := process.run(concat(concat("curl -s -m 5 -o /dev/null -w '%{http_code}' ", BASE), "/healthz"));
    process.assert(wc != null, "curl spawn failed (idle-conn case)");
    process.assert(wc!.stdout == "200", "request must be served while an idle connection is held open");
    print("  idle connection cannot wedge the loop\n");

    // ── freshness: publish B while the server runs ────────────────────
    rb := process.run(publish_cmd(path_join(MDIR, "b.json")));
    process.assert(rb != null and rb!.exit_code == 0, "publish B must exit 0");
    det2 := parse_body(fetch("/api/apps/acme-app"), "detail after B", xx arena);
    process.assert(get_arr(det2, "releases").len == 2, "server reflects the new release without restart");
    print("  per-request reload ok\n");

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