import { describe, expect, it, vi } from "vitest";
import type { ReplyPayload } from "../auto-reply/types.js";

vi.mock("./exec-approval-surface.js", () => ({
  describeNativeExecApprovalClientSetup: vi.fn(
    (params: {
      channel?: string | null;
      channelLabel?: string | null;
      accountId?: string | null;
    }) => {
      const channel = (params.channel ?? "").trim().toLowerCase();
      const label = params.channelLabel ?? channel;
      const accountId = params.accountId?.trim();
      const accountPrefix =
        accountId && accountId !== "default"
          ? `channels.${channel}.accounts.${accountId}`
          : `channels.${channel}`;
      if (channel === "matrix") {
        return `Approve it from the Web UI or terminal UI for now. ${label} supports native exec approvals for this account. Configure \`${accountPrefix}.execApprovals.approvers\` or \`${accountPrefix}.dm.allowFrom\`; leave \`${accountPrefix}.execApprovals.enabled\` unset/\`auto\` or set it to \`true\`.`;
      }
      if (channel === "discord") {
        return `Approve it from the Web UI or terminal UI for now. ${label} supports native exec approvals for this account. Configure \`${accountPrefix}.execApprovals.approvers\` or \`commands.ownerAllowFrom\`; leave \`${accountPrefix}.execApprovals.enabled\` unset/\`auto\` or set it to \`true\`.`;
      }
      if (channel === "slack") {
        return `Approve it from the Web UI or terminal UI for now. ${label} supports native exec approvals for this account. Configure \`${accountPrefix}.execApprovals.approvers\` or \`commands.ownerAllowFrom\`; leave \`${accountPrefix}.execApprovals.enabled\` unset/\`auto\` or set it to \`true\`.`;
      }
      if (channel === "telegram") {
        return `Approve it from the Web UI or terminal UI for now. ${label} supports native exec approvals for this account. Configure \`${accountPrefix}.execApprovals.approvers\`; if you leave it unset, OpenClaw can infer numeric owner IDs from \`${accountPrefix}.allowFrom\` or direct-message \`${accountPrefix}.defaultTo\` when possible. Leave \`${accountPrefix}.execApprovals.enabled\` unset/\`auto\` or set it to \`true\`.`;
      }
      return null;
    },
  ),
  listNativeExecApprovalClientLabels: vi.fn(() => ["Discord", "Matrix", "Slack", "Telegram"]),
  supportsNativeExecApprovalClient: vi.fn((channel?: string | null) =>
    ["discord", "matrix", "slack", "telegram"].includes((channel ?? "").trim().toLowerCase()),
  ),
}));

import {
  buildExecApprovalActionDescriptors,
  buildExecApprovalCommandText,
  buildExecApprovalInteractiveReply,
  buildExecApprovalPendingReplyPayload,
  buildExecApprovalUnavailableReplyPayload,
  getExecApprovalApproverDmNoticeText,
  getExecApprovalReplyMetadata,
  parseExecApprovalCommandText,
} from "./exec-approval-reply.js";

describe("exec approval reply helpers", () => {
  const invalidReplyMetadataCases = [
    { name: "empty object", payload: {} },
    { name: "null channelData", payload: { channelData: null } },
    { name: "array channelData", payload: { channelData: [] } },
    { name: "null execApproval", payload: { channelData: { execApproval: null } } },
    { name: "array execApproval", payload: { channelData: { execApproval: [] } } },
    {
      name: "blank approval slug",
      payload: { channelData: { execApproval: { approvalId: "req-1", approvalSlug: "  " } } },
    },
    {
      name: "blank approval id",
      payload: { channelData: { execApproval: { approvalId: "  ", approvalSlug: "slug-1" } } },
    },
  ] as const;

  const unavailableReasonCases = [
    {
      reason: "initiating-platform-disabled" as const,
      channelLabel: "Slack",
      expected:
        "Exec approval is required, but native chat exec approvals are not configured on Slack.",
    },
    {
      reason: "initiating-platform-unsupported" as const,
      channelLabel: undefined,
      expected:
        "Exec approval is required, but this platform does not support chat exec approvals.",
    },
    {
      reason: "no-approval-route" as const,
      channelLabel: undefined,
      expected:
        "Exec approval is required, but no interactive approval client is currently available.",
    },
  ] as const;

  it("returns the approver DM notice text", () => {
    expect(getExecApprovalApproverDmNoticeText()).toBe(
      "Approval required. I sent approval DMs to the approvers for this account.",
    );
  });

  it("mentions Matrix in the fallback native approval guidance", () => {
    const text = buildExecApprovalUnavailableReplyPayload({
      reason: "no-approval-route",
    }).text;
    expect(text).toContain("native chat approval client such as");
    expect(text).toContain("Discord");
    expect(text).toContain("Matrix");
    expect(text).toContain("Slack");
    expect(text).toContain("Telegram");
  });

  it("avoids repeating allowFrom guidance in the no-route fallback", () => {
    const text = buildExecApprovalUnavailableReplyPayload({
      reason: "no-approval-route",
    }).text;

    expect(text).not.toContain(
      "Then retry the command. If those accounts already know your owner ID via allowFrom or owner config",
    );
    expect(text).toContain(
      "You can usually leave execApprovals.approvers unset when owner config already identifies the approvers.",
    );
  });

  it("explains how to enable Matrix native approvals when Matrix is the initiating platform", () => {
    const text = buildExecApprovalUnavailableReplyPayload({
      reason: "initiating-platform-disabled",
      channel: "matrix",
      channelLabel: "Matrix",
    }).text;

    expect(text).toContain("native chat exec approvals are not configured on Matrix");
    expect(text).toContain("Matrix supports native exec approvals for this account");
    expect(text).toContain("`channels.matrix.execApprovals.approvers`");
    expect(text).toContain("`channels.matrix.dm.allowFrom`");
  });

  it.each([
    {
      channel: "discord",
      channelLabel: "Discord",
      expected: "`commands.ownerAllowFrom`",
      unexpected: "`channels.discord.dm.allowFrom`",
    },
    {
      channel: "slack",
      channelLabel: "Slack",
      expected: "`commands.ownerAllowFrom`",
      unexpected: "`channels.slack.dm.allowFrom`",
    },
    {
      channel: "telegram",
      channelLabel: "Telegram",
      expected: "`channels.telegram.allowFrom`",
      unexpected: "`channels.telegram.dm.allowFrom`",
    },
  ])(
    "uses channel-specific disabled setup guidance for $channelLabel",
    ({ channel, channelLabel, expected, unexpected }) => {
      const text = buildExecApprovalUnavailableReplyPayload({
        reason: "initiating-platform-disabled",
        channel,
        channelLabel,
      }).text;

      expect(text).toContain(expected);
      expect(text).not.toContain(unexpected);
    },
  );

  it.each([
    {
      channel: "discord",
      channelLabel: "Discord",
      accountId: "work",
      expected: "`channels.discord.accounts.work.execApprovals.approvers`",
      unexpected: "`channels.discord.execApprovals.approvers`",
    },
    {
      channel: "slack",
      channelLabel: "Slack",
      accountId: "work",
      expected: "`channels.slack.accounts.work.execApprovals.approvers`",
      unexpected: "`channels.slack.execApprovals.approvers`",
    },
    {
      channel: "telegram",
      channelLabel: "Telegram",
      accountId: "work",
      expected: "`channels.telegram.accounts.work.allowFrom`",
      unexpected: "`channels.telegram.allowFrom`",
    },
    {
      channel: "matrix",
      channelLabel: "Matrix",
      accountId: "work",
      expected: "`channels.matrix.accounts.work.dm.allowFrom`",
      unexpected: "`channels.matrix.dm.allowFrom`",
    },
  ])(
    "uses account-scoped disabled setup guidance for $channelLabel named account",
    ({ channel, channelLabel, accountId, expected, unexpected }) => {
      const text = buildExecApprovalUnavailableReplyPayload({
        reason: "initiating-platform-disabled",
        channel,
        channelLabel,
        accountId,
      }).text;

      expect(text).toContain(expected);
      expect(text).not.toContain(unexpected);
    },
  );

  it.each(invalidReplyMetadataCases)(
    "returns null for invalid reply metadata payload: $name",
    ({ payload }) => {
      expect(getExecApprovalReplyMetadata(payload as ReplyPayload)).toBeNull();
    },
  );

  it("normalizes reply metadata and filters invalid decisions", () => {
    expect(
      getExecApprovalReplyMetadata({
        channelData: {
          execApproval: {
            approvalId: " req-1 ",
            approvalSlug: " slug-1 ",
            agentId: " agent-1 ",
            allowedDecisions: ["allow-once", "bad", "deny", "allow-always", 3],
            sessionKey: " session-1 ",
          },
        },
      }),
    ).toEqual({
      approvalId: "req-1",
      approvalSlug: "slug-1",
      approvalKind: "exec",
      agentId: "agent-1",
      allowedDecisions: ["allow-once", "deny", "allow-always"],
      sessionKey: "session-1",
    });
  });

  it("builds pending reply payloads with trimmed warning text and slug fallback", () => {
    const payload = buildExecApprovalPendingReplyPayload({
      warningText: "  Heads up.  ",
      approvalId: "req-1",
      approvalSlug: "slug-1",
      command: "echo ok",
      cwd: "/tmp/work",
      host: "gateway",
      nodeId: "node-1",
      expiresAtMs: 2500,
      nowMs: 1000,
    });

    expect(payload.channelData).toEqual({
      execApproval: {
        approvalId: "req-1",
        approvalSlug: "slug-1",
        approvalKind: "exec",
        agentId: undefined,
        allowedDecisions: ["allow-once", "allow-always", "deny"],
        sessionKey: undefined,
      },
    });
    expect(payload.interactive).toEqual({
      blocks: [
        {
          type: "buttons",
          buttons: [
            {
              label: "Allow Once",
              value: "/approve req-1 allow-once",
              style: "success",
            },
            {
              label: "Allow Always",
              value: "/approve req-1 allow-always",
              style: "primary",
            },
            {
              label: "Deny",
              value: "/approve req-1 deny",
              style: "danger",
            },
          ],
        },
      ],
    });
    expect(payload.text).toContain("Heads up.");
    expect(payload.text).toContain("```txt\n/approve slug-1 allow-once\n```");
    expect(payload.text).toContain("```sh\necho ok\n```");
    expect(payload.text).toContain("Host: gateway\nNode: node-1\nCWD: /tmp/work\nExpires in: 2s");
    expect(payload.text).toContain("Full id: `req-1`");
  });

  it("omits allow-always actions when the effective policy requires approval every time", () => {
    const payload = buildExecApprovalPendingReplyPayload({
      approvalId: "req-ask-always",
      approvalSlug: "slug-always",
      ask: "always",
      command: "echo ok",
      host: "gateway",
    });

    expect(payload.channelData).toEqual({
      execApproval: {
        approvalId: "req-ask-always",
        approvalSlug: "slug-always",
        approvalKind: "exec",
        allowedDecisions: ["allow-once", "deny"],
      },
    });
    expect(payload.text).toContain("```txt\n/approve slug-always allow-once\n```");
    expect(payload.text).not.toContain("allow-always");
    expect(payload.text).toContain(
      "The effective approval policy requires approval every time, so Allow Always is unavailable.",
    );
    expect(payload.interactive).toEqual({
      blocks: [
        {
          type: "buttons",
          buttons: [
            {
              label: "Allow Once",
              value: "/approve req-ask-always allow-once",
              style: "success",
            },
            {
              label: "Deny",
              value: "/approve req-ask-always deny",
              style: "danger",
            },
          ],
        },
      ],
    });
  });

  it("stores agent and session metadata for downstream suppression checks", () => {
    const payload = buildExecApprovalPendingReplyPayload({
      approvalId: "req-meta",
      approvalSlug: "slug-meta",
      agentId: "ops-agent",
      sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
      command: "echo ok",
      host: "gateway",
    });

    expect(payload.channelData).toEqual({
      execApproval: {
        approvalId: "req-meta",
        approvalSlug: "slug-meta",
        approvalKind: "exec",
        agentId: "ops-agent",
        allowedDecisions: ["allow-once", "allow-always", "deny"],
        sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
      },
    });
  });

  it("uses a longer fence for commands containing triple backticks", () => {
    const payload = buildExecApprovalPendingReplyPayload({
      approvalId: "req-2",
      approvalSlug: "slug-2",
      approvalCommandId: " req-cmd-2 ",
      command: "echo ```danger```",
      host: "sandbox",
    });

    expect(payload.text).toContain("```txt\n/approve req-cmd-2 allow-once\n```");
    expect(payload.text).toContain("````sh\necho ```danger```\n````");
    expect(payload.text).not.toContain("Expires in:");
  });

  it("clamps pending reply expiration to zero seconds", () => {
    const payload = buildExecApprovalPendingReplyPayload({
      approvalId: "req-3",
      approvalSlug: "slug-3",
      command: "echo later",
      host: "gateway",
      expiresAtMs: 1000,
      nowMs: 3000,
    });

    expect(payload.text).toContain("Expires in: 0s");
  });

  it("formats longer approval windows in minutes", () => {
    const payload = buildExecApprovalPendingReplyPayload({
      approvalId: "req-30m",
      approvalSlug: "slug-30m",
      command: "echo later",
      host: "gateway",
      expiresAtMs: 1_801_000,
      nowMs: 1_000,
    });

    expect(payload.text).toContain("Expires in: 30m");
  });

  it("builds shared exec approval action descriptors and interactive replies", () => {
    expect(
      buildExecApprovalActionDescriptors({
        approvalCommandId: "req-1",
      }),
    ).toEqual([
      {
        decision: "allow-once",
        label: "Allow Once",
        style: "success",
        command: "/approve req-1 allow-once",
      },
      {
        decision: "allow-always",
        label: "Allow Always",
        style: "primary",
        command: "/approve req-1 allow-always",
      },
      {
        decision: "deny",
        label: "Deny",
        style: "danger",
        command: "/approve req-1 deny",
      },
    ]);

    expect(
      buildExecApprovalInteractiveReply({
        approvalCommandId: "req-1",
      }),
    ).toEqual({
      blocks: [
        {
          type: "buttons",
          buttons: [
            { label: "Allow Once", value: "/approve req-1 allow-once", style: "success" },
            { label: "Allow Always", value: "/approve req-1 allow-always", style: "primary" },
            { label: "Deny", value: "/approve req-1 deny", style: "danger" },
          ],
        },
      ],
    });
  });

  it("builds and parses shared exec approval command text", () => {
    expect(
      buildExecApprovalCommandText({
        approvalCommandId: "req-1",
        decision: "allow-always",
      }),
    ).toBe("/approve req-1 allow-always");

    expect(parseExecApprovalCommandText("/approve req-1 deny")).toEqual({
      approvalId: "req-1",
      decision: "deny",
    });
    expect(parseExecApprovalCommandText("approve req-1 allow-once")).toEqual({
      approvalId: "req-1",
      decision: "allow-once",
    });
    expect(parseExecApprovalCommandText("/approve@clover req-1 allow-once")).toEqual({
      approvalId: "req-1",
      decision: "allow-once",
    });
    expect(parseExecApprovalCommandText("  /approve req-1 always")).toEqual({
      approvalId: "req-1",
      decision: "allow-always",
    });
    expect(parseExecApprovalCommandText("/approve req-1 allow-always")).toEqual({
      approvalId: "req-1",
      decision: "allow-always",
    });
    expect(parseExecApprovalCommandText("/approve req-1 maybe")).toBeNull();
  });

  it("builds unavailable payloads for approver DMs", () => {
    expect(
      buildExecApprovalUnavailableReplyPayload({
        warningText: "  Careful.  ",
        reason: "no-approval-route",
        sentApproverDms: true,
      }),
    ).toEqual({
      text: "Careful.\n\nApproval required. I sent approval DMs to the approvers for this account.",
    });
  });

  it.each(unavailableReasonCases)(
    "builds unavailable payload for reason $reason",
    ({ reason, channelLabel, expected }) => {
      expect(
        buildExecApprovalUnavailableReplyPayload({
          reason,
          channelLabel,
        }).text,
      ).toContain(expected);
    },
  );
});
