#!/usr/bin/env node
"use strict";

const fs = require("fs");
const path = require("path");
const { EVENT_TYPES, parseTrace } = require("./trace_parser.js");
const {
  buildWorkerSpans,
  attachCpuSamples,
  buildActiveTaskTimeline,
  computeSchedulingDelays,
  filterPointsOfInterest,
  buildFlamegraphTree,
  flattenFlamegraph,
  buildFgData,
  buildSpanData,
} = require("./trace_analysis.js");

async function main() {
  const tracePath = process.argv[2] || path.join(__dirname, "demo-trace.bin");

  if (!fs.existsSync(tracePath)) {
    console.error(`Trace file not found: ${tracePath}`);
    process.exit(1);
  }

  function fail(msg) {
    console.log(`✗ ${msg}`);
    process.exit(1);
  }

  function pass(msg) {
    console.log(`✓ ${msg}`);
  }

  const trace = await parseTrace(fs.readFileSync(tracePath));
  const evts = trace.events;

  const wSet = new Set();
  evts.forEach((e) => {
    if (
      e.eventType !== EVENT_TYPES.QueueSample &&
      e.eventType !== EVENT_TYPES.WakeEvent
    )
      wSet.add(e.workerId);
  });
  const workerIds = [...wSet].sort((a, b) => a - b);

  let minTs = evts[0].timestamp;
  let maxTs = evts[evts.length - 1].timestamp;
  for (const e of evts) {
    if (e.timestamp < minTs) minTs = e.timestamp;
    if (e.timestamp > maxTs) maxTs = e.timestamp;
  }

  // ── buildWorkerSpans ──

  const { workerSpans, perWorker, queueSamples, workerQueueSamples, maxLocalQueue, wakesByTask, wakesByWorker } = buildWorkerSpans(
    evts,
    workerIds,
    maxTs
  );

  function testPollsHaveValidRange() {
    for (const w of workerIds) {
      for (const p of workerSpans[w].polls) {
        if (p.start > p.end)
          fail(`Worker ${w}: poll start > end (${p.start} > ${p.end})`);
      }
    }
    pass("All polls have start <= end");
  }

  function testNoOverlappingPolls() {
    for (const w of workerIds) {
      const polls = workerSpans[w].polls;
      for (let i = 1; i < polls.length; i++) {
        if (polls[i].start < polls[i - 1].end)
          fail(`Worker ${w}: overlapping polls at index ${i}`);
      }
    }
    pass("No overlapping polls on same worker");
  }

  function testActiveRatiosInRange() {
    for (const w of workerIds) {
      for (const a of workerSpans[w].actives) {
        if (a.ratio < 0 || a.ratio > 1)
          fail(`Worker ${w}: active ratio ${a.ratio} out of [0, 1]`);
      }
    }
    pass("Active period ratios in [0, 1]");
  }

  function testParksHaveValidRange() {
    for (const w of workerIds) {
      for (const p of workerSpans[w].parks) {
        if (p.start > p.end) fail(`Worker ${w}: park start > end`);
      }
    }
    pass("All parks have start <= end");
  }

  function testQueueSamplesExist() {
    if (queueSamples.length === 0) fail("No queue samples");
    pass(`${queueSamples.length} queue samples`);
  }

  // ── attachCpuSamples ──

  const cpuResult = attachCpuSamples(trace.cpuSamples, workerSpans);

  function testAttachedSamplesWithinPollBounds() {
    for (const w of workerIds) {
      for (const p of workerSpans[w].polls) {
        if (p.cpuSamples) {
          for (const s of p.cpuSamples) {
            if (s.timestamp < p.start || s.timestamp > p.end)
              fail(
                `Worker ${w}: cpu sample at ${s.timestamp} outside poll [${p.start}, ${p.end}]`
              );
          }
        }
        if (p.schedSamples) {
          for (const s of p.schedSamples) {
            if (s.timestamp < p.start || s.timestamp > p.end)
              fail(
                `Worker ${w}: sched sample at ${s.timestamp} outside poll [${p.start}, ${p.end}]`
              );
          }
        }
      }
    }
    pass("All attached samples fall within poll bounds");
  }

  function testCpuResultCounts() {
    if (
      cpuResult.pollsWithCpuSamples < 0 ||
      cpuResult.pollsWithSchedSamples < 0
    )
      fail("Negative sample counts");
    pass(
      `${cpuResult.pollsWithCpuSamples} polls with cpu samples, ${cpuResult.pollsWithSchedSamples} with sched samples`
    );
  }

  // ── extractLocalQueueSamples (via buildWorkerSpans) ──

  function testLocalQueueNonNegative() {
    for (const w of workerIds) {
      for (const s of workerQueueSamples[w]) {
        if (s.local < 0) fail(`Worker ${w}: negative local queue ${s.local}`);
      }
    }
    pass("All local queue depths non-negative");
  }

  function testMaxLocalQueue() {
    if (maxLocalQueue < 1) fail(`maxLocalQueue ${maxLocalQueue} < 1`);
    pass(`maxLocalQueue = ${maxLocalQueue}`);
  }

  // ── buildActiveTaskTimeline ──

  const { activeTaskSamples, taskFirstPoll } = buildActiveTaskTimeline(
    trace.taskSpawnTimes,
    trace.taskTerminateTimes
  );

  function testTimelineSorted() {
    for (let i = 1; i < activeTaskSamples.length; i++) {
      if (activeTaskSamples[i].t < activeTaskSamples[i - 1].t)
        fail(`Timeline not sorted at index ${i}`);
    }
    pass("Timeline sorted by timestamp");
  }

  function testCountNonNegative() {
    for (const s of activeTaskSamples) {
      if (s.count < 0) fail(`Negative task count ${s.count}`);
    }
    pass("Task counts non-negative");
  }

  // ── indexWakeEvents (via buildWorkerSpans) ──

  function testWakesByTaskSorted() {
    for (const arr of Object.values(wakesByTask)) {
      for (let i = 1; i < arr.length; i++) {
        if (arr[i].timestamp < arr[i - 1].timestamp)
          fail("wakesByTask not sorted");
      }
    }
    pass("wakesByTask arrays sorted by timestamp");
  }

  function testWakesByWorkerSorted() {
    for (const arr of Object.values(wakesByWorker)) {
      for (let i = 1; i < arr.length; i++) {
        if (arr[i].timestamp < arr[i - 1].timestamp)
          fail("wakesByWorker not sorted");
      }
    }
    pass("wakesByWorker arrays sorted by timestamp");
  }

  function testWakeCountsConsistent() {
    let taskTotal = 0;
    for (const arr of Object.values(wakesByTask)) taskTotal += arr.length;
    let workerTotal = 0;
    for (const arr of Object.values(wakesByWorker)) workerTotal += arr.length;
    if (taskTotal !== workerTotal)
      fail(
        `wakesByTask total ${taskTotal} != wakesByWorker total ${workerTotal}`
      );
    pass(`${taskTotal} wake events indexed consistently`);
  }

  // ── computeSchedulingDelays ──

  const schedDelays = computeSchedulingDelays(
    workerSpans,
    workerIds,
    wakesByTask
  );

  function testDelaysPositive() {
    for (const sd of schedDelays) {
      if (sd.delay <= 0) fail(`Non-positive delay: ${sd.delay}`);
    }
    pass("All delays positive");
  }

  function testDelaysBounded() {
    for (const sd of schedDelays) {
      if (sd.delay >= 1e9) fail(`Delay >= 1s: ${sd.delay}`);
    }
    pass("All delays < 1s");
  }

  function testWakeBeforePoll() {
    for (const sd of schedDelays) {
      if (sd.wakeTime >= sd.pollTime)
        fail(`wakeTime ${sd.wakeTime} >= pollTime ${sd.pollTime}`);
    }
    pass("wakeTime < pollTime for all delays");
  }

  function testDelaysSorted() {
    for (let i = 1; i < schedDelays.length; i++) {
      if (schedDelays[i].wakeTime < schedDelays[i - 1].wakeTime)
        fail("schedDelays not sorted by wakeTime");
    }
    pass("schedDelays sorted by wakeTime");
  }

  // ── filterPointsOfInterest ──

  function testLongPollFilter() {
    const pois = filterPointsOfInterest(
      "long-poll",
      workerSpans,
      workerIds,
      schedDelays,
      { hasSchedWait: trace.hasSchedWait }
    );
    if (pois.length === 0) fail("No long-poll points of interest found");
    for (const p of pois) {
      if (p.type !== "long-poll") fail(`Wrong type: ${p.type}`);
      if (p.value <= 1) fail(`long-poll value ${p.value} <= 1ms`);
    }
    pass(`long-poll filter: ${pois.length} results, all > 1ms`);
  }

  function testCpuSampledFilter() {
    const pois = filterPointsOfInterest(
      "cpu-sampled",
      workerSpans,
      workerIds,
      schedDelays,
      { hasSchedWait: trace.hasSchedWait }
    );
    if (pois.length === 0) fail("No cpu-sampled points of interest found");
    for (const p of pois) {
      if (p.type !== "cpu-sampled") fail(`Wrong type: ${p.type}`);
      if (p.value <= 0) fail(`cpu-sampled value ${p.value} <= 0`);
    }
    pass(`cpu-sampled filter: ${pois.length} results, all with samples`);
  }

  function testWakeDelayFilter() {
    const pois = filterPointsOfInterest(
      "wake-delay",
      workerSpans,
      workerIds,
      schedDelays,
      { hasSchedWait: trace.hasSchedWait }
    );
    if (pois.length === 0) fail("No wake-delay points of interest found");
    for (const p of pois) {
      if (p.type !== "wake-delay") fail(`Wrong type: ${p.type}`);
      if (p.value <= 100) fail(`wake-delay value ${p.value} <= 100µs`);
    }
    pass(`wake-delay filter: ${pois.length} results, all > 100µs`);
  }

  function testSortByWorst() {
    const pois = filterPointsOfInterest(
      "long-poll",
      workerSpans,
      workerIds,
      schedDelays,
      { hasSchedWait: trace.hasSchedWait, sortByWorst: true }
    );
    for (let i = 1; i < pois.length; i++) {
      if (pois[i].value > pois[i - 1].value) fail("sortByWorst not descending");
    }
    pass("sortByWorst produces descending order");
  }

  // ── buildFlamegraphTree / flattenFlamegraph ──

  function testFlamegraphTree() {
    const cpuSamples = trace.cpuSamples.filter((s) => s.source !== 1);
    if (cpuSamples.length === 0) fail("No CPU samples found");

    const root = buildFlamegraphTree(cpuSamples, trace.callframeSymbols);
    if (root.count !== cpuSamples.length)
      fail(`Root count ${root.count} != sample count ${cpuSamples.length}`);
    pass(`Root count matches sample count (${root.count})`);
  }

  function testFlattenFlamegraph() {
    const cpuSamples = trace.cpuSamples.filter((s) => s.source !== 1);
    if (cpuSamples.length === 0) fail("No CPU samples found");

    const root = buildFlamegraphTree(cpuSamples, trace.callframeSymbols);
    const { nodes, maxDepth } = flattenFlamegraph(root, cpuSamples.length);
    for (const n of nodes) {
      if (n.x < 0 || n.x >= 1) fail(`Node x=${n.x} out of [0, 1)`);
      if (n.w <= 0) fail(`Node w=${n.w} <= 0`);
    }
    if (maxDepth < 0) fail(`maxDepth ${maxDepth} < 0`);
    pass(`${nodes.length} flamegraph nodes, maxDepth=${maxDepth}`);
  }

  function testBuildFgData() {
    const cpuSamples = trace.cpuSamples.filter((s) => s.source !== 1);
    if (cpuSamples.length === 0) fail("No CPU samples found");

    const data = buildFgData(cpuSamples, trace.callframeSymbols);
    if (!data) fail("buildFgData returned null for non-empty samples");
    if (data.totalSamples !== cpuSamples.length)
      fail(`totalSamples ${data.totalSamples} != ${cpuSamples.length}`);
    pass(
      `buildFgData: ${data.nodes.length} nodes, ${data.totalSamples} samples`
    );
  }

  function testBuildFgDataEmpty() {
    const data = buildFgData([], trace.callframeSymbols);
    if (data !== null) fail("buildFgData should return null for empty samples");
    pass("buildFgData returns null for empty samples");
  }

  // ── buildSpanData ──

  function testBuildSpanDataPairing() {
    const customEvents = [
      { name: "SpanEnterEvent", timestamp: 1000, fields: { worker_id: 0, span_id: 1, parent_span_id: null, span_name: "handle_request", fields: { user_id: "42" } } },
      { name: "SpanEnterEvent", timestamp: 1100, fields: { worker_id: 0, span_id: 2, parent_span_id: 1, span_name: "redis_get", fields: { key: "foo" } } },
      { name: "SpanExitEvent",  timestamp: 1200, fields: { worker_id: 0, span_id: 2, span_name: "redis_get", fields: { key: "foo" } } },
      { name: "SpanExitEvent",  timestamp: 1300, fields: { worker_id: 0, span_id: 1, span_name: "handle_request", fields: { user_id: "42" } } },
    ];
    const { spansByWorker, spanMeta } = buildSpanData(customEvents);
    const w0 = spansByWorker[0] || [];
    if (w0.length !== 2) fail(`Expected 2 span intervals on worker 0, got ${w0.length}`);
    if (w0[0].spanName !== "handle_request" && w0[1].spanName !== "handle_request")
      fail("Missing handle_request span");
    if (w0[0].spanName !== "redis_get" && w0[1].spanName !== "redis_get")
      fail("Missing redis_get span");
    // Verify sorted by start time
    if (w0[0].start > w0[1].start) fail("Spans not sorted by start time");
    // Verify enter/exit pairing
    const redis = w0.find(s => s.spanName === "redis_get");
    if (redis.start !== 1100 || redis.end !== 1200) fail("redis_get timing wrong");
    if (!spanMeta.has(1) || !spanMeta.has(2)) fail("spanMeta missing entries");
    pass(`${w0.length} span intervals paired correctly`);
  }

  function testBuildSpanDataParent() {
    const customEvents = [
      { name: "SpanEnterEvent", timestamp: 1000, fields: { worker_id: 0, span_id: 10, parent_span_id: null, span_name: "root", fields: {} } },
      { name: "SpanEnterEvent", timestamp: 1100, fields: { worker_id: 0, span_id: 20, parent_span_id: 10, span_name: "child", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1200, fields: { worker_id: 0, span_id: 20, span_name: "child", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1300, fields: { worker_id: 0, span_id: 10, span_name: "root", fields: {} } },
    ];
    const { spansByWorker } = buildSpanData(customEvents);
    const child = spansByWorker[0].find(s => s.spanName === "child");
    if (child.parentSpanId !== 10) fail(`Expected parentSpanId=10, got ${child.parentSpanId}`);
    const root = spansByWorker[0].find(s => s.spanName === "root");
    if (root.parentSpanId !== null) fail(`Expected root parentSpanId=null, got ${root.parentSpanId}`);
    pass("Parent span IDs preserved correctly");
  }

  function testBuildSpanDataEmpty() {
    const { spansByWorker, spanMeta } = buildSpanData([]);
    if (Object.keys(spansByWorker).length !== 0) fail("Expected empty spansByWorker");
    if (spanMeta.size !== 0) fail("Expected empty spanMeta");
    pass("Empty input produces empty output");
  }

  function testBuildSpanDataDepth() {
    // Three levels of nesting via explicit parent
    const customEvents = [
      { name: "SpanEnterEvent", timestamp: 1000, fields: { worker_id: 0, span_id: 1, parent_span_id: null, span_name: "root", fields: {} } },
      { name: "SpanEnterEvent", timestamp: 1100, fields: { worker_id: 0, span_id: 2, parent_span_id: 1, span_name: "mid", fields: {} } },
      { name: "SpanEnterEvent", timestamp: 1200, fields: { worker_id: 0, span_id: 3, parent_span_id: 2, span_name: "leaf", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1300, fields: { worker_id: 0, span_id: 3, span_name: "leaf", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1400, fields: { worker_id: 0, span_id: 2, span_name: "mid", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1500, fields: { worker_id: 0, span_id: 1, span_name: "root", fields: {} } },
    ];
    const { spansByWorker, maxDepth } = buildSpanData(customEvents);
    const spans = spansByWorker[0];
    const root = spans.find(s => s.spanName === "root");
    const mid = spans.find(s => s.spanName === "mid");
    const leaf = spans.find(s => s.spanName === "leaf");
    if (root.depth !== 0) fail(`root depth=${root.depth}, expected 0`);
    if (mid.depth !== 1) fail(`mid depth=${mid.depth}, expected 1`);
    if (leaf.depth !== 2) fail(`leaf depth=${leaf.depth}, expected 2`);
    if (maxDepth !== 2) fail(`maxDepth=${maxDepth}, expected 2`);
    pass("Depth computed correctly for 3-level nesting");
  }

  function testBuildSpanDataCycleDetection() {
    // Cyclic parent chain: A -> B -> A (should not stack overflow)
    const customEvents = [
      { name: "SpanEnterEvent", timestamp: 1000, fields: { worker_id: 0, span_id: 1, parent_span_id: 2, span_name: "a", fields: {} } },
      { name: "SpanEnterEvent", timestamp: 1100, fields: { worker_id: 0, span_id: 2, parent_span_id: 1, span_name: "b", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1200, fields: { worker_id: 0, span_id: 2, span_name: "b", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1300, fields: { worker_id: 0, span_id: 1, span_name: "a", fields: {} } },
    ];
    const { spansByWorker } = buildSpanData(customEvents);
    if (!spansByWorker[0] || spansByWorker[0].length !== 2) fail("Expected 2 spans");
    // Just verify it didn't crash; depths may be arbitrary due to cycle
    pass("Cyclic parent chain does not stack overflow");
  }

  function testBuildSpanDataRecycledId() {
    // Span ID 1 used first as "alpha", then recycled as "beta"
    const customEvents = [
      { name: "SpanEnterEvent", timestamp: 1000, fields: { worker_id: 0, span_id: 1, parent_span_id: null, span_name: "alpha", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 1100, fields: { worker_id: 0, span_id: 1, span_name: "alpha", fields: {} } },
      // Same span_id reused with different name
      { name: "SpanEnterEvent", timestamp: 2000, fields: { worker_id: 0, span_id: 1, parent_span_id: null, span_name: "beta", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 2100, fields: { worker_id: 0, span_id: 1, span_name: "beta", fields: {} } },
      // Child of the recycled span
      { name: "SpanEnterEvent", timestamp: 3000, fields: { worker_id: 0, span_id: 2, parent_span_id: 1, span_name: "child", fields: {} } },
      { name: "SpanExitEvent",  timestamp: 3100, fields: { worker_id: 0, span_id: 2, span_name: "child", fields: {} } },
    ];
    const { spansByWorker } = buildSpanData(customEvents);
    const spans = spansByWorker[0];
    if (spans.length !== 3) fail(`Expected 3 spans, got ${spans.length}`);
    const alpha = spans.find(s => s.spanName === "alpha");
    const beta = spans.find(s => s.spanName === "beta");
    if (!alpha || !beta) fail("Missing alpha or beta span");
    // Both should exist as separate intervals despite same span_id
    if (alpha.start !== 1000 || beta.start !== 2000) fail("Span intervals not distinct");
    pass("Recycled span IDs produce separate intervals");
  }

  function testBuildSpanDataPerCallsiteSchema() {
    // New format: schema names are "SpanEnter:target::name:file:line"
    // User fields are top-level (not nested in a "fields" StringMap)
    const customEvents = [
      { name: "SpanEnter:myapp::handle:src/main.rs:10", timestamp: 1000, fields: { worker_id: 0, span_id: 1, parent_span_id: null, span_name: "handle", request_id: "abc-123" } },
      { name: "SpanExit:myapp::handle:src/main.rs:10",  timestamp: 1100, fields: { worker_id: 0, span_id: 1, span_name: "handle", request_id: "abc-123" } },
    ];
    const { spansByWorker } = buildSpanData(customEvents);
    const spans = spansByWorker[0];
    if (!spans || spans.length !== 1) fail(`Expected 1 span, got ${spans?.length}`);
    if (spans[0].spanName !== "handle") fail(`Expected span name 'handle', got '${spans[0].spanName}'`);
    if (spans[0].fields.request_id !== "abc-123") fail(`Expected request_id='abc-123', got '${spans[0].fields.request_id}'`);
    // Base fields should NOT appear in the user fields
    if (spans[0].fields.worker_id) fail("worker_id should not be in user fields");
    if (spans[0].fields.span_name) fail("span_name should not be in user fields");
    pass("Per-callsite schema with typed fields parsed correctly");
  }

  function testBuildSpanDataUnmatched() {
    const customEvents = [
      { name: "SpanEnter:app::a:f:1", timestamp: 1000, fields: { worker_id: 0, span_id: 1, parent_span_id: null, span_name: "a" } },
      { name: "SpanExit:app::a:f:1",  timestamp: 1100, fields: { worker_id: 0, span_id: 1, span_name: "a" } },
      // This enter has no matching exit (trace ended mid-span)
      { name: "SpanEnter:app::b:f:2", timestamp: 1200, fields: { worker_id: 0, span_id: 2, parent_span_id: null, span_name: "b" } },
    ];
    const { spansByWorker, unmatchedSpans } = buildSpanData(customEvents);
    const matched = spansByWorker[0] || [];
    if (matched.length !== 1) fail(`Expected 1 matched span, got ${matched.length}`);
    if (!unmatchedSpans || unmatchedSpans.length !== 1) fail(`Expected 1 unmatched span, got ${unmatchedSpans?.length}`);
    if (unmatchedSpans[0].spanName !== "b") fail(`Expected unmatched span 'b', got '${unmatchedSpans[0].spanName}'`);
    if (unmatchedSpans[0].spanId !== 2) fail(`Expected unmatched spanId 2, got ${unmatchedSpans[0].spanId}`);
    pass("Unmatched spans (enter without exit) detected correctly");
  }

  // ── Regression: open PollStart at trace end must not create phantom poll (#194) ──

  function testOpenPollStartDiscarded() {
    // Simulate a rotated segment where PollStart is the last event (no PollEnd).
    const syntheticEvents = [
      { eventType: EVENT_TYPES.PollStart, timestamp: 1000, workerId: 0, taskId: 1, spawnLocId: null, spawnLoc: null, localQueue: 0 },
      { eventType: EVENT_TYPES.PollEnd,   timestamp: 2000, workerId: 0 },
      // This PollStart has no matching PollEnd — file rotated
      { eventType: EVENT_TYPES.PollStart, timestamp: 3000, workerId: 0, taskId: 2, spawnLocId: null, spawnLoc: null, localQueue: 0 },
    ];
    const syntheticMaxTs = 1_000_000; // 1ms later — would create a huge phantom poll
    const result = buildWorkerSpans(syntheticEvents, [0], syntheticMaxTs);
    const polls = result.workerSpans[0].polls;
    if (polls.length !== 1) fail(`Expected 1 poll, got ${polls.length} — open PollStart was not discarded`);
    if (polls[0].start !== 1000 || polls[0].end !== 2000) fail(`Unexpected poll range`);
    pass("Open PollStart at trace end is discarded (no phantom long poll)");
  }

  // ── Run all tests ──

  console.log("\nbuildWorkerSpans:");
  testOpenPollStartDiscarded();
  testPollsHaveValidRange();
  testNoOverlappingPolls();
  testActiveRatiosInRange();
  testParksHaveValidRange();
  testQueueSamplesExist();

  console.log("\nattachCpuSamples:");
  testAttachedSamplesWithinPollBounds();
  testCpuResultCounts();

  console.log("\nextractLocalQueueSamples:");
  testLocalQueueNonNegative();
  testMaxLocalQueue();

  console.log("\nbuildActiveTaskTimeline:");
  testTimelineSorted();
  testCountNonNegative();

  console.log("\nindexWakeEvents:");
  testWakesByTaskSorted();
  testWakesByWorkerSorted();
  testWakeCountsConsistent();

  console.log("\ncomputeSchedulingDelays:");
  testDelaysPositive();
  testDelaysBounded();
  testWakeBeforePoll();
  testDelaysSorted();

  console.log("\nfilterPointsOfInterest:");
  testLongPollFilter();
  testCpuSampledFilter();
  testWakeDelayFilter();
  testSortByWorst();

  console.log("\nflamegraph:");
  testFlamegraphTree();
  testFlattenFlamegraph();
  testBuildFgData();
  testBuildFgDataEmpty();

  console.log("\nbuildSpanData:");
  testBuildSpanDataPairing();
  testBuildSpanDataParent();
  testBuildSpanDataEmpty();
  testBuildSpanDataDepth();
  testBuildSpanDataCycleDetection();
  testBuildSpanDataRecycledId();
  testBuildSpanDataPerCallsiteSchema();
  testBuildSpanDataUnmatched();

  console.log("\n✓ All analysis checks passed!");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
