import { beforeAll, describe, expect, it, vi } from "vitest";

const createChannelPairingController = vi.hoisted(() => vi.fn());
const evaluateGroupRouteAccessForPolicy = vi.hoisted(() => vi.fn());
const isDangerousNameMatchingEnabled = vi.hoisted(() => vi.fn());
const resolveAllowlistProviderRuntimeGroupPolicy = vi.hoisted(() => vi.fn());
const resolveDefaultGroupPolicy = vi.hoisted(() => vi.fn());
const resolveDmGroupAccessWithLists = vi.hoisted(() => vi.fn());
const resolveInboundMentionDecision = vi.hoisted(() => vi.fn());
const resolveSenderScopedGroupPolicy = vi.hoisted(() => vi.fn());
const warnMissingProviderGroupPolicyFallbackOnce = vi.hoisted(() => vi.fn());
const sendGoogleChatMessage = vi.hoisted(() => vi.fn());

vi.mock("openclaw/plugin-sdk/channel-inbound", () => ({
  resolveInboundMentionDecision,
}));

vi.mock("../runtime-api.js", () => ({
  GROUP_POLICY_BLOCKED_LABEL: { space: "space" },
  createChannelPairingController,
  evaluateGroupRouteAccessForPolicy,
  isDangerousNameMatchingEnabled,
  resolveAllowlistProviderRuntimeGroupPolicy,
  resolveDefaultGroupPolicy,
  resolveDmGroupAccessWithLists,
  resolveSenderScopedGroupPolicy,
  warnMissingProviderGroupPolicyFallbackOnce,
}));

vi.mock("./api.js", () => ({
  sendGoogleChatMessage,
}));

function createCore() {
  return {
    channel: {
      commands: {
        shouldComputeCommandAuthorized: vi.fn(() => false),
        resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
        shouldHandleTextCommands: vi.fn(() => false),
        isControlCommandMessage: vi.fn(() => false),
      },
      text: {
        hasControlCommand: vi.fn(() => false),
      },
    },
  };
}

function primeCommonDefaults() {
  isDangerousNameMatchingEnabled.mockReturnValue(false);
  resolveDefaultGroupPolicy.mockReturnValue("allowlist");
  resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
    groupPolicy: "allowlist",
    providerMissingFallbackApplied: false,
  });
  resolveSenderScopedGroupPolicy.mockImplementation(({ groupPolicy }) => groupPolicy);
  evaluateGroupRouteAccessForPolicy.mockReturnValue({
    allowed: true,
  });
  warnMissingProviderGroupPolicyFallbackOnce.mockReturnValue(undefined);
}

const baseAccessConfig = {
  channels: { googlechat: {} },
  commands: { useAccessGroups: true },
} as const;

const defaultSender = {
  senderId: "users/alice",
  senderName: "Alice",
  senderEmail: "alice@example.com",
} as const;

let applyGoogleChatInboundAccessPolicy: typeof import("./monitor-access.js").applyGoogleChatInboundAccessPolicy;

function allowInboundGroupTraffic(options?: {
  effectiveGroupAllowFrom?: string[];
  effectiveWasMentioned?: boolean;
}) {
  createChannelPairingController.mockReturnValue({
    readAllowFromStore: vi.fn(async () => []),
    issueChallenge: vi.fn(),
  });
  resolveDmGroupAccessWithLists.mockReturnValue({
    decision: "allow",
    effectiveAllowFrom: [],
    effectiveGroupAllowFrom: options?.effectiveGroupAllowFrom ?? ["users/alice"],
  });
  resolveInboundMentionDecision.mockReturnValue({
    shouldSkip: false,
    effectiveWasMentioned: options?.effectiveWasMentioned ?? true,
  });
}

async function applyInboundAccessPolicy(
  overrides: Partial<Parameters<typeof applyGoogleChatInboundAccessPolicy>[0]>,
) {
  return applyGoogleChatInboundAccessPolicy({
    account: {
      accountId: "default",
      config: {},
    } as never,
    config: baseAccessConfig as never,
    core: createCore() as never,
    space: { name: "spaces/AAA", displayName: "Team Room" } as never,
    message: { annotations: [] } as never,
    isGroup: true,
    rawBody: "hello team",
    logVerbose: vi.fn(),
    ...defaultSender,
    ...overrides,
  } as never);
}

describe("googlechat inbound access policy", () => {
  beforeAll(async () => {
    ({ applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"));
  });

  it("issues a pairing challenge for unauthorized DMs in pairing mode", async () => {
    primeCommonDefaults();
    const issueChallenge = vi.fn(async ({ onCreated, sendPairingReply }) => {
      onCreated?.();
      await sendPairingReply("pairing text");
    });
    createChannelPairingController.mockReturnValue({
      readAllowFromStore: vi.fn(async () => []),
      issueChallenge,
    });
    resolveDmGroupAccessWithLists.mockReturnValue({
      decision: "pairing",
      reason: "pairing_required",
      effectiveAllowFrom: [],
      effectiveGroupAllowFrom: [],
    });
    sendGoogleChatMessage.mockResolvedValue({ ok: true });

    const statusSink = vi.fn();
    const logVerbose = vi.fn();

    await expect(
      applyGoogleChatInboundAccessPolicy({
        account: {
          accountId: "default",
          config: {
            dm: { policy: "pairing" },
          },
        } as never,
        config: {
          channels: { googlechat: {} },
        } as never,
        core: createCore() as never,
        space: { name: "spaces/AAA", displayName: "DM" } as never,
        message: { annotations: [] } as never,
        isGroup: false,
        senderId: "users/abc",
        senderName: "Alice",
        senderEmail: "alice@example.com",
        rawBody: "hello",
        statusSink,
        logVerbose,
      }),
    ).resolves.toEqual({ ok: false });

    expect(issueChallenge).toHaveBeenCalledTimes(1);
    expect(sendGoogleChatMessage).toHaveBeenCalledWith({
      account: expect.anything(),
      space: "spaces/AAA",
      text: "pairing text",
    });
    expect(statusSink).toHaveBeenCalledWith(
      expect.objectContaining({
        lastOutboundAt: expect.any(Number),
      }),
    );
  });

  it("allows group traffic when sender and mention gates pass", async () => {
    primeCommonDefaults();
    allowInboundGroupTraffic();
    const core = createCore();
    core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true);
    core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(true);

    await expect(
      applyInboundAccessPolicy({
        account: {
          accountId: "default",
          config: {
            botUser: "users/app-bot",
            groups: {
              "spaces/AAA": {
                users: ["users/alice"],
                requireMention: true,
                systemPrompt: " group prompt ",
              },
            },
          },
        } as never,
        core: core as never,
        message: {
          annotations: [
            {
              type: "USER_MENTION",
              userMention: { user: { name: "users/app-bot" } },
            },
          ],
        } as never,
      }),
    ).resolves.toEqual({
      ok: true,
      commandAuthorized: true,
      effectiveWasMentioned: true,
      groupSystemPrompt: "group prompt",
    });
  });

  it("preserves allowlist group policy when a routed space has no sender allowlist", async () => {
    primeCommonDefaults();
    allowInboundGroupTraffic({
      effectiveGroupAllowFrom: [],
      effectiveWasMentioned: false,
    });
    resolveSenderScopedGroupPolicy.mockReturnValue("open");
    resolveSenderScopedGroupPolicy.mockClear();
    resolveDmGroupAccessWithLists.mockClear();

    await expect(
      applyInboundAccessPolicy({
        account: {
          accountId: "default",
          config: {
            groups: {
              "spaces/AAA": {
                enabled: true,
              },
            },
          },
        } as never,
      }),
    ).resolves.toEqual({
      ok: true,
      commandAuthorized: undefined,
      effectiveWasMentioned: false,
      groupSystemPrompt: undefined,
    });

    expect(resolveSenderScopedGroupPolicy).not.toHaveBeenCalled();
    expect(resolveDmGroupAccessWithLists).toHaveBeenCalledWith(
      expect.objectContaining({
        groupPolicy: "allowlist",
        groupAllowFrom: [],
      }),
    );
  });

  it("drops unauthorized group control commands", async () => {
    primeCommonDefaults();
    allowInboundGroupTraffic({
      effectiveGroupAllowFrom: [],
      effectiveWasMentioned: false,
    });
    const core = createCore();
    core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true);
    core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(false);
    core.channel.commands.isControlCommandMessage.mockReturnValue(true);
    const logVerbose = vi.fn();

    await expect(
      applyInboundAccessPolicy({
        core: core as never,
        rawBody: "/admin",
        logVerbose,
      }),
    ).resolves.toEqual({ ok: false });

    expect(logVerbose).toHaveBeenCalledWith("googlechat: drop control command from users/alice");
  });

  it("does not match group policy by mutable space displayName when the stable id differs", async () => {
    primeCommonDefaults();
    allowInboundGroupTraffic();
    const logVerbose = vi.fn();

    await expect(
      applyInboundAccessPolicy({
        account: {
          accountId: "default",
          config: {
            groups: {
              "Finance Ops": {
                users: ["users/alice"],
                requireMention: true,
                systemPrompt: "finance-only prompt",
              },
            },
          },
        } as never,
        core: createCore() as never,
        space: { name: "spaces/BBB", displayName: "Finance Ops" } as never,
        message: {
          annotations: [
            {
              type: "USER_MENTION",
              userMention: { user: { name: "users/app" } },
            },
          ],
        } as never,
        rawBody: "show quarter close status",
        logVerbose,
      }),
    ).resolves.toEqual({ ok: false });

    expect(logVerbose).toHaveBeenCalledWith(
      "Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: Finance Ops",
    );
    expect(logVerbose).toHaveBeenCalledWith(
      "drop group message (deprecated mutable group key matched, space=spaces/BBB)",
    );
  });

  it("fails closed instead of falling back to wildcard when a deprecated room key matches", async () => {
    primeCommonDefaults();
    resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
      groupPolicy: "open",
      providerMissingFallbackApplied: false,
    });
    allowInboundGroupTraffic();
    const logVerbose = vi.fn();

    await expect(
      applyInboundAccessPolicy({
        account: {
          accountId: "default",
          config: {
            groupPolicy: "open",
            groups: {
              "*": {
                users: ["users/alice"],
              },
              "Finance Ops": {
                enabled: false,
                users: ["users/bob"],
              },
            },
          },
        } as never,
        core: createCore() as never,
        space: { name: "spaces/BBB", displayName: "Finance Ops" } as never,
        rawBody: "show quarter close status",
        logVerbose,
      }),
    ).resolves.toEqual({ ok: false });

    expect(logVerbose).toHaveBeenCalledWith(
      "drop group message (deprecated mutable group key matched, space=spaces/BBB)",
    );
  });
});
