#!/usr/bin/env bash
# Generate a fresh test-vector corpus into a scratch directory.
# Writes corpus.json next to this script with the temp keystore path and
# per-vector expected outcomes.
#
# The corpus is regenerated on every run -- intentionally not checked in.
# Test vectors should always reflect the current sign / verify code path,
# not a frozen snapshot from an older release.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
CORPUS_JSON="$SCRIPT_DIR/corpus.json"

# Locate the treeship binary. The orchestrator (run.sh) is responsible
# for ensuring an up-to-date debug build, so we prefer debug -- a stale
# release build sitting around from a prior `cargo build --release`
# would otherwise silently shadow the freshly-built debug and run the
# wrong code under test. TREESHIP_BIN env var still wins for callers
# who genuinely want to test a specific binary (CI release smoke).
if [[ -n "${TREESHIP_BIN:-}" ]]; then
  TS="$TREESHIP_BIN"
elif [[ -x "$REPO_ROOT/target/debug/treeship" ]]; then
  TS="$REPO_ROOT/target/debug/treeship"
elif [[ -x "$REPO_ROOT/target/release/treeship" ]]; then
  TS="$REPO_ROOT/target/release/treeship"
else
  echo "error: no treeship binary found. Build with 'cargo build' first." >&2
  exit 2
fi

# Always work in a fresh scratch dir so vector IDs are deterministic-ish
# (within a single run) and the real keystore stays untouched.
SCRATCH="$(mktemp -d -t treeship-cross-sdk-XXXXXX)"
CFG="$SCRATCH/config.json"
echo "scratch keystore: $SCRATCH" >&2

trap 'echo "(scratch keystore left at $SCRATCH for inspection)" >&2' EXIT

"$TS" --config "$CFG" --quiet init

# --- valid vectors ----------------------------------------------------------

extract_id() {
  python3 -c 'import sys, json; print(json.load(sys.stdin)["id"])'
}

ACTION_ID=$(
  "$TS" --config "$CFG" --format json attest action \
    --actor "agent://cross-sdk-test" --action "tool.call" \
    | extract_id
)

DECISION_ID=$(
  "$TS" --config "$CFG" --format json attest decision \
    --actor "agent://cross-sdk-test" --model "claude-opus-4-7" \
    | extract_id
)

# Approval requires a description; nonce is auto-generated by the CLI.
# --unscoped is the explicit opt-in for bearer approvals, required since
# v0.9.6 made scoped the safe default. This vector exists to test that
# the bare approval shape still serializes/verifies cleanly across both
# SDKs; a separate vector below tests a scoped approval.
APPROVAL_ID=$(
  "$TS" --config "$CFG" --format json attest approval \
    --approver "human://test-suite" --description "ok to proceed" \
    --unscoped \
    | extract_id
)

# --- broken-chain vectors ---------------------------------------------------

# Tampered payload: take an action with a UNIQUE actor/action pair and
# flip a byte in its stored signature. Verify must reject (the signature
# bytes no longer match the canonical payload).
#
# The actor/action must differ from the valid action vector above --
# Treeship artifacts are content-addressed, so identical inputs collapse
# to the same artifact id. Sharing the id between "valid" and "tampered"
# vectors would mean tampering corrupts both at once, and the suite would
# silently report a divergence-free FAIL/FAIL instead of catching the
# tamper-detection contract.
TAMPER_ID=$(
  "$TS" --config "$CFG" --format json attest action \
    --actor "agent://cross-sdk-test-tamper" --action "tool.call.tampered" \
    | extract_id
)
TAMPER_PATH="$SCRATCH/artifacts/$TAMPER_ID.json"
if [[ ! -f "$TAMPER_PATH" ]]; then
  echo "error: expected tampered artifact at $TAMPER_PATH" >&2
  exit 3
fi
python3 - "$TAMPER_PATH" <<'PY'
import json, sys
p = sys.argv[1]
obj = json.load(open(p))
# Mutate the DSSE signature -- flip one base64url character. The signature
# bytes no longer match the payload, so verify must reject. Targeting the
# signature instead of the payload is more robust: any field outside the
# envelope (artifact_id, digest, etc.) is recomputed by the verifier and
# wouldn't be caught as "bad signature" -- it'd be "wrong digest" or
# silently ignored.
sig = obj["envelope"]["signatures"][0]["sig"]
flipped = ("B" if sig[0] != "B" else "C") + sig[1:]
obj["envelope"]["signatures"][0]["sig"] = flipped
open(p, "w").write(json.dumps(obj, indent=2))
PY

# --- write corpus.json -----------------------------------------------------

python3 - "$CFG" "$ACTION_ID" "$DECISION_ID" "$APPROVAL_ID" "$TAMPER_ID" <<'PY' > "$CORPUS_JSON"
import json, sys
cfg, action_id, decision_id, approval_id, tamper_id = sys.argv[1:6]
out = {
    "config_path": cfg,
    "vectors": [
        {
            "name": "valid.action.tool-call",
            "artifact_id": action_id,
            "category": "valid",
            "expected_outcome": "pass",
            # Lone artifact, no parent: chain length is 1.
            "expected_chain": 1,
        },
        {
            "name": "valid.decision",
            "artifact_id": decision_id,
            "category": "valid",
            "expected_outcome": "pass",
            # `treeship attest decision` auto-parents to the most recent
            # artifact in the keystore (the action above), so verifying
            # the decision walks the parent chain and returns 2.
            "expected_chain": 2,
        },
        {
            "name": "valid.approval",
            "artifact_id": approval_id,
            "category": "valid",
            "expected_outcome": "pass",
            # Approvals do NOT auto-parent (they're roots of new
            # approval trees), so chain length is 1 even when the
            # keystore already has artifacts.
            "expected_chain": 1,
        },
        {
            "name": "broken.tampered-payload",
            "artifact_id": tamper_id,
            "category": "broken-chain",
            "expected_outcome": "fail",
            # Per the SDK contract, `chain` on outcome=fail reports the
            # NUMBER OF FAILED checks, not chain length. One artifact's
            # signature was tampered, so one check failed.
            "expected_chain": 1,
        },
    ],
}
json.dump(out, sys.stdout, indent=2)
PY

echo "wrote $CORPUS_JSON" >&2
