// End-to-end round-trip tests for the @intx/inference-testing harness.
//
// These tests exercise complete turn-1 to turn-2 inference cycles through the
// production stack — the real provider adapter, the real `parseSSE`, and SSE
// bytes generated by `wire.*`. Nothing is synthesized: every InferenceEvent
// the tests observe is produced by the real harness pipeline parsing real
// wire bytes the harness served through `deps.fetch`.
//
// Two driver styles are exercised:
//
//   - The default: `harness.runInference(opts)` wraps the production
//     `runInference` and auto-fires registered `onTool` handlers when it
//     observes `inference.tool_call.end` events. The round-trip variants
//     use this path.
//   - The escape hatch: production `runInference` invoked directly with
//     `deps: harness.deps`. The delayed-envelope and async-promise variants
//     use this path because they need to observe dispatch timing before any
//     auto-dispatch can fire.
//
// Variants covered:
//
//   1. Anthropic single tool call → tool dispatch → turn 2 final text.
//   2. OpenAI single tool call → tool dispatch → turn 2 final text.
//   3. Tool handler with async / delayed-envelope scheduling: the dispatch
//      callback firing is observable AFTER promise resolution / virtual time
//      advance, not before.
//   4. Anthropic with TWO parallel tool calls in a single turn → both
//      dispatches → turn 2 carries both tool_result blocks.
//
// The tests live at the integration layer (not co-located with the reactor or
// the inference-testing package) because they are cross-package integration
// proofs.

import { afterEach, describe, expect, test } from "bun:test";

import { runInference } from "@intx/inference";
import { setupHarness, userTurn, wire } from "@intx/inference-testing";
import type { Harness } from "@intx/inference-testing";
import type {
  ConversationTurn,
  InferenceEvent,
  InferenceSource,
} from "@intx/types/runtime";

const ANTHROPIC_SOURCE: InferenceSource = {
  id: "anthropic:claude-test",
  provider: "anthropic",
  baseURL: "https://api.anthropic.com",
  apiKey: "test",
  model: "claude-test",
};

const OPENAI_SOURCE: InferenceSource = {
  id: "openai:gpt-test",
  provider: "openai",
  baseURL: "https://api.openai.com/v1",
  apiKey: "test",
  model: "gpt-test",
};

const TURN1_USAGE_HEAD = {
  input: 10,
  output: 0,
  cacheRead: 0,
  cacheWrite: 0,
  thinking: 0,
};
const TURN1_USAGE_TAIL = {
  input: 0,
  output: 5,
  cacheRead: 0,
  cacheWrite: 0,
  thinking: 0,
};
const TURN2_USAGE_HEAD = {
  input: 15,
  output: 0,
  cacheRead: 0,
  cacheWrite: 0,
  thinking: 0,
};
const TURN2_USAGE_TAIL = {
  input: 0,
  output: 8,
  cacheRead: 0,
  cacheWrite: 0,
  thinking: 0,
};

let activeHarness: Harness | null = null;

afterEach(() => {
  if (activeHarness !== null) {
    activeHarness.dispose();
    activeHarness = null;
  }
});

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function parseRequestBody(
  request: Request,
): Promise<Record<string, unknown>> {
  const text = await request.text();
  const parsed: unknown = JSON.parse(text);
  if (!isRecord(parsed)) {
    throw new Error("captured request body was not a JSON object");
  }
  return parsed;
}

// Drives the production runInference DIRECTLY against the harness's deps,
// bypassing the auto-dispatch wrapper. Used by the delayed-envelope and
// promise-handler tests below, which are documenting the manual escape-hatch
// path through `scenario.invokeTool` and need to observe dispatch timing
// before any auto-dispatch fires.
async function collectInferenceManual(
  harness: Harness,
  turns: ConversationTurn[],
  nextSeq: () => number,
  source: InferenceSource,
): Promise<InferenceEvent[]> {
  const events: InferenceEvent[] = [];
  for await (const ev of runInference({
    turns,
    source,
    nextSeq,
    deps: harness.deps,
  })) {
    events.push(ev);
  }
  return events;
}

// Drives inference through `harness.runInference`, the default auto-dispatch
// path: registered onTool handlers fire automatically when the iterator
// emits `inference.tool_call.end`. Tests assert against the handler's
// captured arguments and `scenario.lastToolDispatch(name)`.
async function collectInferenceAuto(
  harness: Harness,
  turns: ConversationTurn[],
  nextSeq: () => number,
  source: InferenceSource,
): Promise<InferenceEvent[]> {
  const events: InferenceEvent[] = [];
  for await (const ev of harness.runInference({
    turns,
    source,
    nextSeq,
  })) {
    events.push(ev);
  }
  return events;
}

describe("inference-testing harness — multi-turn round-trip", () => {
  test("Anthropic: drives turn-1 tool call, dispatches tool, and propagates result into turn-2 inference", async () => {
    const harness = setupHarness();
    activeHarness = harness;

    // ---------------------------------------------------------------------
    // Tool handler registration. The handler fires automatically via the
    // `harness.runInference` auto-dispatch path: when the iterator emits an
    // `inference.tool_call.end` for "weather", the registered handler runs
    // and the result is captured on `scenario.lastToolDispatch("weather")`.
    // The handler asserts the arguments it received and returns a
    // structured weather observation.
    // ---------------------------------------------------------------------
    const handlerCalls: { args: unknown }[] = [];
    harness.scenario.onTool("weather", (args) => {
      handlerCalls.push({ args });
      return { temperatureF: 68, conditions: "fog" };
    });

    // ---------------------------------------------------------------------
    // Turn 1 wire bytes: a complete Anthropic response containing one
    // tool_use block invoking `weather` with `{"location":"SF"}`. The args
    // payload is a hand-written JSON literal; the wire DSL accepts the
    // string verbatim and emits it as an `input_json_delta`.
    // ---------------------------------------------------------------------
    const turn1Stream = harness.scenario.createStream();
    const turn1Chunks = wire.completeResponse("anthropic", {
      toolCalls: [
        {
          callId: "call_weather_1",
          name: "weather",
          argsJSON: '{"location":"SF"}',
        },
      ],
      headUsage: TURN1_USAGE_HEAD,
      tailUsage: TURN1_USAGE_TAIL,
    });
    turn1Stream.enqueueAll(turn1Chunks, { startAt: 10 });
    const turn1Close = 10 + turn1Chunks.length;

    // ---------------------------------------------------------------------
    // Turn 2 wire bytes: a final assistant text response. The matcher for
    // turn 2 captures the request body so the test can assert the tool
    // result propagated into the turn-2 `messages` array.
    // ---------------------------------------------------------------------
    const turn2Stream = harness.scenario.createStream();
    const turn2Chunks = wire.completeResponse("anthropic", {
      text: "It is 68F and foggy in SF.",
      headUsage: TURN2_USAGE_HEAD,
      tailUsage: TURN2_USAGE_TAIL,
    });
    turn2Stream.enqueueAll(turn2Chunks, { startAt: turn1Close + 50 });
    const turn2Close = turn1Close + 50 + turn2Chunks.length;

    // The predicates are sync; they cannot await the request body. Instead
    // each predicate clones the Request and stashes it, then the test reads
    // bodies asynchronously after the matcher has fired. The harness never
    // consumes the request body itself, so the clone is safe. Each matcher
    // fires at most once (the harness enforces single-use), so the stash
    // variables capture the request from the matcher's binding pass.
    let capturedTurn1Request: Request | null = null;
    let capturedTurn2Request: Request | null = null;
    harness.scenario.whenRequestMatches((req) => {
      capturedTurn1Request = req.clone();
      return true;
    }, turn1Stream);
    harness.scenario.whenRequestMatches((req) => {
      capturedTurn2Request = req.clone();
      return true;
    }, turn2Stream);

    // ---------------------------------------------------------------------
    // Drive turn 1 through `harness.runInference`. The auto-dispatch path
    // fires the registered "weather" handler when the iterator emits the
    // `inference.tool_call.end` event, with no explicit `invokeTool` call.
    // We start the collection task before advancing the clock so the fetch
    // parks in the harness's waiting set and the registered matcher routes
    // it.
    // ---------------------------------------------------------------------
    let seq = 0;
    const turn1Events = collectInferenceAuto(
      harness,
      [userTurn("What is the weather in SF?")],
      () => ++seq,
      ANTHROPIC_SOURCE,
    );
    await harness.advanceTo(turn1Close + 10);
    const events1 = await turn1Events;

    // The turn-1 iterator yielded the tool call the wire bytes encoded.
    const turn1End = events1.find((e) => e.type === "inference.tool_call.end");
    if (turn1End === undefined || turn1End.type !== "inference.tool_call.end") {
      throw new Error("expected an inference.tool_call.end event in turn 1");
    }
    expect(turn1End.data.name).toBe("weather");
    expect(turn1End.data.callId).toBe("call_weather_1");
    expect(turn1End.data.arguments).toEqual({ location: "SF" });

    const turn1Done = events1.find((e) => e.type === "inference.done");
    if (turn1Done === undefined || turn1Done.type !== "inference.done") {
      throw new Error("expected an inference.done event in turn 1");
    }
    const assistantTurn = turn1Done.data.turn;

    // Auto-dispatch fired the handler with the parsed arguments, and the
    // returned weather observation is now recorded on the harness. No
    // explicit `scenario.invokeTool` call is needed.
    expect(handlerCalls).toEqual([{ args: { location: "SF" } }]);
    expect(harness.scenario.lastToolDispatch("weather")).toEqual({
      temperatureF: 68,
      conditions: "fog",
    });

    // ---------------------------------------------------------------------
    // Build turn 2's conversation: original user turn + the assistant turn
    // emitted by turn 1 (which carries the tool_call block) + a new user
    // turn containing the tool_result that references `call_weather_1`.
    // ---------------------------------------------------------------------
    const turn2Conversation: ConversationTurn[] = [
      userTurn("What is the weather in SF?"),
      assistantTurn,
      {
        role: "user",
        content: [
          {
            type: "tool_result",
            callId: "call_weather_1",
            content: [{ type: "text", text: "68F, fog" }],
          },
        ],
        timestamp: 0,
      },
    ];

    // ---------------------------------------------------------------------
    // Drive turn 2. The turn-2 matcher routes the fetch to `turn2Stream`,
    // which carries the final assistant text.
    // ---------------------------------------------------------------------
    const turn2Events = collectInferenceAuto(
      harness,
      turn2Conversation,
      () => ++seq,
      ANTHROPIC_SOURCE,
    );
    await harness.advanceTo(turn2Close + 10);
    const events2 = await turn2Events;

    // The turn-2 iterator yielded the final assistant text the wire bytes
    // encoded.
    const turn2Done = events2.find((e) => e.type === "inference.done");
    if (turn2Done === undefined || turn2Done.type !== "inference.done") {
      throw new Error("expected an inference.done event in turn 2");
    }
    const turn2Content = turn2Done.data.turn.content;
    const textBlock = turn2Content.find((c) => c.type === "text");
    if (textBlock === undefined || textBlock.type !== "text") {
      throw new Error("expected a text block in turn 2 final turn");
    }
    expect(textBlock.text).toBe("It is 68F and foggy in SF.");

    const turn2Delta = events2.find((e) => e.type === "inference.text.delta");
    if (
      turn2Delta === undefined ||
      turn2Delta.type !== "inference.text.delta"
    ) {
      throw new Error("expected an inference.text.delta event in turn 2");
    }
    expect(turn2Delta.data.token).toBe("It is 68F and foggy in SF.");

    // ---------------------------------------------------------------------
    // The turn-2 request body actually contained the tool_result block in
    // its `messages` array, proving the round-trip propagated state through
    // the production adapter.
    // ---------------------------------------------------------------------
    if (capturedTurn1Request === null) {
      throw new Error("turn-1 matcher never fired");
    }
    if (capturedTurn2Request === null) {
      throw new Error("turn-2 matcher never fired");
    }
    const turn2Body = await parseRequestBody(capturedTurn2Request);
    const messages = turn2Body["messages"];
    if (!Array.isArray(messages)) {
      throw new Error("turn-2 body did not carry a messages array");
    }
    expect(messages.length).toBe(3);

    const toolResultMessage = messages[2];
    if (!isRecord(toolResultMessage)) {
      throw new Error("turn-2 final message was not an object");
    }
    const content = toolResultMessage["content"];
    if (!Array.isArray(content)) {
      throw new Error("turn-2 tool_result message content was not an array");
    }
    const firstBlock = content[0];
    if (!isRecord(firstBlock)) {
      throw new Error("turn-2 tool_result block was not an object");
    }
    expect(firstBlock["type"]).toBe("tool_result");
    expect(firstBlock["tool_use_id"]).toBe("call_weather_1");
    expect(firstBlock["content"]).toEqual([{ type: "text", text: "68F, fog" }]);

    // The turn-1 body, by contrast, carried only the original user turn.
    const turn1Body = await parseRequestBody(capturedTurn1Request);
    const turn1Messages = turn1Body["messages"];
    if (!Array.isArray(turn1Messages)) {
      throw new Error("turn-1 body did not carry a messages array");
    }
    expect(turn1Messages.length).toBe(1);
  });

  test("OpenAI: drives turn-1 tool call, dispatches tool, and propagates result into turn-2 inference", async () => {
    const harness = setupHarness();
    activeHarness = harness;

    // The OpenAI adapter emits index-based tool-call deltas; the harness's
    // production reactor resolves the placeholder callId at tool_call.end
    // time using the start frame's id. The test asserts on the final
    // resolved callId, not the index placeholder.
    const handlerCalls: { args: unknown }[] = [];
    harness.scenario.onTool("lookup", (args) => {
      handlerCalls.push({ args });
      return { city: "SF", population: 815000 };
    });

    // ---------------------------------------------------------------------
    // Turn 1 wire bytes: OpenAI tool-call sequence (start chunk with id +
    // name, then an argument-fragment chunk carrying the JSON args), plus
    // an OpenAI usage chunk and the [DONE] sentinel `parseSSE` consumes.
    // ---------------------------------------------------------------------
    const turn1Stream = harness.scenario.createStream();
    const turn1Chunks: Uint8Array[] = [
      ...wire.openai.toolCallSequence(0, "call_lookup_1", "lookup", [
        '{"city":"SF"}',
      ]),
      wire.openai.usageChunk({
        promptTokens: TURN1_USAGE_HEAD.input,
        completionTokens: TURN1_USAGE_TAIL.output,
      }),
      wire.openai.done(),
    ];
    turn1Stream.enqueueAll(turn1Chunks, { startAt: 10 });
    const turn1Close = 10 + turn1Chunks.length;

    // ---------------------------------------------------------------------
    // Turn 2 wire bytes: a final assistant text chunk + usage + done. The
    // OpenAI adapter emits one `inference.text.delta` per content chunk.
    // ---------------------------------------------------------------------
    const turn2Stream = harness.scenario.createStream();
    const turn2Chunks: Uint8Array[] = [
      wire.openai.chunk({ content: "SF has about 815k people." }),
      wire.openai.usageChunk({
        promptTokens: TURN2_USAGE_HEAD.input,
        completionTokens: TURN2_USAGE_TAIL.output,
      }),
      wire.openai.done(),
    ];
    turn2Stream.enqueueAll(turn2Chunks, { startAt: turn1Close + 50 });
    const turn2Close = turn1Close + 50 + turn2Chunks.length;

    let capturedTurn1Request: Request | null = null;
    let capturedTurn2Request: Request | null = null;
    harness.scenario.whenRequestMatches((req) => {
      capturedTurn1Request = req.clone();
      return true;
    }, turn1Stream);
    harness.scenario.whenRequestMatches((req) => {
      capturedTurn2Request = req.clone();
      return true;
    }, turn2Stream);

    let seq = 0;
    const turn1Events = collectInferenceAuto(
      harness,
      [userTurn("How big is SF?")],
      () => ++seq,
      OPENAI_SOURCE,
    );
    await harness.advanceTo(turn1Close + 10);
    const events1 = await turn1Events;

    const turn1End = events1.find((e) => e.type === "inference.tool_call.end");
    if (turn1End === undefined || turn1End.type !== "inference.tool_call.end") {
      throw new Error("expected an inference.tool_call.end event in turn 1");
    }
    expect(turn1End.data.name).toBe("lookup");
    expect(turn1End.data.callId).toBe("call_lookup_1");
    expect(turn1End.data.arguments).toEqual({ city: "SF" });

    const turn1Done = events1.find((e) => e.type === "inference.done");
    if (turn1Done === undefined || turn1Done.type !== "inference.done") {
      throw new Error("expected an inference.done event in turn 1");
    }
    const assistantTurn = turn1Done.data.turn;

    expect(handlerCalls).toEqual([{ args: { city: "SF" } }]);
    expect(harness.scenario.lastToolDispatch("lookup")).toEqual({
      city: "SF",
      population: 815000,
    });

    const turn2Conversation: ConversationTurn[] = [
      userTurn("How big is SF?"),
      assistantTurn,
      {
        role: "user",
        content: [
          {
            type: "tool_result",
            callId: "call_lookup_1",
            content: [{ type: "text", text: "815000" }],
          },
        ],
        timestamp: 0,
      },
    ];

    const turn2Events = collectInferenceAuto(
      harness,
      turn2Conversation,
      () => ++seq,
      OPENAI_SOURCE,
    );
    await harness.advanceTo(turn2Close + 10);
    const events2 = await turn2Events;

    const turn2Delta = events2.find((e) => e.type === "inference.text.delta");
    if (
      turn2Delta === undefined ||
      turn2Delta.type !== "inference.text.delta"
    ) {
      throw new Error("expected an inference.text.delta event in turn 2");
    }
    expect(turn2Delta.data.token).toBe("SF has about 815k people.");

    if (capturedTurn1Request === null) {
      throw new Error("turn-1 matcher never fired");
    }
    if (capturedTurn2Request === null) {
      throw new Error("turn-2 matcher never fired");
    }

    // The turn-2 request body carried a `tool` role message referencing
    // `call_lookup_1` — OpenAI's tool-result wire shape.
    const turn2Body = await parseRequestBody(capturedTurn2Request);
    const messages = turn2Body["messages"];
    if (!Array.isArray(messages)) {
      throw new Error("turn-2 body did not carry a messages array");
    }
    const toolMessage = messages.find(
      (m): m is Record<string, unknown> => isRecord(m) && m["role"] === "tool",
    );
    if (toolMessage === undefined) {
      throw new Error("turn-2 body did not carry a tool role message");
    }
    expect(toolMessage["tool_call_id"]).toBe("call_lookup_1");
    expect(toolMessage["content"]).toBe("815000");

    // The turn-1 body carried only the user turn.
    const turn1Body = await parseRequestBody(capturedTurn1Request);
    const turn1Messages = turn1Body["messages"];
    if (!Array.isArray(turn1Messages)) {
      throw new Error("turn-1 body did not carry a messages array");
    }
    expect(turn1Messages.length).toBe(1);
  });

  test("Anthropic: delayed-envelope tool handler defers dispatch until virtualDelayMs elapses", async () => {
    const harness = setupHarness();
    activeHarness = harness;

    // The handler returns a `{ result, virtualDelayMs }` envelope. The
    // harness must schedule the dispatch callback at clock.now() + delay;
    // the test observes that the callback has NOT fired before the clock
    // reaches that virtual deadline and HAS fired once it crosses it.
    const HANDLER_DELAY_MS = 200;
    harness.scenario.onTool("weather", () => ({
      result: { temperatureF: 72, conditions: "clear" },
      virtualDelayMs: HANDLER_DELAY_MS,
    }));

    const turn1Stream = harness.scenario.createStream();
    const turn1Chunks = wire.completeResponse("anthropic", {
      toolCalls: [
        {
          callId: "call_weather_delayed",
          name: "weather",
          argsJSON: '{"location":"LA"}',
        },
      ],
      headUsage: TURN1_USAGE_HEAD,
      tailUsage: TURN1_USAGE_TAIL,
    });
    turn1Stream.enqueueAll(turn1Chunks, { startAt: 10 });
    const turn1Close = 10 + turn1Chunks.length;

    harness.scenario.whenRequestMatches(() => true, turn1Stream);

    let seq = 0;
    const turn1Events = collectInferenceManual(
      harness,
      [userTurn("What is the weather in LA?")],
      () => ++seq,
      ANTHROPIC_SOURCE,
    );
    await harness.advanceTo(turn1Close + 10);
    const events1 = await turn1Events;

    const turn1End = events1.find((e) => e.type === "inference.tool_call.end");
    if (turn1End === undefined || turn1End.type !== "inference.tool_call.end") {
      throw new Error("expected an inference.tool_call.end event in turn 1");
    }

    // Capture the wall-clock virtual time at invocation; the dispatch
    // callback should run exactly HANDLER_DELAY_MS later.
    const invokedAt = harness.clock.now();
    const dispatched: { at: number; result: unknown }[] = [];
    harness.scenario.invokeTool(
      "weather",
      turn1End.data.arguments,
      (result) => {
        dispatched.push({ at: harness.clock.now(), result });
      },
    );

    // Immediately after `invokeTool` the dispatch must not have fired —
    // the envelope's virtualDelayMs > 0 forces a scheduled callback.
    expect(dispatched).toEqual([]);

    // Advance to one tick before the deadline: still no dispatch.
    await harness.advanceTo(invokedAt + HANDLER_DELAY_MS - 1);
    expect(dispatched).toEqual([]);

    // Advance exactly to the deadline: dispatch fires.
    await harness.advanceTo(invokedAt + HANDLER_DELAY_MS);
    expect(dispatched).toEqual([
      {
        at: invokedAt + HANDLER_DELAY_MS,
        result: { temperatureF: 72, conditions: "clear" },
      },
    ]);
  });

  test("Anthropic: promise-returning tool handler defers dispatch until the promise resolves", async () => {
    const harness = setupHarness();
    activeHarness = harness;

    // The handler returns a Promise the test controls via an explicit gate.
    // The harness must NOT call the dispatch callback until that promise
    // resolves; running `harness.run()` is what awaits the in-flight
    // promise and surfaces the dispatch.
    const { promise: handlerGate, resolve: releaseHandler } =
      Promise.withResolvers<unknown>();
    harness.scenario.onTool("weather", async () => {
      const value = await handlerGate;
      return value;
    });

    const turn1Stream = harness.scenario.createStream();
    const turn1Chunks = wire.completeResponse("anthropic", {
      toolCalls: [
        {
          callId: "call_weather_async",
          name: "weather",
          argsJSON: '{"location":"NYC"}',
        },
      ],
      headUsage: TURN1_USAGE_HEAD,
      tailUsage: TURN1_USAGE_TAIL,
    });
    turn1Stream.enqueueAll(turn1Chunks, { startAt: 10 });
    const turn1Close = 10 + turn1Chunks.length;
    harness.scenario.whenRequestMatches(() => true, turn1Stream);

    let seq = 0;
    const turn1Events = collectInferenceManual(
      harness,
      [userTurn("What is the weather in NYC?")],
      () => ++seq,
      ANTHROPIC_SOURCE,
    );
    await harness.advanceTo(turn1Close + 10);
    const events1 = await turn1Events;

    const turn1End = events1.find((e) => e.type === "inference.tool_call.end");
    if (turn1End === undefined || turn1End.type !== "inference.tool_call.end") {
      throw new Error("expected an inference.tool_call.end event in turn 1");
    }

    const dispatched: unknown[] = [];
    harness.scenario.invokeTool(
      "weather",
      turn1End.data.arguments,
      (result) => {
        dispatched.push(result);
      },
    );

    // The handler is parked on `handlerGate`; the harness has registered
    // the in-flight promise but has not yet resolved it. The dispatch
    // callback must not have fired.
    expect(dispatched).toEqual([]);

    // Release the gate. The microtask resolution propagates through the
    // harness's in-flight tracker; `harness.run()` is the entry point that
    // awaits any pending in-flight promises before declaring quiescence.
    releaseHandler({ temperatureF: 33, conditions: "snow" });

    await harness.run();
    expect(dispatched).toEqual([{ temperatureF: 33, conditions: "snow" }]);
  });

  test("Anthropic: parallel tool calls in a single turn round-trip both tool_results", async () => {
    const harness = setupHarness();
    activeHarness = harness;

    // Two handlers; each is invoked once. The turn-1 wire bytes serve TWO
    // tool_use content blocks in one Anthropic response.
    const weatherCalls: { args: unknown }[] = [];
    const timeCalls: { args: unknown }[] = [];
    harness.scenario.onTool("weather", (args) => {
      weatherCalls.push({ args });
      return { temperatureF: 60, conditions: "rain" };
    });
    harness.scenario.onTool("time", (args) => {
      timeCalls.push({ args });
      return { iso: "2026-05-16T12:00:00Z" };
    });

    const turn1Stream = harness.scenario.createStream();
    const turn1Chunks = wire.completeResponse("anthropic", {
      toolCalls: [
        {
          callId: "call_weather_par",
          name: "weather",
          argsJSON: '{"location":"PDX"}',
        },
        {
          callId: "call_time_par",
          name: "time",
          argsJSON: '{"zone":"UTC"}',
        },
      ],
      headUsage: TURN1_USAGE_HEAD,
      tailUsage: TURN1_USAGE_TAIL,
    });
    turn1Stream.enqueueAll(turn1Chunks, { startAt: 10 });
    const turn1Close = 10 + turn1Chunks.length;

    const turn2Stream = harness.scenario.createStream();
    const turn2Chunks = wire.completeResponse("anthropic", {
      text: "PDX: 60F rain at 12:00 UTC.",
      headUsage: TURN2_USAGE_HEAD,
      tailUsage: TURN2_USAGE_TAIL,
    });
    turn2Stream.enqueueAll(turn2Chunks, { startAt: turn1Close + 50 });
    const turn2Close = turn1Close + 50 + turn2Chunks.length;

    let capturedTurn2Request: Request | null = null;
    harness.scenario.whenRequestMatches(() => true, turn1Stream);
    harness.scenario.whenRequestMatches((req) => {
      capturedTurn2Request = req.clone();
      return true;
    }, turn2Stream);

    let seq = 0;
    const turn1Events = collectInferenceAuto(
      harness,
      [userTurn("PDX weather and time?")],
      () => ++seq,
      ANTHROPIC_SOURCE,
    );
    await harness.advanceTo(turn1Close + 10);
    const events1 = await turn1Events;

    // Both tool_call.end events surface from the turn-1 iterator with the
    // right name + callId + parsed arguments.
    const toolEnds = events1.filter(
      (e) => e.type === "inference.tool_call.end",
    );
    expect(toolEnds.length).toBe(2);
    const endsByName = new Map<string, (typeof toolEnds)[number]>();
    for (const ev of toolEnds) {
      if (ev.type !== "inference.tool_call.end") continue;
      endsByName.set(ev.data.name, ev);
    }
    const weatherEnd = endsByName.get("weather");
    const timeEnd = endsByName.get("time");
    if (
      weatherEnd === undefined ||
      weatherEnd.type !== "inference.tool_call.end" ||
      timeEnd === undefined ||
      timeEnd.type !== "inference.tool_call.end"
    ) {
      throw new Error("expected tool_call.end events for both tools");
    }
    expect(weatherEnd.data.callId).toBe("call_weather_par");
    expect(weatherEnd.data.arguments).toEqual({ location: "PDX" });
    expect(timeEnd.data.callId).toBe("call_time_par");
    expect(timeEnd.data.arguments).toEqual({ zone: "UTC" });

    const turn1Done = events1.find((e) => e.type === "inference.done");
    if (turn1Done === undefined || turn1Done.type !== "inference.done") {
      throw new Error("expected an inference.done event in turn 1");
    }
    const assistantTurn = turn1Done.data.turn;

    // Auto-dispatch fired both handlers once apiece via the harness's
    // observation of the two `inference.tool_call.end` events. The
    // captured arguments and the last-dispatch records prove each
    // handler ran with the right input and produced the right result.
    expect(weatherCalls).toEqual([{ args: { location: "PDX" } }]);
    expect(timeCalls).toEqual([{ args: { zone: "UTC" } }]);
    expect(harness.scenario.lastToolDispatch("weather")).toEqual({
      temperatureF: 60,
      conditions: "rain",
    });
    expect(harness.scenario.lastToolDispatch("time")).toEqual({
      iso: "2026-05-16T12:00:00Z",
    });

    // Construct turn 2's conversation explicitly: original user turn +
    // assistant turn (carries both tool_call blocks) + a single user turn
    // containing BOTH tool_result blocks. Anthropic permits multiple
    // tool_result blocks in one user message.
    const turn2Conversation: ConversationTurn[] = [
      userTurn("PDX weather and time?"),
      assistantTurn,
      {
        role: "user",
        content: [
          {
            type: "tool_result",
            callId: "call_weather_par",
            content: [{ type: "text", text: "60F, rain" }],
          },
          {
            type: "tool_result",
            callId: "call_time_par",
            content: [{ type: "text", text: "12:00 UTC" }],
          },
        ],
        timestamp: 0,
      },
    ];

    const turn2Events = collectInferenceAuto(
      harness,
      turn2Conversation,
      () => ++seq,
      ANTHROPIC_SOURCE,
    );
    await harness.advanceTo(turn2Close + 10);
    const events2 = await turn2Events;

    const turn2Delta = events2.find((e) => e.type === "inference.text.delta");
    if (
      turn2Delta === undefined ||
      turn2Delta.type !== "inference.text.delta"
    ) {
      throw new Error("expected an inference.text.delta event in turn 2");
    }
    expect(turn2Delta.data.token).toBe("PDX: 60F rain at 12:00 UTC.");

    if (capturedTurn2Request === null) {
      throw new Error("turn-2 matcher never fired");
    }
    const turn2Body = await parseRequestBody(capturedTurn2Request);
    const messages = turn2Body["messages"];
    if (!Array.isArray(messages)) {
      throw new Error("turn-2 body did not carry a messages array");
    }
    // The turn-2 messages array carries both tool_result blocks, each
    // referencing the correct callId.
    const finalMessage = messages[messages.length - 1];
    if (!isRecord(finalMessage)) {
      throw new Error("turn-2 final message was not an object");
    }
    const finalContent = finalMessage["content"];
    if (!Array.isArray(finalContent)) {
      throw new Error("turn-2 final message content was not an array");
    }
    const toolResults = finalContent.filter(
      (b): b is Record<string, unknown> =>
        isRecord(b) && b["type"] === "tool_result",
    );
    expect(toolResults.length).toBe(2);
    const idToContent = new Map<unknown, unknown>();
    for (const tr of toolResults) {
      idToContent.set(tr["tool_use_id"], tr["content"]);
    }
    expect(idToContent.get("call_weather_par")).toEqual([
      { type: "text", text: "60F, rain" },
    ]);
    expect(idToContent.get("call_time_par")).toEqual([
      { type: "text", text: "12:00 UTC" },
    ]);
  });
});
