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

# task-channel-authority.sh enforces the canonical Agent Task->board Channel
# authorizer. Every Agent Task write/read gate must route its decision through
# access.AuthorizeActiveAgentChannelMembership (the shared SQL ACL) or a
# taskboard/app helper that delegates to it; no production file may add a new
# direct Task scope_memberships-only authorization gate for Agents. Direct Task
# scope_memberships is state only (participant/watch/notification/read cursor)
# and must never be the authorization fact.

ROOT="${AGENTTEAM_MESSAGE_TASK_CHANNEL_AUTHORITY_GUARDRAIL_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)}"
cd "$ROOT"

failures=0

fail() {
  printf 'FAIL: %s\n' "$1" >&2
  failures=$((failures + 1))
}

require_contains() {
  local file="$1"
  local needle="$2"
  local label="${3:-$file must contain $needle}"
  [[ -f "$file" ]] || { fail "missing $file"; return; }
  if ! grep -Fq -- "$needle" "$file"; then fail "$label"; fi
}

require_not_contains() {
  local file="$1"
  local needle="$2"
  local label="${3:-$file must not contain $needle}"
  [[ -f "$file" ]] || { fail "missing $file"; return; }
  if grep -Fq -- "$needle" "$file"; then fail "$label"; fi
}

# The canonical shared SQL ACL must exist and be the single source of truth.
require_contains internal/store/access/authorizer.go \
  'func AuthorizeActiveAgentChannelMembership' \
  'canonical access authorizer must exist'
require_contains internal/store/access/agent_authority_predicates.go \
  "m.type = 'agent'" \
  'canonical access authorizer must require an active same-workspace agent member'
require_contains internal/store/access/authorizer.go \
  'func AuthorizeActiveAgentTaskBoardChannel' \
  'canonical Task->board Channel authorizer must exist'
require_contains internal/store/access/authorizer.go \
  'func ResolveActiveTaskBoardChannel' \
  'canonical Task->board Channel resolver must exist'

# taskboard and app helpers must delegate to the canonical authorizer.
require_contains internal/store/taskboard/agent_tasks.go \
  'AuthorizeActiveAgentChannelMembership' \
  'taskboard agent channel helper must delegate to the canonical authorizer'
require_contains internal/store/app/agent_messages.go \
  'AuthorizeActiveAgentTaskBoardChannel' \
  'app agent task resolution must delegate to the canonical Task-board resolver'
require_contains internal/store/app/attachment_upload_authority.go \
  'AuthorizeActiveAgentTaskBoardChannel' \
  'app attachment task authority must delegate to the canonical Task-board resolver'
require_contains internal/store/app/domain_adapters.go \
  'AuthorizeActiveAgentTaskBoardChannel' \
  'app task upload authority must delegate to the canonical Task-board resolver'
require_contains internal/store/reaction/reactions.go \
  'ResolveActiveTaskBoardChannel' \
  'reaction task authority must delegate to the canonical Task-board resolver'

# No production Agent Task gate may grant on a direct task-scope membership as
# its only check. The old function is removed; a new one must not reappear.
require_not_contains internal/store/taskboard/discussion_entry.go \
  'ensureAgentCanWriteTaskScope' \
  'the retired direct task-scope membership gate must stay removed'

# The reaction task target must not grant on task scope membership.
require_not_contains internal/store/reaction/reactions.go \
  'sm.scope_id = t.task_scope_id' \
  'reaction task target must not grant on task scope membership'
require_not_contains internal/store/reaction/reactions.go \
  'task scope membership authorizes reaction' \
  'reaction basis must be truthful task_board_channel, not task membership'

# Agent task permission metadata must be truthful task_board_channel.
require_not_contains internal/store/app/agent_target_resolution.go \
  'task scope membership' \
  'agent task target basis must not claim task scope membership'

# Canonical Task topology must not regress to the rolling-era board aliases or
# the board_id-null fallback. task.channel_id remains an integrity equality
# fence only; authority comes from task.board_id -> task_boards.channel_id.
for canonical_file in \
  internal/store/message/scope_lifecycle.go \
  internal/store/app/service_event_target_contract.go \
  internal/store/app/app_channel_agent_task_summaries.go \
  internal/store/taskboard/user_tasks.go \
  internal/store/taskboard/taxonomy.go \
  internal/store/taskboard/filter_options.go \
  internal/store/taskboard/related_tasks.go \
  internal/store/taskboard/query_builders.go \
  internal/store/taskboard/service.go \
  internal/store/tasktimeline/writer.go; do
  require_not_contains "$canonical_file" 'workspace_ref' "canonical Task topology must not use workspace_ref in $canonical_file"
  require_not_contains "$canonical_file" 'channel_ref' "canonical Task topology must not use channel_ref in $canonical_file"
  require_not_contains "$canonical_file" 'board_id IS NULL' "canonical Task topology must not recover from NULL board_id in $canonical_file"
done

# The current bootstrap is the post-cutover schema: task_boards must not keep
# the retired denormalized aliases or their unique key. Historical migration
# fixtures intentionally retain them so the single 00366 cutover can remove
# them during upgrade.
require_not_contains internal/tidb/bootstrap.sql \
  'task_boards_workspace_channel_ref_key' \
  'current task_boards bootstrap must not retain the legacy unique key'
require_not_contains internal/tidb/bootstrap.sql \
  '`workspace_ref` varchar(255)' \
  'current task_boards bootstrap must not retain workspace_ref'
require_not_contains internal/tidb/bootstrap.sql \
  '`channel_ref` varchar(255)' \
  'current task_boards bootstrap must not retain channel_ref'
require_contains internal/tidb/migrations/00366_task_parent_authority_frontier.sql \
  'DROP COLUMN IF EXISTS `workspace_ref`' \
  'one-way task board alias cutover migration must drop workspace_ref'
require_contains internal/tidb/migrations/00366_task_parent_authority_frontier.sql \
  'DROP COLUMN IF EXISTS `channel_ref`' \
  'one-way task board alias cutover migration must drop channel_ref'
require_contains internal/tidb/migrations/00366_task_parent_authority_frontier.sql \
  'DROP INDEX IF EXISTS `task_boards_workspace_channel_ref_key`' \
  'one-way task board alias cutover migration must drop the retired unique key'

# The TOCTOU fence must exist and every taskboard write/read path that gates on
# the canonical Task->board Channel authority must revalidate the actor's board
# Channel membership under lock (lockTaskBoardChannelAuthority /
# lockActiveBoardChannelMembership), so a concurrent board-membership revoke /
# Task delete / board move serializes deterministically and authority-loss-first
# fails closed with zero side effects. This prevents the fence and its barrier
# tests from silently disappearing.
require_contains internal/store/taskboard/task_channel_fence.go \
  'func (txs TxStore) lockTaskBoardChannelAuthority' \
  'centralized Agent TOCTOU fence must exist'
require_contains internal/store/taskboard/task_channel_fence.go \
  'func (txs TxStore) lockHumanTaskBoardChannelAuthority' \
  'centralized Human TOCTOU fence must exist'
require_contains internal/store/taskboard/task_channel_fence.go \
  'type taskAuthorityFencer interface' \
  'principal-typed fencer abstraction must exist'
require_contains internal/store/access/human_authority_locks.go \
  'func LockActiveHumanChannelAuthority' \
  'human locked channel authority helper must exist'
require_contains internal/store/access/authorizer.go \
  'func LockActiveAgentChannelAuthority' \
  'agent locked channel authority helper must exist'
require_contains internal/store/taskboard/task_scope_write_authority.go \
  'lockHumanTaskBoardChannelAuthority' \
  'Task discussion write must revalidate through the typed Human TOCTOU fence'
require_contains internal/store/taskboard/user_tasks.go \
  'lockHumanTaskBoardChannelAuthority' \
  'Task delete must revalidate through the typed Human TOCTOU fence'
require_contains internal/store/taskboard/detail_mutation.go \
  'lockExistingTaskChannel' \
  'Task update/status mutation must revalidate through the typed fence'
require_contains internal/store/taskboard/detail_read.go \
  'lockHumanTaskBoardChannelAuthority' \
  'Task detail/read must revalidate through the typed Human TOCTOU fence'
# Run the structural production inventory. It constant-folds every non-test Go
# SQL expression referencing scope_memberships (including concatenated string
# constants), normalizes identifiers so alias renames cannot bypass, and
# classifies each normalized digest against the committed inventory
# (task-channel-authority-inventory.gen.go). Any new/changed/unclassified
# occurrence fails by default.
#
# This guardrail is a Go-based structural check and is only meaningful where a
# Go toolchain exists. The language-agnostic harness-fast CI image has no Go, so
# when `go` is absent we SKIP with a warning instead of failing: the check runs
# in the Go-equipped backend/required CI and in developer machines, where an
# unclassified occurrence still fails hard. A missing Go toolchain is an
# environment property, not a violation of the authority invariant.
if ! command -v go >/dev/null 2>&1; then
  echo "go is not available; skipping the task-channel-authority inventory guardrail (runs where a Go toolchain exists)" >&2
else
  if ! go run "$ROOT/guardrails/message/task-channel-authority-inventory.go" "$ROOT/guardrails/message/task-channel-authority-inventory.gen.go" "$ROOT/guardrails/message/task-channel-authority-policy.gen.go" "$ROOT/guardrails/message/task-channel-authority-mutator-closure.go" -root "$ROOT"; then
    fail 'task-channel-authority structural inventory or mutator closure guardrail does not match production code'
  fi
  # Run the self-tests proving bypass attempts (alternate aliases, scope-first
  # queries, concatenated SQL, and every mutator-closure mutation) are caught.
  if ! (cd "$ROOT/guardrails/message" && go test -run 'TestInventory|TestMutatorClosure' -count=1 ./...); then
    fail 'task-channel-authority inventory or mutator-closure self-tests failed'
  fi
fi

if (( failures > 0 )); then
  printf '\ntask-channel-authority guardrail failed with %d issue(s).\n' "$failures" >&2
  exit 1
fi
printf 'task-channel-authority guardrail passed.\n'
