#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
CHECK="${SCRIPT_DIR}/tidb-migration-history.sh"
BACKEND_VALIDATE="${SCRIPT_DIR}/../../scripts/ci-backend-required.sh"
FIXTURE_ROOT="$(mktemp -d)"
trap 'rm -rf "${FIXTURE_ROOT}"' EXIT

REPO=""
BASE_301=""
checks=0
REAL_GIT="$(command -v git)"
FAKE_GIT_DIR="${FIXTURE_ROOT}/fake-bin"

mkdir -p "${FAKE_GIT_DIR}"
cat >"${FAKE_GIT_DIR}/git" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

if [[ "${AGENTTEAM_TEST_GIT_FAILURE:-}" == "ls-tree" && "${1:-}" == "ls-tree" ]]; then
  exit 42
fi
if [[ "${AGENTTEAM_TEST_GIT_FAILURE:-}" == "candidate-diff" && "${1:-}" == "diff" ]]; then
  for arg in "$@"; do
    if [[ "${arg}" == "--name-status" ]]; then
      exit 43
    fi
  done
fi
exec "${AGENTTEAM_TEST_REAL_GIT}" "$@"
EOF
chmod +x "${FAKE_GIT_DIR}/git"

fail_test() {
  printf 'FAIL: %s\n' "$1" >&2
  exit 1
}

init_repo() {
  local name="$1"

  REPO="${FIXTURE_ROOT}/${name}"
  git init -q "${REPO}"
  git -C "${REPO}" config user.email "guardrail@example.test"
  git -C "${REPO}" config user.name "Guardrail Test"
  mkdir -p "${REPO}/internal/tidb/migrations"
  cat >"${REPO}/internal/tidb/migrations/00301_base.sql" <<'EOF'
-- +goose Up
SELECT 301;
EOF
  git -C "${REPO}" add internal/tidb/migrations/00301_base.sql
  git -C "${REPO}" commit -q -m "test: add base migration"
  BASE_301="$(git -C "${REPO}" rev-parse HEAD)"
  git -C "${REPO}" update-ref refs/remotes/origin/main "${BASE_301}"
}

add_migration() {
  local version="$1"
  local suffix="$2"
  local padded

  printf -v padded '%05d' "${version}"
  cat >"${REPO}/internal/tidb/migrations/${padded}_${suffix}.sql" <<EOF
-- +goose Up
SELECT ${version};
EOF
  git -C "${REPO}" add "internal/tidb/migrations/${padded}_${suffix}.sql"
}

commit_changes() {
  git -C "${REPO}" commit -q -m "test: update fixture"
}

run_check() {
  local target_ref="$1"

  (
    cd "${REPO}"
    if [[ "${target_ref}" == "__default__" ]]; then
      unset AGENTTEAM_TIDB_MIGRATION_BASE_REF
      "${CHECK}"
    else
      AGENTTEAM_TIDB_MIGRATION_BASE_REF="${target_ref}" "${CHECK}"
    fi
  )
}

run_check_with_git_failure() {
  local target_ref="$1"
  local failure="$2"

  (
    cd "${REPO}"
    PATH="${FAKE_GIT_DIR}:${PATH}" \
      AGENTTEAM_TEST_REAL_GIT="${REAL_GIT}" \
      AGENTTEAM_TEST_GIT_FAILURE="${failure}" \
      AGENTTEAM_TIDB_MIGRATION_BASE_REF="${target_ref}" \
      "${CHECK}"
  )
}

expect_pass() {
  local label="$1"
  local target_ref="$2"
  local output

  if ! output="$(run_check "${target_ref}" 2>&1)"; then
    fail_test "${label} should pass: ${output}"
  fi
  grep -F "TiDB migration target ref input:" <<<"${output}" >/dev/null ||
    fail_test "${label} did not print the target ref input"
  grep -F "TiDB migration target resolved commit:" <<<"${output}" >/dev/null ||
    fail_test "${label} did not print the resolved target commit"
  grep -F "TiDB migration merge-base:" <<<"${output}" >/dev/null ||
    fail_test "${label} did not print the merge-base"
  grep -F "TiDB migration target maximum:" <<<"${output}" >/dev/null ||
    fail_test "${label} did not print the target maximum"
  checks=$((checks + 1))
}

expect_fail() {
  local label="$1"
  local target_ref="$2"
  local expected="$3"
  local output

  if output="$(run_check "${target_ref}" 2>&1)"; then
    fail_test "${label} should fail"
  fi
  grep -F "${expected}" <<<"${output}" >/dev/null ||
    fail_test "${label} missing diagnostic '${expected}': ${output}"
  checks=$((checks + 1))
}

expect_git_failure() {
  local label="$1"
  local target_ref="$2"
  local failure="$3"
  local expected="$4"
  local output

  if output="$(run_check_with_git_failure "${target_ref}" "${failure}" 2>&1)"; then
    fail_test "${label} should fail"
  fi
  grep -F "${expected}" <<<"${output}" >/dev/null ||
    fail_test "${label} missing diagnostic '${expected}': ${output}"
  checks=$((checks + 1))
}

# The default origin/main target and an explicit commit both accept a new
# migration strictly above the target's current maximum.
init_repo "new-version"
add_migration 302 "candidate"
commit_changes
expect_pass "base 00301 plus 00302 with default ref" "__default__"
expect_pass "base 00301 plus 00302 with explicit ref" "${BASE_301}"

# Versions at or below the target high-water mark are rejected.
init_repo "version-300"
add_migration 300 "candidate"
commit_changes
expect_fail "candidate 00300" "${BASE_301}" \
  "candidate migration version 00300 must be greater than target maximum 00301; next available migration version is 00302"

init_repo "version-301"
add_migration 301 "candidate"
commit_changes
expect_fail "candidate 00301" "${BASE_301}" \
  "candidate migration version 00301 must be greater than target maximum 00301; next available migration version is 00302"

# The high-water mark comes from the current target tree, not the merge-base.
init_repo "target-305"
add_migration 305 "target"
commit_changes
TARGET_305="$(git -C "${REPO}" rev-parse HEAD)"
git -C "${REPO}" switch -q --detach "${BASE_301}"
add_migration 305 "candidate"
commit_changes
expect_fail "candidate matching target 00305" "${TARGET_305}" \
  "candidate migration version 00305 must be greater than target maximum 00305; next available migration version is 00306"

# Candidate versions must also be unique within one branch.
init_repo "duplicate-candidate"
add_migration 302 "candidate_a"
add_migration 302 "candidate_b"
commit_changes
expect_fail "duplicate candidate version" "${BASE_301}" \
  "candidate migration version 00302 is not unique"

# Historical migration content, presence, names, and file types are immutable.
init_repo "modified-history"
printf '\nSELECT 999;\n' >>"${REPO}/internal/tidb/migrations/00301_base.sql"
git -C "${REPO}" add internal/tidb/migrations/00301_base.sql
commit_changes
expect_fail "modified migration" "${BASE_301}" "forbidden migration change status M"

# A failed, unapplied production migration may be repaired only when the
# manifest pins the exact target and candidate blobs. The repair reason is the
# fourth tab-separated field so reviewers have an auditable incident record.
init_repo "approved-repair"
OLD_BLOB="$(git -C "${REPO}" rev-parse "${BASE_301}:internal/tidb/migrations/00301_base.sql")"
printf '\nSELECT 302;\n' >>"${REPO}/internal/tidb/migrations/00301_base.sql"
NEW_BLOB="$(git -C "${REPO}" hash-object internal/tidb/migrations/00301_base.sql)"
mkdir -p "${REPO}/guardrails/storage"
printf '%s\t%s\t%s\t%s\n' \
  "internal/tidb/migrations/00301_base.sql" "${OLD_BLOB}" "${NEW_BLOB}" \
  "fixture failed before its version was recorded" \
  >"${REPO}/guardrails/storage/tidb-migration-repairs.tsv"
git -C "${REPO}" add internal/tidb/migrations/00301_base.sql guardrails/storage/tidb-migration-repairs.tsv
commit_changes
expect_pass "exactly pinned migration repair" "${BASE_301}"

init_repo "mismatched-repair"
OLD_BLOB="$(git -C "${REPO}" rev-parse "${BASE_301}:internal/tidb/migrations/00301_base.sql")"
printf '\nSELECT 302;\n' >>"${REPO}/internal/tidb/migrations/00301_base.sql"
mkdir -p "${REPO}/guardrails/storage"
printf '%s\t%s\t%s\t%s\n' \
  "internal/tidb/migrations/00301_base.sql" "${OLD_BLOB}" \
  "0000000000000000000000000000000000000000" "wrong candidate blob" \
  >"${REPO}/guardrails/storage/tidb-migration-repairs.tsv"
git -C "${REPO}" add internal/tidb/migrations/00301_base.sql guardrails/storage/tidb-migration-repairs.tsv
commit_changes
expect_fail "migration repair with mismatched candidate blob" "${BASE_301}" \
  "forbidden migration change status M"

init_repo "deleted-history"
git -C "${REPO}" rm -q internal/tidb/migrations/00301_base.sql
commit_changes
expect_fail "deleted migration" "${BASE_301}" "forbidden migration change status D"

init_repo "renamed-history"
git -C "${REPO}" mv \
  internal/tidb/migrations/00301_base.sql \
  internal/tidb/migrations/00302_renamed.sql
commit_changes
expect_fail "renamed migration" "${BASE_301}" "forbidden migration change status D"

init_repo "type-changed-history"
rm "${REPO}/internal/tidb/migrations/00301_base.sql"
ln -s missing.sql "${REPO}/internal/tidb/migrations/00301_base.sql"
git -C "${REPO}" add internal/tidb/migrations/00301_base.sql
commit_changes
expect_fail "type-changed migration" "${BASE_301}" "forbidden migration change status T"

# Changes outside the migration directory do not affect the guard.
init_repo "non-migration-change"
printf 'fixture\n' >"${REPO}/README.md"
git -C "${REPO}" add README.md
commit_changes
expect_pass "non-migration change" "${BASE_301}"

# The target must resolve to a commit.
init_repo "missing-ref"
expect_fail "missing target ref" "refs/remotes/origin/missing" \
  "cannot resolve TiDB migration target ref"

# Disconnected histories cannot define a candidate merge-base.
init_repo "missing-merge-base"
git -C "${REPO}" switch -q --orphan unrelated
printf 'unrelated\n' >"${REPO}/README.md"
git -C "${REPO}" add README.md
commit_changes
expect_fail "missing merge-base" "${BASE_301}" "cannot compute merge-base"

# Both target and candidate migration filenames must follow the canonical
# five-digit SQL shape.
init_repo "invalid-target-filename"
printf '%s\n' '-- +goose Up' >"${REPO}/internal/tidb/migrations/not_numbered.sql"
git -C "${REPO}" add internal/tidb/migrations/not_numbered.sql
commit_changes
INVALID_TARGET="$(git -C "${REPO}" rev-parse HEAD)"
git -C "${REPO}" switch -q --detach "${BASE_301}"
expect_fail "invalid target migration filename" "${INVALID_TARGET}" \
  "target contains an invalid migration filename"

init_repo "invalid-candidate-filename"
printf '%s\n' '-- +goose Up' >"${REPO}/internal/tidb/migrations/not_numbered.sql"
git -C "${REPO}" add internal/tidb/migrations/not_numbered.sql
commit_changes
expect_fail "invalid candidate migration filename" "${BASE_301}" \
  "added file in internal/tidb/migrations is not a valid migration"

# Git producer failures must not turn into empty, passing input streams.
init_repo "failed-target-read"
expect_git_failure "failed target tree read" "${BASE_301}" "ls-tree" \
  "cannot read TiDB migration tree from target commit"

init_repo "failed-candidate-read"
add_migration 302 "candidate"
commit_changes
expect_git_failure "failed candidate diff read" "${BASE_301}" "candidate-diff" \
  "cannot read candidate TiDB migration changes"

# A target-only migration raises the target maximum without becoming a
# candidate change when HEAD has no migration delta from the merge-base.
init_repo "target-only"
add_migration 302 "target_only"
commit_changes
TARGET_ONLY="$(git -C "${REPO}" rev-parse HEAD)"
git -C "${REPO}" switch -q --detach "${BASE_301}"
expect_pass "target-only migration with unchanged candidate" "${TARGET_ONLY}"

# A branch created from an older target must be rechecked against the latest
# target: the old target maximum fails, while the next version passes.
init_repo "old-candidate"
add_migration 305 "target"
commit_changes
LATEST_TARGET="$(git -C "${REPO}" rev-parse HEAD)"
git -C "${REPO}" switch -q --detach "${BASE_301}"
add_migration 305 "old_candidate"
commit_changes
expect_fail "old candidate at latest target maximum" "${LATEST_TARGET}" \
  "candidate migration version 00305 must be greater than target maximum 00305; next available migration version is 00306"
git -C "${REPO}" reset -q --hard "${BASE_301}"
add_migration 306 "next_candidate"
commit_changes
expect_pass "old candidate using next target version" "${LATEST_TARGET}"

# Local migration changes are rejected because the candidate is the committed
# HEAD, and accepting a dirty directory would leave those changes unchecked.
init_repo "untracked"
add_migration 302 "untracked"
git -C "${REPO}" reset -q
expect_fail "untracked migration" "${BASE_301}" "untracked migration path"

init_repo "staged"
printf '\nSELECT 302;\n' >>"${REPO}/internal/tidb/migrations/00301_base.sql"
git -C "${REPO}" add internal/tidb/migrations/00301_base.sql
expect_fail "staged tracked migration change" "${BASE_301}" "staged migration changes"

init_repo "unstaged"
printf '\nSELECT 302;\n' >>"${REPO}/internal/tidb/migrations/00301_base.sql"
expect_fail "unstaged tracked migration change" "${BASE_301}" "unstaged migration changes"

# Two branches may independently select 00306 against a 00305 target. Once
# the first branch becomes the target, the second must fail its final recheck.
init_repo "parallel"
add_migration 305 "target"
commit_changes
PARALLEL_TARGET="$(git -C "${REPO}" rev-parse HEAD)"
git -C "${REPO}" switch -q -c candidate-one "${PARALLEL_TARGET}"
add_migration 306 "candidate_one"
commit_changes
CANDIDATE_ONE="$(git -C "${REPO}" rev-parse HEAD)"
expect_pass "first parallel 00306 against old target" "${PARALLEL_TARGET}"
git -C "${REPO}" switch -q -c candidate-two "${PARALLEL_TARGET}"
add_migration 306 "candidate_two"
commit_changes
expect_pass "second parallel 00306 against old target" "${PARALLEL_TARGET}"
expect_fail "second parallel 00306 after first enters target" "${CANDIDATE_ONE}" \
  "candidate migration version 00306 must be greater than target maximum 00306; next available migration version is 00307"

# Backend integration cannot silently use the local origin/main convenience or
# another mutable ref; its caller must provide the target's full commit SHA.
if output="$(env -u AGENTTEAM_TIDB_MIGRATION_BASE_REF "${BACKEND_VALIDATE}" validate 2>&1)"; then
  fail_test "backend validation without target SHA should fail"
fi
grep -F "must be set to the integration target commit SHA" <<<"${output}" >/dev/null ||
  fail_test "backend validation missing-target diagnostic: ${output}"
checks=$((checks + 1))

if output="$(AGENTTEAM_TIDB_MIGRATION_BASE_REF=origin/main "${BACKEND_VALIDATE}" validate 2>&1)"; then
  fail_test "backend validation with mutable target ref should fail"
fi
grep -F "must be an immutable full commit SHA" <<<"${output}" >/dev/null ||
  fail_test "backend validation mutable-target diagnostic: ${output}"
checks=$((checks + 1))

printf 'TiDB migration history guardrail tests passed (%d checks).\n' "${checks}"
