# Shared bats harness for antidote tests.
#
# Two styles, in order of preference:
#
# 1. Canonical bats (default): `antidote_test_home` in setup(), then one
#    `run antidote ...` per statement with focused asserts. Works for
#    anything whose state lives on disk (clones, files). The antidote()
#    wrapper runs antidote.zsh as a subprocess in an isolated HOME.
#    Extra zstyles go in $ZSTYLES; use subenv_output to fold tmpdir
#    paths back to $HOME/$ANTIDOTE_HOME before asserting.
#
#    When a file's tests all want the standard fixture clones, pair
#    `setup_file() { antidote_fixture_proto; }` with
#    `antidote_test_home_cached` in setup(). That clones once per file
#    instead of once per test; the clone dominates fixture test time.
#
# 2. run_session: for behavior that needs a live zsh session (dynamic
#    `antidote init` mode, setopts, parent-shell wrappers like load and
#    autoloading). Arrange steps go in SESSION_PRELUDE; the session
#    body should act and emit facts (labeled lines, raw values), with
#    judgment done bats-side via assert_line/assert_output. Never emit
#    a verdict the session already decided: `[[ $x == y ]] && echo ok`
#    tells a failure nothing about what x was, so print x and assert on
#    it instead.
#
#    Exit codes: end the body with the command under test and use
#    assert_success/assert_failure. `echo "exit: $?"` is only for a
#    code from the middle of a body, which $status cannot reach.
#
# Assertions: prefer bats-assert (assert_output, assert_line --index,
# assert_output --partial, refute_*). Keep `expect` for whole-output
# golden compares. It prints a unified diff on failure. Golden blocks
# under ~20 lines stay inline in the test; larger or shared contracts
# live in tests/testdata (eg: usage_dispatch.txt).

# Vendored assertion libs (tests/bats/lib): assert_success, assert_line,
# assert_output, refute_*, etc.
source "${BASH_SOURCE[0]%/*}/../lib/bats-support/load.bash"
source "${BASH_SOURCE[0]%/*}/../lib/bats-assert/load.bash"

# pintest/pinme fixture commits, oldest to newest (generated by
# tests/bin/init_fixtures.zsh; v1.2.0 plays the bad supply chain HEAD).
PIN_V100=64642c5691051ba0d82f5bda60b745f6fd042325
PIN_V110=c87216c18d3f0301fa1ed669b6c1ad76056271ca
PIN_V120=d54e0cad999d196822584f2cca72f7c7bd908ea9

antidote_common_setup() {
  cd "$BATS_TEST_DIRNAME" || return 1
  while [[ ! -f antidote.zsh && "$PWD" != / ]]; do cd ..; done
  [[ -f antidote.zsh ]] || return 1
  PRJDIR=$PWD
  # Version tests assert against the source of truth, so version bumps
  # never touch test files.
  EXPECTED_VERSION=$(sed -n 's/.*_ANTIDOTE_VERSION="\([^"]*\)".*/\1/p' antidote.zsh)
  [ -n "$EXPECTED_VERSION" ] || return 1
}

# Locate the generated git fixtures (tests/run pre-generates them).
antidote_fixture_dir() {
  local d="$PRJDIR/tests/fixtures"
  if [[ -d "$d/bare" && -f "$d/.git_version" &&
        "$(cat "$d/.git_version")" == "$(git --version)" ]]; then
    echo "$d"
  else
    echo /tmp/antidote-fixtures
  fi
}

# Build an isolated HOME for subprocess tests: tmp_home contents, the
# test config, and a gitconfig that maps fakegitsite.com to the local
# bare fixtures. Exports TESTHOME, ZDOTDIR, AHOME, and ACONFIG.
# Takes an optional base dir (default $BATS_TEST_TMPDIR) so setup_file
# can build a prototype home outside any single test's tmpdir.
antidote_test_home() {
  local base=${1:-$BATS_TEST_TMPDIR}
  # Resolve symlinks (macOS /var/folders -> /private/var) so subprocess
  # path prefix checks against $HOME hold.
  mkdir -p "$base/home"
  TESTHOME="$(cd "$base/home" && pwd -P)"
  ZDOTDIR="$TESTHOME/.zsh"
  AHOME="$TESTHOME/.cache/antidote"
  ACONFIG="$TESTHOME/.config/antidote/test_config.zsh"
  mkdir -p "$ZDOTDIR" "$AHOME"
  cp -Rf "$PRJDIR/tests/tmp_home/." "$TESTHOME"
  local fixdir
  fixdir=$(antidote_fixture_dir)
  sed "s|/[^ \"]*/tests/fixtures/|${fixdir}/|g" "$fixdir/gitconfig" >"$TESTHOME/.gitconfig"
  # background maintenance off since detached git gc processes race
  # teardown's rm -rf of the test home.
  printf '[gc]\n\tauto = 0\n[maintenance]\n\tauto = false\n' >>"$TESTHOME/.gitconfig"
}

# Run antidote (or `antidote __private__ <fn>`) as a subprocess in the
# isolated test home. Extra zstyles go in $ZSTYLES.
# Set AHOME="" to test antidote's own ANTIDOTE_HOME resolution.
# Set ACONFIG="" to test antidote's own config file discovery.
antidote() {
  # EXTRA_ENV stays unquoted on purpose: it word-splits into VAR=value
  # assignments, and quoting it hands env an empty-string utility name.
  ( cd "$TESTHOME" && env \
      -u XDG_CACHE_HOME -u XDG_DATA_HOME -u XDG_CONFIG_HOME \
      -u ANTIDOTE_CONFIG \
      HOME="$TESTHOME" ZDOTDIR="$ZDOTDIR" T_PRJDIR="$PRJDIR" \
      ${AHOME:+ANTIDOTE_HOME="$AHOME"} \
      ${ACONFIG:+ANTIDOTE_CONFIG="$ACONFIG"} \
      ANTIDOTE_ZSTYLES="${ZSTYLES:-}" ${EXTRA_ENV:-} \
      zsh "$PRJDIR/antidote.zsh" "$@" )
}

# Clone the standard test fixtures into the test home, quietly.
antidote_clone_fixtures() {
  antidote bundle <"$ZDOTDIR/.base_test_fixtures.txt" &>/dev/null
}

# setup_file() half of the cached-fixture pair: build one prototype home
# with the fixtures already cloned, in $BATS_FILE_TMPDIR. Pair with
# antidote_test_home_cached in setup(). Cloning is the dominant cost of
# a fixture test, so doing it once per file beats once per test.
antidote_fixture_proto() {
  antidote_common_setup || return 1
  antidote_test_home "$BATS_FILE_TMPDIR/proto"
  antidote_clone_fixtures
}

# setup() half: copy the prototype into this test's tmpdir so each test
# still gets a private writable home, without re-cloning.
antidote_test_home_cached() {
  cp -R "$BATS_FILE_TMPDIR/proto/home" "$BATS_TEST_TMPDIR/home"
  TESTHOME="$(cd "$BATS_TEST_TMPDIR/home" && pwd -P)"
  ZDOTDIR="$TESTHOME/.zsh"
  AHOME="$TESTHOME/.cache/antidote"
  ACONFIG="$TESTHOME/.config/antidote/test_config.zsh"
}

# git against the test home, so the fixture gitconfig (insteadOf rules
# for fakegitsite.com) applies to fetch/pull.
tgit() {
  HOME="$TESTHOME" git "$@"
}

# Make a bundle's full history available for a test that needs it. A
# clone usually deepens itself, in which case --unshallow errors, so ask
# for the end state rather than running the command blind.
tgit_deepen() {
  [ "$(tgit -C "$1" rev-parse --is-shallow-repository)" = false ] && return 0
  tgit -C "$1" fetch --quiet --unshallow
}

# SESSION_PRELUDE, when set, is injected after setup. Use it for
# per-file shorthand. SESSION_SETUP overrides the setup function
# (default t_setup; real tests use t_setup_real). $status is the exit
# of the LAST body command (teardown still runs), so a session ending
# in the probe command can assert_success/assert_failure directly.
# Never pipe INTO run_session: `run` would execute in a pipeline
# subshell and $output would be lost.
run_session() {
  local script="$BATS_TEST_TMPDIR/session.zsh"
  {
    echo "source ${PRJDIR}/tests/__init__.zsh || exit 9"
    echo "${SESSION_SETUP:-t_setup} || exit 9"
    [ -n "${SESSION_PRELUDE:-}" ] && printf '%s\n' "$SESSION_PRELUDE"
    cat
    echo '__t_status=$?'
    echo "t_teardown"
    echo 'exit $__t_status'
  } >"$script"
  run env -i PATH="$PATH" PAGER=cat TERM=dumb zsh -f "$script"
}

# Like run_session, but clones the standard local fixtures first.
# Per-test arrange steps still go in SESSION_PRELUDE; they run after
# the fixture clone.
fixture_session() {
  SESSION_PRELUDE='antidote bundle <$ZDOTDIR/.base_test_fixtures.txt &>/dev/null
'"${SESSION_PRELUDE:-}" run_session
}

# tests/bin/subenv, the same filter session tests pipe through, fed the
# test home's values instead of the bats process's own env.
subenv() {
  env HOME="$TESTHOME" ZDOTDIR="$ZDOTDIR" \
    ${AHOME:+ANTIDOTE_HOME="$AHOME"} \
    "$PRJDIR/tests/bin/subenv" "$@"
}

# Rewrite $output (and $lines) through subenv after a run, so asserts can
# compare against $VAR placeholders instead of tmpdir paths.
subenv_output() {
  output=$(subenv "$@" <<<"$output")
  mapfile -t lines <<<"$output"
}

# Compare $output against an expected string, showing a diff on failure.
expect() {
  if [ "$output" != "$1" ]; then
    echo "=== diff (expected vs got) ==="
    diff <(printf '%s\n' "$1") <(printf '%s\n' "$output") || true
    return 1
  fi
}
