#!/bin/sh
# script/verify-build: assert the compiled DEBUG state of the emitted
# bundles. Our own extension to scripts-to-rule-them-all, run at the end of
# make build / make build-debug.
#
# Why this exists: DEBUG makes the publicly committed test recovery phrase the
# output of wallet creation, so a release artifact built with it live hands
# every new wallet to anyone who reads the repo. The test suite cannot see
# this, because it loads src/shared/constants.js outside a bundle and takes
# the fallback branch; the property only exists in the emitted output, so it
# has to be asserted against the emitted output.
#
# What it reads: dist/constants-bundles.txt, written by build.js from
# esbuild's metafile, naming every emitted bundle that contains
# src/shared/constants.js. Each of those must carry exactly one of the two
# BUILD_DEBUG_MARKER literals that constants.js folds down to.
#
# It fails rather than passes whenever it cannot determine a bundle's state.
# Minified output is not a stable contract, so "matched neither form" is not
# evidence of anything and must never read as green.
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"

MANIFEST="dist/constants-bundles.txt"
MARKER_ON="autistmask-build-debug=on"
MARKER_OFF="autistmask-build-debug=off"

# Set by read_marker.
MARKER=""

fail() {
    echo "verify-build: FAIL: $*" >&2
    exit 1
}

# Is the literal $1 present in the file $2? Match (grep exit 0) and no-match
# (exit 1) are answers about the emitted output. Anything else (exit 2: the
# file could not be read) is not an answer at all, and must not be reported as
# "no marker" — that would blame the bundle for a permissions or I/O fault.
has_marker() {
    _hm_status=0
    grep -q -F -e "$1" -- "$2" || _hm_status=$?
    case "$_hm_status" in
    0) return 0 ;;
    1) return 1 ;;
    *)
        fail "grep exited $_hm_status reading $2, so the file could not be
    searched and its DEBUG state was not checked at all. That is a permissions
    or I/O fault on the artifact, not a change in the emitted output. Refusing
    to report success."
        ;;
    esac
}

# Does the manifest list the path $1, as a whole line? Same discipline as
# has_marker: exit 0 and 1 are answers about the manifest, exit 2 means the
# manifest could not be read and is not an answer at all. Without this, an
# unreadable manifest reads as "this file is not listed" and every emitted
# bundle gets reported as an unlisted one.
is_listed() {
    _il_status=0
    grep -q -x -F -e "$1" -- "$MANIFEST" || _il_status=$?
    case "$_il_status" in
    0) return 0 ;;
    1) return 1 ;;
    *)
        fail "grep exited $_il_status reading $MANIFEST, so it could not be
    searched and nothing was established about which bundles it lists. That is
    a permissions or I/O fault on the manifest, not a stale manifest. Refusing
    to report success."
        ;;
    esac
}

# Read one bundle's DEBUG state into MARKER. Exactly one marker must be
# present. Both means the ternary in constants.js was never folded, which is
# what happens when the __BUILD_DEBUG__ define goes missing from build.js:
# DEBUG stops being known at build time. Neither means we are reading output
# we do not understand. Both are hard failures; neither is ever treated as
# absence of a problem.
read_marker() {
    _file="$1"
    _on=no
    _off=no
    if has_marker "$MARKER_ON" "$_file"; then _on=yes; fi
    if has_marker "$MARKER_OFF" "$_file"; then _off=yes; fi

    if [ "$_on" = yes ] && [ "$_off" = yes ]; then
        fail "$_file carries both debug markers, so DEBUG was not resolved at
    build time: the ternary in src/shared/constants.js survived into the
    emitted output. This does not mean the debug branch is live in this
    artifact: an unresolved __BUILD_DEBUG__ is undeclared in extension
    context, so DEBUG evaluates to false at runtime. It does mean the
    release/debug distinction is no longer enforced at build time, and which
    way that fallback happens to evaluate is then an accident a refactor can
    flip. Check that build.js still defines __BUILD_DEBUG__."
    fi
    if [ "$_on" = no ] && [ "$_off" = no ]; then
        fail "$_file carries no debug marker, so its DEBUG state cannot be
    determined. Either BUILD_DEBUG_MARKER is gone from src/shared/constants.js
    or the emitted output changed shape. Refusing to report success."
    fi

    if [ "$_on" = yes ]; then
        MARKER="$MARKER_ON"
    else
        MARKER="$MARKER_OFF"
    fi
}

# The manifest says which bundles must carry a marker. This says no other
# emitted file may carry one, which catches a manifest that has gone stale
# or short rather than trusting whatever it happens to list.
#
# Deliberately unfiltered by extension. build.js selects manifest entries with
# an endsWith(".js") test; repeating that literal here would mean a bundle
# emitted under some other extension escaped the manifest AND this check at
# once, which is the correlated blind spot the two-source design exists to
# avoid. Every file under dist/ is searched, so build.js's filter is the only
# place the assumption lives and this check is what catches it being wrong.
#
# That claim only holds if the walk is exhaustive, so two things are enforced
# here rather than assumed:
#
#   - find's exit status is checked. A subtree it cannot descend is reported on
#     stderr and then simply missing from the listing, so an unchecked status
#     turns "could not look" into "nothing was there" — the same conflation
#     has_marker exists to prevent. The status cannot be read off a pipeline
#     ending in sort, so the sort is a separate step.
#   - symlinks are walked too (-type l), not skipped. A marker-carrying bundle
#     reachable under an unlisted path in dist/ is a stale manifest whether the
#     path is a link or a file, and grep reads through the link. A link that
#     cannot be read through — dangling, or pointing at a directory — fails
#     hard via has_marker's exit-2 path, which is the fail-closed answer: the
#     build emits neither, so their DEBUG state is unproven, not fine.
check_unlisted_bundles() {
    _find_status=0
    _listing="$(find dist \( -type f -o -type l \) -print)" || _find_status=$?
    [ "$_find_status" -eq 0 ] ||
        fail "find exited $_find_status enumerating dist/, so part of the tree
    was never walked and nothing was established about the files in it. Any
    unlisted bundle there went unchecked. That is a permissions or I/O fault on
    the artifact, not a stale manifest. Refusing to report success."
    _listing="$(printf '%s\n' "$_listing" | sort)"

    while read -r _file; do
        [ -n "$_file" ] || continue
        if is_listed "$_file"; then
            continue
        fi
        if has_marker "$MARKER_ON" "$_file" ||
            has_marker "$MARKER_OFF" "$_file"; then
            fail "$_file carries a debug marker but is absent from $MANIFEST,
    so the manifest no longer describes the emitted bundles."
        fi
    done <<EOF
$_listing
EOF
}

# The requested mode, read from our own environment using build.js's exact
# rule: only the literal 1 opts in. Deliberately not taken from anything
# build.js records about itself, so build.js cannot vouch for build.js.
expected_marker() {
    if [ "${AUTISTMASK_DEBUG-}" = "1" ]; then
        echo "$MARKER_ON"
    else
        echo "$MARKER_OFF"
    fi
}

main() {
    cd "$ROOT"

    expected="$(expected_marker)"
    echo "Verifying emitted bundles (expecting $expected)..."

    [ -f "$MANIFEST" ] ||
        fail "$MANIFEST is missing. build.js writes it at the end of a
    successful build; run make build first."
    [ -s "$MANIFEST" ] ||
        fail "$MANIFEST is empty, so no emitted bundle was found to contain
    src/shared/constants.js. That is never correct, so it is a failure and not
    a pass."
    [ -r "$MANIFEST" ] ||
        fail "$MANIFEST is not readable, so nothing was inspected. That is a
    permissions or I/O fault, not a pass."

    count=0
    while read -r file; do
        [ -n "$file" ] || continue
        [ -f "$file" ] ||
            fail "$MANIFEST lists $file, which does not exist."
        [ -s "$file" ] ||
            fail "$MANIFEST lists $file, which is empty. An empty bundle
    carries no marker and proves nothing, so this is a failure and not a pass."
        read_marker "$file"
        [ "$MARKER" = "$expected" ] ||
            fail "$file is $MARKER but this build expects $expected."
        echo "  ok: $file ($MARKER)"
        count=$((count + 1))
    done <"$MANIFEST"

    [ "$count" -gt 0 ] || fail "no bundles were inspected."

    check_unlisted_bundles

    echo "verify-build: $count bundle(s) verified $expected"
}

main "$@"
