import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { afterEach, describe, expect, it } from "vitest";
import {
  getMatrixExecApprovalApprovers,
  isMatrixExecApprovalApprover,
  isMatrixExecApprovalAuthorizedSender,
  isMatrixExecApprovalClientEnabled,
  isMatrixExecApprovalTargetRecipient,
  normalizeMatrixApproverId,
  resolveMatrixExecApprovalTarget,
  shouldHandleMatrixExecApprovalRequest,
  shouldSuppressLocalMatrixExecApprovalPrompt,
} from "./exec-approvals.js";

const tempDirs: string[] = [];

afterEach(() => {
  for (const dir of tempDirs.splice(0)) {
    fs.rmSync(dir, { recursive: true, force: true });
  }
});

function createTempDir(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-exec-approvals-"));
  tempDirs.push(dir);
  return dir;
}

function buildConfig(
  execApprovals?: NonNullable<NonNullable<OpenClawConfig["channels"]>["matrix"]>["execApprovals"],
  channelOverrides?: Partial<NonNullable<NonNullable<OpenClawConfig["channels"]>["matrix"]>>,
): OpenClawConfig {
  return {
    channels: {
      matrix: {
        homeserver: "https://matrix.example.org",
        userId: "@bot:example.org",
        accessToken: "tok",
        ...channelOverrides,
        execApprovals,
      },
    },
  } as OpenClawConfig;
}

describe("matrix exec approvals", () => {
  it("auto-enables when approvers resolve and disables only when forced off", () => {
    expect(isMatrixExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
    expect(
      isMatrixExecApprovalClientEnabled({
        cfg: buildConfig(undefined, { dm: { allowFrom: ["@owner:example.org"] } }),
      }),
    ).toBe(true);
    expect(isMatrixExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
    expect(
      isMatrixExecApprovalClientEnabled({
        cfg: buildConfig({ enabled: true }, { dm: { allowFrom: ["@owner:example.org"] } }),
      }),
    ).toBe(true);
    expect(
      isMatrixExecApprovalClientEnabled({
        cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
      }),
    ).toBe(true);
  });

  it("prefers explicit approvers when configured", () => {
    const cfg = buildConfig(
      { enabled: true, approvers: ["user:@override:example.org"] },
      { dm: { allowFrom: ["@owner:example.org"] } },
    );

    expect(getMatrixExecApprovalApprovers({ cfg })).toEqual(["@override:example.org"]);
    expect(isMatrixExecApprovalApprover({ cfg, senderId: "@override:example.org" })).toBe(true);
    expect(isMatrixExecApprovalApprover({ cfg, senderId: "@owner:example.org" })).toBe(false);
  });

  it("ignores wildcard allowlist entries when inferring exec approvers", () => {
    const cfg = buildConfig({ enabled: true }, { dm: { allowFrom: ["*"] } });

    expect(getMatrixExecApprovalApprovers({ cfg })).toEqual([]);
    expect(isMatrixExecApprovalClientEnabled({ cfg })).toBe(false);
  });

  it("defaults target to dm", () => {
    expect(
      resolveMatrixExecApprovalTarget({
        cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
      }),
    ).toBe("dm");
  });

  it("matches matrix target recipients from generic approval forwarding targets", () => {
    const cfg = {
      channels: {
        matrix: {
          homeserver: "https://matrix.example.org",
          userId: "@bot:example.org",
          accessToken: "tok",
        },
      },
      approvals: {
        exec: {
          enabled: true,
          mode: "targets",
          targets: [
            { channel: "matrix", to: "user:@target:example.org" },
            { channel: "matrix", to: "room:!ops:example.org" },
          ],
        },
      },
    } as OpenClawConfig;

    expect(isMatrixExecApprovalTargetRecipient({ cfg, senderId: "@target:example.org" })).toBe(
      true,
    );
    expect(isMatrixExecApprovalTargetRecipient({ cfg, senderId: "@other:example.org" })).toBe(
      false,
    );
    expect(isMatrixExecApprovalAuthorizedSender({ cfg, senderId: "@target:example.org" })).toBe(
      true,
    );
  });

  it("suppresses local prompts only when the native client is enabled", () => {
    const payload = {
      channelData: {
        execApproval: {
          approvalId: "req-1",
          approvalSlug: "req-1",
          agentId: "ops-agent",
          sessionKey: "agent:ops-agent:matrix:channel:!ops:example.org",
        },
      },
    };

    expect(
      shouldSuppressLocalMatrixExecApprovalPrompt({
        cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
        payload,
      }),
    ).toBe(true);

    expect(
      shouldSuppressLocalMatrixExecApprovalPrompt({
        cfg: buildConfig(),
        payload,
      }),
    ).toBe(false);
  });

  it("keeps local prompts when filters exclude the request", () => {
    const payload = {
      channelData: {
        execApproval: {
          approvalId: "req-1",
          approvalSlug: "req-1",
          agentId: "other-agent",
          sessionKey: "agent:other-agent:matrix:channel:!ops:example.org",
        },
      },
    };

    expect(
      shouldSuppressLocalMatrixExecApprovalPrompt({
        cfg: buildConfig({
          enabled: true,
          approvers: ["@owner:example.org"],
          agentFilter: ["ops-agent"],
        }),
        payload,
      }),
    ).toBe(false);
  });

  it("suppresses local prompts for generic exec payloads when metadata matches filters", () => {
    const payload = {
      channelData: {
        execApproval: {
          approvalId: "req-1",
          approvalSlug: "req-1",
          approvalKind: "exec",
          agentId: "ops-agent",
          sessionKey: "agent:ops-agent:matrix:channel:!ops:example.org",
        },
      },
    };

    expect(
      shouldSuppressLocalMatrixExecApprovalPrompt({
        cfg: buildConfig({
          enabled: true,
          approvers: ["@owner:example.org"],
          agentFilter: ["ops-agent"],
          sessionFilter: ["matrix:channel:"],
        }),
        payload,
      }),
    ).toBe(true);
  });

  it("does not suppress local prompts for plugin approval payloads", () => {
    const payload = {
      channelData: {
        execApproval: {
          approvalId: "plugin:req-1",
          approvalSlug: "plugin:r",
          approvalKind: "plugin",
        },
      },
    };

    expect(
      shouldSuppressLocalMatrixExecApprovalPrompt({
        cfg: buildConfig({ enabled: true, approvers: ["@owner:example.org"] }),
        payload,
      }),
    ).toBe(false);
  });

  it("normalizes prefixed approver ids", () => {
    expect(normalizeMatrixApproverId("matrix:@owner:example.org")).toBe("@owner:example.org");
    expect(normalizeMatrixApproverId("user:@owner:example.org")).toBe("@owner:example.org");
  });

  it("applies agent and session filters to request handling", () => {
    const cfg = buildConfig({
      enabled: true,
      approvers: ["@owner:example.org"],
      agentFilter: ["ops-agent"],
      sessionFilter: ["matrix:channel:", "ops$"],
    });

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        request: {
          id: "req-1",
          request: {
            command: "echo hi",
            agentId: "ops-agent",
            sessionKey: "agent:ops-agent:matrix:channel:!room:example.org:ops",
          },
          createdAtMs: 0,
          expiresAtMs: 1000,
        },
      }),
    ).toBe(true);

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        request: {
          id: "req-2",
          request: {
            command: "echo hi",
            agentId: "other-agent",
            sessionKey: "agent:other-agent:matrix:channel:!room:example.org:ops",
          },
          createdAtMs: 0,
          expiresAtMs: 1000,
        },
      }),
    ).toBe(false);
  });

  it("scopes non-matrix turn sources to the stored matrix account", () => {
    const tmpDir = createTempDir();
    const storePath = path.join(tmpDir, "sessions.json");
    fs.writeFileSync(
      storePath,
      JSON.stringify({
        "agent:ops-agent:matrix:channel:!room:example.org": {
          sessionId: "main",
          updatedAt: 1,
          origin: {
            provider: "matrix",
            accountId: "ops",
          },
          lastChannel: "slack",
          lastTo: "channel:C999",
          lastAccountId: "work",
        },
      }),
      "utf-8",
    );
    const cfg = {
      session: { store: storePath },
      channels: {
        matrix: {
          accounts: {
            default: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-default:example.org",
              accessToken: "tok-default",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
            ops: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-ops:example.org",
              accessToken: "tok-ops",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
          },
        },
      },
    } as OpenClawConfig;
    const request = {
      id: "req-3",
      request: {
        command: "echo hi",
        agentId: "ops-agent",
        sessionKey: "agent:ops-agent:matrix:channel:!room:example.org",
        turnSourceChannel: "slack",
        turnSourceTo: "channel:C123",
      },
      createdAtMs: 0,
      expiresAtMs: 1000,
    };

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "default",
        request,
      }),
    ).toBe(false);
    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "ops",
        request,
      }),
    ).toBe(true);
  });

  it("rejects unbound foreign-channel approvals in multi-account matrix configs", () => {
    const cfg = {
      channels: {
        matrix: {
          accounts: {
            default: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-default:example.org",
              accessToken: "tok-default",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
            ops: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-ops:example.org",
              accessToken: "tok-ops",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
          },
        },
      },
    } as OpenClawConfig;
    const request = {
      id: "req-4",
      request: {
        command: "echo hi",
        agentId: "ops-agent",
        sessionKey: "agent:ops-agent:missing",
        turnSourceChannel: "slack",
        turnSourceTo: "channel:C123",
      },
      createdAtMs: 0,
      expiresAtMs: 1000,
    };

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "default",
        request,
      }),
    ).toBe(false);
    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "ops",
        request,
      }),
    ).toBe(false);
  });

  it("allows unbound foreign-channel approvals when only one matrix account can handle them", () => {
    const cfg = {
      channels: {
        matrix: {
          accounts: {
            default: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-default:example.org",
              accessToken: "tok-default",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
            ops: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-ops:example.org",
              accessToken: "tok-ops",
              execApprovals: {
                enabled: false,
                approvers: ["@owner:example.org"],
              },
            },
          },
        },
      },
    } as OpenClawConfig;
    const request = {
      id: "req-5",
      request: {
        command: "echo hi",
        agentId: "ops-agent",
        sessionKey: "agent:ops-agent:missing",
        turnSourceChannel: "slack",
        turnSourceTo: "channel:C123",
      },
      createdAtMs: 0,
      expiresAtMs: 1000,
    };

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "default",
        request,
      }),
    ).toBe(true);
    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "ops",
        request,
      }),
    ).toBe(false);
  });

  it("uses request filters when checking foreign-channel matrix ambiguity", () => {
    const cfg = {
      channels: {
        matrix: {
          accounts: {
            default: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-default:example.org",
              accessToken: "tok-default",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
                agentFilter: ["ops-agent"],
              },
            },
            ops: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-ops:example.org",
              accessToken: "tok-ops",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
                agentFilter: ["other-agent"],
              },
            },
          },
        },
      },
    } as OpenClawConfig;
    const request = {
      id: "req-6",
      request: {
        command: "echo hi",
        agentId: "ops-agent",
        sessionKey: "agent:ops-agent:missing",
        turnSourceChannel: "slack",
        turnSourceTo: "channel:C123",
      },
      createdAtMs: 0,
      expiresAtMs: 1000,
    };

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "default",
        request,
      }),
    ).toBe(true);
    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "ops",
        request,
      }),
    ).toBe(false);
  });

  it("ignores disabled matrix accounts when checking foreign-channel ambiguity", () => {
    const cfg = {
      channels: {
        matrix: {
          accounts: {
            default: {
              homeserver: "https://matrix.example.org",
              userId: "@bot-default:example.org",
              accessToken: "tok-default",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
            ops: {
              enabled: false,
              homeserver: "https://matrix.example.org",
              userId: "@bot-ops:example.org",
              accessToken: "tok-ops",
              execApprovals: {
                enabled: true,
                approvers: ["@owner:example.org"],
              },
            },
          },
        },
      },
    } as OpenClawConfig;
    const request = {
      id: "req-7",
      request: {
        command: "echo hi",
        agentId: "ops-agent",
        sessionKey: "agent:ops-agent:missing",
        turnSourceChannel: "slack",
        turnSourceTo: "channel:C123",
      },
      createdAtMs: 0,
      expiresAtMs: 1000,
    };

    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "default",
        request,
      }),
    ).toBe(true);
    expect(
      shouldHandleMatrixExecApprovalRequest({
        cfg,
        accountId: "ops",
        request,
      }),
    ).toBe(false);
  });
});
