#!/bin/bash
#
# test-install-dotfiles - exercise install-dotfiles against a throwaway $HOME
#
# Usage: ./dots/tests/test-install-dotfiles   (or via ./dots/tests/run-tests)
#
# Exits non-zero if any assertion fails, so it is usable from CI.
#
# Everything happens under a temp dir used as $HOME. Nothing here touches the real one.

set -o errexit
set -o nounset
set -o pipefail

cd "$(dirname "$0")/../.."
REPO="$PWD"

# shellcheck source=dots/tests/lib.sh
source "$REPO/dots/tests/lib.sh"
trap cleanup_fake_homes EXIT

# ============ Tests ============

function run_tests() {
  local out out2 before rc stale leftovers

  echo "===== a clean install ====="
  new_fake_home
  install_into_fake_home > /dev/null

  assert_symlink_to '.bashrc'    "$REPO/dots/all-os/flat/bashrc.sh"  'flat tree: .bashrc links to bashrc.sh'
  assert_symlink_to '.profile'   "$REPO/dots/all-os/flat/profile.sh" 'flat tree: .profile links to profile.sh'
  assert_symlink_to '.zshrc'     "$REPO/dots/all-os/flat/zshrc.zsh"  'munging: .zshrc comes from zshrc.zsh'
  assert_symlink_to '.hushlogin' "$REPO/dots/all-os/flat/hushlogin"  'no extension to strip: .hushlogin'
  assert_symlink_to '.gitconfig' "$REPO/dots/macos/flat/gitconfig"    'OS-specific file shadows all-os: .gitconfig'
  assert_symlink_to '.dotlib'    "$REPO/dots/all-os/flat/dotlib"     'a directory in the flat tree links whole: .dotlib'
  assert_symlink_to '.hgignore_global' "$REPO/dots/all-os/flat/hgignore_global" \
      'flat tree: a symlink source installs as a link to the symlink itself'

  assert_symlink_to '.config/btop/btop.conf'   "$REPO/dots/all-os/nested/_config/btop/btop.conf"  'nested tree: nested file is linked'
  assert_symlink_to '.config/fish/config.fish' "$REPO/dots/all-os/nested/_config/fish/config.fish" 'nested tree: another nested file'
  assert_symlink_to '.config/htop/htoprc'      "$REPO/dots/all-os/nested/_config/htop/htoprc"     'nested tree: two levels deep'
  assert_real_dir   '.config'                                                      'nested tree: .config is a real dir, not a link'
  assert_real_dir   '.config/btop'                                                 'nested tree: intermediate dirs are real'

  assert_absent '.gitkeep' 'literal-dot repo file (macos/nested/.gitkeep) is never linked'

  # The trees are siblings so the flat scanner can't see nested/ as an entry to link. An
  # earlier layout put it inside flat/, and the scanner duly linked the whole thing into ~.
  assert_absent '.nested' 'the nested/ tree is not itself linked into ~'
  assert_absent '.flat'   'the flat/ tree is not itself linked into ~'

  assert_symlink_to 'bin' "$REPO/home-bin" 'the ~/bin dir links to the repo home-bin/'

  echo "===== the invocation path doesn't leak into the links ====="
  # install_into_fake_home calls the installer by absolute path, so nothing above here
  # exercises a relative invocation -- which is how the README says to run it, and which
  # used to write a self-referential "./bin" into ~/bin.
  new_fake_home
  ( cd "$REPO" && HOME="$FAKE_HOME" ./install-dotfiles ) > /dev/null 2>&1
  assert_symlink_to 'bin' "$REPO/home-bin" 'run as ./install-dotfiles, ~/bin is still absolute'
  new_fake_home
  ( cd "$REPO/dots" && HOME="$FAKE_HOME" ../install-dotfiles ) > /dev/null 2>&1
  assert_symlink_to 'bin' "$REPO/home-bin" 'run from a subdir as ../install-dotfiles, likewise'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" \
      'relative invocation gets the flat tree right too'

  echo "===== a repo path with a dot component still installs the nested tree ====="
  # Regression test: the nested-tree find used to filter on find's ABSOLUTE path, so
  # '*/.* ' also matched a dot component in an ancestor dir (e.g. ~/.dotfiles) and
  # silently skipped every nested file, while the flat tree (which uses ls) kept working.
  new_fake_home
  local dotpath
  dotpath=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test.XXXXXX")
  dotpath=$(cd "$dotpath" && pwd)  # collapse a double slash from a trailing-slash $TMPDIR,
                                    # same normalization install-dotfiles applies to itself
  ln -s "$REPO" "$dotpath/.repo-link"
  HOME="$FAKE_HOME" "$dotpath/.repo-link/install-dotfiles" > /dev/null 2>&1
  # The recorded link target is the invocation path (through .repo-link), not $REPO -- the
  # installer never resolves symlinks in its own location, same as the existing
  # relative-invocation tests above expect an absolute (not resolved) source.
  assert_symlink_to '.config/btop/btop.conf' \
      "$dotpath/.repo-link/dots/all-os/nested/_config/btop/btop.conf" \
      'nested tree installs even when invoked through a path with a dot component'
  rm -rf "$dotpath"

  echo "===== underscore-to-dot munging ====="
  mkdir -p "$REPO/dots/all-os/nested/_munge-test/completions"
  echo 'test content' > "$REPO/dots/all-os/nested/_munge-test/_leaf"
  # Shaped like the case this escape exists for: a zsh completion function, whose name
  # really does start with an underscore.
  echo 'compdef stub' > "$REPO/dots/all-os/nested/_munge-test/completions/__zcomp"
  install_into_fake_home > /dev/null
  assert_symlink_to '.munge-test/.leaf' "$REPO/dots/all-os/nested/_munge-test/_leaf" \
      'both a dir and a leaf file with a leading _ munge to a leading .'
  assert_symlink_to '.munge-test/completions/_zcomp' \
      "$REPO/dots/all-os/nested/_munge-test/completions/__zcomp" \
      'a leading __ escapes to a literal leading _'
  rm -rf "$REPO/dots/all-os/nested/_munge-test"

  echo "===== \$OSNAME shadows all-os in the nested tree ====="
  new_fake_home
  mkdir -p "$REPO/dots/macos/nested/_config/btop"
  echo 'macos override' > "$REPO/dots/macos/nested/_config/btop/btop.conf"
  install_into_fake_home > /dev/null
  assert_symlink_to '.config/btop/btop.conf' "$REPO/dots/macos/nested/_config/btop/btop.conf" \
      'macos/nested overrides all-os/nested on a matching target'
  rm -rf "$REPO/dots/macos/nested/_config"

  echo "===== idempotence ====="
  new_fake_home
  install_into_fake_home > /dev/null
  out2=$(install_into_fake_home -v)
  # The target is printf-padded to 18 columns, so keep a trailing space in the needle: it
  # anchors the end of the name and stops '.bashrc' from matching a longer '.bashrcfoo'.
  assert_contains "$out2" 'Already set up: .bashrc ' 'second run reports flat-tree file unchanged'
  assert_contains "$out2" 'Already set up: .config/btop/btop.conf ' \
      'second run reports nested-tree file unchanged'
  if [[ "$(readlink "$FAKE_HOME/.bashrc")" == "$REPO/dots/all-os/flat/bashrc.sh" ]]; then
    ok 'second run left the link pointing where it was'
  else
    fail 'second run left the link pointing where it was'
  fi

  echo "===== a regular file is not clobbered ====="
  new_fake_home
  echo 'hand-written, do not touch' > "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home)
  assert_contains "$out" 'SKIPPED: .bashrc is a regular file' 'regular file is reported as skipped'
  if [[ "$(cat "$FAKE_HOME/.bashrc")" == 'hand-written, do not touch' ]]; then
    ok 'regular file kept its contents'
  else
    fail 'regular file kept its contents'
  fi

  echo "===== --dry-run ====="
  new_fake_home
  # `|| true` because this script runs under errexit and the helper returns the
  # installer's exit status: a non-zero run would abort here instead of failing the
  # assertions below, which is precisely when we most want them to report.
  out=$(install_into_fake_home --dry-run) || true
  # A dry run runs the normal logging path on purpose: the real "Linked:" output is the
  # preview, and it exercises those branches. The raw `ln` line stays verbose-only.
  assert_contains "$out" 'Linked: .bashrc' 'dry run previews the normal log output'
  assert_not_contains "$out" 'dry-run: would: ln' \
      'the raw command is verbose-only detail, not shown by default'
  assert_contains "$out" 'DRY-RUN: This was a dry run. No data was actually written.' \
      'dry run ends with an unmistakable trailer'
  out2=$(install_into_fake_home --dry-run --verbose) || true
  assert_contains "$out2" 'dry-run: would: ln -sfn' \
      'a verbose dry run does show the raw commands'
  # No `find | head` here: head closes the pipe early, and under errexit + pipefail the
  # SIGPIPE aborts the whole test run rather than failing this one assertion -- which
  # would hide the failure at exactly the moment it mattered.
  leftovers=$(find "$FAKE_HOME" -mindepth 1 2>/dev/null || true)
  if [[ -z "$leftovers" ]]; then
    ok 'dry run leaves the target home completely untouched'
  else
    fail 'dry run leaves the target home completely untouched' \
        "created: $(printf '%s' "$leftovers" | tr '\n' ' ')"
  fi

  echo "===== the flat-tree collision guard ====="
  new_fake_home
  install_into_fake_home > /dev/null
  before=$(readlink "$FAKE_HOME/.bashrc")
  stale="$REPO/dots/all-os/flat/bashrc.zsh"  # stale rename leftover; both munge to .bashrc
  echo '# stale leftover from a rename' > "$stale"
  rc=0
  out=$(install_into_fake_home) || rc=$?
  rm -f "$stale"
  if [[ $rc -ne 0 ]]; then
    ok 'collision makes the installer exit non-zero'
  else
    fail 'collision makes the installer exit non-zero' "exited 0"
  fi
  assert_contains "$out" 'both install as' 'collision is explained in the error'
  if [[ "$(readlink "$FAKE_HOME/.bashrc")" == "$before" ]]; then
    ok 'collision changed nothing in ~ (validation runs before linking)'
  else
    fail 'collision changed nothing in ~ (validation runs before linking)' \
        "was $before, now $(readlink "$FAKE_HOME/.bashrc")"
  fi

  echo "===== .linkdir: a marked directory links whole ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  echo 'content' > "$FAKE_DOTS/all-os/nested/_foo/leaf.txt"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS")
  assert_symlink_to '.foo' "$FAKE_DOTS/all-os/nested/_foo" \
      '.linkdir makes the directory itself one symlink'
  # Not assert_absent on '.foo/leaf.txt' or '.foo/.linkdir': .foo is a symlink to the
  # source dir, so its contents are reachable through it regardless. What matters -- that
  # leaf.txt is not ALSO linked individually, i.e. .foo is a symlink rather than a real
  # directory containing its own leaf symlinks -- is exactly what assert_symlink_to above
  # already proves.
  assert_absent '.linkdir' '.linkdir marker files are never linked into ~'
  assert_not_contains "$out" '.linkdir ' \
      '.linkdir is never itself treated as something to link'

  echo "===== .linkdirs: each child directory links whole ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_skills/one" "$FAKE_DOTS/all-os/nested/_skills/two"
  touch "$FAKE_DOTS/all-os/nested/_skills/.linkdirs"
  echo 'one' > "$FAKE_DOTS/all-os/nested/_skills/one/SKILL.md"
  echo 'two' > "$FAKE_DOTS/all-os/nested/_skills/two/SKILL.md"
  echo 'sibling file' > "$FAKE_DOTS/all-os/nested/_skills/README.md"
  install_into_fake_home --dots-dir "$FAKE_DOTS" > /dev/null
  assert_symlink_to '.skills/one' "$FAKE_DOTS/all-os/nested/_skills/one" \
      'each child directory of a .linkdirs dir links whole'
  assert_symlink_to '.skills/two' "$FAKE_DOTS/all-os/nested/_skills/two" \
      'including a second child directory'
  assert_real_dir '.skills' 'the .linkdirs-marked directory itself stays real'
  assert_symlink_to '.skills/README.md' "$FAKE_DOTS/all-os/nested/_skills/README.md" \
      'a non-directory sibling of the marked children still links as a leaf file'
  assert_absent '.linkdirs' '.linkdirs marker files are never linked into ~'

  echo "===== .linkdirs: a child also marked .linkdir is linked once, not twice ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_skills/one"
  touch "$FAKE_DOTS/all-os/nested/_skills/.linkdirs"
  touch "$FAKE_DOTS/all-os/nested/_skills/one/.linkdir"
  echo 'one' > "$FAKE_DOTS/all-os/nested/_skills/one/SKILL.md"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS")
  assert_symlink_to '.skills/one' "$FAKE_DOTS/all-os/nested/_skills/one" \
      'a child covered by both markers still links whole'
  assert_eq "$(printf '%s\n' "$out" | grep -c 'Linked: \.skills/one ')" 1 \
      'and is linked exactly once, not once per marker'

  echo "===== .linkdir at a tree root is a hard error ====="
  new_fake_home
  new_fake_dots
  touch "$FAKE_DOTS/all-os/nested/.linkdir"
  rc=0
  install_into_fake_home --dots-dir "$FAKE_DOTS" > /dev/null 2>&1 || rc=$?
  assert_eq "$rc" 1 '.linkdir at a nested tree root exits non-zero'
  leftovers=$(find "$FAKE_HOME" -mindepth 1 2>/dev/null || true)
  assert_eq "$leftovers" '' '.linkdir at a nested tree root writes nothing to ~'

  echo "===== a leaf file target nested under a whole-linked directory is an error ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  mkdir -p "$FAKE_DOTS/macos/nested/_foo"
  echo 'stray leaf' > "$FAKE_DOTS/macos/nested/_foo/leaf.txt"
  rc=0
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS") || rc=$?
  assert_eq "$rc" 1 'a file target under a dir target exits non-zero'
  assert_contains "$out" 'nested under' 'the error names the containment'
  leftovers=$(find "$FAKE_HOME" -mindepth 1 2>/dev/null || true)
  assert_eq "$leftovers" '' 'nothing was written to ~'
  # The bug this specifically guards against: validating per-tier on source paths (instead
  # of in target space) would miss this case, and the leaf file's `ln` would then resolve
  # THROUGH the newly created .foo symlink and write into the source tree itself.
  assert_eq "$(find "$FAKE_DOTS" -type f | sort)" \
      "$(printf '%s\n%s' \
          "$FAKE_DOTS/all-os/nested/_foo/.linkdir" \
          "$FAKE_DOTS/macos/nested/_foo/leaf.txt" | sort)" \
      'the source tree itself was not modified'

  echo "===== a directory target nested under another directory target is an error ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo/bar"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  touch "$FAKE_DOTS/all-os/nested/_foo/bar/.linkdir"
  rc=0
  install_into_fake_home --dots-dir "$FAKE_DOTS" > /dev/null 2>&1 || rc=$?
  assert_eq "$rc" 1 'a dir target nested under another dir target exits non-zero'

  echo "===== the same target as a directory in one tier and a file in another is an error ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  mkdir -p "$FAKE_DOTS/macos/nested"
  echo 'a file where a dir is expected' > "$FAKE_DOTS/macos/nested/_foo"
  rc=0
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS") || rc=$?
  assert_eq "$rc" 1 'dir-vs-file kind mismatch across tiers exits non-zero'
  assert_contains "$out" 'whole-linked directory' 'the error names the conflict'

  echo "===== \$OSNAME's .linkdir shadows all-os's .linkdir on the same target ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  mkdir -p "$FAKE_DOTS/macos/nested/_foo"
  touch "$FAKE_DOTS/macos/nested/_foo/.linkdir"
  install_into_fake_home --dots-dir "$FAKE_DOTS" > /dev/null
  assert_symlink_to '.foo' "$FAKE_DOTS/macos/nested/_foo" \
      "\$OSNAME .linkdir overrides all-os .linkdir on a matching target"

  echo "===== dry run previews a whole-directory link and writes nothing ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS" --dry-run) || true
  assert_contains "$out" 'Linked: .foo' \
      'dry run previews the whole-directory link like any other'
  leftovers=$(find "$FAKE_HOME" -mindepth 1 2>/dev/null || true)
  assert_eq "$leftovers" '' 'dry run creates nothing, including no mkdir-ed parents'

  echo "===== idempotence for a whole-directory link ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  install_into_fake_home --dots-dir "$FAKE_DOTS" > /dev/null
  out2=$(install_into_fake_home --dots-dir "$FAKE_DOTS" -v)
  assert_contains "$out2" 'Already set up: .foo ' \
      'second run reports the whole-directory link unchanged'

  echo "===== a real directory at a .linkdir target is skipped, not clobbered ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  mkdir -p "$FAKE_HOME/.foo"
  echo 'hand-made, do not touch' > "$FAKE_HOME/.foo/mine.txt"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS")
  assert_contains "$out" 'SKIPPED: .foo is a non-symlink directory' \
      'a real directory at a .linkdir target is reported as skipped'
  assert_real_dir '.foo' 'and is left as a real directory, not replaced'
  if [[ -f "$FAKE_HOME/.foo/mine.txt" ]]; then
    ok 'its contents survived'
  else
    fail 'its contents survived'
  fi

  echo "===== a repo-managed-but-stale leaf-file symlink is relinked by default ====="
  # Regression test: dotinstall::symlink used to fall through its -L branch with no else
  # when the existing symlink pointed somewhere else -- not relinked, not counted, not
  # reported. A real migration (an install-dotfiles-private tree moving from files/ to
  # dots/) hit this: every entry silently did nothing, and the summary claimed everything
  # was already up to date while every link still pointed at the old location.
  #
  # The stale target has to be a real file -- assert_symlink_to also checks the symlink
  # isn't dangling, and a fictional path would always be.
  local stale_file="$REPO/dots/all-os/flat/profile.sh"
  new_fake_home
  ln -s "$stale_file" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home -v)
  assert_contains "$out" 'Relinked: .bashrc' 'relinking happens with no flag needed'
  assert_contains "$out" "(was -> $stale_file)" 'and names what it was pointing at before'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" \
      'and it now points at the correct target'

  echo "===== --no-relink opts back out, to the old default behavior ====="
  new_fake_home
  ln -s "$stale_file" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --no-relink)
  assert_contains "$out" "SKIPPED: .bashrc is a symlink to '$stale_file'" \
      '--no-relink reports it as skipped, naming its current target'
  assert_symlink_to '.bashrc' "$stale_file" \
      'and --no-relink leaves it untouched, not silently ignored nor relinked'

  echo "===== --relink is still accepted explicitly, redundant with the default ====="
  new_fake_home
  ln -s "$stale_file" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --relink -v)
  assert_contains "$out" 'Relinked: .bashrc' 'explicit --relink still reports the relink'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" \
      'and it now points at the correct target'

  echo "===== --dry-run previews the now-default relink without writing ====="
  new_fake_home
  ln -s "$stale_file" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --dry-run) || true
  assert_contains "$out" 'Relinked: .bashrc' 'dry run previews the relink'
  assert_symlink_to '.bashrc' "$stale_file" 'dry run leaves the stale link exactly as it was'

  echo "===== a second run against an already-correct link is a no-op ====="
  new_fake_home
  ln -s "$stale_file" "$FAKE_HOME/.bashrc"
  install_into_fake_home > /dev/null
  out2=$(install_into_fake_home -v)
  assert_contains "$out2" 'Already set up: .bashrc ' \
      'a link already pointing correctly is reported unchanged, not relinked again'
  assert_not_contains "$out2" 'Relinked:' 'and no Relinked line appears'

  echo "===== a .linkdir whole-directory symlink pointing elsewhere behaves the same way ====="
  # The exact real-world shape of the bug above: a Claude Code skill directory symlink left
  # pointing at the pre-migration location.
  local stale_dir="$REPO/dots/all-os/flat/dotlib"
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  ln -s "$stale_dir" "$FAKE_HOME/.foo"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS")
  assert_contains "$out" 'Relinked: .foo' 'relinked by default, no flag needed'
  assert_symlink_to '.foo' "$FAKE_DOTS/all-os/nested/_foo" \
      'and now points at the correct whole-linked directory'

  echo "===== --no-relink leaves a stale whole-directory link alone too ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  ln -s "$stale_dir" "$FAKE_HOME/.foo"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS" --no-relink)
  assert_contains "$out" "SKIPPED: .foo is a symlink to '$stale_dir'" \
      '--no-relink reports the whole-directory link as skipped'
  assert_symlink_to '.foo' "$stale_dir" 'and left untouched'

  echo "===== --relink does not touch a symlink pointing outside any recognized repo ====="
  new_fake_home
  local foreign_file="$FAKE_HOME/.elsewhere"
  echo 'not ours' > "$foreign_file"
  ln -s "$foreign_file" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --relink)
  assert_contains "$out" "SKIPPED: .bashrc is a symlink to '$foreign_file'" \
      'a symlink to somewhere outside any recognized repo is reported as skipped'
  assert_contains "$out" 'not managed by a' 'the message explains why, and names --clobber'
  assert_symlink_to '.bashrc' "$foreign_file" '--relink alone does not touch it'
  if find "$FAKE_HOME" -name '*.bak*' 2>/dev/null | grep -q .; then
    fail 'no .bak file is ever created without --clobber'
  else
    ok 'no .bak file is ever created without --clobber'
  fi

  echo "===== --clobber replaces a symlink pointing outside any recognized repo ====="
  new_fake_home
  local foreign_file2="$FAKE_HOME/.elsewhere2"
  echo 'not ours either' > "$foreign_file2"
  ln -s "$foreign_file2" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --clobber --yes-really -v)
  # Relative, not absolute: dotinstall::symlink is called with a target relative to cwd
  # (main() cds to $HOME before linking anything), and _backup operates on that same path.
  assert_contains "$out" 'Backed up: .bashrc -> .bashrc.bak' \
      'the foreign symlink itself is backed up'
  assert_contains "$out" 'Clobbered: .bashrc' 'and reported as clobbered'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" \
      'and it now points at the correct target'
  assert_symlink_to '.bashrc.bak' "$foreign_file2" \
      'the backup is the original symlink itself, unchanged'

  echo "===== a relative symlink target is always treated as foreign, never resolved ====="
  new_fake_home
  ln -s '../nonexistent-relative-target' "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --relink)
  assert_contains "$out" 'not managed by a' \
      'a relative symlink target is never recognized as repo-managed'

  echo "===== a dangling symlink whose target STRING is under the repo is still relink-eligible ====="
  new_fake_home
  local dangling_repo_target="$REPO/dots/all-os/flat/this-file-does-not-exist.sh"
  ln -s "$dangling_repo_target" "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --relink)
  assert_contains "$out" 'Relinked: .bashrc' \
      'recognized as repo-managed by the target string, not by the target existing'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" \
      'and it now points at the correct, real target'

  echo "===== a dangling foreign symlink is clobberable ====="
  new_fake_home
  ln -s '/nonexistent/foreign/path' "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --relink)
  assert_contains "$out" 'not managed by a' \
      'a dangling foreign symlink is still just skipped without --clobber'
  out=$(install_into_fake_home --clobber --yes-really)
  assert_contains "$out" 'Clobbered: .bashrc' 'and is clobbered with --clobber --yes-really'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" 'ending up at the correct target'
  assert_eq "$(readlink "$FAKE_HOME/.bashrc.bak")" '/nonexistent/foreign/path' \
      'the backup preserves the original (dangling) target string'

  echo "===== --clobber without --yes-really refuses to run at all ====="
  new_fake_home
  echo 'hand-written' > "$FAKE_HOME/.bashrc"
  rc=0
  out=$(install_into_fake_home --clobber) || rc=$?
  assert_eq "$rc" 1 '--clobber alone exits non-zero'
  assert_contains "$out" '--yes-really' 'the error names --yes-really'
  if [[ "$(cat "$FAKE_HOME/.bashrc")" == 'hand-written' ]]; then
    ok 'and the file was never touched'
  else
    fail 'and the file was never touched'
  fi
  if find "$FAKE_HOME" -name '*.bak*' 2>/dev/null | grep -q .; then
    fail 'no .bak file was created'
  else
    ok 'no .bak file was created'
  fi

  echo "===== --clobber combined with --help just prints help, in either order ====="
  new_fake_home
  rc=0
  out=$(install_into_fake_home --clobber --help) || rc=$?
  assert_eq "$rc" 0 '--clobber --help exits zero'
  assert_contains "$out" 'Usage:' 'and prints usage'
  rc=0
  out=$(install_into_fake_home --help --clobber) || rc=$?
  assert_eq "$rc" 0 '--help --clobber exits zero too'
  assert_contains "$out" 'Usage:' 'and also prints usage'

  echo "===== --clobber backs up a regular file before replacing it ====="
  new_fake_home
  echo 'hand-written, do not touch' > "$FAKE_HOME/.bashrc"
  out=$(install_into_fake_home --clobber --yes-really)
  assert_contains "$out" 'Clobbered: .bashrc' 'the file is reported as clobbered'
  assert_symlink_to '.bashrc' "$REPO/dots/all-os/flat/bashrc.sh" \
      'and now points at the correct target'
  if [[ "$(cat "$FAKE_HOME/.bashrc.bak")" == 'hand-written, do not touch' ]]; then
    ok 'the original content is preserved in .bashrc.bak'
  else
    fail 'the original content is preserved in .bashrc.bak'
  fi

  echo "===== a second clobber of the same target uses a dated backup name instead ====="
  new_fake_home
  echo 'first version' > "$FAKE_HOME/.bashrc"
  install_into_fake_home --clobber --yes-really > /dev/null
  rm -f "$FAKE_HOME/.bashrc"
  echo 'second version' > "$FAKE_HOME/.bashrc"
  install_into_fake_home --clobber --yes-really > /dev/null
  if [[ "$(cat "$FAKE_HOME/.bashrc.bak" 2>/dev/null)" == 'first version' ]]; then
    ok 'the first backup is untouched'
  else
    fail 'the first backup is untouched' "got: $(cat "$FAKE_HOME/.bashrc.bak" 2>/dev/null)"
  fi
  local dated_backup
  dated_backup=$(find "$FAKE_HOME" -maxdepth 1 -name '.bashrc.bak-*' | head -n 1)
  if [[ -n "$dated_backup" && "$(cat "$dated_backup" 2>/dev/null)" == 'second version' ]]; then
    ok 'the second backup lands in a dated .bak-<datetime>.bak file instead'
  else
    fail 'the second backup lands in a dated .bak-<datetime>.bak file instead' \
        "found: $dated_backup"
  fi

  echo "===== --clobber backs up a real directory before replacing it with a whole-dir link ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  mkdir -p "$FAKE_HOME/.foo"
  echo 'hand-made, do not touch' > "$FAKE_HOME/.foo/mine.txt"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS" --clobber --yes-really)
  assert_contains "$out" 'Clobbered: .foo' 'the directory is reported as clobbered'
  assert_symlink_to '.foo' "$FAKE_DOTS/all-os/nested/_foo" \
      'and now points at the correct whole-linked directory'
  if [[ "$(cat "$FAKE_HOME/.foo.bak/mine.txt")" == 'hand-made, do not touch' ]]; then
    ok 'the original directory and its contents survive at .foo.bak'
  else
    fail 'the original directory and its contents survive at .foo.bak'
  fi

  echo "===== a foreign whole-directory symlink is backed up as a symlink, not copied ====="
  new_fake_home
  new_fake_dots
  mkdir -p "$FAKE_DOTS/all-os/nested/_foo"
  touch "$FAKE_DOTS/all-os/nested/_foo/.linkdir"
  local foreign_dir="$FAKE_HOME/.elsewhere-dir"
  mkdir -p "$foreign_dir"
  echo 'not ours' > "$foreign_dir/other.txt"
  ln -s "$foreign_dir" "$FAKE_HOME/.foo"
  out=$(install_into_fake_home --dots-dir "$FAKE_DOTS" --clobber --yes-really)
  assert_contains "$out" 'Clobbered: .foo' 'reported as clobbered'
  assert_symlink_to '.foo' "$FAKE_DOTS/all-os/nested/_foo" \
      'and now points at the correct whole-linked directory'
  assert_symlink_to '.foo.bak' "$foreign_dir" \
      'the backup is the symlink itself, not a copy of the foreign directory'
  if [[ "$(cat "$foreign_dir/other.txt")" == 'not ours' ]]; then
    ok "the foreign directory's own contents are untouched"
  else
    fail "the foreign directory's own contents are untouched"
  fi

  echo "===== --clobber --dry-run previews without writing anything ====="
  new_fake_home
  echo 'hand-written' > "$FAKE_HOME/.bashrc"
  local before_listing after_listing
  before_listing=$(find "$FAKE_HOME" -mindepth 1 | sort)
  out=$(install_into_fake_home --clobber --yes-really --dry-run) || true
  assert_contains "$out" 'Backed up:' 'dry run previews the backup'
  assert_contains "$out" 'Clobbered: .bashrc' 'and the clobber'
  after_listing=$(find "$FAKE_HOME" -mindepth 1 | sort)
  assert_eq "$after_listing" "$before_listing" \
      'dry run leaves the target home completely unchanged'

  echo "===== --no-relink together with --clobber is refused as likely a mistake ====="
  new_fake_home
  ln -s "$stale_file" "$FAKE_HOME/.bashrc"
  rc=0
  out=$(install_into_fake_home --no-relink --clobber --yes-really) || rc=$?
  assert_eq "$rc" 1 '--no-relink with --clobber exits non-zero'
  assert_contains "$out" '--no-relink' 'the error names --no-relink'
  assert_contains "$out" '--clobber' 'and --clobber'
  assert_symlink_to '.bashrc' "$stale_file" 'and nothing was changed'

  echo "===== the real \$HOME was never touched ====="
  # Compare against a snapshot taken before any test ran, rather than against the current
  # repo layout. Comparing to the layout conflates two things, and trips on a rearranged
  # tree or a home that just needs a re-install -- neither of which says anything about
  # whether the tests escaped their fake $HOME, which is the only thing this guards.
  if [[ "$(real_bashrc_link)" == "$REAL_BASHRC_BEFORE" ]]; then
    ok "the real ~/.bashrc is exactly as the tests found it"
  else
    fail "the real ~/.bashrc is exactly as the tests found it" \
        "was $REAL_BASHRC_BEFORE, now $(real_bashrc_link)"
  fi
}

function install_into_fake_home() {
  # errexit off: failures are the caller's to interpret, not ours to abort on.
  local rc=0
  set +o errexit
  HOME="$FAKE_HOME" "$REPO/install-dotfiles" "$@" 2>&1
  rc=$?
  set -o errexit
  return $rc
}

function real_bashrc_link() {
  readlink "$HOME/.bashrc" 2>/dev/null || echo '(not a symlink)'
}

function print_summary() {
  echo ''
  if [[ $FAILS -eq 0 ]]; then
    echo "===== all $TESTS assertions passed ====="
  else
    echo "===== $FAILS of $TESTS assertions FAILED ====="
  fi
}

# ============ Main script ============

REAL_BASHRC_BEFORE=$(real_bashrc_link)   # snapshot before anything runs

run_tests
print_summary
exit $(( FAILS > 0 ? 1 : 0 ))
