#!/usr/bin/env bash

# Integration tests for scripts/publish-cli.sh.
#
# The script runs local build-and-test (lint, typecheck, test, build), bumps
# cli/package.json, pushes a `release/cli-vX.Y.Z` branch, and opens a PR via
# `gh pr create`. These tests build a self-contained fake repo per scenario,
# using a real bare repository as origin so git fetch/pull/push are actually
# exercised. `bun` and `gh` are stubbed to keep the heavy steps deterministic.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PUBLISH_SCRIPT="$REPO_ROOT/scripts/publish-cli.sh"

TMP_DIRS=()
cleanup() {
  local d
  for d in "${TMP_DIRS[@]+"${TMP_DIRS[@]}"}"; do
    rm -rf "$d"
  done
}
trap cleanup EXIT

new_tmp() {
  local d
  d="$(mktemp -d)"
  TMP_DIRS+=("$d")
  echo "$d"
}

fail() {
  echo "FAIL: $*" >&2
  exit 1
}

# init_repo <repo_dir> [initial_pkg_version] [tag_to_seed ...]
#
# Builds a minimal repo with:
#   - cli/package.json at the requested version
#   - cli/src/generated/pkg-info.ts (committed, so script's checkout works)
#   - scripts/publish-cli.sh (the script under test)
#   - bin/bun stub: handles install / run lint|typecheck|build|<codegen> / test
#   - bin/gh stub: handles `auth status` and `pr create` (logs to gh-stub.log)
#   - a sibling bare repo as `origin`
#   - HEAD on `main` with the init commit already pushed
#   - optional pre-seeded `cli-v*` tags (created locally AND on origin)
init_repo() {
  local repo="$1"
  local version="${2:-0.1.0}"
  local tags=()
  if [[ $# -gt 2 ]]; then
    tags=("${@:3}")
  fi

  local origin="$repo.origin.git"
  TMP_DIRS+=("$origin")

  mkdir -p "$repo/cli/src/generated" "$repo/scripts" "$repo/bin"
  cp "$PUBLISH_SCRIPT" "$repo/scripts/publish-cli.sh"

  cat >"$repo/cli/package.json" <<EOF
{
  "name": "@astron-team/skillhub",
  "version": "$version",
  "bin": { "skillhub": "./dist/index.js" },
  "files": ["dist", "README.md", "LICENSE"],
  "publishConfig": { "access": "public" }
}
EOF

  cat >"$repo/cli/src/generated/pkg-info.ts" <<EOF
// Generated by scripts/generate-pkg-info.ts - do not edit by hand.
export const PKG_NAME = "@astron-team/skillhub"
export const PKG_VERSION = "$version"
EOF

  # Stub `bun`: handles every subcommand the publish script invokes.
  # Failure injection via BUN_FAIL_AT (substring match against full args).
  cat >"$repo/bin/bun" <<'BUN_EOF'
#!/usr/bin/env bash
set -euo pipefail

if [[ -n "${BUN_FAIL_AT:-}" ]] && [[ "$*" == *"$BUN_FAIL_AT"* ]]; then
  echo "stub bun: forced failure at: $*" >&2
  exit 1
fi

case "${1:-}" in
  install)
    exit 0
    ;;
  test)
    exit 0
    ;;
  run)
    case "${2:-}" in
      lint|typecheck|build)
        exit 0
        ;;
      scripts/generate-pkg-info.ts)
        node -e "
          const fs = require('fs');
          const path = require('path');
          const cliRoot = process.cwd();
          const pkg = JSON.parse(fs.readFileSync(path.join(cliRoot, 'package.json'), 'utf8'));
          const out = path.join(cliRoot, 'src/generated/pkg-info.ts');
          fs.mkdirSync(path.dirname(out), { recursive: true });
          fs.writeFileSync(out,
            '// Generated by scripts/generate-pkg-info.ts - do not edit by hand.\n' +
            'export const PKG_NAME = ' + JSON.stringify(pkg.name) + '\n' +
            'export const PKG_VERSION = ' + JSON.stringify(pkg.version) + '\n');
        "
        exit 0
        ;;
      *)
        echo "stub bun: unsupported run target: ${2:-}" >&2
        exit 1
        ;;
    esac
    ;;
  *)
    echo "stub bun: unsupported subcommand: ${1:-}" >&2
    exit 1
    ;;
esac
BUN_EOF
  chmod +x "$repo/bin/bun"

  # Stub `gh`: handles `auth status` (always 0) and `pr create` (logs args).
  # Failure injection via GH_FAIL_AT="auth"|"pr-create".
  cat >"$repo/bin/gh" <<'GH_EOF'
#!/usr/bin/env bash
set -euo pipefail

if [[ -n "${GH_FAIL_AT:-}" ]]; then
  if [[ "$GH_FAIL_AT" == "auth" && "${1:-}" == "auth" ]]; then
    exit 1
  fi
  if [[ "$GH_FAIL_AT" == "pr-create" && "${1:-}" == "pr" && "${2:-}" == "create" ]]; then
    echo "stub gh: forced PR create failure" >&2
    exit 1
  fi
fi

case "${1:-}" in
  auth)
    exit 0
    ;;
  pr)
    if [[ "${2:-}" == "create" ]]; then
      printf '%s\n' "$*" >> "${GH_LOG_FILE:-/dev/null}"
      exit 0
    fi
    exit 1
    ;;
  *)
    echo "stub gh: unsupported: ${1:-}" >&2
    exit 1
    ;;
esac
GH_EOF
  chmod +x "$repo/bin/gh"

  cat >"$repo/.gitignore" <<EOF
stdout.log
stderr.log
gh-stub.log
bin-git/
EOF

  git init -q --bare "$origin"
  git -C "$repo" init -q -b main
  git -C "$repo" config user.name "Test User"
  git -C "$repo" config user.email "test@example.com"
  git -C "$repo" remote add origin "$origin"
  git -C "$repo" add cli/package.json cli/src/generated/pkg-info.ts \
                     scripts/publish-cli.sh bin/bun bin/gh .gitignore
  git -C "$repo" commit -q -m "init"
  git -C "$repo" push -q -u origin main

  local tag
  for tag in "${tags[@]+"${tags[@]}"}"; do
    git -C "$repo" tag "$tag"
    git -C "$repo" push -q origin "$tag"
  done
}

# run_publish <repo> <bump> [stdin] [extra_env=...]
#
# Writes stdout/stderr to $repo/{stdout,stderr}.log and prints exit code.
# `extra_env` (e.g. "BUN_FAIL_AT=lint") is forwarded as bash env assignments.
run_publish() {
  local repo="$1"
  local bump="$2"
  local input="${3-}"
  local extra="${4-}"
  local status=0
  local cmd=(env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE
    REPO_ROOT="$repo" PATH="$repo/bin:$PATH"
    GH_LOG_FILE="$repo/gh-stub.log")
  if [[ -n "$extra" ]]; then
    # Split "K1=V1 K2=V2" into separate env entries.
    local kv
    for kv in $extra; do
      cmd+=("$kv")
    done
  fi
  cmd+=(bash "$repo/scripts/publish-cli.sh" "$bump")

  if [[ -n "$input" ]]; then
    printf '%s' "$input" | "${cmd[@]}" >"$repo/stdout.log" 2>"$repo/stderr.log" || status=$?
  else
    "${cmd[@]}" >"$repo/stdout.log" 2>"$repo/stderr.log" || status=$?
  fi
  echo "$status"
}

# ----------------------------------------------------------------------------
# Test 1: invalid bump type → usage error, exit non-zero
# ----------------------------------------------------------------------------
echo "[test] invalid bump type"
REPO1="$(new_tmp)"
init_repo "$REPO1"
status="$(run_publish "$REPO1" "foo")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit for invalid bump type"
grep -F "Usage:" "$REPO1/stderr.log" >/dev/null

# ----------------------------------------------------------------------------
# Test 2: dirty working tree → abort before any side effect
# ----------------------------------------------------------------------------
echo "[test] dirty working tree aborts"
REPO2="$(new_tmp)"
init_repo "$REPO2"
echo "junk" > "$REPO2/cli/package.json"
status="$(run_publish "$REPO2" "patch")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit for dirty tree"
grep -F "git working tree is not clean" "$REPO2/stderr.log" >/dev/null

# ----------------------------------------------------------------------------
# Test 3: not on main branch → abort
# ----------------------------------------------------------------------------
echo "[test] non-main branch aborts"
REPO3="$(new_tmp)"
init_repo "$REPO3"
git -C "$REPO3" checkout -q -b feature/x
status="$(run_publish "$REPO3" "patch")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when not on main"
grep -F "releases must be cut from 'main'" "$REPO3/stderr.log" >/dev/null
grep -F "feature/x" "$REPO3/stderr.log" >/dev/null

# ----------------------------------------------------------------------------
# Test 4: gh auth fails → abort early
# ----------------------------------------------------------------------------
echo "[test] gh auth failure aborts"
REPO4="$(new_tmp)"
init_repo "$REPO4"
status="$(run_publish "$REPO4" "patch" "" "GH_FAIL_AT=auth")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when gh auth fails"
grep -F "gh CLI is not authenticated" "$REPO4/stderr.log" >/dev/null

# ----------------------------------------------------------------------------
# Test 5: release branch already exists → abort before build (fail-fast)
# ----------------------------------------------------------------------------
echo "[test] existing release branch aborts before build"
REPO5="$(new_tmp)"
init_repo "$REPO5" "0.1.0" "cli-v0.1.0"
# Baseline is cli-v0.1.0, patch target is cli-v0.1.1, branch=release/cli-v0.1.1.
# Create that branch locally so the pre-flight check catches it.
git -C "$REPO5" branch "release/cli-v0.1.1"
status="$(run_publish "$REPO5" "patch")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when release branch exists"
grep -F "branch release/cli-v0.1.1 already exists locally" "$REPO5/stderr.log" >/dev/null \
  || { cat "$REPO5/stderr.log" >&2; fail "expected 'branch already exists locally' error"; }

# ----------------------------------------------------------------------------
# Test 6: confirmation cancel → no branch, no commit, no push
# ----------------------------------------------------------------------------
echo "[test] confirmation cancel leaves no side effects"
REPO6="$(new_tmp)"
init_repo "$REPO6" "0.1.0" "cli-v0.1.0"
INITIAL_HEAD="$(git -C "$REPO6" rev-parse HEAD)"
status="$(run_publish "$REPO6" "patch" $'n\n')"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit on cancel"
grep -F "release cancelled" "$REPO6/stderr.log" >/dev/null
[[ "$(git -C "$REPO6" rev-parse HEAD)" == "$INITIAL_HEAD" ]] \
  || fail "HEAD advanced after cancel"
[[ "$(git -C "$REPO6" rev-parse --abbrev-ref HEAD)" == "main" ]] \
  || fail "not on main after cancel"
if git -C "$REPO6" rev-parse -q --verify "refs/heads/release/cli-v0.1.1" >/dev/null 2>&1; then
  fail "release branch must not exist after cancel"
fi
[[ -z "$(git -C "$REPO6" status --porcelain)" ]] \
  || fail "working tree not clean after cancel"

# ----------------------------------------------------------------------------
# Test 7: happy path — branch pushed, PR opened, returned to main
# ----------------------------------------------------------------------------
echo "[test] happy path pushes branch and opens PR"
REPO7="$(new_tmp)"
init_repo "$REPO7" "0.5.0"
status="$(run_publish "$REPO7" "patch" $'y\n')"
[[ "$status" -eq 0 ]] || { cat "$REPO7/stderr.log" >&2; fail "expected success, got $status"; }

# Returned to main with branch deleted locally.
[[ "$(git -C "$REPO7" rev-parse --abbrev-ref HEAD)" == "main" ]] \
  || fail "not back on main after success"
if git -C "$REPO7" rev-parse -q --verify "refs/heads/release/cli-v0.5.1" >/dev/null 2>&1; then
  fail "local release branch should be deleted after success"
fi

# Branch on origin with bump commit.
ORIGIN7="$REPO7.origin.git"
git -C "$ORIGIN7" rev-parse "refs/heads/release/cli-v0.5.1" >/dev/null \
  || fail "origin missing release branch"
TIP_MSG="$(git -C "$ORIGIN7" log -1 --format=%s "refs/heads/release/cli-v0.5.1")"
[[ "$TIP_MSG" == "chore(cli): bump version to 0.5.1" ]] \
  || fail "unexpected commit message on release branch: $TIP_MSG"

# pkg-info.ts AND package.json both updated in the bump commit.
git -C "$ORIGIN7" show "refs/heads/release/cli-v0.5.1:cli/package.json" \
  | grep -F '"version": "0.5.1"' >/dev/null \
  || fail "package.json on branch missing new version"
git -C "$ORIGIN7" show "refs/heads/release/cli-v0.5.1:cli/src/generated/pkg-info.ts" \
  | grep -F 'PKG_VERSION = "0.5.1"' >/dev/null \
  || fail "pkg-info.ts on branch missing new version"

# gh pr create was invoked with expected args.
[[ -f "$REPO7/gh-stub.log" ]] || fail "gh stub log missing"
grep -F "pr create" "$REPO7/gh-stub.log" >/dev/null \
  || fail "gh pr create not invoked"
grep -F -- "--head release/cli-v0.5.1" "$REPO7/gh-stub.log" >/dev/null \
  || fail "gh pr create missing --head"
grep -F -- "--base main" "$REPO7/gh-stub.log" >/dev/null \
  || fail "gh pr create missing --base"

# ----------------------------------------------------------------------------
# Test 8: baseline from latest cli-v* tag (not package.json)
# ----------------------------------------------------------------------------
echo "[test] baseline taken from latest cli-v* tag"
REPO8="$(new_tmp)"
init_repo "$REPO8" "0.1.0" "cli-v0.2.0"
status="$(run_publish "$REPO8" "patch" $'y\n')"
[[ "$status" -eq 0 ]] || { cat "$REPO8/stderr.log" >&2; fail "expected success, got $status"; }
grep -F "baseline: 0.2.0 (from origin cli-v0.2.0)" "$REPO8/stdout.log" >/dev/null \
  || fail "baseline log missing — version computed from wrong source"
git -C "$REPO8.origin.git" rev-parse "refs/heads/release/cli-v0.2.1" >/dev/null \
  || fail "expected branch release/cli-v0.2.1 on origin"

# ----------------------------------------------------------------------------
# Test 9: gh pr create fails → branch stays on origin, recovery printed
# ----------------------------------------------------------------------------
echo "[test] gh pr create failure triggers pushed-stage cleanup"
REPO9="$(new_tmp)"
init_repo "$REPO9" "0.6.0"
status="$(run_publish "$REPO9" "patch" $'y\n' "GH_FAIL_AT=pr-create")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when gh pr create fails"

# Recovery instructions emitted by the trap.
grep -F "release branch 'release/cli-v0.6.1' was pushed but PR creation failed" \
  "$REPO9/stderr.log" >/dev/null \
  || { cat "$REPO9/stderr.log" >&2; fail "missing pushed-stage recovery message"; }
grep -F "git push origin --delete release/cli-v0.6.1" "$REPO9/stderr.log" >/dev/null \
  || fail "missing rollback hint in recovery message"

# Branch is on origin (push succeeded before gh failed).
git -C "$REPO9.origin.git" rev-parse "refs/heads/release/cli-v0.6.1" >/dev/null \
  || fail "expected branch on origin after push, before failed PR"

# ----------------------------------------------------------------------------
# Test 10: push fails → committed-stage cleanup, branch deleted locally
# ----------------------------------------------------------------------------
echo "[test] push failure rolls back local branch"
REPO10="$(new_tmp)"
init_repo "$REPO10" "0.7.0"
mkdir -p "$REPO10/bin-git"
cat >"$REPO10/bin-git/git" <<'WRAPPER'
#!/usr/bin/env bash
if [[ "${1:-}" == "-C" && "${3:-}" == "push" ]]; then
  echo "fatal: forced push failure" >&2
  exit 128
fi
exec /usr/bin/git "$@"
WRAPPER
chmod +x "$REPO10/bin-git/git"
status=0
printf 'y\n' | env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE \
  REPO_ROOT="$REPO10" PATH="$REPO10/bin-git:$REPO10/bin:$PATH" \
  GH_LOG_FILE="$REPO10/gh-stub.log" \
  bash "$REPO10/scripts/publish-cli.sh" "patch" \
  >"$REPO10/stdout.log" 2>"$REPO10/stderr.log" || status=$?
[[ "$status" -ne 0 ]] || fail "expected non-zero exit when push fails"

grep -F "rolling back local branch" "$REPO10/stderr.log" >/dev/null \
  || { cat "$REPO10/stderr.log" >&2; fail "missing committed-stage rollback message"; }

# Back on main, release branch deleted.
[[ "$(git -C "$REPO10" rev-parse --abbrev-ref HEAD)" == "main" ]] \
  || fail "not on main after push failure cleanup"
if git -C "$REPO10" rev-parse -q --verify "refs/heads/release/cli-v0.7.1" >/dev/null 2>&1; then
  fail "local release branch should be deleted after push failure"
fi

# Origin must NOT have the branch (push was blocked).
if git -C "$REPO10.origin.git" rev-parse -q --verify "refs/heads/release/cli-v0.7.1" >/dev/null 2>&1; then
  fail "origin should not have branch when push failed"
fi

# gh pr create must NOT have run.
if [[ -f "$REPO10/gh-stub.log" ]] && grep -F "pr create" "$REPO10/gh-stub.log" >/dev/null; then
  fail "gh pr create ran despite push failure"
fi

# ----------------------------------------------------------------------------
# Test 11: prerelease tag → abort with clear message
# ----------------------------------------------------------------------------
echo "[test] prerelease tag aborts with message"
REPO11="$(new_tmp)"
init_repo "$REPO11" "0.1.0" "cli-v0.2.0-rc.1"
status="$(run_publish "$REPO11" "patch")"
[[ "$status" -ne 0 ]] || fail "expected non-zero exit for prerelease tag"
grep -F "contains prerelease suffix" "$REPO11/stderr.log" >/dev/null \
  || { cat "$REPO11/stderr.log" >&2; fail "expected prerelease rejection message"; }
grep -F "only supports pure X.Y.Z" "$REPO11/stderr.log" >/dev/null \
  || fail "expected X.Y.Z hint in error"

# ----------------------------------------------------------------------------
# Test 12: local-only orphan tag must not influence baseline
# ----------------------------------------------------------------------------
# Simulates the failure mode where `git push origin cli-vX.Y.Z` failed after
# `git tag cli-vX.Y.Z origin/main` succeeded locally. The orphan tag exists
# locally but not on origin. Baseline must come from origin only.
echo "[test] local-only orphan tag ignored for baseline"
REPO12="$(new_tmp)"
init_repo "$REPO12" "0.1.0" "cli-v0.1.0"
git -C "$REPO12" tag "cli-v0.3.0"
status="$(run_publish "$REPO12" "patch" $'y\n')"
[[ "$status" -eq 0 ]] || { cat "$REPO12/stderr.log" >&2; fail "expected success, got $status"; }
grep -F "baseline: 0.1.0 (from origin cli-v0.1.0)" "$REPO12/stdout.log" >/dev/null \
  || { cat "$REPO12/stdout.log" >&2; fail "baseline must come from origin (0.1.0), not local orphan (0.3.0)"; }
git -C "$REPO12.origin.git" rev-parse "refs/heads/release/cli-v0.1.1" >/dev/null \
  || fail "expected branch release/cli-v0.1.1 on origin (orphan should not have shifted target to 0.3.1)"

echo "all tests passed"
