#!/bin/ksh
# Enforcement tests for Workflow.txt against the `test` repo.
#
#   ./run              all tests (normal, then abuse)
#   ./run release      one test
#
# Assumes the current hook-signed-tag (tag only required for r-/h- merges)
# and VREF/ASSURANCE_MAIN are deployed.
#
# Server: keydir/{moon,redqueen,hmech}.pub, users in gitolite.conf,
#         allowed_signers has moon@host / redqueen@host / hmech@host.
# Client: ~/.ssh/config defines git, git-redqueen, git-hmech.
#
# First run against a fresh test.git:  ./run
# Later runs: main already exists, so `bootstrap` will FAIL — that is
# expected. To test the bootstrap path again, delete test.git on the
# server and re-run.

set -u
ROOT=/tmp/gitolite_test
REPO=test
FAILED=0

# ---- users ----------------------------------------------------------------
domain=sophia.host
ghost=git;              ghost_key=~/.ssh/ghost
moon=git-moon;              moon_key=~/.ssh/moon
redqueen=git-redqueen; redqueen_key=~/.ssh/redqueen
hmech=git-hmech;       hmech_key=~/.ssh/hmech

# ---- helpers --------------------------------------------------------------
say() { printf '%s\n' "---- $*"; }
ok()  { printf '%s\n' "PASS: $*"; }
bad() { printf '%s\n' "FAIL: $*" >&2; FAILED=$((FAILED + 1)); }

# Fresh v-* tag per call. Matches refs/tags/v-[0-9]* policy.
tag() { printf 'v-0.0.%s-%s' "$(date +%s)" "$1"; }

# as <user> — clone fresh and configure identity for that user.
as() {
    user=$1
    eval alias=\$$user
    eval key=\$${user}_key
    [ -f "$key" ] || { bad "missing key: $key"; return 1; }
    rm -rf $ROOT
    git clone -q $alias:$REPO $ROOT || return 1
    cd $ROOT
    git config user.name       $user
    git config user.email      $user@$domain
    git config gpg.format      ssh
    git config user.signingkey $key
}

sync()  { git switch -q main && git pull -q --ff-only; }
clean() { cd /tmp && rm -rf $ROOT; }

# Delete a remote branch or tag; ignore failures (used only for tidying).
drop() {
    [ -n "${1:-}" ] || return 0
    git push -q origin ":$1" 2>/dev/null
    return 0
}

expect_ok() {
    d=$1; shift
    if "$@" >/dev/null 2>&1; then ok "$d"; else bad "$d"; fi
}
expect_fail() {
    d=$1; shift
    if "$@" >/dev/null 2>&1; then bad "$d"; else ok "$d"; fi
}

# ---- normal ---------------------------------------------------------------

bootstrap() {
    say "bootstrap — moon creates main on empty repo (§003-07)"
    as ghost || return
    git switch -q -c main
    print "bootstrap $(date)" > test.txt
    git add test.txt
    git commit -q -m "initial test.txt"
    expect_ok "moon pushes initial main" git push -u origin main
    clean
}

feature() {
    say "feature — f- merge to main, no tag required (§003-04)"
    as moon || return
    sync

    git switch -q -c f-test
    print "$(date +%s)" > result
    git add result
    git commit -q -m "feature on f-test"
    expect_ok "push f-test" git push -u origin f-test

    git switch -q main
    git merge -q --no-ff f-test -m "merge f-test"
    expect_fail "f- merge without tag rejected" git push origin main

    drop f-test
    clean
}

release() {
    say "release — r- merge to main, signed tag required (§003-05)"
    as moon || return
    sync

    t=$(tag release)
    git switch -q -c r-1.0.0
    print "$(date +%s)" > relfile
    git add relfile
    git commit -q -m "release prep 1.0.0"
    expect_ok "push r-1.0.0" git push -u origin r-1.0.0

    git switch -q main
    git merge -q --no-ff r-1.0.0 -m "merge r-1.0.0"
    git tag -s $t -m "Release $t"
    expect_ok "r- merge with signed tag accepted" git push origin main --follow-tags

    drop r-1.0.0
    clean
}

# ---- abuse ----------------------------------------------------------------

direct_commit() {
    say "abuse — ops direct commit to main (§006-01)"
    as moon || return
    sync
    print x > direct
    git add direct
    git commit -q -m "direct"
    expect_fail "direct push to main rejected" git push origin main
    clean
}

dev_to_main() {
    say "abuse — dev merges to main (§006-02)"
    as redqueen || return
    sync

    git switch -q -c f-dev
    print x > devfile
    git add devfile
    git commit -q -m "dev"
    expect_ok "dev pushes f-dev" git push -u origin f-dev

    git switch -q main
    git merge -q --no-ff f-dev -m "dev merge"
    expect_fail "dev cannot merge to main" git push origin main
    clean

    as moon || return
    drop f-dev
    clean
}

user_to_main() {
    say "abuse — user pushes to main (§006-02)"
    as hmech || return
    sync
    print x > userfile
    git add userfile
    git commit -q -m "user"
    expect_fail "user push to main rejected" git push origin main
    clean
}

user_to_feature() {
    say "abuse — user pushes to f- (§006-02)"
    as hmech || return
    sync
    git switch -q -c f-hmech
    print x > hfile
    git add hfile
    git commit -q -m "user"
    expect_fail "user push to f- rejected" git push -u origin f-hmech
    clean
}

release_no_tag() {
    say "abuse — r- merge without signed tag (§003-05)"
    as moon || return
    sync

    git switch -q -c r-1.0.1
    print "$(date +%s)" > relnofile
    git add relnofile
    git commit -q -m "release prep 1.0.1"
    expect_ok "push r-1.0.1" git push -u origin r-1.0.1

    git switch -q main
    git merge -q --no-ff r-1.0.1 -m "merge r-1.0.1"
    expect_fail "r- merge without tag rejected" git push origin main
    clean

    as moon || return
    drop r-1.0.1
    clean
}

release_unsigned_tag() {
    say "abuse — r- merge with unsigned tag (§006-01)"
    as moon || return
    sync

    t=$(tag unsigned)
    git switch -q -c r-1.0.2
    print "$(date +%s)" > relunfile
    git add relunfile
    git commit -q -m "release prep 1.0.2"
    expect_ok "push r-1.0.2" git push -u origin r-1.0.2

    git switch -q main
    git merge -q --no-ff r-1.0.2 -m "merge r-1.0.2"
    git -c tag.gpgsign=false tag -a $t -m "unsigned"
    expect_fail "unsigned tag rejected" git push origin main --follow-tags
    git tag -d $t >/dev/null
    clean

    as moon || return
    drop r-1.0.2
    clean
}

tag_outside_main() {
    say "abuse — v- tag pointing outside main (§006-01)"
    as moon || return
    sync

    git switch -q -c f-side
    print x > sidefile
    git add sidefile
    git commit -q -m "side"
    git push -q -u origin f-side

    t=$(tag outside)
    git tag -s $t -m "tag on non-main commit"
    expect_fail "tag outside main rejected" git push origin $t
    git tag -d $t >/dev/null
    drop f-side
    clean
}

tag_deletion() {
    say "abuse — delete a v- tag (§006-01)"
    as moon || return
    sync

    t=$(tag delme)
    git switch -q -c r-1.0.3
    print x > delfile
    git add delfile
    git commit -q -m "release prep 1.0.3"
    git switch -q main
    git merge -q --no-ff r-1.0.3 -m "merge r-1.0.3"
    git tag -s $t -m "will be deleted"
    git push -q origin main --follow-tags
    drop r-1.0.3

    expect_fail "tag deletion rejected" git push origin :refs/tags/$t
    clean
}

force_no_reason() {
    say "abuse — force-push without reason= (§006-07)"
    as moon || return
    sync
    git commit -q --amend --allow-empty -m "rewritten"
    expect_fail "force-push without reason= rejected" git push -f origin main
    clean
}

# ---- emergency (enable once merged into ASSURANCE_MAIN) -------------------
emergency_with_reason() {
    say "emergency — force-push with reason= (§006-07)"
    as moon || return
    sync
    git commit -q --amend --allow-empty -m "emergency"
    expect_ok "force-push with reason= accepted" \
              git push -f -o reason="test emergency" origin main
    clean
}

# ---- runner ---------------------------------------------------------------
default="bootstrap feature release
         direct_commit dev_to_main user_to_main user_to_feature
         release_no_tag release_unsigned_tag
         tag_outside_main tag_deletion force_no_reason"
[ $# -eq 0 ] && set -- $default

for t in "$@"; do
    cd /tmp
    $t || bad "test $t aborted"
done

print ""
print "failures: $FAILED"
exit $((FAILED > 0))
