import { describe, expect, it } from "vitest";
import type { ItemId, ProviderTurnActionPolicy, ThreadMetadata } from "@best-agent/protocol";
import type { RunCheckpoint } from "../core/thread-store.js";
import {
  parseProviderRequestCheckpointPayload,
  parseToolOutputCheckpointPayload,
  serializeToolOutputCheckpointPayload,
} from "./checkpoint-codec.js";
import { PLAN_UPDATE_TOOL_NAME } from "../provider/provider-tool-names.js";

const DEFAULT_DECIDE_ACTION_POLICY: ProviderTurnActionPolicy = {
  allowedActions: ["respond", "tool"],
  directResponseAllowed: true,
};

describe("checkpoint codec", () => {
  it("serializes provider tool names on active and completed tool-output checkpoints", () => {
    const payload = serializeToolOutputCheckpointPayload({
      kind: "tool-output",
      activeSkillIds: [],
      availableTools: [
        {
          name: "mcp:docs:search",
          description: "Search docs.",
          inputSchema: {
            type: "object",
            additionalProperties: false,
            properties: { input: { type: "string" } },
            required: ["input"],
          },
        },
      ],
      messages: [],
      mode: "provider-tool-loop",
      profileName: "default",
      toolCallId: "item_1" as ItemId,
      toolInput: { input: "query" },
      toolName: "mcp:docs:search",
      providerToolName: "mcp_docs_search_abcd1234",
      toolStepCount: 1,
      completedToolCalls: [
        {
          toolCallId: "item_0" as ItemId,
          toolName: "mcp:docs:search",
          providerToolName: "mcp_docs_search_abcd1234",
          toolInput: { input: "query" },
          toolOutput: "result",
          outcome: "success",
        },
      ],
      toolOutput: "result",
    });

    expect(payload.providerToolName).toBe("mcp_docs_search_abcd1234");
    expect((payload.completedToolCalls as Array<Record<string, unknown>>)[0]).toMatchObject({
      providerToolName: "mcp_docs_search_abcd1234",
    });
  });

  it("ignores invalid persisted provider tool names on checkpoint parse", () => {
    const payload = parseToolOutputCheckpointPayload(
      makeCheckpoint({
        kind: "tool-output",
        activeSkillIds: [],
        availableTools: [
          {
            name: "mcp:docs:search",
            description: "Search docs.",
          },
        ],
        messages: [
          {
            role: "assistant",
            content: "",
            toolCalls: [
              {
                input: { input: "query" },
                providerToolName: "mcp:docs:search",
                toolCallId: "item_0",
                toolName: "mcp:docs:search",
              },
            ],
          },
          {
            role: "tool",
            content: "result",
            outcome: "success",
            providerToolName: "mcp:docs:search",
            toolCallId: "item_0",
            toolName: "mcp:docs:search",
          },
        ],
        mode: "provider-tool-loop",
        profileName: "default",
        providerToolName: "mcp:docs:search",
        toolCallId: "item_1",
        toolInput: { input: "query" },
        toolName: "mcp:docs:search",
        toolStepCount: 1,
        completedToolCalls: [
          {
            toolCallId: "item_0",
            toolName: "mcp:docs:search",
            providerToolName: "mcp:docs:search",
            toolInput: { input: "query" },
            toolOutput: "result",
            outcome: "success",
          },
        ],
        toolOutput: "result",
      }),
    );

    expect(payload.providerToolName).toBeUndefined();
    expect(payload.completedToolCalls?.[0]?.providerToolName).toBeUndefined();
    const assistantMessage = payload.messages[0];
    expect(assistantMessage.role).toBe("assistant");
    if (assistantMessage.role === "assistant") {
      expect(assistantMessage.toolCalls?.[0]?.providerToolName).toBeUndefined();
    }
    const toolMessage = payload.messages[1];
    expect(toolMessage.role).toBe("tool");
    if (toolMessage.role === "tool") {
      expect(toolMessage.providerToolName).toBeUndefined();
    }
  });

  it("ignores reserved persisted provider tool names on checkpoint parse", () => {
    const payload = parseToolOutputCheckpointPayload(
      makeCheckpoint({
        kind: "tool-output",
        activeSkillIds: [],
        availableTools: [
          {
            name: "plugin:plan_update",
            description: "Update a plan.",
          },
        ],
        messages: [
          {
            role: "assistant",
            content: "",
            toolCalls: [
              {
                input: {},
                providerToolName: PLAN_UPDATE_TOOL_NAME,
                toolCallId: "item_0",
                toolName: "plugin:plan_update",
              },
            ],
          },
          {
            role: "tool",
            content: "result",
            outcome: "success",
            providerToolName: PLAN_UPDATE_TOOL_NAME,
            toolCallId: "item_0",
            toolName: "plugin:plan_update",
          },
        ],
        mode: "provider-tool-loop",
        profileName: "default",
        providerToolName: PLAN_UPDATE_TOOL_NAME,
        toolCallId: "item_1",
        toolInput: {},
        toolName: "plugin:plan_update",
        toolStepCount: 1,
        completedToolCalls: [
          {
            toolCallId: "item_0",
            toolName: "plugin:plan_update",
            providerToolName: PLAN_UPDATE_TOOL_NAME,
            toolInput: {},
            toolOutput: "result",
            outcome: "success",
          },
        ],
        toolOutput: "result",
      }),
    );

    expect(payload.providerToolName).toBeUndefined();
    expect(payload.completedToolCalls?.[0]?.providerToolName).toBeUndefined();
    const assistantMessage = payload.messages[0];
    expect(assistantMessage.role).toBe("assistant");
    if (assistantMessage.role === "assistant") {
      expect(assistantMessage.toolCalls?.[0]?.providerToolName).toBeUndefined();
    }
    const toolMessage = payload.messages[1];
    expect(toolMessage.role).toBe("tool");
    if (toolMessage.role === "tool") {
      expect(toolMessage.providerToolName).toBeUndefined();
    }
  });

  it("hydrates legacy digest fields into explicit raw and stable digests", () => {
    const payload = parseToolOutputCheckpointPayload(
      makeCheckpoint({
        kind: "tool-output",
        activeSkillIds: [],
        availableTools: [],
        messages: [],
        mode: "provider-tool-loop",
        profileName: "default",
        toolCallId: "item_1",
        toolInput: {},
        toolName: "echo",
        toolOutput: "result",
        completedToolCalls: [
          {
            inputDigest: "legacy-stable-digest",
            outcome: "success",
            toolCallId: "item_0",
            toolInput: {},
            toolName: "echo",
            toolOutput: "result",
          },
        ],
        toolEnvelope: {
          inputDigest: "legacy-raw-digest",
          outcome: "success",
          summary: "result",
          toolCallId: "item_1",
          toolName: "echo",
        },
        toolStepCount: 1,
      }),
    );

    expect(payload.completedToolCalls?.[0]?.inputStableDigest).toBe("legacy-stable-digest");
    expect(payload.completedToolCalls?.[0]?.inputDigest).toBeUndefined();
    expect(payload.toolEnvelope?.inputRawDigest).toBe("legacy-raw-digest");
    expect(payload.toolEnvelope?.inputDigest).toBeUndefined();
  });

  it("serializes explicit digest fields without legacy inputDigest", () => {
    const payload = serializeToolOutputCheckpointPayload({
      kind: "tool-output",
      activeSkillIds: [],
      availableTools: [],
      messages: [],
      mode: "provider-tool-loop",
      profileName: "default",
      toolCallId: "item_1" as ItemId,
      toolInput: {},
      toolName: "echo",
      toolOutput: "result",
      completedToolCalls: [
        {
          inputStableDigest: "stable-digest",
          outcome: "success",
          toolCallId: "item_0" as ItemId,
          toolInput: {},
          toolName: "echo",
          toolOutput: "result",
        },
      ],
      toolEnvelope: {
        inputRawDigest: "raw-digest",
        inputStableDigest: "stable-digest",
        outcome: "success",
        summary: "result",
        toolCallId: "item_1" as ItemId,
        toolName: "echo",
      },
      toolStepCount: 1,
    });

    expect(payload.completedToolCalls?.[0]).toMatchObject({
      inputStableDigest: "stable-digest",
    });
    expect(payload.toolEnvelope).toMatchObject({
      inputRawDigest: "raw-digest",
      inputStableDigest: "stable-digest",
    });
    expect(JSON.stringify(payload)).not.toContain('"inputDigest"');
  });

  it("normalizes legacy strict JSON transport fields while parsing provider checkpoints", () => {
    const payload = parseProviderRequestCheckpointPayload({
      ...makeCheckpoint({
        decisionTransport: "strict_json_envelope",
        kind: "provider-request",
        operation: "decide",
        providerConfig: {
          apiKey: "redacted",
          decisionTransport: "strict_json_envelope",
          kind: "openai",
          model: "gpt-test",
        },
        replayContext: {
          payloadDecisionTransport: "strict_json_envelope",
          requiredToolConstraint: {
            allowedToolNames: ["workspace-files", "workspace-search"],
            forbiddenToolNames: [],
            primaryToolName: "workspace-files",
            reason: "repo-wide-coverage",
            requiredReleasePolicy: "after-required-tools-succeeded",
            requiredToolNames: ["workspace-files"],
            restrictionScope: "until-required-satisfied",
          },
        },
        request: {
          decisionTransport: "strict_json_envelope",
          messages: [],
          tools: [],
        },
        requestKind: "decide",
      }),
      kind: "provider-request",
    });

    expect(payload.decisionTransport).toBe("native_tools");
    expect(payload.providerConfig?.decisionTransport).toBe("native_tools");
    expect(payload.providerConfig).not.toHaveProperty("apiKey");
    expect(payload.replayContext?.payloadDecisionTransport).toBe("native_tools");
    expect(payload.replayContext?.requiredToolConstraint).toMatchObject({
      requiredToolNames: ["workspace-files"],
      selectableToolNames: ["workspace-files", "workspace-search"],
      visibleToolNames: ["workspace-files", "workspace-search"],
    });
    expect(payload.requestKind).toBe("decide");
    if (payload.requestKind === "decide") {
      expect(payload.request.decisionTransport).toBe("native_tools");
    }
  });

  it("normalizes persisted required any tool fields while parsing provider checkpoints", () => {
    const payload = parseProviderRequestCheckpointPayload({
      ...makeCheckpoint({
        kind: "provider-request",
        operation: "decide",
        providerConfig: {
          kind: "openai",
          model: "gpt-test",
        },
        replayContext: {
          requiredToolConstraint: {
            forbiddenToolNames: [],
            reason: "explicit-required-tool",
            requiredAnyToolGroups: [["read-file", "workspace-search"]],
            requiredAnyToolNames: ["shell", "delegate_tasks"],
            requiredReleasePolicy: "after-required-tools-succeeded",
            requiredToolNames: ["read-file", "shell"],
            restrictionScope: "until-required-satisfied",
          },
        },
        request: {
          decisionTransport: "native_tools",
          actionPolicy: DEFAULT_DECIDE_ACTION_POLICY,
          messages: [],
          tools: [],
        },
        requestKind: "decide",
      }),
      kind: "provider-request",
    });

    const constraint = payload.replayContext?.requiredToolConstraint;
    expect(constraint).toMatchObject({
      requiredAnyToolGroups: [
        ["read-file", "workspace-search"],
        ["shell", "delegate_tasks"],
      ],
      requiredToolNames: ["read-file", "shell"],
    });
    expect(constraint?.requiredAnyToolNames).toBeUndefined();
  });

  it("rebuilds aggregate requiredToolNames from canonical all/any checkpoint fields", () => {
    const payload = parseProviderRequestCheckpointPayload({
      ...makeCheckpoint({
        kind: "provider-request",
        operation: "decide",
        providerConfig: {
          kind: "openai",
          model: "gpt-test",
        },
        replayContext: {
          requiredToolConstraint: {
            forbiddenToolNames: [],
            reason: "explicit-required-tool",
            requiredAllToolNames: ["read-file"],
            requiredAnyToolGroups: [["workspace-search", "content-search"]],
            requiredReleasePolicy: "after-required-tools-succeeded",
            requiredToolNames: [],
            restrictionScope: "until-required-satisfied",
          },
        },
        request: {
          decisionTransport: "native_tools",
          actionPolicy: DEFAULT_DECIDE_ACTION_POLICY,
          messages: [],
          tools: [],
        },
        requestKind: "decide",
      }),
      kind: "provider-request",
    });

    expect(payload.replayContext?.requiredToolConstraint).toMatchObject({
      requiredAllToolNames: ["read-file"],
      requiredAnyToolGroups: [["workspace-search", "content-search"]],
      requiredToolNames: ["read-file", "workspace-search"],
    });
  });

  it("round-trips requiredToolConstraint conflicts while parsing provider checkpoints", () => {
    const payload = parseProviderRequestCheckpointPayload({
      ...makeCheckpoint({
        kind: "provider-request",
        operation: "decide",
        providerConfig: {
          kind: "openai",
          model: "gpt-test",
        },
        replayContext: {
          requiredToolConstraint: {
            conflicts: [
              {
                evidence: [
                  {
                    end: 18,
                    parser: "explicit-tool-set-parser",
                    start: 0,
                    text: "Only use read-file",
                  },
                ],
                kind: "allow-only-conflict",
                toolNames: ["read-file", "workspace-search"],
              },
            ],
            forbiddenToolNames: [],
            reason: "explicit-exclusive-tool-set",
            requiredReleasePolicy: "none",
            requiredToolNames: [],
            restrictionScope: "turn",
          },
        },
        request: {
          decisionTransport: "native_tools",
          actionPolicy: DEFAULT_DECIDE_ACTION_POLICY,
          messages: [],
          tools: [],
        },
        requestKind: "decide",
      }),
      kind: "provider-request",
    });

    expect(payload.replayContext?.requiredToolConstraint?.conflicts).toEqual([
      {
        evidence: [
          {
            end: 18,
            parser: "explicit-tool-set-parser",
            start: 0,
            text: "Only use read-file",
          },
        ],
        kind: "allow-only-conflict",
        toolNames: ["read-file", "workspace-search"],
      },
    ]);
  });

  it("does not synthesize actionPolicy while parsing legacy provider checkpoints", () => {
    const payload = parseProviderRequestCheckpointPayload({
      ...makeCheckpoint({
        kind: "provider-request",
        operation: "decide",
        providerConfig: {
          kind: "openai",
          model: "gpt-test",
        },
        replayContext: {
          requiredToolConstraint: {
            forbiddenToolNames: [],
            reason: "explicit-required-tool",
            requiredReleasePolicy: "after-required-tools-succeeded",
            requiredToolNames: ["read-file"],
            restrictionScope: "until-required-satisfied",
          },
        },
        request: {
          decisionTransport: "native_tools",
          messages: [],
          tools: [{ description: "Read a file.", name: "read-file" }],
        },
        requestKind: "decide",
      }),
      kind: "provider-request",
    });

    expect(payload.requestKind).toBe("decide");
    if (payload.requestKind === "decide") {
      expect(payload.request.actionPolicy).toBeUndefined();
    }
  });
});

function makeCheckpoint(payload: ThreadMetadata): RunCheckpoint {
  return {
    createdAt: "2026-05-14T00:00:00.000Z",
    id: "step_1" as RunCheckpoint["id"],
    kind: "tool-output",
    payload,
    runId: "run_1" as RunCheckpoint["runId"],
    turnId: "turn_1" as RunCheckpoint["turnId"],
    updatedAt: "2026-05-14T00:00:00.000Z",
  };
}
