#!/usr/bin/env bash
# Regression harness for ci-queue-wait.sh's --no-ci-expected assertion:
# the sanctioned merge path for a repository with no CI configured at all.
#
# Zero status contexts ("no-status") stays fail-closed for --purpose merge
# by default, because at merge time no-status can also mean "CI has not
# reported yet". --no-ci-expected reclassifies ONLY that zero-context case
# as queue-clear, and only for a caller whose acting token holds repository
# admin. This harness pins:
#   (a) merge + no-status + flag + admin     -> exit 0, audit line + JSONL.
#   (b) merge + no-status, no flag           -> exit 3, existing text (unchanged).
#   (c) merge + no-status + flag + non-admin -> exit 77 ASSERTION_REFUSED
#       (distinct text, exit code NOT 3) + JSONL refusal record.
#   (c2) flag + admin payload without the admin field -> fail closed as (c).
#   (d) flag + --require-status              -> usage error, before any network.
#   (e) flag + a real pending context        -> still holds (timeout 124),
#       and the admin endpoint is never consulted.
#   (f) push + no-status, with and without the flag -> push queue-clear
#       unchanged; no admin consultation on push.
#   (g) flag + admin lookup unreachable      -> CANNOT_ASSERT hold (75),
#       not a silent pass and not a refusal.
#   (h) flag + admin stub + NO MOSAIC_GIT_IDENTITY -> refusal BEFORE
#       queue-clear and BEFORE the admin lookup: exit 78, no queue-clear
#       line, an ASSERTION_UNATTRIBUTABLE JSONL record, no repos/ call.

set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/ci-queue-wait-no-ci-expected}"
REPO_DIR="$WORK_DIR/repo"
STUB_DIR="$WORK_DIR/stubs"
URL_LOG="$WORK_DIR/urls.log"

rm -rf "$WORK_DIR"
mkdir -p "$REPO_DIR" "$STUB_DIR"

git -C "$REPO_DIR" init -q
git -C "$REPO_DIR" remote add origin https://git.example.test/acme/widgets.git

# Same stub conventions as test-ci-queue-wait-no-status.sh; adds the
# repository-object endpoint (admin state) selected by MOSAIC_STUB_ADMIN_MODE.
cat > "$STUB_DIR/curl" <<'SH'
#!/usr/bin/env bash
set -euo pipefail

has_w=0
url=""
for arg in "$@"; do
    case "$arg" in
        -w) has_w=1 ;;
        http://*|https://*) url="$arg" ;;
    esac
done
printf '%s\n' "$url" >> "${MOSAIC_STUB_URL_LOG:?}"

case "$url" in
    */branches/*)
        body='{"commit":{"id":"deadbeefcafef00d0123456789abcdef01234567"}}'
        if [[ "$has_w" == 1 ]]; then
            printf '%s\n200' "$body"
        else
            printf '%s' "$body"
        fi
        exit 0
        ;;
    */status)
        mode="${MOSAIC_STUB_STATUS_MODE:?MOSAIC_STUB_STATUS_MODE not set}"
        case "$mode" in
            no-status)   body='{"state":"","statuses":[]}' ;;
            real-pending) body='{"state":"pending","statuses":[{"context":"ci/woodpecker","status":"running","target_url":""}]}' ;;
            *) echo "curl stub: unknown status mode=$mode" >&2; exit 2 ;;
        esac
        printf '%s' "$body"
        exit 0
        ;;
    */repos/*)
        mode="${MOSAIC_STUB_ADMIN_MODE:?MOSAIC_STUB_ADMIN_MODE not set}"
        case "$mode" in
            admin)          body='{"permissions":{"admin":true,"push":true,"pull":true}}' ;;
            non-admin)      body='{"permissions":{"admin":false,"push":true,"pull":true}}' ;;
            no-admin-field) body='{"permissions":{}}' ;;
            unreachable)    exit 7 ;;
            *) echo "curl stub: unknown admin mode=$mode" >&2; exit 2 ;;
        esac
        if [[ "$has_w" == 1 ]]; then
            printf '%s\n200' "$body"
        else
            printf '%s' "$body"
        fi
        exit 0
        ;;
    *)
        echo "curl stub: unrecognized URL: $url" >&2
        exit 2
        ;;
esac
SH
chmod +x "$STUB_DIR/curl"

failures=0

run_guard() {
    local name="$1"; shift
    (
        cd "$REPO_DIR" || exit
        export PATH="$STUB_DIR:$PATH"
        export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
        export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit-$name.jsonl"
        export MOSAIC_STUB_URL_LOG="$URL_LOG"
        export GITEA_TOKEN="stub-token"
        export GITEA_URL="https://git.example.test"
        export MOSAIC_GIT_IDENTITY="test-identity"
        "$SCRIPT_DIR/ci-queue-wait.sh" -B main -t 3 -i 1 "$@"
    )
}

# The suite exports test-identity globally, so the unattributable-caller
# case must strip it from the child environment at invocation with env -u,
# not rely on the export order.
run_guard_no_identity() {
    local name="$1"; shift
    (
        cd "$REPO_DIR" || exit
        export PATH="$STUB_DIR:$PATH"
        export MOSAIC_CREDENTIALS_FILE="$WORK_DIR/no-credentials.json"
        export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit-$name.jsonl"
        export MOSAIC_STUB_URL_LOG="$URL_LOG"
        export GITEA_TOKEN="stub-token"
        export GITEA_URL="https://git.example.test"
        export MOSAIC_GIT_IDENTITY="test-identity"
        env -u MOSAIC_GIT_IDENTITY \
            "$SCRIPT_DIR/ci-queue-wait.sh" -B main -t 3 -i 1 "$@"
    )
}

expect_rc() {
    local name="$1" want="$2" got="$3"
    if [[ "$want" == "not3" ]]; then
        if [[ "$got" -eq 0 || "$got" -eq 3 ]]; then
            echo "FAIL $name: expected a refusal rc (nonzero, not 3), got $got" >&2
            failures=$((failures + 1))
            return 1
        fi
    elif [[ "$got" -ne "$want" ]]; then
        echo "FAIL $name: expected rc=$want, got rc=$got" >&2
        failures=$((failures + 1))
        return 1
    fi
    return 0
}

expect_text() {
    local name="$1" want="$2" output="$3" polarity="${4:-present}"
    if [[ "$polarity" == "present" && "$output" != *"$want"* ]]; then
        echo "FAIL $name: output missing '$want'" >&2
        printf '%s\n' "$output" >&2
        failures=$((failures + 1))
    elif [[ "$polarity" == "absent" && "$output" == *"$want"* ]]; then
        echo "FAIL $name: output unexpectedly contains '$want'" >&2
        printf '%s\n' "$output" >&2
        failures=$((failures + 1))
    fi
}

repo_root_fetched() {
    grep -q 'repos/acme/widgets$' "$URL_LOG"
}

# (a) merge + no-status + flag + admin -> exit 0, assertion line, JSONL record.
: > "$URL_LOG"
set +e
out_a=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=admin run_guard a --purpose merge --no-ci-expected 2>&1)
rc_a=$?
set -u
if expect_rc a 0 "$rc_a"; then
    expect_text a "queue-clear state=no-status purpose=merge asserted-by=test-identity reason=no-ci-expected branch=main" "$out_a"
    expect_text a "ASSERTED_NOT_READY" "$out_a" absent
    if ! grep -q '"outcome":"NO_CI_ASSERTED"' "$WORK_DIR/audit-a.jsonl" 2>/dev/null; then
        echo "FAIL a: expected a NO_CI_ASSERTED JSONL audit record" >&2
        failures=$((failures + 1))
    elif ! grep -q '"asserted_by":"test-identity"' "$WORK_DIR/audit-a.jsonl"; then
        echo "FAIL a: audit record does not name the asserting identity" >&2
        failures=$((failures + 1))
    fi
fi

# (b) merge + no-status, no flag -> exit 3, existing error text unchanged.
: > "$URL_LOG"
set +e
out_b=$(MOSAIC_STUB_STATUS_MODE=no-status run_guard b --purpose merge 2>&1)
rc_b=$?
set -u
if expect_rc b 3 "$rc_b"; then
    expect_text b "Error: ASSERTED_NOT_READY state=no-status purpose=merge branch=main." "$out_b"
    expect_text b "asserted-by" "$out_b" absent
fi
if repo_root_fetched; then
    echo "FAIL b: admin endpoint consulted without the flag" >&2
    failures=$((failures + 1))
fi

# (c) merge + no-status + flag + non-admin -> distinct refusal, rc NOT 3.
: > "$URL_LOG"
set +e
out_c=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=non-admin run_guard c --purpose merge --no-ci-expected 2>&1)
rc_c=$?
set -u
if expect_rc c not3 "$rc_c"; then
    if [[ "$rc_c" -ne 77 ]]; then
        echo "FAIL c: expected the documented refusal rc=77, got $rc_c" >&2
        failures=$((failures + 1))
    fi
    expect_text c "ASSERTION_REFUSED state=no-status purpose=merge asserted-by=test-identity reason=no-ci-expected branch=main" "$out_c"
    expect_text c "ASSERTED_NOT_READY" "$out_c" absent
    if ! grep -q '"outcome":"ASSERTION_REFUSED"' "$WORK_DIR/audit-c.jsonl" 2>/dev/null; then
        echo "FAIL c: expected an ASSERTION_REFUSED JSONL audit record" >&2
        failures=$((failures + 1))
    fi
fi

# (c2) admin payload with no admin field -> fail closed as non-admin.
: > "$URL_LOG"
set +e
out_c2=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=no-admin-field run_guard c2 --purpose merge --no-ci-expected 2>&1)
rc_c2=$?
set -u
if expect_rc c2 77 "$rc_c2"; then
    expect_text c2 "ASSERTION_REFUSED" "$out_c2"
fi

# (d) flag + --require-status -> usage error before any network I/O.
: > "$URL_LOG"
set +e
out_d=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=admin run_guard d --purpose merge --no-ci-expected --require-status 2>&1)
rc_d=$?
set -u
if expect_rc d 1 "$rc_d"; then
    expect_text d "--no-ci-expected and --require-status contradict" "$out_d"
fi
if [[ -s "$URL_LOG" ]]; then
    echo "FAIL d: usage error must precede every network call" >&2
    failures=$((failures + 1))
fi

# (e) flag + a real pending context -> still holds; admin endpoint never asked.
: > "$URL_LOG"
set +e
out_e=$(MOSAIC_STUB_STATUS_MODE=real-pending MOSAIC_STUB_ADMIN_MODE=admin run_guard e --purpose merge --no-ci-expected 2>&1)
rc_e=$?
set -u
if expect_rc e 124 "$rc_e"; then
    expect_text e "ASSERTED_NOT_READY" "$out_e"
    expect_text e "ci/woodpecker=running" "$out_e"
fi
if repo_root_fetched; then
    echo "FAIL e: a pending context must not trigger the admin assertion" >&2
    failures=$((failures + 1))
fi

# (f) push + no-status stays queue-clear, with and without the flag.
: > "$URL_LOG"
set +e
out_f=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=non-admin run_guard f --purpose push 2>&1)
rc_f=$?
set -u
if expect_rc f 0 "$rc_f"; then
    expect_text f "queue-clear state=no-status purpose=push branch=main; no queued or running CI." "$out_f"
fi
: > "$URL_LOG"
set +e
out_f2=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=non-admin run_guard f2 --purpose push --no-ci-expected 2>&1)
rc_f2=$?
set -u
if expect_rc f2 0 "$rc_f2"; then
    expect_text f2 "queue-clear state=no-status purpose=push branch=main; no queued or running CI." "$out_f2"
    expect_text f2 "asserted-by" "$out_f2" absent
fi
if repo_root_fetched; then
    echo "FAIL f: push must not consult the admin endpoint" >&2
    failures=$((failures + 1))
fi

# (g) flag + admin lookup unreachable -> CANNOT_ASSERT hold (75), not a pass.
: > "$URL_LOG"
set +e
out_g=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=unreachable run_guard g --purpose merge --no-ci-expected 2>&1)
rc_g=$?
set -u
if expect_rc g 75 "$rc_g"; then
    expect_text g "CANNOT_ASSERT reason=repo-permissions-unavailable" "$out_g"
fi
if ! grep -q '"outcome":"CANNOT_ASSERT"' "$WORK_DIR/audit-g.jsonl" 2>/dev/null; then
    echo "FAIL g: expected a CANNOT_ASSERT JSONL audit record" >&2
    failures=$((failures + 1))
fi

# (h) flag + admin stub + no asserting identity -> refusal before queue-clear
# and before the admin lookup: rc 78, no queue-clear line, an
# ASSERTION_UNATTRIBUTABLE JSONL record, and zero repos/ network calls.
: > "$URL_LOG"
set +e
out_h=$(MOSAIC_STUB_STATUS_MODE=no-status MOSAIC_STUB_ADMIN_MODE=admin run_guard_no_identity h --purpose merge --no-ci-expected 2>&1)
rc_h=$?
set -u
if expect_rc h 78 "$rc_h"; then
    expect_text h "ASSERTION_UNATTRIBUTABLE state=no-status purpose=merge asserted-by=unknown reason=no-ci-expected branch=main" "$out_h"
    expect_text h "queue-clear" "$out_h" absent
    if ! grep -q '"outcome":"ASSERTION_UNATTRIBUTABLE"' "$WORK_DIR/audit-h.jsonl" 2>/dev/null; then
        echo "FAIL h: expected an ASSERTION_UNATTRIBUTABLE JSONL audit record" >&2
        failures=$((failures + 1))
    fi
fi
if repo_root_fetched; then
    echo "FAIL h: an unattributable caller must not trigger the permission lookup" >&2
    failures=$((failures + 1))
fi

if [[ "$failures" -ne 0 ]]; then
    echo "ci-queue-wait no-ci-expected regression failed ($failures assertions)" >&2
    exit 1
fi

echo "ci-queue-wait no-ci-expected regression passed (all outcome classes)"
