#!/usr/bin/env bash
#
# Generate the jj/git fixture corpus the indexer is tested against.
#
# Spec §12: "a script that drives the real jj and git CLIs to produce repositories
# exhibiting every state the indexer must handle. Commit the script, generate the
# fixtures in CI. Never hand-author Git objects."
#
# Output: fixtures/repos/<case>.git — bare repositories, exactly what Dogfood
# stores on disk. The indexer reads these; nothing here is hand-written.
#
# Usage:  fixtures/gen.sh [output_dir]
# Env:    JJ_VERSION  pin (default below). CI must pin so a jj format change fails
#                     a test instead of silently corrupting the index.

set -euo pipefail

JJ_VERSION="${JJ_VERSION:-0.43.0}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT="${1:-$ROOT/repos}"
BIN="$ROOT/bin"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

# ─── toolchain ───────────────────────────────────────────────────────────────

export PATH="$BIN:$PATH"

if ! command -v jj >/dev/null 2>&1 || [[ "$(jj --version | awk '{print $2}' | cut -d- -f1)" != "$JJ_VERSION" ]]; then
    echo "==> fetching jj $JJ_VERSION"
    mkdir -p "$BIN"
    curl -fsSL "https://github.com/jj-vcs/jj/releases/download/v${JJ_VERSION}/jj-v${JJ_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
        | tar xz -C "$BIN" jj
    chmod +x "$BIN/jj"
fi

echo "==> jj: $(jj --version)"
echo "==> git: $(git --version)"

# Deterministic identity and timestamps. Without pinned times the change ids stay
# stable (they are random) but the commit ids do not, and the tests assert on the
# relationship between them, not on literal values — so we pin what we can and
# the corpus manifest records the rest.
export JJ_CONFIG="$WORK/jjconfig.toml"
cat > "$JJ_CONFIG" <<'EOF'
[user]
name = "Fixture Author"
email = "fixture@dogfood.sh"

[ui]
paginate = "never"
color = "never"

# The `rewritten` case force-pushes the same change five times. jj defaults to
# treating commits reachable from a remote bookmark as immutable, which is right
# for humans and wrong for a fixture that must simulate exactly that workflow.
[revset-aliases]
"immutable_heads()" = "none()"
EOF

export GIT_AUTHOR_NAME="Fixture Author"
export GIT_AUTHOR_EMAIL="fixture@dogfood.sh"
export GIT_COMMITTER_NAME="Fixture Author"
export GIT_COMMITTER_EMAIL="fixture@dogfood.sh"

rm -rf "$OUT"
mkdir -p "$OUT"

MANIFEST="$OUT/manifest.json"
# Opened here, closed at the bottom. `note` appends ",<key>:<value>" entries, so
# the object starts with a sentinel key to make the leading comma always valid.
printf '{"jj_version":"%s","cases":{"_generated_by":"fixtures/gen.sh"' "$JJ_VERSION" > "$MANIFEST.tmp"

# Record a case's interesting revisions into the manifest so Rust tests can assert
# against named commits without re-deriving them.
note() { # note <case> <json>
    printf ',"%s":%s' "$1" "$2" >> "$MANIFEST.tmp"
}

# Publish a working repo as a bare repo under $OUT, the way a push would land it.
publish() { # publish <case> <workdir> [default_branch]
    local case="$1" work="$2" head="${3:-main}"
    git init --bare -q "$OUT/$case.git"
    git -C "$work" push -q --all "$OUT/$case.git"
    git -C "$work" push -q --tags "$OUT/$case.git" 2>/dev/null || true
    # `git init --bare` leaves HEAD on refs/heads/master, which none of these
    # repos have. Dogfood creates repos with HEAD pointing at the default
    # bookmark, so the corpus must match or every fixture has an unborn HEAD.
    git --git-dir="$OUT/$case.git" symbolic-ref HEAD "refs/heads/$head"
}

cid() { jj -R "$1" log --no-graph --ignore-working-copy -T 'commit_id' -r "$2"; }
chid() { jj -R "$1" log --no-graph --ignore-working-copy -T 'change_id' -r "$2"; }

# ─────────────────────────────────────────────────────────────────────────────
# case: basic — jj-authored linear history, one bookmark
#
# The baseline. Every commit carries a `change-id` header.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: basic"
W="$WORK/basic"
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    echo "one" > a.txt
    jj describe -m "add a" >/dev/null
    jj new -m "add b" >/dev/null
    echo "two" > b.txt
    jj new -m "add c" >/dev/null
    echo "three" > c.txt
    jj bookmark create main -r @- >/dev/null
)
note basic "$(printf '{"head_change":"%s","head_commit":"%s"}' "$(chid "$W" 'bookmarks(main)')" "$(cid "$W" 'bookmarks(main)')")"
publish basic "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: rewritten — the property the whole product rests on
#
# One change, rewritten five times (spec §5: "a change that is rewritten five
# times"). The change id must be identical across all five; the commit ids must
# all differ. Every intermediate commit is kept reachable via a tag so the bare
# repo actually contains them and the test can walk the whole rewrite chain.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: rewritten"
W="$WORK/rewritten"
jj git init --colocate "$W" >/dev/null 2>&1
git init --bare -q "$OUT/rewritten.git"
(cd "$W" && jj git remote add origin "$OUT/rewritten.git" >/dev/null 2>&1)
(
    cd "$W"
    echo "v1" > f.txt
    jj describe -m "evolving change v1" >/dev/null
    jj bookmark create main -r @ >/dev/null
    jj git push -b main --allow-empty-description >/dev/null 2>&1
)
TARGET="$(chid "$W" 'bookmarks(main)')"
REWRITES="$(cid "$W" 'bookmarks(main)')"
for i in 2 3 4 5; do
    (
        cd "$W"
        echo "v$i" > f.txt
        jj describe -r 'bookmarks(main)' -m "evolving change v$i" >/dev/null
        # Force-push each rewrite, exactly as a user amending and re-pushing does.
        # The bare repo accumulates every revision as an unreferenced object, which
        # is precisely the state the indexer sees in production.
        jj git push -b main >/dev/null 2>&1
    )
    REWRITES="$REWRITES $(cid "$W" 'bookmarks(main)')"
done
git --git-dir="$OUT/rewritten.git" symbolic-ref HEAD refs/heads/main
# All five commit ids must differ; the change id must be identical across them.
note rewritten "$(printf '{"stable_change":"%s","revisions":["%s"]}' \
    "$TARGET" "$(echo "$REWRITES" | sed 's/ /","/g')")"

# ─────────────────────────────────────────────────────────────────────────────
# case: plain-git — no change-id header anywhere
#
# Drives the synthetic-identity fallback (spec §4). extract_change_id must return
# None for every commit here.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: plain-git"
W="$WORK/plaingit"
git init -q "$W"
(
    cd "$W"
    git symbolic-ref HEAD refs/heads/main
    echo "x" > f.txt && git add -A && git commit -qm "plain commit one"
    echo "y" >> f.txt && git add -A && git commit -qm "plain commit two"
)
note plain_git "$(printf '{"head_commit":"%s"}' "$(git -C "$W" rev-parse HEAD)")"
publish plain-git "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: mixed — jj commits on top of plain-git commits in one history
#
# The realistic case for a repo migrated to jj. The indexer must produce real
# changes for the jj commits and synthetic ones for the git commits, in a single
# connected graph.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: mixed"
W="$WORK/mixed"
git init -q "$W"
(
    cd "$W"
    git symbolic-ref HEAD refs/heads/main
    echo "legacy" > old.txt && git add -A && git commit -qm "pre-jj history"
)
(
    cd "$W"
    jj git init --colocate . >/dev/null 2>&1
    jj new main -m "first jj change on top" >/dev/null
    echo "new" > new.txt
    jj bookmark set main -r @ >/dev/null 2>&1 || jj bookmark create main -r @ >/dev/null
)
publish mixed "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: stack — a chain of changes, none merged
#
# Exercises stack edge computation (spec §4). Four changes stacked on main.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: stack"
W="$WORK/stack"
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    echo "base" > base.txt
    jj describe -m "base of stack" >/dev/null
    jj bookmark create main -r @ >/dev/null
    for n in 1 2 3 4; do
        jj new -m "stacked change $n" >/dev/null
        echo "layer $n" > "layer$n.txt"
    done
    jj bookmark create top -r @ >/dev/null
)
note stack "$(printf '{"bottom":"%s","top":"%s"}' "$(chid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(top)')")"
publish stack "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: merge — a two-parent jj commit
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: merge"
W="$WORK/merge"
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    # NOTE: revisions are addressed by captured change id, never by a
    # description() revset. In jj 0.43 `description("x")` is an EXACT match, not a
    # substring match (substring: needs an explicit `substring:` prefix), which
    # silently returns the empty set and produces a corrupt fixture.
    echo "base" > base.txt
    jj describe -m "merge base" >/dev/null
    BASE="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
    jj new -m "left side" >/dev/null && echo L > l.txt
    LEFT="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
    jj new -m "right side" "$BASE" >/dev/null && echo R > r.txt
    RIGHT="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
    jj new "$LEFT" "$RIGHT" -m "the merge" >/dev/null
    jj bookmark create main -r @ >/dev/null
)
note merge "$(printf '{"merge_commit":"%s","merge_change":"%s"}' "$(cid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(main)')")"
publish merge "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: conflict — a real 2-sided conflict
#
# Produces the `jj:trees` + `jj:conflict-labels` headers and the
# .jjconflict-side-N / .jjconflict-base-N tree layout documented in
# docs/change-id-format.md §6. Drives conflict detection and the conflict viewer.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: conflict"
W="$WORK/conflict"
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    printf 'line1\nline2\nline3\n' > c.txt
    jj describe -m "conflict base" >/dev/null
    BASE="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
    jj new -m "side A" >/dev/null && printf 'line1\nAAA\nline3\n' > c.txt
    SIDEA="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
    jj new -m "side B" "$BASE" >/dev/null && printf 'line1\nBBB\nline3\n' > c.txt
    SIDEB="$(jj log --no-graph --ignore-working-copy -T change_id -r @)"
    # Both sides edit line 2 of c.txt from the same base -> a real 2-sided conflict.
    jj new "$SIDEA" "$SIDEB" -m "conflicted merge" >/dev/null
    jj bookmark create main -r @ >/dev/null
)
note conflict "$(printf '{"conflicted_commit":"%s","conflicted_change":"%s"}' "$(cid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(main)')")"
publish conflict "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: signed — change-id alongside a multi-line gpgsig header
#
# The parser hazard from docs/change-id-format.md §2: a naive line scan can walk
# into the base64 signature body. Uses an SSH signing key so CI needs no GPG.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: signed"
W="$WORK/signed"
KEY="$WORK/signkey"
ssh-keygen -t ed25519 -N "" -f "$KEY" -q
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    git config gpg.format ssh
    git config user.signingkey "$KEY.pub"
    echo "signed content" > s.txt
    jj describe -m "a signed change" >/dev/null
    jj sign -r @ \
        --config 'signing.behavior="own"' \
        --config 'signing.backend="ssh"' \
        --config "signing.key=\"$KEY\"" >/dev/null 2>&1
    jj bookmark create main -r @ >/dev/null
)
note signed "$(printf '{"signed_commit":"%s","signed_change":"%s"}' "$(cid "$W" 'bookmarks(main)')" "$(chid "$W" 'bookmarks(main)')")"
publish signed "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: renames — for comment anchor rebasing across a file rename (spec §5)
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: renames"
W="$WORK/renames"
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    printf 'alpha\nbravo\ncharlie\ndelta\necho\n' > original.txt
    jj describe -m "before rename" >/dev/null
    jj bookmark create main -r @ >/dev/null
    jj new -m "rename and edit" >/dev/null
    git mv original.txt renamed.txt 2>/dev/null || mv original.txt renamed.txt
    printf 'alpha\nbravo\nCHARLIE\ndelta\necho\nfoxtrot\n' > renamed.txt
    jj bookmark create renamed -r @ >/dev/null
)
publish renames "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: whitespace — anchor rebasing must not treat reindentation as a content
# change (spec §5: "whitespace-only changes")
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: whitespace"
W="$WORK/whitespace"
jj git init --colocate "$W" >/dev/null 2>&1
(
    cd "$W"
    printf 'fn main() {\nlet x = 1;\nprintln!("{}", x);\n}\n' > m.rs
    jj describe -m "unindented" >/dev/null
    jj bookmark create main -r @ >/dev/null
    jj new -m "reindent only" >/dev/null
    printf 'fn main() {\n    let x = 1;\n    println!("{}", x);\n}\n' > m.rs
    jj bookmark create reindented -r @ >/dev/null
)
publish whitespace "$W"

# ─────────────────────────────────────────────────────────────────────────────
# case: hostile — inputs the security tests in spec §9 need
#
# A repo containing a symlink escaping the repo root, a path with unusual
# characters, and a deeply nested tree. Serving any of these naively is a CVE.
# ─────────────────────────────────────────────────────────────────────────────
echo "==> case: hostile"
W="$WORK/hostile"
git init -q "$W"
(
    cd "$W"
    git symbolic-ref HEAD refs/heads/main
    ln -s /etc/passwd escape-absolute
    ln -s ../../../../etc/shadow escape-relative
    mkdir -p a/b/c/d/e/f/g/h
    echo "deep" > a/b/c/d/e/f/g/h/deep.txt
    printf 'no trailing newline' > weird-name$'\t'tab.txt 2>/dev/null || echo skip > normal.txt
    echo "content" > "spaces in name.txt"
    git add -A && git commit -qm "hostile paths"
)
publish hostile "$W"

# ─── finish ──────────────────────────────────────────────────────────────────

printf '}}' >> "$MANIFEST.tmp"
# Reformat, and fail loudly on malformed JSON rather than shipping a corpus whose
# manifest the Rust tests cannot parse.
python3 -c "
import json
raw = open('$MANIFEST.tmp').read()
json.dump(json.loads(raw), open('$MANIFEST','w'), indent=2)
print('==> manifest ok')
"
rm -f "$MANIFEST.tmp"

echo
echo "==> corpus written to $OUT"
ls -1 "$OUT" | sed 's/^/    /'
