import { describe, expect, it } from "vitest";
import {
  isContentBearingToolInputForCheckpoint,
  renderToolInputForProviderHistory,
  renderToolInputForProviderHistoryFromDefinition,
  toHistorySafeDisplayInput,
  toHistorySafeToolInput,
  toHistorySafeToolInputFromDefinition,
} from "./tool-history-input.js";
import type { RuntimeToolDefinition } from "./contracts.js";

describe("tool history input", () => {
  it("adds local refs to omitted content markers", () => {
    const rendered = renderToolInputForProviderHistory("edit", {
      file_path: "src/app.ts",
      new_string: "bb",
      old_string: "aa",
      replace_all: false,
    });

    expect(rendered).toEqual({
      file_path: "src/app.ts",
      new_string: "[content omitted from provider follow-up: 2 chars, ref:content-1]",
      old_string: "[content omitted from provider follow-up: 2 chars, ref:content-2]",
      replace_all: false,
    });
  });

  it("keeps marker refs local to one rendered input", () => {
    expect(
      renderToolInputForProviderHistory("write-workspace", {
        content: "one",
        file_path: "a.txt",
      }),
    ).toMatchObject({
      content: "[content omitted from provider follow-up: 3 chars, ref:content-1]",
    });
    expect(
      renderToolInputForProviderHistory("write-workspace", {
        content: "two",
        file_path: "b.txt",
      }),
    ).toMatchObject({
      content: "[content omitted from provider follow-up: 3 chars, ref:content-1]",
    });
  });

  it("omits process-session-write stdin while preserving session id", () => {
    const input = "secret stdin\n".repeat(10);

    expect(
      renderToolInputForProviderHistory("process-session-write", {
        input,
        session_id: "proc_123",
      }),
    ).toEqual({
      input: `[content omitted from provider follow-up: ${input.length} chars, ref:content-1]`,
      session_id: "proc_123",
    });
    expect(
      toHistorySafeToolInput("process-session-write", {
        input,
        session_id: "proc_123",
      }),
    ).toEqual({
      input_bytes: Buffer.byteLength(input, "utf8"),
      session_id: "proc_123",
    });
  });

  it("omits process-session-write stdin-shaped aliases while preserving session id", () => {
    const stdin = "secret alias stdin\n".repeat(1_000);
    const rendered = renderToolInputForProviderHistory("process-session-write", {
      session_id: "sess_1",
      stdin,
    });

    expect(JSON.stringify(rendered)).not.toContain("secret alias stdin");
    expect(rendered).toMatchObject({
      session_id: "sess_1",
      stdin: expect.stringContaining("content omitted from provider follow-up"),
    });
  });

  it("omits non-record process-session-write input", () => {
    const rendered = renderToolInputForProviderHistory("process-session-write", "secret raw stdin");

    expect(rendered).toBe("[content omitted from provider follow-up: 16 chars, ref:content-1]");
  });

  it("does not preserve arbitrary name fields as path-like content", () => {
    const secretName = "secret\n".repeat(1_000);
    const rendered = renderToolInputForProviderHistory("write-workspace", {
      name: secretName,
      text: "payload\n".repeat(1_000),
    });

    expect(JSON.stringify(rendered)).not.toContain("secret");
    expect(rendered).toMatchObject({
      name: expect.stringContaining("content omitted from provider follow-up"),
      text: expect.stringContaining("content omitted from provider follow-up"),
    });
  });

  it("preserves path-like name fields", () => {
    expect(
      renderToolInputForProviderHistory("write-workspace", {
        content: "payload",
        name: "notes.txt",
      }),
    ).toMatchObject({
      content: expect.stringContaining("content omitted from provider follow-up"),
      name: "notes.txt",
    });
  });

  it("does not preserve non-path file-like aliases as raw content", () => {
    const secret = "secret\n".repeat(1_000);
    const rendered = renderToolInputForProviderHistory("write-workspace", {
      file: secret,
      target: secret,
      text: secret,
    });

    expect(JSON.stringify(rendered)).not.toContain("secret");
    expect(rendered).toMatchObject({
      file: expect.stringContaining("content omitted from provider follow-up"),
      target: expect.stringContaining("content omitted from provider follow-up"),
      text: expect.stringContaining("content omitted from provider follow-up"),
    });
  });

  it("keeps short shell commands in provider history", () => {
    expect(
      renderToolInputForProviderHistory("shell", {
        command: "pnpm test",
        timeout_ms: 10_000,
      }),
    ).toEqual({
      command: "pnpm test",
      timeout_ms: 10_000,
    });
  });

  it("redacts short shell secrets in provider and tool-call history", () => {
    const command = "curl -H 'Authorization: Bearer sk-1234567890abcdef' https://api.example.com";

    const providerInput = renderToolInputForProviderHistory("shell", {
      command,
      timeout_ms: 10_000,
    });
    const historyInput = toHistorySafeToolInput("shell", {
      command,
      timeout_ms: 10_000,
    });

    expect(JSON.stringify(providerInput)).not.toContain("sk-1234567890abcdef");
    expect(JSON.stringify(historyInput)).not.toContain("sk-1234567890abcdef");
    expect(providerInput).toMatchObject({
      command: expect.stringContaining("Bearer [secret-redacted]"),
      timeout_ms: 10_000,
    });
    expect(historyInput).toMatchObject({
      command: expect.stringContaining("Bearer [secret-redacted]"),
      timeout_ms: 10_000,
    });
  });

  it("omits multiline shell command bodies in provider history", () => {
    const command = "python3 - <<'PY'\nprint('secret shell payload')\nPY";
    const rendered = renderToolInputForProviderHistory("shell", {
      command,
      timeout_ms: 10_000,
    });

    expect(JSON.stringify(rendered)).not.toContain("secret shell payload");
    expect(rendered).toEqual({
      command: `[content omitted from provider follow-up: ${command.length} chars, ref:content-1]`,
      command_bytes: Buffer.byteLength(command, "utf8"),
      timeout_ms: 10_000,
    });
  });

  it("omits shell alias-shaped invalid script content from provider history", () => {
    const secret = "secret shell payload\n".repeat(1_000);
    const rendered = renderToolInputForProviderHistory("shell", {
      script: secret,
      timeout_ms: 10_000,
    });

    expect(JSON.stringify(rendered)).not.toContain("secret shell payload");
    expect(rendered).toEqual({
      script: `[content omitted from provider follow-up: ${secret.length} chars, ref:content-1]`,
      timeout_ms: 10_000,
    });
  });

  it("omits extra shell content fields even when command is short", () => {
    const secret = "secret shell payload\n".repeat(1_000);
    const rendered = renderToolInputForProviderHistory("shell", {
      command: "echo ok",
      script: secret,
    });

    expect(JSON.stringify(rendered)).toContain("echo ok");
    expect(JSON.stringify(rendered)).not.toContain("secret shell payload");
    expect(rendered).toEqual({
      command: "echo ok",
      script: `[content omitted from provider follow-up: ${secret.length} chars, ref:content-1]`,
    });
  });

  it("omits multiline shell command bodies in tool-call history", () => {
    const command = "python3 - <<'PY'\n" + "secret shell payload\n".repeat(1_000) + "PY";

    const historyInput = toHistorySafeToolInput("shell", {
      command,
      cwd: "/workspace/project",
      timeout_ms: 10_000,
    });
    const displayInput = toHistorySafeDisplayInput("shell", command, {
      command,
      cwd: "/workspace/project",
      timeout_ms: 10_000,
    });

    expect(JSON.stringify(historyInput)).not.toContain("secret shell payload");
    expect(historyInput).toEqual({
      command: `[content omitted from tool history: ${command.length} chars]`,
      command_bytes: Buffer.byteLength(command, "utf8"),
      cwd: "/workspace/project",
      timeout_ms: 10_000,
    });
    expect(displayInput).not.toContain("secret shell payload");
    expect(displayInput).toBe(
      `shell command: [content omitted from tool history: ${command.length} chars] cwd=/workspace/project`,
    );
    expect(displayInput).not.toContain("shell|");
  });

  it("omits shell alias-shaped invalid content from tool-call history", () => {
    const secret = "secret shell payload\n".repeat(1_000);
    const historyInput = toHistorySafeToolInput("shell", {
      command: "echo ok",
      script: secret,
      timeout_ms: 10_000,
    });

    expect(JSON.stringify(historyInput)).toContain("echo ok");
    expect(JSON.stringify(historyInput)).not.toContain("secret shell payload");
    expect(historyInput).toEqual({
      command: "echo ok",
      script: `[content omitted from tool history: ${secret.length} chars]`,
      timeout_ms: 10_000,
    });
  });

  it("omits raw multiline shell string input", () => {
    const secret = "secret shell payload\n".repeat(1_000);
    const providerInput = renderToolInputForProviderHistory("shell", secret);
    const historyInput = toHistorySafeToolInput("shell", secret);

    expect(providerInput).toBe(
      `[content omitted from provider follow-up: ${secret.length} chars, ref:content-1]`,
    );
    expect(historyInput).toBe(`[content omitted from tool history: ${secret.length} chars]`);
  });

  it("omits long content-like external tool input by tool definition source", () => {
    const externalTool = {
      name: "mcp:docs:write",
      source: "mcp",
    } as RuntimeToolDefinition;
    const content = "external secret payload\n".repeat(300);
    const input = {
      input: content,
      path: "notes.txt",
      query: "short query",
    };

    const providerInput = renderToolInputForProviderHistoryFromDefinition(externalTool, input);
    const historyInput = toHistorySafeToolInputFromDefinition(externalTool, input);

    expect(isContentBearingToolInputForCheckpoint(externalTool, input)).toBe(true);
    expect(JSON.stringify(providerInput)).not.toContain("external secret payload");
    expect(providerInput).toEqual({
      input: `[content omitted from provider follow-up: ${content.length} chars, ref:content-1]`,
      path: "notes.txt",
      query: "short query",
    });
    expect(historyInput).toEqual({
      input: `[content omitted from tool history: ${content.length} chars]`,
      path: "notes.txt",
      query: "short query",
    });
  });

  it("sanitizes extension tool input as external input", () => {
    const extensionTool = {
      name: "ext:helper:run",
      source: "extension",
    } as RuntimeToolDefinition;
    const secret = "short-secret";
    const longInput = "secret input\n".repeat(1_000);

    const providerInput = renderToolInputForProviderHistoryFromDefinition(extensionTool, {
      content: secret,
      input: "search query",
      nested: { payload: secret },
      path: "notes.txt",
    });
    const historyInput = toHistorySafeToolInputFromDefinition(extensionTool, {
      content: secret,
      input: longInput,
      path: "notes.txt",
    });

    expect(isContentBearingToolInputForCheckpoint(extensionTool, { content: secret })).toBe(true);
    expect(JSON.stringify(providerInput)).not.toContain(secret);
    expect(providerInput).toEqual({
      content: `[content omitted from provider follow-up: ${secret.length} chars, ref:content-1]`,
      input: "search query",
      nested: {
        payload: `[content omitted from provider follow-up: ${secret.length} chars, ref:content-2]`,
      },
      path: "notes.txt",
    });
    expect(JSON.stringify(historyInput)).not.toContain("secret input");
    expect(historyInput).toEqual({
      content: `[content omitted from tool history: ${secret.length} chars]`,
      input: `[content omitted from tool history: ${longInput.length} chars]`,
      path: "notes.txt",
    });
  });

  it("omits non-path values for external path-like keys", () => {
    const externalTool = {
      name: "mcp:docs:search",
      source: "mcp",
    } as RuntimeToolDefinition;
    const secret = "secret\n".repeat(100);

    const providerInput = renderToolInputForProviderHistoryFromDefinition(externalTool, {
      file_path: secret,
      input: "normal query",
      target: secret,
    });
    const historyInput = toHistorySafeToolInputFromDefinition(externalTool, {
      file_path: secret,
      input: "normal query",
      target: secret,
    });

    expect(isContentBearingToolInputForCheckpoint(externalTool, { file_path: secret })).toBe(true);
    expect(JSON.stringify(providerInput)).not.toContain("secret");
    expect(JSON.stringify(providerInput)).toContain("normal query");
    expect(providerInput).toEqual({
      file_path: `[content omitted from provider follow-up: ${secret.length} chars, ref:content-1]`,
      input: "normal query",
      target: `[content omitted from provider follow-up: ${secret.length} chars, ref:content-2]`,
    });
    expect(JSON.stringify(historyInput)).not.toContain("secret");
    expect(historyInput).toEqual({
      file_path: `[content omitted from tool history: ${secret.length} chars]`,
      input: "normal query",
      target: `[content omitted from tool history: ${secret.length} chars]`,
    });
  });

  it("omits credential-like external fields even when short", () => {
    const externalTool = {
      name: "mcp:docs:search",
      source: "mcp",
    } as RuntimeToolDefinition;
    const apiKey = "sk-1234567890abcdef";
    const token = "abc123";
    const password = "hunter2";

    const providerInput = renderToolInputForProviderHistoryFromDefinition(externalTool, {
      apiKey,
      input: "normal search query",
      password,
      token,
    });
    const historyInput = toHistorySafeToolInputFromDefinition(externalTool, {
      apiKey,
      input: "normal search query",
      password,
      token,
    });

    expect(isContentBearingToolInputForCheckpoint(externalTool, { token })).toBe(true);
    expect(JSON.stringify(providerInput)).not.toContain(apiKey);
    expect(JSON.stringify(providerInput)).not.toContain(token);
    expect(JSON.stringify(providerInput)).not.toContain(password);
    expect(JSON.stringify(providerInput)).toContain("normal search query");
    expect(providerInput).toEqual({
      apiKey: `[content omitted from provider follow-up: ${apiKey.length} chars, ref:content-1]`,
      input: "normal search query",
      password: `[content omitted from provider follow-up: ${password.length} chars, ref:content-2]`,
      token: `[content omitted from provider follow-up: ${token.length} chars, ref:content-3]`,
    });
    expect(JSON.stringify(historyInput)).not.toContain(apiKey);
    expect(JSON.stringify(historyInput)).not.toContain(token);
    expect(JSON.stringify(historyInput)).not.toContain(password);
    expect(historyInput).toEqual({
      apiKey: `[content omitted from tool history: ${apiKey.length} chars]`,
      input: "normal search query",
      password: `[content omitted from tool history: ${password.length} chars]`,
      token: `[content omitted from tool history: ${token.length} chars]`,
    });
  });

  it("omits credential-like external fields regardless of key casing or separators", () => {
    const externalTool = {
      name: "mcp:docs:search",
      source: "mcp",
    } as RuntimeToolDefinition;

    const providerInput = renderToolInputForProviderHistoryFromDefinition(externalTool, {
      "api-key": "abc123",
      Authorization: "Bearer shorttoken123456",
      CLIENT_SECRET: "hunter2",
      FilePath: "secret\n".repeat(10),
      input: "normal query",
      "private-key": "-----BEGIN PRIVATE KEY-----",
    });

    expect(JSON.stringify(providerInput)).not.toContain("shorttoken123456");
    expect(JSON.stringify(providerInput)).not.toContain("abc123");
    expect(JSON.stringify(providerInput)).not.toContain("hunter2");
    expect(JSON.stringify(providerInput)).not.toContain("secret");
    expect(JSON.stringify(providerInput)).toContain("normal query");
  });

  it("omits short header-style secrets in external text-like fields", () => {
    const externalTool = {
      name: "mcp:test:lookup",
      source: "mcp",
    } as RuntimeToolDefinition;

    const providerInput = renderToolInputForProviderHistoryFromDefinition(externalTool, {
      input: "API_KEY=abc123",
      query: "normal search query",
    });
    const historyInput = toHistorySafeToolInputFromDefinition(externalTool, {
      input: "X-API-Key: abc123",
      query: "normal search query",
    });

    expect(JSON.stringify(providerInput)).not.toContain("abc123");
    expect(JSON.stringify(providerInput)).toContain("normal search query");
    expect(JSON.stringify(historyInput)).not.toContain("abc123");
    expect(JSON.stringify(historyInput)).toContain("normal search query");
  });

  it("omits root string external tool input", () => {
    const externalTool = {
      name: "mcp:legacy:lookup",
      source: "mcp",
    } as RuntimeToolDefinition;
    const secret = "short-secret";

    expect(isContentBearingToolInputForCheckpoint(externalTool, secret)).toBe(true);
    expect(renderToolInputForProviderHistoryFromDefinition(externalTool, secret)).toBe(
      `[content omitted from provider follow-up: ${secret.length} chars, ref:content-1]`,
    );
    expect(toHistorySafeToolInputFromDefinition(externalTool, secret)).toBe(
      `[content omitted from tool history: ${secret.length} chars]`,
    );
  });

  it("allows tool definition hooks to override external history policy", () => {
    const externalTool = {
      historyInput: () => ({ custom: "history" }),
      isContentBearingInput: () => true,
      name: "plugin:custom",
      providerHistoryInput: () => ({ custom: "provider" }),
      source: "plugin",
    } as RuntimeToolDefinition;

    expect(renderToolInputForProviderHistoryFromDefinition(externalTool, { input: "raw" })).toEqual(
      {
        custom: "provider",
      },
    );
    expect(toHistorySafeToolInputFromDefinition(externalTool, { input: "raw" })).toEqual({
      custom: "history",
    });
    expect(isContentBearingToolInputForCheckpoint(externalTool, { input: "raw" })).toBe(true);
  });

  it("still sanitizes external history hook output", () => {
    const externalTool = {
      historyInput: () => ({
        apiKey: "abc123",
        input: "normal query",
      }),
      name: "plugin:custom",
      providerHistoryInput: () => ({
        apiKey: "abc123",
        input: "normal query",
      }),
      source: "plugin",
    } as RuntimeToolDefinition;

    const providerInput = renderToolInputForProviderHistoryFromDefinition(externalTool, {});
    const historyInput = toHistorySafeToolInputFromDefinition(externalTool, {});

    expect(JSON.stringify(providerInput)).not.toContain("abc123");
    expect(JSON.stringify(providerInput)).toContain("normal query");
    expect(JSON.stringify(historyInput)).not.toContain("abc123");
    expect(JSON.stringify(historyInput)).toContain("normal query");
  });

  it("does not let external isContentBearingInput=false bypass checkpoint content detection", () => {
    const externalTool = {
      isContentBearingInput: () => false,
      name: "plugin:unsafe",
      source: "plugin",
    } as RuntimeToolDefinition;

    expect(
      isContentBearingToolInputForCheckpoint(externalTool, {
        apiKey: "sk-1234567890abcdef",
        input: "normal query",
      }),
    ).toBe(true);
  });
});
