// Pinned acceptance for P4.6 — install pages with accurate iOS modes
// (subplan 04 Slice 5; PLAN.md "iOS Install Policy").
//
// Publishes the APK+IPA example into a fresh store, starts the BUILT
// server, and walks `dist app set` through the three iOS modes asserting
// the page and OTA manifest at each step:
//
//   * default (artifact_only): the IPA is a labeled download that
//     explicitly does NOT claim on-device install; manifest.plist is 404
//     (install.not_enterprise). The APK row shows its sha256.
//   * testflight: setting the mode without a URL exits 1
//     (app.testflight_url_required); with one, the page links TestFlight.
//   * enterprise: without a bundle id exits 1 (app.bundle_id_required);
//     with one, the page carries the itms-services deep link and
//     manifest.plist serves XML naming bundle id, version, and an
//     https://<host>/download/<sha> package URL. Flipping the mode back
//     404s the plist again — all without restarting the server.
//   * UA detection: an iPhone UA marks the iOS section, an Android UA
//     the Android section.
//   * 404s: unknown slug/channel, channel-less path; the index links
//     every channel to its install page.
#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/server_install";
PORT  :: "18797";
BASE  :: "http://127.0.0.1:18797";

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

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

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

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

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

fetch_ua :: (path: string, ua: string) -> string {
    cmd := concat("curl -s -m 2 -A '", concat(ua, "' "));
    r := process.run(concat(cmd, concat(BASE, path)));
    process.assert(r != null, concat("curl-ua spawn failed: ", path));
    return r!.stdout;
}

// Run `dist app set` with extra `flags`; returns the run result.
app_set :: (flags: string) -> ?process.ProcessResult {
    cmd := "build/dist app set --app acme-app --local-store .sx-tmp/server_install ";
    return process.run(concat(cmd, concat(flags, " --json 2>/dev/null")));
}

assert_set_fails :: (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(get_obj(o, "error"), "code") == code,
                   concat(concat("error code must be ", code), concat(": ", what)));
}

// sha256 of a fixture file as a heap string.
file_sha :: (path: string) -> string {
    b := fs.read_file(path);
    process.assert(b != null, concat("fixture must be readable: ", path));
    d := hash.sha256_hex(b!);
    v := string.{ ptr = @d[0], len = 64 };
    return substr(v, 0, 64);
}

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/server_install' 2>/dev/null");
    process.run(concat("rm -rf ", STORE));

    pub := process.run("build/dist ci publish --manifest examples/dist.json --local-store .sx-tmp/server_install --json 2>/dev/null >/dev/null");
    process.assert(pub != null and pub!.exit_code == 0, "publish must exit 0");

    apk_sha := file_sha("examples/fixtures/acme-1.2.3-android.apk");
    ipa_sha := file_sha("examples/fixtures/acme-1.2.3-ios.ipa");

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

    PAGE :: "/install/acme-app/stable";
    PLIST :: "/install/acme-app/stable/manifest.plist";

    // ── default: artifact_only, honest labeling ───────────────────────
    process.assert(fetch_code(PAGE) == "200", "install page is 200");
    page := fetch(PAGE);
    process.assert(contains(page, "cannot be installed on an iPhone"),
                   "artifact_only page disclaims on-device install");
    process.assert(!contains(page, "itms-services") and !contains(page, "TestFlight"),
                   "artifact_only page offers no install action");
    process.assert(contains(page, apk_sha), "APK sha256 is visible on the page");
    process.assert(contains(page, concat("/download/", ipa_sha)), "IPA is downloadable");
    process.assert(fetch_code(PLIST) == "404", "plist is 404 outside enterprise mode");
    pe := parse_body(fetch(PLIST), "plist error", xx arena);
    process.assert(get_str(get_obj(pe, "error"), "code") == "install.not_enterprise",
                   "plist names install.not_enterprise");
    print("  artifact_only: labeled honestly, sha visible, no plist\n");

    // ── testflight ─────────────────────────────────────────────────────
    assert_set_fails(app_set("--ios-mode testflight"), "app.testflight_url_required",
                     "testflight without url", xx arena);
    tf := app_set("--ios-mode testflight --testflight-url https://testflight.apple.com/join/abc123");
    process.assert(tf != null and tf!.exit_code == 0, "testflight set must exit 0");
    page2 := fetch(PAGE);
    process.assert(contains(page2, "Open in TestFlight"), "testflight page links TestFlight");
    process.assert(contains(page2, "https://testflight.apple.com/join/abc123"), "page carries the TestFlight URL");
    print("  testflight: precondition gated, page links Apple's flow\n");

    // ── enterprise ─────────────────────────────────────────────────────
    assert_set_fails(app_set("--ios-mode enterprise"), "app.bundle_id_required",
                     "enterprise without bundle id", xx arena);
    en := app_set("--ios-mode enterprise --ios-bundle-id co.acme.app");
    process.assert(en != null and en!.exit_code == 0, "enterprise set must exit 0");
    page3 := fetch(PAGE);
    process.assert(contains(page3, "itms-services://"), "enterprise page carries the OTA deep link");

    process.assert(fetch_code(PLIST) == "200", "enterprise plist is 200");
    plist := fetch(PLIST);
    process.assert(contains(plist, "<key>bundle-identifier</key><string>co.acme.app</string>"),
                   "plist names the bundle id");
    process.assert(contains(plist, "<key>bundle-version</key><string>1.2.3</string>"),
                   "plist names the release version");
    process.assert(contains(plist, concat("https://127.0.0.1:18797/download/", ipa_sha)),
                   "plist package URL is https and content-addressed");
    print("  enterprise: plist serves bundle id, version, https package URL\n");

    // mode change is visible without a restart (per-request reload)
    back := app_set("--ios-mode testflight");
    process.assert(back != null and back!.exit_code == 0, "flip back to testflight must exit 0");
    process.assert(fetch_code(PLIST) == "404", "plist 404s again after leaving enterprise mode");
    print("  mode flips apply without restart\n");

    // ── UA detection ───────────────────────────────────────────────────
    ipage := fetch_ua(PAGE, "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)");
    process.assert(contains(ipage, "<section class=\"detected\"><h2>iOS"),
                   "iPhone UA marks the iOS section");
    apage := fetch_ua(PAGE, "Mozilla/5.0 (Linux; Android 14; Pixel 8)");
    process.assert(contains(apage, "<section class=\"detected\"><h2>Android"),
                   "Android UA marks the Android section");
    print("  UA detection: device platform marked first\n");

    // ── 404s + index links ─────────────────────────────────────────────
    process.assert(fetch_code("/install/nope/stable") == "404", "unknown slug is 404");
    ue := parse_body(fetch("/install/nope/stable"), "unknown slug", xx arena);
    process.assert(get_str(get_obj(ue, "error"), "code") == "install.unknown_app", "names install.unknown_app");
    ce := parse_body(fetch("/install/acme-app/nope"), "unknown channel", xx arena);
    process.assert(get_str(get_obj(ce, "error"), "code") == "install.unknown_channel", "names install.unknown_channel");
    process.assert(fetch_code("/install/acme-app") == "404", "channel-less install path is 404");
    idx := fetch("/");
    process.assert(contains(idx, "href=\"/install/acme-app/stable\""), "index links the install page");
    print("  404s precise; index links install pages\n");

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