#!/usr/bin/env bash
# Acceptance harness for the graph MCP integration.
#
# Spawns the graph shim, drives it as a JSON-RPC client over stdio, runs
# one query per acceptance class, prints PASS/FAIL per check, and exits
# non-zero if any check fails. Assumes the brand's Neo4j is running and
# the fixture in fixture.cypher has been seeded.
#
# The tools/list assertion guards against FastMCP-joiner drift: the
# upstream server joins NEO4J_NAMESPACE to each tool name with a hyphen,
# and the admin allowlist is string-exact. If a future upstream upgrade
# changes the joiner, the assertion fails with a named reason before any
# functional check runs.
#
# Usage:
#   PLATFORM_ROOT=~/.maxy/platform bash accept.sh           # infers NEO4J_URI from env
#   PLATFORM_ROOT=~/.maxy/platform NEO4J_URI=bolt://localhost:7687 bash accept.sh
#
# Dependencies: node, python3 (for JSON-RPC framing), uvx (via the shim).
#
# Exit codes:
#   0  all PASS
#   1  one or more FAIL
#   2  setup error (shim missing, uvx missing, etc.)

set -euo pipefail

PLATFORM_ROOT="${PLATFORM_ROOT:-$(cd "$(dirname "$0")/../../../../.." && pwd)}"
SHIM="${PLATFORM_ROOT}/lib/graph-mcp/dist/index.js"

if [[ ! -f "$SHIM" ]]; then
  echo "FAIL: shim not built at $SHIM — run 'npm run build:lib' in $PLATFORM_ROOT" >&2
  exit 2
fi

if ! command -v uvx >/dev/null 2>&1; then
  echo "FAIL: uvx not on PATH — run: curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y" >&2
  exit 2
fi

PASS=0
FAIL=0
TOTAL=0

# Exported env used by the inline Node clients below.
export SHIM
export PLATFORM_ROOT

# ---------------------------------------------------------------------------
# Regression guard: tools/list must register hyphen-joined names only.
#
# FastMCP joins NEO4J_NAMESPACE ("maxy-graph") to each upstream tool with a
# hyphen, producing `maxy-graph-read_neo4j_cypher` and
# `maxy-graph-get_neo4j_schema`. Claude Code's MCP client adds `mcp__graph__`
# on top when registering the server — the admin allowlist in
# platform/ui/app/lib/claude-agent.ts must match those exact names.
# A future upstream version that changes the joiner (e.g. to underscore)
# silently re-introduces a class of denial unless this fires.
# ---------------------------------------------------------------------------
assert_tools_list() {
  TOTAL=$(( TOTAL + 1 ))
  local out
  if ! out=$(
    node -e "
      const { spawn } = require('node:child_process');
      const proc = spawn('node', [process.env.SHIM], { stdio: ['pipe', 'pipe', 'inherit'] });
      let buf = '';
      proc.stdout.on('data', (chunk) => { buf += chunk.toString('utf8'); });
      const send = (obj) => proc.stdin.write(JSON.stringify(obj) + '\n');
      send({ jsonrpc: '2.0', id: 1, method: 'initialize',
             params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'accept.sh', version: '1.0' } } });
      setTimeout(() => {
        send({ jsonrpc: '2.0', method: 'notifications/initialized' });
        send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
      }, 1500);
      setTimeout(() => {
        proc.kill('SIGTERM');
        const lines = buf.split('\n').filter(Boolean);
        for (const line of lines) {
          try { const m = JSON.parse(line); if (m.id === 2) {
            if (m.error) { console.error('RPC_ERROR: ' + JSON.stringify(m.error)); process.exit(1); }
            const names = (m.result && m.result.tools || []).map(t => t.name);
            process.stdout.write(JSON.stringify(names));
            process.exit(0);
          } } catch (_) {}
        }
        console.error('NO_RESPONSE');
        process.exit(1);
      }, 10000);
    " 2>/dev/null
  ); then
    echo "FAIL: tools/list RPC did not respond"
    FAIL=$(( FAIL + 1 ))
    return 1
  fi

  local missing=""
  for expected in "maxy-graph-read_neo4j_cypher" "maxy-graph-get_neo4j_schema"; do
    if ! grep -q "\"$expected\"" <<<"$out"; then
      missing+=" $expected"
    fi
  done
  if [[ -n "$missing" ]]; then
    echo "FAIL: tools/list missing hyphen-joined names —${missing} (reason: missing-hyphen-tool)"
    echo "       received: $out"
    FAIL=$(( FAIL + 1 ))
    return 1
  fi

  if grep -qE 'maxy-graph_(read|get|write)' <<<"$out"; then
    local stray
    stray=$(grep -oE 'maxy-graph_[a-z_]+' <<<"$out" | sort -u | tr '\n' ' ')
    echo "FAIL: tools/list contains underscore-joined variant(s) — ${stray}(reason: stray-underscore-tool)"
    FAIL=$(( FAIL + 1 ))
    return 1
  fi

  echo "PASS: tools/list registers hyphen-joined names only (${#out}b response)"
  PASS=$(( PASS + 1 ))
  return 0
}

run_check() {
  local label="$1"
  local query="$2"
  local tool="${3:-maxy-graph-read_neo4j_cypher}"
  TOTAL=$(( TOTAL + 1 ))

  # Drive the shim via a short Node inline client. Exits 0 with the response
  # body on stdout when the tool call succeeds; exits 1 on any RPC error.
  if out=$(
    node -e "
      const { spawn } = require('node:child_process');
      const shim = process.env.SHIM;
      const tool = process.env.TOOL;
      const query = process.env.QUERY;
      const proc = spawn('node', [shim], { stdio: ['pipe', 'pipe', 'inherit'] });
      let buf = '';
      proc.stdout.on('data', (chunk) => { buf += chunk.toString('utf8'); });
      const send = (obj) => proc.stdin.write(JSON.stringify(obj) + '\n');
      send({ jsonrpc: '2.0', id: 1, method: 'initialize',
             params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'accept.sh', version: '1.0' } } });
      setTimeout(() => {
        send({ jsonrpc: '2.0', method: 'notifications/initialized' });
        const params = tool === 'maxy-graph-get_neo4j_schema' ? {} : { query };
        send({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: tool, arguments: params } });
      }, 1500);
      setTimeout(() => {
        proc.kill('SIGTERM');
        // Look for id:2 response in the buffer
        const lines = buf.split('\n').filter(Boolean);
        for (const line of lines) {
          try { const m = JSON.parse(line); if (m.id === 2) {
            if (m.error) { console.error('RPC_ERROR: ' + JSON.stringify(m.error)); process.exit(1); }
            const text = m.result && m.result.content && m.result.content[0] && m.result.content[0].text || '';
            process.stdout.write(text);
            process.exit(0);
          } } catch (_) {}
        }
        console.error('NO_RESPONSE');
        process.exit(1);
      }, 10000);
    " 2>/dev/null
  ); then
    if [[ -n "$out" ]]; then
      echo "PASS: ${label} (response ${#out}b)"
      PASS=$(( PASS + 1 ))
    else
      echo "FAIL: ${label} (empty response)"
      FAIL=$(( FAIL + 1 ))
    fi
  else
    echo "FAIL: ${label} (RPC error or timeout)"
    FAIL=$(( FAIL + 1 ))
  fi
}

# Run the regression guard first. If it fails, abort early — running
# functional queries with stale tool names produces misleading failures.
if ! assert_tools_list; then
  echo ""
  echo "FAIL — tool registration contract violated; aborting functional checks"
  exit 1
fi

export TOOL="maxy-graph-get_neo4j_schema"
export QUERY=""
run_check "get_neo4j_schema returns labels" ""

export TOOL="maxy-graph-read_neo4j_cypher"

export QUERY="MATCH (n) RETURN labels(n)[0] AS type, count(*) AS n ORDER BY n DESC LIMIT 20"
run_check "enumerate by label" "$QUERY"

export QUERY="MATCH (n:Task) RETURN count(n) AS total"
run_check "count Tasks" "$QUERY"

export QUERY="MATCH (p:Person {email: 'graph-accept@test.maxy.local'}) RETURN elementId(p) AS id, p.givenName, p.email LIMIT 1"
run_check "find person by email" "$QUERY"

export QUERY="MATCH (p:Person {email: 'graph-accept@test.maxy.local'})-[r]-(m) RETURN type(r) AS rel, labels(m)[0] AS neighbourType LIMIT 10"
run_check "neighbours of fixture person" "$QUERY"

export QUERY="MATCH (n) WHERE n.createdAt IS NOT NULL RETURN labels(n)[0] AS type, toString(n.createdAt) AS createdAt ORDER BY n.createdAt DESC LIMIT 5"
run_check "recent nodes projection" "$QUERY"

export QUERY="CALL db.labels() YIELD label RETURN label ORDER BY label"
run_check "db.labels() schema introspection" "$QUERY"

echo ""
if [[ $FAIL -eq 0 ]]; then
  echo "ALL PASS — ${PASS}/${TOTAL}"
  exit 0
else
  echo "FAIL — ${PASS}/${TOTAL} passed, ${FAIL} failed"
  exit 1
fi
