#!/usr/bin/env bash
# The only CI this repo has. Everything a branch must pass before it can
# squash-merge, in one script, so what the runner executes and what you
# run before pushing are one definition rather than two that drift.
set -euo pipefail

# Run from the repo root whatever the caller's working directory is:
# .amadan/ci execs this file, an amadan runner with step support runs it
# directly, and you will run it by hand from wherever you happen to be.
cd "$(dirname "$0")/../.."

# rustup installs to ~/.cargo/bin, which is on neither a systemd user
# unit's PATH nor a login shell's when rustup ran with --no-modify-path.
# Source it here rather than trusting whoever set the runner up to have
# got it right — a gate that only works on one machine is not a gate.
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"

for tool in cargo rustc rustfmt go node npm; do
  command -v "$tool" >/dev/null || { echo "gate: $tool not on PATH" >&2; exit 1; }
done
rustup target list --installed 2>/dev/null | grep -qx wasm32-unknown-unknown \
  || { echo "gate: wasm32-unknown-unknown target missing — rustup target add wasm32-unknown-unknown" >&2; exit 1; }

# The stack is cgo-free by design, here as in scripts/run-dev.sh and the
# Makefile. A build that quietly used cgo would not be the binary that
# ships to an arm64 instance.
export CGO_ENABLED=0
export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}amadan.net/oficina/estilo.git"

# Two of the steps below regenerate a committed artifact and then require
# the tree to be unchanged. Both have the same shape, so they share one
# definition:
#
#   git diff alone is not enough. It sees a tracked file that changed or
#   was deleted, and never the NEW file a changed content hash writes
#   beside the old one — so a rebuild whose bytes moved leaves both a
#   deletion and an untracked file, and checking only one of them lets
#   half of that through.
#
#   On failure the TRACKED files are put back and the untracked ones
#   are left where they are, listed. The check mutated a committed
#   directory as a side effect of reading it, and leaving a modified
#   tracked file behind would make a second run of this same script
#   pass with nobody having committed anything — the worst possible
#   outcome for a freshness check. But an untracked file here is
#   exactly the one the developer has to commit next (a vector for a
#   new data file, the bundle under its new hash), and deleting it —
#   as this once did — threw away a freshly generated artifact before
#   anybody could `git add` it. So it stays, and the message names it.
require_committed() {
  local path=$1 fix=$2 ok=1
  # --name-status, not a content diff: both directories hold minified
  # or binary artifacts, and the useful answer is which file moved, not
  # 371 KB of bundle in the runner's log.
  git diff --exit-code --name-status -- "$path" || ok=0
  if [ -n "$(git status --porcelain -- "$path")" ]; then
    git status --porcelain -- "$path" >&2
    ok=0
  fi
  if [ "$ok" -eq 0 ]; then
    echo "gate: $fix" >&2
    echo "gate: stale or uncommitted under $path (M = restored to the committed version, ?? = left for you to commit):" >&2
    git status --porcelain -- "$path" >&2
    git checkout -- "$path"
    exit 1
  fi
}

echo "=== rust: fmt"
# --manifest-path, because there is no workspace at the repo root: the
# crate lives in core/ and a bare `cargo fmt` here finds no Cargo.toml.
cargo fmt --manifest-path core/Cargo.toml --check

echo "=== rust: clippy"
# clippy.toml's disallowed-methods is the real enforcement of the
# Doc-construction rule that core/src/lib.rs documents: Doc::new() and
# Doc::with_client_id() both draw entropy this build has no safe source
# for, and a doc comment alone has been got around before.
#
# scripts/build-wasm.sh below runs this same invocation, flag for flag,
# before it builds. That is deliberate: it fails here in a few seconds
# rather than forty seconds into the wasm build, and because the two
# spellings are identical there is no looser check to sail through —
# the drift Docs' gate hit when its two clippy runs disagreed.
cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D clippy::disallowed_methods

echo "=== wasm: the artifact the host embeds"
# Before cargo test, not after. core/tests/no_host_imports.rs asserts the
# module imports nothing from the host by reading the wasm32 artifact off
# disk, and that file only exists once this line has run. Run the tests
# first and the assertion's own presence becomes a race it reports as a
# skip.
./scripts/build-wasm.sh

echo "=== the embedded wasm must match a fresh build"
if ! cmp -s core/target/wasm32-unknown-unknown/release/slides_core.wasm internal/slides/core/testdata/slides_core.wasm; then
  echo "gate: internal/slides/core/testdata/slides_core.wasm does not match a fresh build — rebuild and commit the wasm:" >&2
  echo "  ./scripts/build-wasm.sh && cp core/target/wasm32-unknown-unknown/release/slides_core.wasm internal/slides/core/testdata/slides_core.wasm" >&2
  exit 1
fi

echo "=== rust: test"
# SLIDES_REQUIRE_WASM=1 turns no_host_imports.rs's "artifact missing"
# branch from a skip into a failure. Safe to set unconditionally here
# because the build above just produced it; left unset, a bare local
# `cargo test` on a fresh clone still skips rather than forcing a wasm
# build first.
SLIDES_REQUIRE_WASM=1 cargo test --manifest-path core/Cargo.toml

# The PDF module (core/pdf, hosted by internal/slides/pdfcore) is a
# second crate with the same shape and the same four steps, in the same
# order and for the same reasons as the CRDT core's above: fmt, clippy
# (the exact invocation scripts/build-pdf-wasm.sh runs, so nothing
# looser sails through), build the artifact BEFORE its own
# no_host_imports test reads it, and the byte comparison against the
# committed copy go:embed actually ships. A stale slides_pdf.wasm is
# the same trap as a stale slides_core.wasm: the Go tests pass against
# whatever is committed, and only this cmp knows the difference.
echo "=== rust: fmt (pdf)"
cargo fmt --manifest-path core/pdf/Cargo.toml --check

echo "=== rust: clippy (pdf)"
cargo clippy --manifest-path core/pdf/Cargo.toml --all-targets -- -D warnings

echo "=== wasm: the pdf artifact the host embeds"
./scripts/build-pdf-wasm.sh

echo "=== the embedded pdf wasm must match a fresh build"
if ! cmp -s core/pdf/target/wasm32-unknown-unknown/release/slides_pdf.wasm internal/slides/pdfcore/testdata/slides_pdf.wasm; then
  echo "gate: internal/slides/pdfcore/testdata/slides_pdf.wasm does not match a fresh build — rebuild and commit the wasm:" >&2
  echo "  ./scripts/build-pdf-wasm.sh && cp core/pdf/target/wasm32-unknown-unknown/release/slides_pdf.wasm internal/slides/pdfcore/testdata/slides_pdf.wasm" >&2
  exit 1
fi

echo "=== rust: test (pdf)"
SLIDES_REQUIRE_WASM=1 cargo test --manifest-path core/pdf/Cargo.toml

echo "=== fixtures: node dependencies"
# The versions are printed rather than pinned. What Node is only matters
# as a floor — 20+, for `node --test` and the ESM loading this tree
# assumes — because yjs and the rest come from the lockfile and esbuild's
# actual work happens inside a pinned native binary.
node --version
npm --version

# The runner clones a fresh worktree per branch, so fixtures/node_modules
# (gitignored, like any node_modules) never carries over from a previous
# run. npm ci reads fixtures/package-lock.json rather than resolving from
# package.json, so the installed yjs is exactly the pinned 13.6.27 and not
# whatever a bare `npm install` would pick today.
#
# NPM_CACHE exists because the cache directory is the one part of this
# that is not the same everywhere. The runner's HOME is writable and
# $HOME/.npm is right there; a sandboxed local run may have neither, and
# npm then fails with an EROFS on its cache rather than with anything to
# do with the install. Point NPM_CACHE somewhere writable in that case.
echo "=== fixtures: npm ci (needs the registry — see .amadan/README.md)"
(cd fixtures && npm ci --cache "${NPM_CACHE:-$HOME/.npm}")

echo "=== fixtures: the JS side's own tests"
# From fixtures/ with an explicit glob, never `node --test fixtures/`:
# Node 24 takes the directory argument as a test file, runs nothing and
# exits 0, which is a green-looking way to run no tests at all.
(cd fixtures && node --test *.test.mjs)

echo "=== the vectors must match a fresh generation"
# fixtures/vectors is the contract between the two implementations of the
# op vocabulary (spec §2.8): generate.mjs builds each deck through
# ops.mjs and writes the Y update plus the JS side's view of it, and
# internal/slides/core/vectors_test.go — run by `make ci` below — loads
# the same bytes through the Rust core and requires the same view.
#
# A vector nobody regenerated is the failure this catches, and it is a
# silent one: the Go test still passes, because it is comparing the core
# against a JS implementation that no longer exists. Both sides change or
# neither does.
(cd fixtures && node generate.mjs)
require_committed fixtures/vectors \
  "vectors are stale: run node fixtures/generate.mjs and commit"

echo "=== the editor bundle must match a fresh build"
# build-bundles.mjs is byte-reproducible: no sourcemap, no banner, no
# timestamp, no absolute path in the output, and esbuild's work happens
# in the pinned 0.28.2 native binary rather than in interpreted JS, so
# the Node running it does not enter into the result either.
#
# This is the same check as the wasm cmp above and it is here for the
# same reason: nothing builds the bundle at run time, the artifact is
# committed, and a server handed a stale one serves it perfectly happily.
# assets_embed.go's exactly-one rule makes a leftover hashed file a panic
# at boot rather than a wrong editor, which is why the untracked half of
# require_committed matters here too.
node fixtures/build-bundles.mjs
require_committed internal/slides/static \
  "the editor bundle is stale: run node fixtures/build-bundles.mjs and commit internal/slides/static"

echo "=== go: vet, gofmt, test, migration check"
# The Makefile's own ci target, so the Go half of this gate and the
# `make ci` you run between edits cannot come apart.
make ci

echo "gate: ok"
