#!/bin/bash
# Shared guards for the PR-writing scripts (create-pr.sh, edit-pr.sh,
# edit-pr-comment.sh).
#
# Motivation: a PR was once created with its body silently replaced by a stray
# positional argument. The wrapper accepted the call, gh succeeded, and success
# was reported while the composed body never reached GitHub. These functions
# make that class of failure loud, in both directions: a body must carry the
# sections the workflow requires before it is submitted, and after submission
# the PR is read back from GitHub and compared to what was composed.

# Refuse to run outside the repository, because the scripts here address GitHub
# through gh's {owner}/{repo} placeholder and gh expands it from the git remote
# of the current directory. Run from anywhere else, gh prints
#   unable to expand placeholder in path: failed to run git: fatal: not a git repository
# and the call fails. That failure used to reach the caller wearing the wrong
# clothes: list-pr-comments.sh reported a real PR as having no comments and
# exited 0, and get-pr-comment.sh blamed the comment id. A wrong answer that
# looks like a right one is the failure this library exists to prevent, so the
# check happens before any call is made.
#
# Deliberately local: it asks git, not GitHub. A round trip to the API on every
# invocation would buy nothing here, since a repository that exists but cannot
# be reached fails at the real call anyway, and is reported there.
require_repo_context() {
    if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
        echo "Error: this is not a git working directory: $PWD" >&2
        echo "The owner and repository are resolved from the git remote of the current" >&2
        echo "directory, so this script must be run from inside the repository." >&2
        echo "cd to the repository, or one of its worktrees, and run it again." >&2
        return 1
    fi
    if [ -z "$(git remote 2>/dev/null)" ]; then
        echo "Error: this git repository has no remote: $PWD" >&2
        echo "The owner and repository are resolved from the git remote, and there is" >&2
        echo "none here, so gh cannot tell which GitHub repository is meant." >&2
        echo "Run this from a checkout of the repository you mean to address." >&2
        return 1
    fi
}

# Sections every PR body must contain, per docs/GITHUB_WORKFLOW_SPEC.md
# ("PR Body Structure"). Kept to the three that never legitimately vary;
# the full structure remains the spec's to define.
require_pr_body_sections() {
    local body="$1"
    local missing=()
    local section
    for section in "## Summary" "## Related Jira Issue" "## Test Evidence"; do
        case "$body" in
            *"$section"*) ;;
            *) missing+=("$section") ;;
        esac
    done
    if [ "${#missing[@]}" -gt 0 ]; then
        echo "Error: PR body is missing required section(s):" >&2
        printf '  %s\n' "${missing[@]}" >&2
        echo "Required structure: docs/GITHUB_WORKFLOW_SPEC.md -> 'PR Body Structure'." >&2
        echo "Refusing to submit." >&2
        return 1
    fi
}

# The one tolerated difference between what was submitted and what landed.
#
# The Atlassian GitHub app appends reference-link definitions for Jira keys to
# PR bodies and PR comments shortly after they are written: blank lines
# followed by lines like
#   [QUE-123]: https://<site>.atlassian.net/browse/QUE-123?atlOrigin=...
# Every line of the tail must be blank or such a link definition. Anything else
# fails, loudly: a false alarm costs an investigation, a silent mismatch costs a
# wrong PR. Kept in one function so the rule cannot drift between the body
# verifier and the comment verifier.
verify_appended_tail() {
    local subject="$1"      # what the failure names, e.g. "42" or "comment 5652360015"
    local tail_landed="$2"

    local line
    while IFS= read -r line; do
        if [ -z "$line" ]; then
            continue
        fi
        case "$line" in
            "["*"]: https://"*"atlassian.net/browse/"*) ;;
            *)
                echo "Error: read-back verification failed for $subject." >&2
                echo "Unexpected content follows the submitted body:" >&2
                echo "  $line" >&2
                echo "Only Jira link definitions appended by the Atlassian app are tolerated there." >&2
                echo "Do not report this operation as a success." >&2
                return 1
                ;;
        esac
    done <<< "$tail_landed"
}

# Read the PR back from GitHub and confirm what landed is what was composed.
#
# The landed body must start with the submitted body byte for byte, and
# whatever follows must pass verify_appended_tail.
verify_pr_content() {
    local pr_ref="$1"           # PR number or URL
    local expected_title="$2"   # empty string skips the title check
    local expected_body="$3"    # empty string skips the body check

    if [ -n "$expected_title" ]; then
        local landed_title
        if ! landed_title="$(gh pr view "$pr_ref" --json title --jq .title)"; then
            echo "Error: could not read $pr_ref back from GitHub after writing to it." >&2
            echo "gh reported the failure above. The write may or may not have landed." >&2
            echo "Check the PR before doing anything else." >&2
            echo "Do not report this operation as a success." >&2
            return 1
        fi
        if [ "$landed_title" != "$expected_title" ]; then
            echo "Error: read-back verification failed for $pr_ref." >&2
            echo "  Title on GitHub: $landed_title" >&2
            echo "  Title submitted: $expected_title" >&2
            echo "Do not report this operation as a success." >&2
            return 1
        fi
    fi

    if [ -n "$expected_body" ]; then
        local want got
        # tr strips CR in case either side carries CRLF endings; the command
        # substitutions strip trailing newlines from both sides identically.
        want="$(printf '%s' "$expected_body" | tr -d '\r')"
        if ! got="$(gh pr view "$pr_ref" --json body --jq .body)"; then
            echo "Error: could not read $pr_ref back from GitHub after writing to it." >&2
            echo "gh reported the failure above. The write may or may not have landed." >&2
            echo "Check the PR before doing anything else." >&2
            echo "Do not report this operation as a success." >&2
            return 1
        fi
        got="$(printf '%s' "$got" | tr -d '\r')"
        if [ "${got:0:${#want}}" != "$want" ]; then
            echo "Error: read-back verification failed for $pr_ref." >&2
            echo "The body on GitHub does not begin with the submitted body." >&2
            echo "The PR exists but its content is wrong. Repair it with edit-pr.sh." >&2
            echo "Do not report this operation as a success." >&2
            return 1
        fi
        verify_appended_tail "$pr_ref" "${got:${#want}}"
    fi
}

# Read a PR comment back from GitHub and confirm what landed is what was
# submitted. Same rule as verify_pr_content, applied to one comment: the landed
# body must start with the submitted body byte for byte, and whatever follows
# must pass verify_appended_tail.
#
# An edit that lands wrong is worse than a failed edit, because the caller has
# already been told the correction is in place. This is the reason
# edit-pr-comment.sh exists as a wrapper instead of a bare gh api call.
verify_pr_comment_content() {
    local comment_id="$1"
    local expected_body="$2"

    local want got
    # tr strips CR in case either side carries CRLF endings; the command
    # substitutions strip trailing newlines from both sides identically.
    want="$(printf '%s' "$expected_body" | tr -d '\r')"
    if ! got="$(gh api "repos/{owner}/{repo}/issues/comments/$comment_id" --jq .body)"; then
        echo "Error: could not read comment $comment_id back from GitHub after editing it." >&2
        echo "gh reported the failure above. The edit may or may not have landed, and" >&2
        echo "an unverified edit is not a verified one." >&2
        echo "Read the comment with get-pr-comment.sh $comment_id before doing anything else." >&2
        echo "Do not report this operation as a success." >&2
        return 1
    fi
    got="$(printf '%s' "$got" | tr -d '\r')"
    if [ "${got:0:${#want}}" != "$want" ]; then
        echo "Error: read-back verification failed for comment $comment_id." >&2
        echo "The comment on GitHub does not begin with the submitted body." >&2
        echo "The comment exists but its content is wrong. Repair it with edit-pr-comment.sh." >&2
        echo "Do not report this operation as a success." >&2
        return 1
    fi
    verify_appended_tail "comment $comment_id" "${got:${#want}}"
}
