#!/usr/bin/env bats
# [unit->REQ-DEP-04]
#
# bats unit tests for scripts/client-release.sh
#
# Tests cover (per Plan 06.5-03 Task 1 behavior list):
#   1. missing SHA argument                              → exit !=0, "usage" on stderr
#   2. SHA "../etc/passwd"                               → exit 2, "invalid SHA" on stderr
#   3. SHA uppercase 40-hex                              → exit 2 (lowercase only)
#   4. valid SHA + missing tarball                       → exit 3, mentions missing tarball
#   5. valid SHA + tarball missing index.html            → exit 4, `current` unchanged
#   6. valid SHA + valid tarball                         → exit 0, `current` re-pointed, tarball gone
#   7. GC keeps last 5, never deletes current target     → 5 dirs remain, prev-current preserved
#   8. Atomic swap structural check (mv -T present,
#      ln -sfn absent — see SUMMARY for race-test
#      fallback rationale)
#
# Each test runs in an isolated tmpdir; the script is parameterized via
# ROOT and TGZ_DIR env vars so the real /data/client-assets is never
# touched. Run: `bats scripts/test/client-release.bats`.

# Path to the script under test (resolved relative to this bats file).
SCRIPT="${BATS_TEST_DIRNAME}/../client-release.sh"

setup() {
  TMPDIR_TEST="$(mktemp -d)"
  export ROOT="${TMPDIR_TEST}/data/client-assets"
  export TGZ_DIR="${TMPDIR_TEST}/tmp"
  mkdir -p "${ROOT}" "${TGZ_DIR}"
}

teardown() {
  if [[ -n "${TMPDIR_TEST:-}" && -d "${TMPDIR_TEST}" ]]; then
    rm -rf "${TMPDIR_TEST}"
  fi
}

# Helper: build a valid client tarball at $TGZ_DIR/client-assets-<sha>.tgz
# containing index.html + .vite/manifest.json. Optional 3rd arg = skip a
# given file ("index" or "manifest") to produce a corrupt tarball.
make_tarball() {
  local sha="$1"
  local skip="${2:-}"
  local stage
  stage="$(mktemp -d)"
  if [[ "${skip}" != "index" ]]; then
    echo "<!doctype html>" > "${stage}/index.html"
  fi
  mkdir -p "${stage}/.vite"
  if [[ "${skip}" != "manifest" ]]; then
    echo '{}' > "${stage}/.vite/manifest.json"
  fi
  tar -czf "${TGZ_DIR}/client-assets-${sha}.tgz" -C "${stage}" .
  rm -rf "${stage}"
}

# A pre-built valid 40-hex SHA (lowercase) used across tests.
VALID_SHA="0123456789abcdef0123456789abcdef01234567"
OTHER_SHA="abcdef0123456789abcdef0123456789abcdef01"

@test "1. missing SHA argument → exit non-zero with usage on stderr" {
  run bash "${SCRIPT}"
  [ "${status}" -ne 0 ]
  [[ "${output}" == *"usage"* ]] || [[ "${stderr:-${output}}" == *"usage"* ]]
}

@test "2. SHA '../etc/passwd' → exit 2, 'invalid SHA' on stderr" {
  run bash "${SCRIPT}" "../etc/passwd"
  [ "${status}" -eq 2 ]
  [[ "${output}" == *"invalid SHA"* ]]
}

@test "3. SHA uppercase 40-hex → exit 2 (lowercase-only enforcement)" {
  local upper="ABCDEF0123456789ABCDEF0123456789ABCDEF01"
  run bash "${SCRIPT}" "${upper}"
  [ "${status}" -eq 2 ]
  [[ "${output}" == *"invalid SHA"* ]]
}

@test "4. valid 40-hex SHA + missing tarball → exit 3, mentions missing tarball" {
  run bash "${SCRIPT}" "${VALID_SHA}"
  [ "${status}" -eq 3 ]
  [[ "${output}" == *"tarball missing"* ]]
}

@test "5. valid SHA + tarball missing index.html → exit 4, current unchanged" {
  # Seed a prior valid release + symlink so we can assert `current`
  # is preserved across the corrupt-tarball attempt.
  mkdir -p "${ROOT}/releases/${OTHER_SHA}"
  echo "<!doctype html>" > "${ROOT}/releases/${OTHER_SHA}/index.html"
  mkdir -p "${ROOT}/releases/${OTHER_SHA}/.vite"
  echo '{}' > "${ROOT}/releases/${OTHER_SHA}/.vite/manifest.json"
  ln -s "${ROOT}/releases/${OTHER_SHA}" "${ROOT}/current"
  local pre_target
  pre_target="$(readlink "${ROOT}/current")"

  # Build a corrupt tarball that is missing index.html.
  make_tarball "${VALID_SHA}" "index"

  run bash "${SCRIPT}" "${VALID_SHA}"
  [ "${status}" -eq 4 ]
  [[ "${output}" == *"missing index.html"* ]]

  # Critical: current symlink target unchanged.
  local post_target
  post_target="$(readlink "${ROOT}/current")"
  [ "${pre_target}" = "${post_target}" ]
}

@test "6. valid SHA + valid tarball → exit 0, current re-points, tarball removed" {
  make_tarball "${VALID_SHA}"

  run bash "${SCRIPT}" "${VALID_SHA}"
  [ "${status}" -eq 0 ]
  [[ "${output}" == *"swapped"* ]]

  # current symlink resolves to the new release dir.
  local target
  target="$(readlink "${ROOT}/current")"
  [ "${target}" = "${ROOT}/releases/${VALID_SHA}" ]
  [ -f "${ROOT}/releases/${VALID_SHA}/index.html" ]
  [ -f "${ROOT}/releases/${VALID_SHA}/.vite/manifest.json" ]

  # Tarball cleaned up.
  [ ! -f "${TGZ_DIR}/client-assets-${VALID_SHA}.tgz" ]
}

@test "7. GC keeps last 5; never deletes current target" {
  # Pre-create 7 release dirs, mtimes 1 minute apart (oldest first).
  # Bash array of SHAs — all 40-hex, all distinct.
  local shas=(
    "1111111111111111111111111111111111111111"
    "2222222222222222222222222222222222222222"
    "3333333333333333333333333333333333333333"
    "4444444444444444444444444444444444444444"
    "5555555555555555555555555555555555555555"
    "6666666666666666666666666666666666666666"
    "7777777777777777777777777777777777777777"
  )
  local i=0
  for s in "${shas[@]}"; do
    mkdir -p "${ROOT}/releases/${s}"
    # Stamp the dir with an ascending mtime (oldest = first in array).
    # touch -d "now - N minutes" — N decreases as i grows so newer dirs
    # get newer mtimes.
    local minutes_ago=$((100 - i))
    touch -d "${minutes_ago} minutes ago" "${ROOT}/releases/${s}"
    i=$((i + 1))
  done

  # Point `current` at the SECOND-OLDEST release. By mtime ordering
  # this would normally be GC'd when the 8th release lands.
  local protected="${shas[1]}"
  ln -s "${ROOT}/releases/${protected}" "${ROOT}/current"

  # New (8th) release tarball.
  local new_sha="8888888888888888888888888888888888888888"
  make_tarball "${new_sha}"

  run bash "${SCRIPT}" "${new_sha}"
  [ "${status}" -eq 0 ]

  # 8th release present + current points at it.
  [ -d "${ROOT}/releases/${new_sha}" ]
  local target
  target="$(readlink "${ROOT}/current")"
  [ "${target}" = "${ROOT}/releases/${new_sha}" ]

  # Protected previous-current SHA must still exist on disk even
  # though by mtime it would be GC'd.
  [ -d "${ROOT}/releases/${protected}" ]

  # Exactly 5 release dirs remain (KEEP_LAST default).
  local remaining
  remaining="$(ls -1d "${ROOT}/releases/"*/ 2>/dev/null | wc -l)"
  [ "${remaining}" -eq 5 ]
}

@test "8. structural atomicity check — mv -T present, ln -sfn absent" {
  # The mv -T atomicity is RESEARCH-VERIFIED (rename(2) on ext4).
  # Re-proving via a race is flaky in CI (depends on scheduler
  # resolution finer than rename(2)). Structural check is the
  # documented fallback (Plan 06.5-03 Task 1 action note).
  grep -F 'mv -T' "${SCRIPT}"
  ! grep -F 'ln -sfn' "${SCRIPT}"
}
