#!/usr/bin/env bash
# Regression harness for detect-platform.sh's get_gitea_token() per-agent
# identity resolution (Gate-16 author≠reviewer separation) — the API-tooling
# counterpart to git-credential-mosaic, so pr-create.sh/issue-create.sh/etc.
# open records under the resolved agent identity, not the shared account.
#
# Covers:
#   1. Identity resolution priority: MOSAIC_GIT_IDENTITY env > git config
#      mosaic.gitIdentity (per-worktree).
#   2. Correct per-slot token file path chosen per host
#      (gitea-usc-<id>.token vs gitea-mosaicstack-<id>.token).
#   3. Per-slot token present -> that token is returned (agent-authored calls).
#   4. No identity requested -> shared credential-loader token (backward
#      compat, unchanged).
#   5. Patch 2b — explicit identity + recognized Gitea host + ABSENT per-slot
#      token for that identity -> FAIL LOUD (nonzero return, empty stdout, a
#      stderr diagnostic naming identity/source/host/expected path). Must NOT
#      fall through to the shared/default token (Gate-16 author≠reviewer
#      integrity — never silently borrow another slot's credentials). Covered
#      for both identity sources (git config, MOSAIC_GIT_IDENTITY env) and
#      for a same-identity cross-host case (token exists for one host, not
#      the other).
#   6. Scope containment: identity requested + an UNRECOGNIZED Gitea host (no
#      per-slot token scheme) -> Patch 2b does not apply; existing
#      fall-through behavior is unchanged.
#   7. Credential store selection: an identity with a directory under
#      <brain>/fleet/agents/ is a SEAT and is read ONLY from its own secrets/
#      slot; any other identity is a SERVICE and is read from the framework
#      store. No precedence between them and NO fallback from one to the
#      other — a seat with an empty slot is REFUSED even when a same-named
#      token sits in the framework store.
#   8. Fail loud when NO identity resolves on a host that runs a fleet: where
#      seats exist, an unattributable API call is refused rather than made
#      under the shared account. On a host with no fleet the same call still
#      returns the shared token (case 1), which is what keeps this change a
#      no-op for non-fleet operators of the framework.
#
# Uses a stubbed credentials.json + stubbed per-slot token files under a fake
# HOME. NEVER reads real secrets or touches the real ~/.config/mosaic/secrets.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-token-identity}"
FAKE_HOME="$WORK_DIR/home"
REPO_DIR="$WORK_DIR/repo"
CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json"

rm -rf "$WORK_DIR"
mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$REPO_DIR"

git -C "$REPO_DIR" init -q
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git

cat > "$CREDENTIALS_FILE" <<'JSON'
{
  "gitea": {
    "mosaicstack": {
      "url": "https://git.mosaicstack.dev",
      "token": "shared-mosaicstack-token"
    },
    "usc": {
      "url": "https://git.uscllc.com",
      "token": "shared-usc-token"
    }
  }
}
JSON

fail=0
assert_eq() {
  local desc="$1" expected="$2" actual="$3"
  if [[ "$expected" != "$actual" ]]; then
    echo "FAIL: $desc — expected '$expected', got '$actual'" >&2
    fail=1
  fi
}

# Runs get_gitea_token for $1=host inside REPO_DIR (per-worktree git config
# resolves there) with a fake HOME + the stub credentials.json, plus any
# extra env passed in $@.
call_get_gitea_token() {
  local host="$1"; shift
  (
    cd "$REPO_DIR"
    # shellcheck disable=SC2016 # deliberately deferred: $DETECT_PLATFORM_SH is
    # expanded by the INNER bash -c (via the exported env var below), not here.
    env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
      DETECT_PLATFORM_SH="$SCRIPT_DIR/detect-platform.sh" "$@" \
      bash -c 'source "$DETECT_PLATFORM_SH"; get_gitea_token "$1"' _ "$host"
  )
}

# ---------------------------------------------------------------------------
# 1. No identity resolvable -> shared credential-loader token (unchanged).
# ---------------------------------------------------------------------------
git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true
out=$(call_get_gitea_token "git.mosaicstack.dev")
assert_eq "shared fallback (no identity)" "shared-mosaicstack-token" "$out"

# ---------------------------------------------------------------------------
# 2. git config mosaic.gitIdentity resolves to an agent WITH a per-slot
#    token -> that token wins over the shared account.
# ---------------------------------------------------------------------------
echo -n "agentA-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentA.token"
git -C "$REPO_DIR" config mosaic.gitIdentity agentA
out=$(call_get_gitea_token "git.mosaicstack.dev")
assert_eq "git-config identity token" "agentA-mosaicstack-token" "$out"

# ---------------------------------------------------------------------------
# 3. MOSAIC_GIT_IDENTITY env beats git config mosaic.gitIdentity.
# ---------------------------------------------------------------------------
echo -n "agentB-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentB.token"
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=agentB)
assert_eq "env beats git-config identity token" "agentB-mosaicstack-token" "$out"

# ---------------------------------------------------------------------------
# 4. FAIL LOUD (Patch 2b): an identity is explicitly requested (via git config
#    mosaic.gitIdentity, and separately via MOSAIC_GIT_IDENTITY env) for a
#    RECOGNIZED Gitea host, but no per-slot token exists for THAT identity.
#    Must NOT fall through to the shared/default token — silently borrowing
#    another slot's credentials would post PRs/issues/reviews as the WRONG
#    agent (Gate-16 author≠reviewer integrity break). Expect: nonzero return,
#    EMPTY stdout (no token — shared or otherwise — leaked), and a stderr
#    diagnostic naming the identity, its source, the host, and the expected
#    per-slot token path.
# ---------------------------------------------------------------------------
assert_failloud() {
  local desc="$1" host="$2" ident="$3" expected_tok_path="$4"; shift 4
  local stderr_file="$WORK_DIR/stderr.tmp"
  : > "$stderr_file"
  set +e
  local stdout
  stdout=$(call_get_gitea_token "$host" "$@" 2>"$stderr_file")
  local rc=$?
  set -e
  local stderr
  stderr=$(cat "$stderr_file")
  if [[ "$rc" -eq 0 ]]; then
    echo "FAIL: $desc — expected nonzero return, got 0 (stdout='$stdout')" >&2
    fail=1
  fi
  if [[ -n "$stdout" ]]; then
    echo "FAIL: $desc — expected empty stdout (no token leaked), got '$stdout'" >&2
    fail=1
  fi
  if [[ "$stderr" != *"$ident"* ]]; then
    echo "FAIL: $desc — stderr does not name the requested identity '$ident':" >&2
    echo "$stderr" >&2
    fail=1
  fi
  if [[ "$stderr" != *"$host"* ]]; then
    echo "FAIL: $desc — stderr does not name the host '$host':" >&2
    echo "$stderr" >&2
    fail=1
  fi
  if [[ "$stderr" != *"$expected_tok_path"* ]]; then
    echo "FAIL: $desc — stderr does not name the expected per-slot token path '$expected_tok_path':" >&2
    echo "$stderr" >&2
    fail=1
  fi
  if [[ "$stderr" == *"shared"*"token"* ]]; then
    echo "FAIL: $desc — stderr unexpectedly mentions a shared token value:" >&2
    echo "$stderr" >&2
    fail=1
  fi
}

# 4a. git config mosaic.gitIdentity source, recognized host (mosaicstack),
#     shared token IS present but must not be borrowed.
git -C "$REPO_DIR" config mosaic.gitIdentity no-such-agent
assert_failloud "fail-loud via git-config identity (recognized host)" \
  "git.mosaicstack.dev" "no-such-agent" \
  "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-no-such-agent.token"
git -C "$REPO_DIR" config --unset mosaic.gitIdentity

# 4b. MOSAIC_GIT_IDENTITY env source (takes priority over git config), same
#     recognized-host / absent-token scenario -> also fails loud.
assert_failloud "fail-loud via MOSAIC_GIT_IDENTITY env (recognized host)" \
  "git.mosaicstack.dev" "no-such-agent-env" \
  "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-no-such-agent-env.token" \
  MOSAIC_GIT_IDENTITY=no-such-agent-env

# ---------------------------------------------------------------------------
# 5. Correct per-slot token PATH per host: same agent id, only a usc token
#    exists. usc host returns it (happy path, unchanged). mosaicstack host
#    has NO per-slot token for this identity -> Patch 2b fail-loud applies
#    there too (must NOT fall back to the shared mosaicstack token, and must
#    NOT leak the agent's usc token either).
# ---------------------------------------------------------------------------
echo -n "agentD-usc-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-usc-agentD.token"
git -C "$REPO_DIR" config mosaic.gitIdentity agentD
out=$(call_get_gitea_token "git.uscllc.com")
assert_eq "host-scoped token path (usc)" "agentD-usc-token" "$out"
assert_failloud "fail-loud on cross-host absence (no fallback, no cross-host leak)" \
  "git.mosaicstack.dev" "agentD" \
  "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentD.token"
git -C "$REPO_DIR" config --unset mosaic.gitIdentity

# ---------------------------------------------------------------------------
# 6. Scope containment: identity explicitly requested, but the host is NOT a
#    recognized Gitea host (no per-slot token scheme at all) -> Patch 2b does
#    NOT apply; existing fall-through behavior is unchanged (ends in the
#    pre-existing generic failure since no shared credentials match either,
#    NOT the fail-loud diagnostic path).
# ---------------------------------------------------------------------------
git -C "$REPO_DIR" config mosaic.gitIdentity no-such-agent
set +e
out=$(call_get_gitea_token "github.com" 2>"$WORK_DIR/stderr-scope.tmp")
rc=$?
set -e
err=$(cat "$WORK_DIR/stderr-scope.tmp")
if [[ "$rc" -eq 0 ]]; then
  echo "FAIL: unrecognized host + identity — expected nonzero (no credentials configured), got 0" >&2
  fail=1
fi
if [[ "$err" == *"no per-slot token at"* ]]; then
  echo "FAIL: unrecognized host + identity — fail-loud diagnostic must not fire for a host with no per-slot scheme:" >&2
  echo "$err" >&2
  fail=1
fi
git -C "$REPO_DIR" config --unset mosaic.gitIdentity

# ---------------------------------------------------------------------------
# 7. SEAT identity: an id with a directory under <brain>/fleet/agents/ is read
#    from its OWN secrets/ slot, not from the framework store. The brain home
#    is resolved exactly as packages/mosaic/src/fleet/brain-home.ts does it:
#    MOSAIC_BRAIN_HOME, else ~/.mosaic.
# ---------------------------------------------------------------------------
BRAIN_DIR="$WORK_DIR/brain"
mkdir -p "$BRAIN_DIR/fleet/agents/seatE/secrets"
echo -n "seatE-slot-token" > "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token"
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatE MOSAIC_BRAIN_HOME="$BRAIN_DIR")
assert_eq "seat reads its own slot" "seatE-slot-token" "$out"

# ---------------------------------------------------------------------------
# 8. NO CROSS-STORE FALLBACK. seatF is a seat (it has a directory) with an
#    EMPTY slot, while a framework-store token of the identical name is
#    present and readable. It must be REFUSED rather than served that token:
#    one credential, one location. A seat that silently reads a same-named
#    service credential is the same substitution failure as the shared-account
#    fallback, one store further down.
# ---------------------------------------------------------------------------
mkdir -p "$BRAIN_DIR/fleet/agents/seatF/secrets"
echo -n "seatF-SERVICE-STORE-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-seatF.token"
assert_failloud "seat with empty slot does NOT fall back to the framework store" \
  "git.mosaicstack.dev" "seatF" \
  "$BRAIN_DIR/fleet/agents/seatF/secrets/gitea-mosaicstack-seatF.token" \
  MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$BRAIN_DIR"
# assert_failloud only screens stderr for the word "shared"; this store's token
# is not named that, so check for its value explicitly.
set +e
xstore_out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr-xstore.tmp")
set -e
if [[ "$xstore_out$(cat "$WORK_DIR/stderr-xstore.tmp")" == *"seatF-SERVICE-STORE-token"* ]]; then
  echo "FAIL: cross-store fallback — a seat was served the framework store's same-named token" >&2
  fail=1
fi
# Control: that framework-store token IS readable, so the refusal above is the
# store rule firing and not an unreadable file. The same id, resolved against a
# brain home where it is not a seat, gets it.
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$WORK_DIR/no-such-brain")
assert_eq "control — same file IS readable for a non-seat identity" "seatF-SERVICE-STORE-token" "$out"

# ---------------------------------------------------------------------------
# 9. FAIL LOUD — no identity resolvable, but this host runs a fleet. Contrast
#    with case 1: the identical call on a host with no fleet still returns the
#    shared token.
# ---------------------------------------------------------------------------
git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true
set +e
noid_out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr-noid.tmp")
noid_rc=$?
set -e
noid_err=$(cat "$WORK_DIR/stderr-noid.tmp")
if [[ "$noid_rc" -eq 0 ]]; then
  echo "FAIL: no identity on a fleet host — expected nonzero return, got 0 (stdout='$noid_out')" >&2
  fail=1
fi
if [[ -n "$noid_out" ]]; then
  echo "FAIL: no identity on a fleet host — expected empty stdout, got '$noid_out'" >&2
  fail=1
fi
if [[ "$noid_out" == *"shared-mosaicstack-token"* || "$noid_err" == *"shared-mosaicstack-token"* ]]; then
  echo "FAIL: no identity on a fleet host — the shared token was served anyway:" >&2
  echo "$noid_out$noid_err" >&2
  fail=1
fi
if [[ "$noid_err" != *"MOSAIC_GIT_IDENTITY"* ]]; then
  echo "FAIL: no identity on a fleet host — stderr does not say how to set an identity:" >&2
  echo "$noid_err" >&2
  fail=1
fi

# ---------------------------------------------------------------------------
# 10. The brain home defaults to ~/.mosaic when MOSAIC_BRAIN_HOME is unset —
#     the fleet gate must fire on the default path too, not only on an
#     explicitly injected one. Case 1 ran before this directory existed; the
#     same call now refuses, which also proves case 1 was measuring the
#     no-fleet branch rather than passing for an unrelated reason.
# ---------------------------------------------------------------------------
mkdir -p "$FAKE_HOME/.mosaic/fleet/agents"
set +e
dflt_out=$(call_get_gitea_token "git.mosaicstack.dev" 2>"$WORK_DIR/stderr-dflt.tmp")
dflt_rc=$?
set -e
if [[ "$dflt_rc" -eq 0 || -n "$dflt_out" ]]; then
  echo "FAIL: fleet gate did not fire on the default ~/.mosaic brain home (rc=$dflt_rc stdout='$dflt_out')" >&2
  fail=1
fi

if [[ "$fail" -eq 0 ]]; then
  echo "get_gitea_token identity resolution regression passed"
fi

exit "$fail"
