import { describe, expect, it } from "vitest";
import { createSafeTextStreamGate } from "./safe-text-stream-gate.js";

describe("safe text stream gate", () => {
  it("holds short ordinary prose when structured decision previews are deciding", () => {
    const gate = createSafeTextStreamGate({
      mode: "structured-decision-preview",
      toolNames: ["shell"],
    });

    expect(gate.push("I will inspect README first.")).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({
      deltaLength: expect.any(Number),
      kind: "suppress",
      reasonCode: "structured-preview-disabled",
    });
  });

  it("emits long ordinary prose during structured decision previews", () => {
    const gate = createSafeTextStreamGate({
      mode: "structured-decision-preview",
      structuredPreviewMinEmitChars: 32,
      toolNames: ["shell"],
    });

    expect(gate.push("This is a direct response with enough text")).toEqual({
      kind: "emit",
      text: "This is a direct response with enough text",
    });
    expect(gate.push(" zzz")).toEqual({
      kind: "emit",
      text: " zzz",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks runtime decision JSON when structured decision previews disable text", () => {
    const gate = createSafeTextStreamGate({
      mode: "structured-decision-preview",
      toolNames: ["shell"],
    });

    expect(gate.push('{"kind":"tool","toolName":"shell","input":{}}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks split runtime decision JSON before emitting text", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push('{"kind":"tool","toolName":"shell","input":')).toEqual({ kind: "hold" });
    expect(gate.push('{"command":"echo hidden"}}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks malformed runtime decision JSON on flush", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push('{"kind":"tool","toolName":"shell","input":')).toEqual({ kind: "hold" });
    expect(gate.push("}")).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks malformed provider-native JSON on flush", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push('{"tool_calls":[{"name":"shell","arguments":')).toEqual({ kind: "hold" });
    expect(gate.push("}]}")).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("does not block malformed business JSON without runtime-control evidence", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });
    const text = '{"kind":"tool","label":}';

    expect(gate.push(text)).toEqual({ kind: "emit", text });
  });

  it("blocks split runtime decision JSON arrays before emitting text", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push('[{"kind":"tool"')).toEqual({ kind: "hold" });
    expect(gate.push(',"toolName":"shell","input":{}}]')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks runtime decision JSON appended after emitted safe text", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("Plain preview.\n")).toEqual({ kind: "emit", text: "Plain preview.\n" });
    expect(gate.push('{"kind":"tool","toolName":"shell","input":{}}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("holds prose followed by a multi-line runtime JSON prefix before emitting text", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });
    const first = 'I will call:\n{\n  "kind":"tool"';

    expect(gate.push(first)).toEqual({ kind: "hold" });
    expect(gate.push(',"toolName":"shell","input":{"command":"echo hidden"}\n}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("holds prose followed by a multi-line provider-native JSON prefix", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push('I will call:\n{\n  "tool_calls": [')).toEqual({ kind: "hold" });
    expect(gate.push('{"name":"shell","arguments":{"command":"echo hidden"}}]\n}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("emits prose followed by closed multi-line business JSON after it closes", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });
    const first = 'Business object:\n{\n  "label": "';
    const second = `${"Hammer ".repeat(20)}",\n  "price": 10\n}`;

    expect(gate.push(first)).toEqual({ kind: "hold" });
    expect(gate.push(second)).toEqual({ kind: "emit", text: `${first}${second}` });
  });

  it("holds prose followed by a multi-line runtime JSON array prefix", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push('I will call:\n[\n  {"kind":"tool"')).toEqual({ kind: "hold" });
    expect(gate.push(',"toolName":"shell","input":{"command":"echo hidden"}}\n]')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("holds split runtime decision JSON after a long emitted safe prefix", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const safePrefix = "Safe preview. ".repeat(4);

    expect(gate.push(safePrefix)).toEqual({ kind: "emit", text: safePrefix });
    expect(gate.push('{"kind":"tool"')).toEqual({ kind: "hold" });
    expect(gate.push(',"toolName":"shell","input":{}}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks split XML tool-call markup before emitting text", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("<tool")).toEqual({ kind: "hold" });
    expect(gate.push('_call>shell {"command":"echo hidden"}</tool_call>')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("blocks XML tool-call markup appended after emitted safe text", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("Plain preview.\n")).toEqual({ kind: "emit", text: "Plain preview.\n" });
    expect(gate.push('<tool_call>shell {"command":"echo hidden"}</tool_call>')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("holds split textual tool invocation prefixes until they can be blocked", () => {
    const namespacedGate = createSafeTextStreamGate({ toolNames: ["shell"] });
    expect(namespacedGate.push("files.")).toEqual({ kind: "hold" });
    expect(namespacedGate.push('shell{"argv":["ls"]}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });

    const directGate = createSafeTextStreamGate({ toolNames: ["shell"] });
    expect(directGate.push("shell")).toEqual({ kind: "hold" });
    expect(directGate.push('{"command":"npm test"}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("does not keep ordinary prose with tool-name prefixes held indefinitely", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("shell")).toEqual({ kind: "hold" });
    expect(gate.push("s are useful")).toEqual({ kind: "emit", text: "shells are useful" });
  });

  it("emits ordinary text and fenced control examples", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });
    const fenced = '```json\n{"kind":"tool","toolName":"shell","input":{}}\n```';

    expect(gate.push("Plain text.")).toEqual({ kind: "emit", text: "Plain text." });
    expect(gate.push(fenced)).toEqual({ kind: "emit", text: fenced });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("holds split fenced control examples until the fence closes", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("```json\n")).toEqual({ kind: "hold" });
    expect(gate.push('{"kind":"tool","toolName":"shell","input":{}}')).toEqual({
      kind: "hold",
    });
    expect(gate.push("\n```")).toEqual({
      kind: "emit",
      text: '```json\n{"kind":"tool","toolName":"shell","input":{}}\n```',
    });
    expect(gate.flush()).toEqual({ kind: "hold" });
  });

  it("suppresses split fenced examples when structured decision previews disable text", () => {
    const gate = createSafeTextStreamGate({
      mode: "structured-decision-preview",
      toolNames: ["shell"],
    });

    expect(gate.push("```json\n")).toEqual({ kind: "hold" });
    expect(gate.push('{"kind":"tool","toolName":"shell","input":{}}')).toEqual({
      kind: "hold",
    });
    expect(gate.push("\n```")).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({
      deltaLength: expect.any(Number),
      kind: "suppress",
      reasonCode: "structured-preview-disabled",
    });
  });

  it("blocks unclosed fenced content after the open fence buffer limit", () => {
    const gate = createSafeTextStreamGate({
      maxOpenFenceBufferedChars: 16,
      toolNames: ["shell"],
    });

    expect(gate.push("```json\n")).toEqual({ kind: "hold" });
    expect(gate.push('{"kind":"tool"')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("emits inline business JSON", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });
    const text = 'An example object is {"kind":"tool","label":"Hammer"}.';

    expect(gate.push(text)).toEqual({ kind: "emit", text });
  });

  it("emits large standalone business JSON instead of blocking a weak JSON prefix", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const text = `{"kind":"tool","label":"${"Hammer ".repeat(20)}"}`;

    expect(gate.push(text)).toEqual({ kind: "emit", text });
  });

  it("blocks split large business-looking JSON that later becomes runtime-control JSON", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push(`{"kind":"tool","label":"${"Hammer ".repeat(20)}`)).toEqual({
      kind: "hold",
    });
    expect(gate.push('","toolName":"shell","input":{"command":"echo hidden"}}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("emits split large business JSON with runtime-like kind only after it closes safely", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const first = `{"kind":"tool","label":"${"Hammer ".repeat(20)}`;
    const second = '","price":10}';

    expect(gate.push(first)).toEqual({ kind: "hold" });
    expect(gate.push(second)).toEqual({ kind: "emit", text: `${first}${second}` });
  });

  it("emits large standalone business JSON arrays instead of blocking a weak JSON prefix", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const text = `[{"kind":"tool","label":"${"Hammer ".repeat(20)}"}]`;

    expect(gate.push(text)).toEqual({ kind: "emit", text });
  });

  it("holds large standalone business JSON until it closes under the hard safety limit", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });
    const first = `{"label":"${"Hammer ".repeat(1_500)}`;
    const second = '","description":"business JSON"}';

    expect(gate.push(first)).toEqual({ kind: "hold" });
    expect(gate.push(second)).toEqual({ kind: "emit", text: `${first}${second}` });
  });

  it("blocks split large runtime-control JSON arrays after an ambiguous business-looking prefix", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push(`[{"kind":"tool","label":"${"Hammer ".repeat(20)}`)).toEqual({
      kind: "hold",
    });
    expect(gate.push('","toolName":"shell","input":{"command":"echo hidden"}}]')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("holds generic line-start JSON prefixes until the JSON closes", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const first = `{"label":"${"Hammer ".repeat(20)}`;
    const second = '","description":"business JSON"}';

    expect(gate.push(first)).toEqual({ kind: "hold" });
    expect(gate.push(second)).toEqual({ kind: "emit", text: `${first}${second}` });
  });

  it("blocks generic business-looking JSON that later becomes runtime-control JSON", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push(`{"label":"${"Hammer ".repeat(20)}`)).toEqual({ kind: "hold" });
    expect(
      gate.push('","kind":"tool","toolName":"shell","input":{"command":"echo hidden"}}'),
    ).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("blocks generic business-looking JSON that later becomes provider-native JSON", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push(`{"metadata":"${"Hammer ".repeat(20)}`)).toEqual({ kind: "hold" });
    expect(
      gate.push('","tool_calls":[{"name":"shell","arguments":{"command":"echo hidden"}}]}'),
    ).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("holds standalone JSON arrays until they close before classifying them", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push(`[{"label":"${"Hammer ".repeat(20)}`)).toEqual({ kind: "hold" });
    expect(
      gate.push('","kind":"tool","toolName":"shell","input":{"command":"echo hidden"}}]'),
    ).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("blocks long runtime-control JSON prefixes", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push('{"kind":"tool","toolName":"shell","input":')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("blocks long provider-native tool call JSON prefixes", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });

    expect(gate.push('{"tool_calls":[{"name":"shell","arguments":')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("holds ambiguous provider-native JSON prefixes until they close", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const first = `{"tool_calls":[{"name":"shell","description":"${"Hammer ".repeat(20)}`;

    expect(gate.push(first)).toEqual({ kind: "hold" });
    expect(gate.push('","arguments":{"command":"echo hidden"}}]}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("emits closed loose provider-native-shaped business JSON", () => {
    const gate = createSafeTextStreamGate({ maxBufferedChars: 32, toolNames: ["shell"] });
    const text = '{"tool_calls":[]}';

    expect(gate.push(text)).toEqual({ kind: "emit", text });
  });

  it("blocks standalone open JSON prefixes that exceed the standalone safety limit", () => {
    const gate = createSafeTextStreamGate({
      maxStandaloneJsonChars: 32,
      toolNames: ["shell"],
    });

    expect(gate.push(`{"label":"${"Hammer ".repeat(20)}`)).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("holds short split control markup prefixes until they can be blocked or released", () => {
    const toolGate = createSafeTextStreamGate({ toolNames: ["shell"] });
    expect(toolGate.push("<to")).toEqual({ kind: "hold" });
    expect(toolGate.push('ol_call>shell {"command":"echo hidden"}</tool_call>')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });

    const thinkGate = createSafeTextStreamGate({ toolNames: ["shell"] });
    expect(thinkGate.push("<thi")).toEqual({ kind: "hold" });
    expect(thinkGate.push("nk>hidden</think>")).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });

    const pipeGate = createSafeTextStreamGate({ toolNames: ["shell"] });
    expect(pipeGate.push("<|to")).toEqual({ kind: "hold" });
    expect(pipeGate.push("ol_call")).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("releases ordinary HTML-like prefixes once they stop matching control markup", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("<to")).toEqual({ kind: "hold" });
    expect(gate.push("morrow")).toEqual({ kind: "emit", text: "<tomorrow" });
  });

  it("holds one-character textual tool prefixes in structured decision previews", () => {
    const gate = createSafeTextStreamGate({
      mode: "structured-decision-preview",
      toolNames: ["shell"],
    });

    expect(gate.push("s")).toEqual({ kind: "hold" });
    expect(gate.push('hell{"command":"npm test"}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("does not hold ordinary one-character prose in respond previews", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["shell"] });

    expect(gate.push("s")).toEqual({ kind: "emit", text: "s" });
  });

  it("holds one-character visible tool names until invocations can be blocked", () => {
    const gate = createSafeTextStreamGate({ toolNames: ["x"] });

    expect(gate.push("x")).toEqual({ kind: "hold" });
    expect(gate.push('{"arg":1}')).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("emits unclosed fenced content on respond-preview flush", () => {
    const gate = createSafeTextStreamGate({ mode: "respond-preview", toolNames: ["shell"] });
    const text = '```json\n{"kind":"tool","label":"Hammer"}';

    expect(gate.push(text)).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({ kind: "emit", text });
  });

  it("blocks unclosed fenced runtime-control content on respond-preview flush", () => {
    const gate = createSafeTextStreamGate({ mode: "respond-preview", toolNames: ["shell"] });
    const text = '```json\n{"kind":"tool","toolName":"shell","input":{"command":"echo hidden"}}';

    expect(gate.push(text)).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("blocks unclosed fenced malformed runtime-control JSON on respond-preview flush", () => {
    const gate = createSafeTextStreamGate({ mode: "respond-preview", toolNames: ["shell"] });
    const text = '```json\n{"kind":"tool","toolName":"shell","input":}';

    expect(gate.push(text)).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("blocks unclosed fenced malformed provider-native JSON on respond-preview flush", () => {
    const gate = createSafeTextStreamGate({ mode: "respond-preview", toolNames: ["shell"] });
    const text = '```json\n{"tool_calls":[{"name":"shell","arguments":}]}';

    expect(gate.push(text)).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({
      kind: "block",
      reasonCode: "terminal-control-payload",
    });
  });

  it("emits unclosed fenced content on generate-preview flush", () => {
    const gate = createSafeTextStreamGate({ mode: "generate-preview", toolNames: ["shell"] });
    const text = '```json\n{"kind":"tool","label":"Hammer"}';

    expect(gate.push(text)).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({ kind: "emit", text });
  });

  it("suppresses unclosed fenced content on structured-decision-preview flush", () => {
    const gate = createSafeTextStreamGate({
      mode: "structured-decision-preview",
      toolNames: ["shell"],
    });

    expect(gate.push("```json\n")).toEqual({ kind: "hold" });
    expect(gate.flush()).toEqual({
      deltaLength: expect.any(Number),
      kind: "suppress",
      reasonCode: "structured-preview-disabled",
    });
  });
});
