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

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CHAIN_INSIGHTS_DIR="${CHAIN_INSIGHTS_DIR:-$(cd "${SCRIPT_DIR}/../../.." && pwd)}"
MCP_ENDPOINT="${CHAIN_INSIGHTS_GRAPH_ENDPOINT:-${CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT:-http://localhost:8012/mcp}}"
DEBUG_TOKEN="${CHAIN_INSIGHTS_GRAPH_DEBUG_TOKEN:-chain-insights-dev-debug}"
SERVER_PORT="${CHAIN_INSIGHTS_SERVER_PORT:-4321}"
NETWORK="${NETWORK:-bittensor}"
# UAT_ADDRESS is the SS58 substrate member address of the UAT identity; it is
# asserted as a member address collected from the identity's
# (:Identity)-[:HAS_ADDRESS]->(:Address) satellites and resolved through the
# (:Address)<-[:HAS_ADDRESS]-(:Identity) lookup.
UAT_ADDRESS="${UAT_ADDRESS:-5Ccmf1dJKzGtXX7h17eN72MVMRsFwvYjPVmkXPUaapczECf6}"
UAT_MEMBER_NETWORK="${UAT_MEMBER_NETWORK:-substrate}"
# All graph internals use the canonical identity key form
# '<network>:<canonical_evm_address>' (deterministic H160 mapping of
# UAT_ADDRESS). Public AML tool inputs use UAT_ADDRESS and should return member
# addresses as the primary address surface.
UAT_IDENTITY_KEY="${UAT_IDENTITY_KEY:-bittensor:0x1874a43d7c6d888f9eda3d22a3a49704e3cadb24}"
REPORT_DIR="${REPORT_DIR:-${CHAIN_INSIGHTS_DIR}/.tmp/uat}"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
RUN_DIR="${REPORT_DIR}/${RUN_ID}"
WORKSPACE_ROOT="${WORKSPACE_ROOT:-${RUN_DIR}/workspace}"
CHAIN_INSIGHTS_CLI="${CHAIN_INSIGHTS_DIR}/bin/cli.js"
CHAIN_INSIGHTS_PROXY="${CHAIN_INSIGHTS_DIR}/bin/mcp-proxy.cjs"
GLOBAL_REPORTS="${HOME}/.chain-insights/reports"
GLOBAL_SNAPSHOT_BEFORE="${RUN_DIR}/global-output-before.txt"
GLOBAL_SNAPSHOT_AFTER="${RUN_DIR}/global-output-after.txt"
SERVER_PID=""
CONFIG_SNAPSHOT_READY=0

mkdir -p "${RUN_DIR}"

log() {
  printf '[uat] %s\n' "$*"
}

require_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    printf '[uat] missing required command: %s\n' "$1" >&2
    exit 127
  fi
}

snapshot_global_outputs() {
  local output_file="$1"
  : >"${output_file}"
  for dir in "${GLOBAL_REPORTS}"; do
    {
      printf '[%s]\n' "${dir}"
      if [[ -d "${dir}" ]]; then
        (
          cd "${dir}"
          find . -mindepth 1 -type d -print | LC_ALL=C sort | sed 's/^/dir /'
          find . -mindepth 1 -type f -print0 \
            | LC_ALL=C sort -z \
            | xargs -0 -r sha256sum \
            | sed 's/^/file /'
        )
      else
        printf '<missing>\n'
      fi
    } >>"${output_file}"
  done
}

assert_no_global_outputs_changed() {
  snapshot_global_outputs "${GLOBAL_SNAPSHOT_AFTER}"
  if ! cmp -s "${GLOBAL_SNAPSHOT_BEFORE}" "${GLOBAL_SNAPSHOT_AFTER}"; then
    log "global investigation output roots changed; reports must stay workspace-local"
    diff -u "${GLOBAL_SNAPSHOT_BEFORE}" "${GLOBAL_SNAPSHOT_AFTER}" >&2 || true
    return 1
  fi
}

cleanup() {
  if [[ -n "${SERVER_PID}" ]]; then
    kill "${SERVER_PID}" >/dev/null 2>&1 || true
    wait "${SERVER_PID}" >/dev/null 2>&1 || true
  fi
}

restore_config() {
  if [[ "${CONFIG_SNAPSHOT_READY}" != "1" ]]; then
    return
  fi
  if [[ -n "${OLD_GRAPH_MCP_MODE:-}" ]]; then
    node "${CHAIN_INSIGHTS_CLI}" config set graphMcpMode "${OLD_GRAPH_MCP_MODE}" >/dev/null || true
  fi
  if [[ -n "${OLD_GRAPH_MCP_ENDPOINT:-}" ]]; then
    node "${CHAIN_INSIGHTS_CLI}" config set graphMcpEndpoint "${OLD_GRAPH_MCP_ENDPOINT}" >/dev/null || true
  fi
  node "${CHAIN_INSIGHTS_CLI}" config set graphMcpAuthToken "${OLD_GRAPH_MCP_AUTH_TOKEN:-}" >/dev/null || true
  if [[ -n "${OLD_SERVER_PORT:-}" ]]; then
    node "${CHAIN_INSIGHTS_CLI}" config set serverPort "${OLD_SERVER_PORT}" >/dev/null || true
  fi
}

finish() {
  local status="$?"
  set +e
  cleanup
  if [[ -f "${GLOBAL_SNAPSHOT_BEFORE}" ]]; then
    assert_no_global_outputs_changed || status=1
  fi
  restore_config
  exit "${status}"
}
trap finish EXIT

require_cmd node
require_cmd npm
require_cmd npx
require_cmd curl
require_cmd sha256sum

if [[ ! -d "${CHAIN_INSIGHTS_DIR}" ]]; then
  log "missing Chain Insights repo: ${CHAIN_INSIGHTS_DIR}"
  exit 1
fi

log "report directory: ${RUN_DIR}"
snapshot_global_outputs "${GLOBAL_SNAPSHOT_BEFORE}"
OLD_GRAPH_MCP_MODE="$(node "${CHAIN_INSIGHTS_CLI}" config get graphMcpMode || true)"
OLD_GRAPH_MCP_ENDPOINT="$(node "${CHAIN_INSIGHTS_CLI}" config get graphMcpEndpoint || true)"
OLD_GRAPH_MCP_AUTH_TOKEN="$(node "${CHAIN_INSIGHTS_CLI}" config get graphMcpAuthToken || true)"
OLD_SERVER_PORT="$(node "${CHAIN_INSIGHTS_CLI}" config get serverPort || true)"
CONFIG_SNAPSHOT_READY=1
log "using Chain Insights Graph endpoint: ${MCP_ENDPOINT}"

cd "${CHAIN_INSIGHTS_DIR}"

if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
  log "building Chain Insights dist"
  npm run build
fi

log "initializing Chain Insights UAT workspace: ${WORKSPACE_ROOT}"
node "${CHAIN_INSIGHTS_CLI}" init "${WORKSPACE_ROOT}" --force >/dev/null
export CHAIN_INSIGHTS_WORKSPACE="${WORKSPACE_ROOT}"

log "configuring Chain Insights MCP endpoint and debug bearer token"
(
  cd "${WORKSPACE_ROOT}"
  node "${CHAIN_INSIGHTS_CLI}" debug on --token "${DEBUG_TOKEN}" --endpoint "${MCP_ENDPOINT}" >/dev/null
  node "${CHAIN_INSIGHTS_CLI}" config set serverPort "${SERVER_PORT}" >/dev/null
)

if curl -sf "http://127.0.0.1:${SERVER_PORT}/health" >/dev/null 2>&1; then
  log "reusing healthy Chain Insights server on port ${SERVER_PORT}"
else
  log "starting Chain Insights server on port ${SERVER_PORT}"
  (
    cd "${WORKSPACE_ROOT}"
    CHAIN_INSIGHTS_WORKSPACE="${WORKSPACE_ROOT}" node "${CHAIN_INSIGHTS_CLI}" serve -p "${SERVER_PORT}"
  ) >"${RUN_DIR}/chain-insights-server.log" 2>&1 &
  SERVER_PID="$!"
  for _ in $(seq 1 30); do
    if curl -sf "http://127.0.0.1:${SERVER_PORT}/health" >/dev/null 2>&1; then
      break
    fi
    sleep 0.5
  done
  curl -sf "http://127.0.0.1:${SERVER_PORT}/health" >"${RUN_DIR}/server-health.json"
fi

log "refreshing Chain Insights remote tool schema cache"
(
  cd "${WORKSPACE_ROOT}"
  node "${CHAIN_INSIGHTS_CLI}" mcp tools --refresh
) >"${RUN_DIR}/chain-insights-tools.txt"

DIRECT_TOOLS_JSON="${RUN_DIR}/direct-tools-list.json"
log "checking direct Chain Insights Graph tools/list"
npx @modelcontextprotocol/inspector \
  --cli "${MCP_ENDPOINT}" \
  --transport http \
  --header "Authorization: Bearer ${DEBUG_TOKEN}" \
  --header "X-MCP-Debug-Token: ${DEBUG_TOKEN}" \
  --method tools/list >"${DIRECT_TOOLS_JSON}"

node - "${DIRECT_TOOLS_JSON}" "${RUN_DIR}/direct-high-level-tools.txt" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const highLevelFile = process.argv[3]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const tools = data.tools || []
const names = new Set(tools.map((tool) => tool.name))
const required = ['network_capabilities', 'graph_query', 'graph_query_batch']
const missing = required.filter((name) => !names.has(name))
if (missing.length) throw new Error(`direct tools/list missing tools: ${missing.join(', ')}`)
if (JSON.stringify(tools).includes('app_data')) throw new Error('direct tools/list still contains app_data')
const hasHighLevel = ['aml_address_risk', 'aml_trace_victim_funds'].every((name) => names.has(name))
fs.writeFileSync(highLevelFile, hasHighLevel ? 'yes\n' : 'no\n')
console.log(`[uat] direct tools/list ok: ${tools.length} tools (${hasHighLevel ? 'high-level' : 'primitive-only'})`)
NODE

DIRECT_CAPABILITIES_JSON="${RUN_DIR}/direct-network-capabilities.json"
log "checking public network capabilities"
npx @modelcontextprotocol/inspector \
  --cli "${MCP_ENDPOINT}" \
  --transport http \
  --header "Authorization: Bearer ${DEBUG_TOKEN}" \
  --header "X-MCP-Debug-Token: ${DEBUG_TOKEN}" \
  --method tools/call \
  --tool-name network_capabilities >"${DIRECT_CAPABILITIES_JSON}"

node - "${DIRECT_CAPABILITIES_JSON}" "${NETWORK}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const network = process.argv[3]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
let payload = data.structuredContent
if (!payload && data.content?.[0]?.type === 'text') {
  payload = JSON.parse(data.content[0].text)
}
const networks = payload?.facts?.capabilities?.networks || []
const rawPayload = JSON.stringify(payload?.facts?.capabilities || {})
const byName = new Map(networks.map((entry) => [entry.network, entry]))
const source = byName.get(network)
const errors = []
if (!source) errors.push(`network_capabilities missing source network ${network}`)
if (source?.layers?.topology?.enabled !== true) errors.push(`${network} topology is not enabled`)
if (source?.tools?.graph_query !== 'available') errors.push(`${network} graph_query is not available`)
for (const leaked of ['retention', 'window_days', 'aggregations']) {
  if (rawPayload.includes(leaked)) errors.push(`network_capabilities leaked ${leaked} implementation metadata`)
}
for (const alias of ['bittensor_evm', 'base', 'ethereum', 'tron']) {
  if (byName.has(alias)) errors.push(`network_capabilities leaked alias or unsupported network ${alias}`)
}
if (errors.length) throw new Error(errors.join('; '))
console.log(`[uat] network_capabilities ok: source=${network}`)
NODE

IDENTITY_TOPOLOGY_JSON="${RUN_DIR}/direct-live-topology.json"
IDENTITY_TOPOLOGY_QUERY="USE live_topology MATCH (s:Identity)-[f:FLOWS_TO]->(d:Identity) RETURN count(f) AS identity_flows"
log "checking public live_topology identity-grain edges (network=${NETWORK})"
npx @modelcontextprotocol/inspector \
  --cli "${MCP_ENDPOINT}" \
  --transport http \
  --header "Authorization: Bearer ${DEBUG_TOKEN}" \
  --header "X-MCP-Debug-Token: ${DEBUG_TOKEN}" \
  --method tools/call \
  --tool-name graph_query \
  --tool-arg "network=${NETWORK}" \
  --tool-arg "query=${IDENTITY_TOPOLOGY_QUERY}" >"${IDENTITY_TOPOLOGY_JSON}"

node - "${IDENTITY_TOPOLOGY_JSON}" "${NETWORK}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const network = process.argv[3]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const errors = []
if (data.isError) errors.push(`live_topology query returned isError=true: ${data.content?.[0]?.text || 'unknown error'}`)
const facts = data.structuredContent?.facts
const subject = facts?.subject
const routing = facts?.routing
const flows = facts?.query?.results?.[0]?.identity_flows
if (subject?.network !== network) errors.push(`identity subject network mismatch: ${subject?.network}`)
if (routing?.starrocks_database !== `${network}_semantic`) errors.push(`live_topology routing database mismatch: ${routing?.starrocks_database}`)
if (!Number.isFinite(Number(flows)) || Number(flows) <= 0) errors.push(`live_topology FLOWS_TO count not positive: ${flows}`)
if (errors.length) throw new Error(errors.join('; '))
console.log(`[uat] public live_topology identity-grain edges ok: flows=${flows} routing=${routing.starrocks_database}`)
NODE

SEMANTIC_FACTS_JSON="${RUN_DIR}/direct-semantic-facts.json"
SEMANTIC_FACTS_QUERY="USE facts MATCH (a:Identity {identity_id: '${UAT_IDENTITY_KEY}'}) RETURN a.identity_id AS identity_key, a.labels AS labels, a.is_exchange AS is_exchange LIMIT 1"
log "checking facts identity query"
npx @modelcontextprotocol/inspector \
  --cli "${MCP_ENDPOINT}" \
  --transport http \
  --header "Authorization: Bearer ${DEBUG_TOKEN}" \
  --header "X-MCP-Debug-Token: ${DEBUG_TOKEN}" \
  --method tools/call \
  --tool-name graph_query \
  --tool-arg "network=${NETWORK}" \
  --tool-arg "query=${SEMANTIC_FACTS_QUERY}" >"${SEMANTIC_FACTS_JSON}"

node - "${SEMANTIC_FACTS_JSON}" "${NETWORK}" "${UAT_IDENTITY_KEY}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const network = process.argv[3]
const identityKey = process.argv[4]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const errors = []
if (data.isError) errors.push(`facts query returned isError=true: ${data.content?.[0]?.text || 'unknown error'}`)
const subject = data.structuredContent?.facts?.subject
const routing = data.structuredContent?.facts?.routing
const results = data.structuredContent?.facts?.query?.results || []
const match = results.find((row) => row.identity_key === identityKey)
const labels = Array.isArray(match?.labels) ? match.labels.join(',') : (match?.labels || '')
if (subject?.network !== network) errors.push(`semantic facts subject network mismatch: ${subject?.network}`)
if (routing?.starrocks_database !== `${network}_semantic`) errors.push(`facts routing database mismatch: ${routing?.starrocks_database}`)
if (!match) errors.push(`facts query did not return identity key ${identityKey}`)
if (labels.length === 0) errors.push('facts query did not return identity labels')
if (errors.length) throw new Error(errors.join('; '))
console.log(`[uat] facts identity query ok: identity_key=${identityKey} labels=${labels}`)
NODE

# Run the CLI live-topology assertion before proxy tool checks so graph reads
# fail close to their source if the local topology is unhealthy.
GRAPH_QUERY_TEXT="${RUN_DIR}/graph-query-identity.txt"
log "calling Chain Insights CLI graph_query against real MCP"
# Bounded retry: a busy graph store can transiently queue point reads past
# the MCP per-query timeout (e.g. mid-resync); the assertion itself is
# unchanged and still requires the exact UAT identity row.
GRAPH_QUERY_ATTEMPTS="${GRAPH_QUERY_ATTEMPTS:-3}"
for attempt in $(seq 1 "${GRAPH_QUERY_ATTEMPTS}"); do
  (
    cd "${WORKSPACE_ROOT}"
    node "${CHAIN_INSIGHTS_CLI}" mcp call graph_query \
      "network=${NETWORK}" \
      "query=USE live_topology MATCH (i:Identity {identity_id: '${UAT_IDENTITY_KEY}'})-[:HAS_ADDRESS]->(m:Address) RETURN i.identity_id AS identity_id, collect(m.address) AS addresses"
  ) >"${GRAPH_QUERY_TEXT}" || true
  if node -e "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8').trim())" "${GRAPH_QUERY_TEXT}" 2>/dev/null; then
    break
  fi
  if [[ "${attempt}" -lt "${GRAPH_QUERY_ATTEMPTS}" ]]; then
    log "graph_query attempt ${attempt} returned a non-JSON transient error; retrying in 20s"
    sleep 20
  fi
done

node - "${GRAPH_QUERY_TEXT}" "${UAT_IDENTITY_KEY}" "${UAT_ADDRESS}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const identityKey = process.argv[3]
const substrateAddress = process.argv[4]
const text = fs.readFileSync(file, 'utf8').trim()
const data = JSON.parse(text)
const first = data.facts?.query?.results?.[0] || data.results?.[0]
if (!first || first.identity_id !== identityKey) {
  throw new Error(`graph_query did not return expected identity ${identityKey}`)
}
if (!Array.isArray(first.addresses) || first.addresses.length === 0) {
  throw new Error(`HAS_ADDRESS satellite member addresses missing or empty: ${JSON.stringify(first.addresses)}`)
}
if (!first.addresses.includes(substrateAddress)) {
  throw new Error(`HAS_ADDRESS satellites missing SS58 member form ${substrateAddress}: ${JSON.stringify(first.addresses)}`)
}
console.log(`[uat] graph_query ok: ${first.identity_id} (addresses=${first.addresses.join(',')})`)
NODE

# Archive topology compatibility: historical StarRocks-backed topology must
# expose the same Identity -> HAS_ADDRESS -> Address member-address shape.
ARCHIVE_MEMBER_TEXT="${RUN_DIR}/graph-query-archive-member-address.txt"
log "calling Chain Insights CLI graph_query for archive member-address compatibility"
(
  cd "${WORKSPACE_ROOT}"
  node "${CHAIN_INSIGHTS_CLI}" mcp call graph_query \
    "network=${NETWORK}" \
    "query=USE archive_topology MATCH (i:Identity {identity_id: '${UAT_IDENTITY_KEY}'})-[:HAS_ADDRESS]->(m:Address {address: '${UAT_ADDRESS}'}) RETURN i.identity_id AS identity_id, m.address AS address, m.network AS network LIMIT 1"
) >"${ARCHIVE_MEMBER_TEXT}"

node - "${ARCHIVE_MEMBER_TEXT}" "${UAT_IDENTITY_KEY}" "${UAT_ADDRESS}" "${UAT_MEMBER_NETWORK}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const identityKey = process.argv[3]
const memberAddress = process.argv[4]
const network = process.argv[5]
const data = JSON.parse(fs.readFileSync(file, 'utf8').trim())
const first = data.facts?.query?.results?.[0] || data.results?.[0]
if (!first || first.identity_id !== identityKey || first.address !== memberAddress || first.network !== network) {
  throw new Error(`archive HAS_ADDRESS lookup for ${memberAddress} did not return ${identityKey}: ${JSON.stringify(first)}`)
}
console.log(`[uat] archive HAS_ADDRESS ok: ${first.identity_id} -> ${first.address} (${first.network})`)
NODE

# Member-address resolution: an exact, index-backed lookup by any member
# address form (here the SS58 substrate form) must return the identity.
MEMBER_RESOLVE_TEXT="${RUN_DIR}/graph-query-member-resolve.txt"
log "calling Chain Insights CLI graph_query for member-address resolution"
(
  cd "${WORKSPACE_ROOT}"
  node "${CHAIN_INSIGHTS_CLI}" mcp call graph_query \
    "network=${NETWORK}" \
    "query=USE live_topology MATCH (m:Address {address: '${UAT_ADDRESS}'})<-[:HAS_ADDRESS]-(i:Identity) RETURN i.identity_id AS identity_id LIMIT 1"
) >"${MEMBER_RESOLVE_TEXT}"

node - "${MEMBER_RESOLVE_TEXT}" "${UAT_IDENTITY_KEY}" "${UAT_ADDRESS}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const identityKey = process.argv[3]
const memberAddress = process.argv[4]
const data = JSON.parse(fs.readFileSync(file, 'utf8').trim())
const first = data.facts?.query?.results?.[0] || data.results?.[0]
if (!first || first.identity_id !== identityKey) {
  throw new Error(`member-address resolution for ${memberAddress} did not return ${identityKey}: ${JSON.stringify(first)}`)
}
console.log(`[uat] member-address resolution ok: ${memberAddress} -> ${first.identity_id}`)
NODE

DIRECT_JSON="${RUN_DIR}/direct-address-risk.json"
DIRECT_ADDRESS_RISK_SUMMARY="- direct aml_address_risk skipped: direct endpoint is primitive-only"
if [[ "$(cat "${RUN_DIR}/direct-high-level-tools.txt")" == "yes" ]]; then
  log "calling direct Chain Insights Graph aml_address_risk"
  npx @modelcontextprotocol/inspector \
    --cli "${MCP_ENDPOINT}" \
    --transport http \
    --header "Authorization: Bearer ${DEBUG_TOKEN}" \
    --header "X-MCP-Debug-Token: ${DEBUG_TOKEN}" \
    --method tools/call \
    --tool-name aml_address_risk \
    --tool-arg "network=${NETWORK}" \
    --tool-arg "address=${UAT_IDENTITY_KEY}" \
    --tool-arg include_attachments=true >"${DIRECT_JSON}"

  node - "${DIRECT_JSON}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const errors = []
const content = data.content || []
const sc = data.structuredContent || {}
const graphData = data._meta?.chainInsights?.graph?.data
const graphArrayKeys = ['app_data', 'nodes', 'edges', 'flows', 'edge_anchors', 'transfers']
if (data.isError) errors.push('direct aml_address_risk returned isError=true')
if (content[0]?.type !== 'text') errors.push('direct content[0] is not text')
if (sc.schema !== 'chain-insights.result.v1') errors.push(`direct structuredContent schema mismatch: ${sc.schema}`)
for (const key of graphArrayKeys) {
  if (Object.prototype.hasOwnProperty.call(sc, key)) errors.push(`direct structuredContent leaks ${key}`)
}
if (!graphData) errors.push('direct _meta.chainInsights.graph.data missing')
if (graphData?.schema !== 'chain-insights.graph.v1') errors.push(`direct graph schema mismatch: ${graphData?.schema}`)
for (const key of ['nodes', 'edges', 'flows', 'edge_anchors']) {
  if (!Array.isArray(graphData?.[key])) errors.push(`direct graph ${key} is not an array`)
}
if (Object.prototype.hasOwnProperty.call(graphData || {}, 'transfers')) errors.push('direct graph includes transfers')
if (errors.length) throw new Error(errors.join('; '))
console.log(`[uat] direct aml_address_risk ok: nodes=${graphData.nodes.length} edges=${graphData.edges.length} flows=${graphData.flows.length} edge_anchors=${graphData.edge_anchors.length}`)
NODE
  DIRECT_ADDRESS_RISK_SUMMARY="- ${DIRECT_JSON}"
else
  log "direct Chain Insights Graph high-level tools absent; primitive-only endpoint, skipping direct aml_address_risk check"
fi

PROXY_TOOLS_JSON="${RUN_DIR}/proxy-tools-list.json"
log "checking Chain Insights proxy tools/list"
npx @modelcontextprotocol/inspector \
  --cli node "${CHAIN_INSIGHTS_PROXY}" \
  --transport stdio \
  --method tools/list >"${PROXY_TOOLS_JSON}"

node - "${PROXY_TOOLS_JSON}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const tools = data.tools || []
const names = new Set(tools.map((tool) => tool.name))
const required = ['wallet_balance', 'meta_help', 'meta_network_capabilities', 'meta_usage_status', 'aml_address_risk', 'aml_trace_victim_funds', 'aml_trace_suspect_funds', 'aml_trace_deposit_sources', 'graph_query', 'graph_query_batch']
const missing = required.filter((name) => !names.has(name))
if (missing.length) throw new Error(`proxy tools/list missing tools: ${missing.join(', ')}`)
if (names.size !== required.length) {
  const unexpected = [...names].filter((name) => !required.includes(name))
  throw new Error(`proxy tools/list exposed unexpected tools: ${unexpected.join(', ')}`)
}
if (JSON.stringify(tools).includes('app_data')) throw new Error('proxy tools/list still contains app_data')
const graphTools = tools.filter((tool) => tool._meta?.ui?.resourceUri === 'ui://chain-insights/graph').map((tool) => tool.name)
for (const name of ['aml_address_risk', 'aml_trace_victim_funds', 'aml_trace_suspect_funds', 'aml_trace_deposit_sources']) {
  if (!graphTools.includes(name)) throw new Error(`proxy graph app metadata missing for ${name}`)
}
console.log(`[uat] proxy tools/list ok: ${tools.length} tools`)
NODE

PROXY_JSON="${RUN_DIR}/proxy-address-risk.json"
log "calling Chain Insights proxy aml_address_risk"
node --input-type=module - "${CHAIN_INSIGHTS_PROXY}" "${NETWORK}" "${UAT_ADDRESS}" "${PROXY_JSON}" <<'NODE'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import fs from 'node:fs'

const proxy = process.argv[2]
const network = process.argv[3]
const address = process.argv[4]
const outputFile = process.argv[5]
const requestTimeoutMs = 5 * 60 * 1000

const client = new Client({ name: 'chain-insights-uat', version: '0.0.0' })
const transport = new StdioClientTransport({
  command: process.execPath,
  args: [proxy],
  env: process.env,
})

try {
  await client.connect(transport)
  const result = await client.callTool(
    {
      name: 'aml_address_risk',
      arguments: {
        network,
        address,
        include_attachments: true,
      },
    },
    undefined,
    {
      timeout: requestTimeoutMs,
      maxTotalTimeout: requestTimeoutMs,
    },
  )
  fs.writeFileSync(outputFile, `${JSON.stringify(result, null, 2)}\n`)
} finally {
  await client.close()
}
NODE

GRAPH_REPORT_URL="$(
node - "${PROXY_JSON}" "${UAT_ADDRESS}" "${UAT_IDENTITY_KEY}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const memberAddress = process.argv[3]
const identityKey = process.argv[4]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const errors = []
const content = data.content || []
const sc = data.structuredContent || {}
const graph = data._meta?.chainInsights?.graph
if (data.isError) errors.push('proxy aml_address_risk returned isError=true')
if (content[0]?.type !== 'text') errors.push('proxy content[0] is not text')
if (sc.schema !== 'chain-insights.result.v1') errors.push(`proxy structuredContent schema mismatch: ${sc.schema}`)
const subjectAddresses = sc.facts?.subject?.addresses || []
const identityResolution = sc.identity_resolution || []
if (!subjectAddresses.includes(memberAddress)) errors.push(`proxy subject addresses do not include public member address ${memberAddress}`)
if (!identityResolution.some((entry) => entry.canonical_identity_key === identityKey && entry.address === memberAddress)) {
  errors.push('proxy identity_resolution does not map the public member address to the canonical identity key')
}
for (const key of ['app_data', 'nodes', 'edges', 'flows', 'edge_anchors', 'transfers']) {
  if (JSON.stringify(sc).includes(`"${key}"`)) errors.push(`proxy structuredContent leaks ${key}`)
}
if (!graph) errors.push('proxy _meta.chainInsights.graph missing')
if (graph?.data) errors.push('proxy _meta.chainInsights.graph.data leaked')
if (graph?.schema !== 'chain-insights.graph.v1') errors.push(`proxy graph schema mismatch: ${graph?.schema}`)
if (graph?.id) errors.push('proxy graph id should not be returned')
if (!/^http:\/\/127\.0\.0\.1:\d+\/graph-reports\/[A-Za-z0-9._-]+\.graph\.json$/.test(graph?.url || '')) {
  errors.push(`proxy graph url is not a local graph report URL: ${graph?.url}`)
}
if (errors.length) throw new Error(errors.join('; '))
console.error(`[uat] proxy aml_address_risk ok: graph_report=${graph.url}`)
process.stdout.write(graph.url)
NODE
)"
printf '%s\n' "${GRAPH_REPORT_URL}" >"${RUN_DIR}/graph-report-url.txt"

GRAPH_REPORT_JSON="${RUN_DIR}/graph-report.json"
log "fetching local graph report"
curl -sf "${GRAPH_REPORT_URL}" >"${GRAPH_REPORT_JSON}"

node - "${GRAPH_REPORT_JSON}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const errors = []
if (data.schema !== 'chain-insights.graph.v1') errors.push(`graph report schema mismatch: ${data.schema}`)
for (const key of ['nodes', 'edges', 'flows', 'edge_anchors']) {
  if (!Array.isArray(data[key])) errors.push(`graph report ${key} is not an array`)
}
if (Object.prototype.hasOwnProperty.call(data, 'transfers')) errors.push('graph report includes transfers')
if (errors.length) throw new Error(errors.join('; '))
console.log(`[uat] graph report ok: nodes=${data.nodes.length} edges=${data.edges.length} flows=${data.flows.length} edge_anchors=${data.edge_anchors.length}`)
NODE

TRACE_TOOLS_JSON="${RUN_DIR}/proxy-aml-trace-tools.json"
log "calling Chain Insights proxy AML trace tools"
node --input-type=module - "${CHAIN_INSIGHTS_PROXY}" "${NETWORK}" "${UAT_ADDRESS}" "${UAT_IDENTITY_KEY}" "${TRACE_TOOLS_JSON}" <<'NODE'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import fs from 'node:fs'

const proxy = process.argv[2]
const network = process.argv[3]
const memberAddress = process.argv[4]
const identityKey = process.argv[5]
const outputFile = process.argv[6]
const requestTimeoutMs = 5 * 60 * 1000
const calls = [
  { name: 'aml_trace_victim_funds', arguments: { network, victim_addresses: memberAddress, max_hops: 2 } },
  { name: 'aml_trace_suspect_funds', arguments: { network, suspect_addresses: memberAddress, max_hops: 2 } },
  { name: 'aml_trace_deposit_sources', arguments: { network, deposit_addresses: memberAddress, max_hops: 2 } },
]

const client = new Client({ name: 'chain-insights-uat-aml-traces', version: '0.0.0' })
const transport = new StdioClientTransport({
  command: process.execPath,
  args: [proxy],
  env: process.env,
})

try {
  await client.connect(transport)
  const results = {}
  for (const call of calls) {
    const result = await client.callTool(call, undefined, {
      timeout: requestTimeoutMs,
      maxTotalTimeout: requestTimeoutMs,
    })
    const sc = result.structuredContent || {}
    const errors = []
    if (result.isError) errors.push(`${call.name} returned isError=true`)
    if (result.content?.[0]?.type !== 'text') errors.push(`${call.name} content[0] is not text`)
    if (sc.schema !== 'chain-insights.trace.v1') errors.push(`${call.name} schema mismatch: ${sc.schema}`)
    if (sc.tool !== call.name) errors.push(`${call.name} tool mismatch: ${sc.tool}`)
    if (!Array.isArray(sc.input?.addresses) || !sc.input.addresses.includes(memberAddress)) {
      errors.push(`${call.name} input addresses do not include public member address ${memberAddress}`)
    }
    if (!Array.isArray(sc.identity_resolution) || !sc.identity_resolution.some((entry) => entry.canonical_identity_key === identityKey && entry.address === memberAddress)) {
      errors.push(`${call.name} identity_resolution does not map public member address to canonical identity key`)
    }
    if (errors.length) throw new Error(errors.join('; '))
    results[call.name] = result
  }
  fs.writeFileSync(outputFile, `${JSON.stringify({ results }, null, 2)}\n`)
} finally {
  await client.close()
}
NODE

node - "${TRACE_TOOLS_JSON}" <<'NODE'
const fs = require('node:fs')
const file = process.argv[2]
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
const tools = Object.keys(data.results || {})
if (tools.length !== 3) throw new Error(`expected 3 AML trace tool results, got ${tools.length}`)
console.log(`[uat] AML trace tools ok: ${tools.join(', ')}`)
NODE

SUMMARY="${RUN_DIR}/summary.txt"
cat >"${SUMMARY}" <<EOF
Chain Insights against Chain Insights Graph UAT PASS

Endpoint: ${MCP_ENDPOINT}
Network: ${NETWORK}
Identity key: ${UAT_IDENTITY_KEY}
Substrate member address: ${UAT_ADDRESS}
Graph report URL: ${GRAPH_REPORT_URL}

Raw outputs:
- ${DIRECT_TOOLS_JSON}
${DIRECT_ADDRESS_RISK_SUMMARY}
- ${ADDRESS_SCOPE_REJECTION_JSON}
- ${PROXY_TOOLS_JSON}
- ${PROXY_JSON}
- ${GRAPH_REPORT_JSON}
- ${TRACE_TOOLS_JSON}
- ${GRAPH_QUERY_TEXT}

Workspace:
- ${WORKSPACE_ROOT}
EOF

log "PASS"
log "summary: ${SUMMARY}"
