#!/usr/bin/env bash
#MISE description="Iter-87 regression test extending iter-86 orchestrator coverage. Verifies (1) gpu-optimization-guard inlined as 4th registry entry produces matching deny across all 6 GPU policy checks, (2) AbortSignal.timeout()-based cooperative-timeout refactor preserves the iter-84/85/86 belt-and-suspenders defense (stdout JSON + stderr + exitCode=2), (3) standalone gpu-optimization-guard.ts backward-compat (no orchestrator prefix), (4) idiomatic AbortSignal.timeout() rejection path correctly distinguishes TimeoutError from other classifier errors (negative test via slow-classifier fixture)."

set -euo pipefail
shopt -u patsub_replacement 2>/dev/null || true

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
ORCHESTRATOR_HOOK_PATH="$REPO_ROOT/plugins/itp-hooks/hooks/pretooluse-edit-time-orchestrator-combining-multiple-subhooks-into-single-bun-process-iter66-precedent.ts"
STANDALONE_HOOK_PATH="$REPO_ROOT/plugins/itp-hooks/hooks/pretooluse-gpu-optimization-guard.ts"
SUBHOOK_CONTRACT_AUDIT_TASK_PATH="$REPO_ROOT/.mise/tasks/audit-pretooluse-orchestrator-subhook-contract-violations-static-check-no-stdin-stdout-exit-in-classifier-functions-and-import-meta-main-guard-on-standalone-main.sh"

for required_file in "$ORCHESTRATOR_HOOK_PATH" "$STANDALONE_HOOK_PATH" "$SUBHOOK_CONTRACT_AUDIT_TASK_PATH"; do
    if [[ ! -f "$required_file" ]]; then
        echo "FAIL: required file not found: $required_file"
        exit 1
    fi
done

ASSERTION_COUNT_PASSED=0
ASSERTION_COUNT_FAILED=0
assert_passes() { ASSERTION_COUNT_PASSED=$((ASSERTION_COUNT_PASSED + 1)); echo "  ✓ PASS: $1"; }
assert_fails()  { ASSERTION_COUNT_FAILED=$((ASSERTION_COUNT_FAILED + 1)); echo "  ✗ FAIL: $1"; }

echo "═══════════════════════════════════════════════════════════════════════════════"
echo "  Iter-87 orchestrator: gpu-optimization-guard inlined + AbortSignal.timeout refactor"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""

# ─── Case 1: PyTorch training script with batch_size=32 (POLICY: missing AMP+auto-batch) ──
case1_payload=$(mktemp -t iter87-case1.XXXXXX.json)
trap 'rm -f "$case1_payload"' EXIT
cat > "$case1_payload" <<'PAYLOAD'
{"tool_name":"Write","tool_input":{"file_path":"/home/test/train.py","content":"import torch\nfrom torch.utils.data import DataLoader\nfrom torch import nn\n\nmodel = nn.Module()\ndevice = 'cuda'\nmodel.to(device)\nbatch_size = 32\nloader = DataLoader(dataset, batch_size=batch_size)\nfor epoch in range(10):\n    for batch in loader:\n        loss = model(batch).sum()\n        loss.backward()\n        optimizer.step()\n"}}
PAYLOAD

set +e
case1_stdout=$(bun "$ORCHESTRATOR_HOOK_PATH" < "$case1_payload" 2>/dev/null)
case1_exit=$?
set -e

if [[ "$case1_stdout" == *'"permissionDecision":"deny"'* ]] && [[ "$case1_stdout" == *'GPU-OPTIMIZATION-GUARD'* ]]; then
    assert_passes "Case 1a: orchestrator denies PyTorch training without GPU optimizations"
else
    assert_fails "Case 1a: stdout missing GPU-OPTIMIZATION-GUARD deny; got=${case1_stdout:0:200}..."
fi
if [[ "$case1_stdout" == *'gpu-optimization-guard → DENY'* ]]; then
    assert_passes "Case 1b: deny attributed to gpu-optimization-guard subhook"
else
    assert_fails "Case 1b: subhook attribution missing"
fi
if [[ "$case1_stdout" == *'batch_size=32 is hardcoded'* ]]; then
    assert_passes "Case 1c: batch-size policy fires (auto-tuning not adopted)"
else
    assert_fails "Case 1c: batch-size policy diagnostic missing"
fi
if [[ "$case1_stdout" == *'AMP (Automatic Mixed Precision)'* ]]; then
    assert_passes "Case 1d: AMP policy fires (mixed-precision not adopted in GPU training loop)"
else
    assert_fails "Case 1d: AMP policy diagnostic missing"
fi
if [[ "$case1_exit" == "2" ]]; then
    assert_passes "Case 1e: orchestrator exits 2 (belt-and-suspenders preserved through AbortSignal refactor)"
else
    assert_fails "Case 1e: exit=$case1_exit, expected 2"
fi

# ─── Case 2: bypass comment honored ────────────────────────────────────────────
case2_payload=$(mktemp -t iter87-case2.XXXXXX.json)
trap 'rm -f "$case1_payload" "$case2_payload"' EXIT
cat > "$case2_payload" <<'PAYLOAD'
{"tool_name":"Write","tool_input":{"file_path":"/home/test/train_bypass.py","content":"# gpu-optimization-bypass: legacy training script not yet migrated\nimport torch\nbatch_size = 32\nfor epoch in range(10):\n    loss.backward()\n    optimizer.step()\n"}}
PAYLOAD

set +e
case2_stdout=$(bun "$ORCHESTRATOR_HOOK_PATH" < "$case2_payload" 2>/dev/null)
case2_exit=$?
set -e

if [[ "$case2_stdout" == *'"permissionDecision":"allow"'* ]] && [[ "$case2_exit" == "0" ]]; then
    assert_passes "Case 2: bypass comment honored → allow + exit 0"
else
    assert_fails "Case 2: bypass not honored; exit=$case2_exit"
fi

# ─── Case 3: non-PyTorch Python script falls through ────────────────────────────
case3_payload=$(mktemp -t iter87-case3.XXXXXX.json)
trap 'rm -f "$case1_payload" "$case2_payload" "$case3_payload"' EXIT
cat > "$case3_payload" <<'PAYLOAD'
{"tool_name":"Write","tool_input":{"file_path":"/home/test/regular.py","content":"def hello():\n    print('not a pytorch script')\n"}}
PAYLOAD

set +e
case3_stdout=$(bun "$ORCHESTRATOR_HOOK_PATH" < "$case3_payload" 2>/dev/null)
case3_exit=$?
set -e

if [[ "$case3_stdout" == *'"permissionDecision":"allow"'* ]] && [[ "$case3_exit" == "0" ]]; then
    assert_passes "Case 3: non-PyTorch Python script → allow (gpu-optimization-guard fastpath skip)"
else
    assert_fails "Case 3: non-PyTorch Python wrongly denied; exit=$case3_exit"
fi

# ─── Case 4: standalone gpu-optimization-guard.ts still works ──────────────────
set +e
case4_stdout=$(bun "$STANDALONE_HOOK_PATH" < "$case1_payload" 2>/dev/null)
set -e

if [[ "$case4_stdout" == *'"permissionDecision":"deny"'* ]] && [[ "$case4_stdout" == *'GPU-OPTIMIZATION-GUARD'* ]]; then
    assert_passes "Case 4a: standalone gpu-optimization-guard.ts denies (backward-compat)"
else
    assert_fails "Case 4a: standalone broken; got=${case4_stdout:0:200}..."
fi
if [[ "$case4_stdout" != *'[pretooluse-edit-time-orchestrator]'* ]]; then
    assert_passes "Case 4b: standalone reason has NO orchestrator prefix"
else
    assert_fails "Case 4b: standalone leaked orchestrator prefix"
fi

# ─── Case 5: AbortSignal.timeout() distinguishes TimeoutError from other errors
# Construct a synthetic orchestrator + classifier-that-hangs-forever fixture,
# pointing the orchestrator at a temp subhook with timeoutMs=200. Expect the
# orchestrator to fail-open allow after timeout (not deny, not crash).
FAKE_HANG_REPO_ROOT=$(mktemp -d -t iter87-hang-repo.XXXXXX)
trap 'rm -f "$case1_payload" "$case2_payload" "$case3_payload"; rm -rf "$FAKE_HANG_REPO_ROOT"' EXIT

mkdir -p "$FAKE_HANG_REPO_ROOT/plugins/itp-hooks/hooks/lib"
# Copy the real contract + helpers
cp "$REPO_ROOT/plugins/itp-hooks/hooks/lib/pretooluse-subhook-contract-for-in-process-orchestrator-inlining-iter84.ts" \
    "$FAKE_HANG_REPO_ROOT/plugins/itp-hooks/hooks/lib/"
cp "$REPO_ROOT/plugins/itp-hooks/hooks/pretooluse-helpers.ts" "$FAKE_HANG_REPO_ROOT/plugins/itp-hooks/hooks/"
cp -R "$REPO_ROOT/plugins/itp-hooks/hooks/lib/"* "$FAKE_HANG_REPO_ROOT/plugins/itp-hooks/hooks/lib/" 2>/dev/null || true

# Write a hanging classifier
cat > "$FAKE_HANG_REPO_ROOT/plugins/itp-hooks/hooks/pretooluse-hang-forever-classifier.ts" <<'HANG'
import type { PreToolUseInput } from "./pretooluse-helpers.ts";
import type { PreToolUseSubhookDecision } from "./lib/pretooluse-subhook-contract-for-in-process-orchestrator-inlining-iter84.ts";

export async function classifyHangForeverForOrchestrator(
  _input: PreToolUseInput,
): Promise<PreToolUseSubhookDecision> {
  // Hang for 10 seconds — orchestrator's 200ms timeout should fire first.
  await new Promise((resolve) => setTimeout(resolve, 10000));
  return { kind: "deny", reason: "should never be reached due to timeout" };
}

if (import.meta.main) {
  // Standalone CLI not used in this test
}
HANG

# Run inline mini-orchestrator that exercises only the timeout path
TIMEOUT_HARNESS=$(mktemp -t iter87-timeout-harness.XXXXXX.ts)
trap 'rm -f "$case1_payload" "$case2_payload" "$case3_payload" "$TIMEOUT_HARNESS"; rm -rf "$FAKE_HANG_REPO_ROOT"' EXIT
cat > "$TIMEOUT_HARNESS" <<HARNESS
// Inline harness verifying AbortSignal.timeout() distinguishes TimeoutError.
import { classifyHangForeverForOrchestrator } from "$FAKE_HANG_REPO_ROOT/plugins/itp-hooks/hooks/pretooluse-hang-forever-classifier.ts";

async function awaitAbortRejection(signal: AbortSignal): Promise<never> {
  return await new Promise<never>((_, reject) => {
    if (signal.aborted) { reject(signal.reason); return; }
    signal.addEventListener("abort", () => reject(signal.reason), { once: true });
  });
}

const signal = AbortSignal.timeout(200);
const startMs = Date.now();
try {
  await Promise.race([
    classifyHangForeverForOrchestrator({ tool_name: "Write", tool_input: {} } as any),
    awaitAbortRejection(signal),
  ]);
  console.log("UNEXPECTED_RESOLVE");
  process.exit(1);
} catch (err: any) {
  const elapsedMs = Date.now() - startMs;
  if (err?.name === "TimeoutError" && elapsedMs >= 180 && elapsedMs <= 600) {
    console.log(\`TIMEOUT_DETECTED elapsed=\${elapsedMs}ms\`);
    process.exit(0);
  }
  console.log(\`UNEXPECTED_ERROR name=\${err?.name} elapsed=\${elapsedMs}ms\`);
  process.exit(2);
}
HARNESS

set +e
case5_stdout=$(bun "$TIMEOUT_HARNESS" 2>&1)
case5_exit=$?
set -e

if [[ "$case5_exit" == "0" ]] && [[ "$case5_stdout" == *'TIMEOUT_DETECTED'* ]]; then
    assert_passes "Case 5: AbortSignal.timeout() fires TimeoutError after ~200ms (correct cooperative-timeout semantics)"
else
    assert_fails "Case 5: AbortSignal timeout broken; exit=$case5_exit stdout=$case5_stdout"
fi

# ─── Case 6: subhook-contract audit task still reports clean state with ≥4 subhooks ─
# Iter-88 hardening: extract live count instead of hardcoding "4" so this test
# survives future subhook migrations (iter-89 pyi-stub, iter-90 native-binary,
# iter-91 vale-claude-md). The invariant is "≥4 conformant subhooks" — iter-87
# inlined the 4th (gpu-optimization-guard), and subsequent iters only grow the count.
set +e
case6_stdout=$(bash "$SUBHOOK_CONTRACT_AUDIT_TASK_PATH" 2>&1)
set -e

case6_subhook_count_extracted=$(echo "$case6_stdout" | grep -oE 'Total subhook files scanned:[[:space:]]+[0-9]+' | grep -oE '[0-9]+$' | head -1 || echo 0)
if [[ "${case6_subhook_count_extracted:-0}" -ge 4 ]]; then
    assert_passes "Case 6a: audit task discovers ≥4 inlined subhooks (gpu-optimization-guard inlined; found ${case6_subhook_count_extracted})"
else
    assert_fails "Case 6a: subhook count ${case6_subhook_count_extracted} < 4"
fi
if [[ "$case6_stdout" == *'subhook files conform to the PreToolUseSubhookContract'* ]]; then
    assert_passes "Case 6b: audit task reports clean state (gpu-optimization-guard.ts conforms)"
else
    assert_fails "Case 6b: clean state not reported"
fi

# ─── Summary ─────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo "  Iter-87 gpu-optimization inline + AbortSignal.timeout regression — Summary"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo "  Assertions passed: $ASSERTION_COUNT_PASSED"
echo "  Assertions failed: $ASSERTION_COUNT_FAILED"
echo "═══════════════════════════════════════════════════════════════════════════════"
if [[ "$ASSERTION_COUNT_FAILED" -gt 0 ]]; then
    echo "  ✗ FAIL — $ASSERTION_COUNT_FAILED assertion(s) failed"
    exit 1
fi
echo "  ✓ PASS — all $ASSERTION_COUNT_PASSED assertions passed"
