import { describe, expect, it } from "vitest";
import {
  DM_GROUP_ACCESS_REASON,
  readStoreAllowFromForDmPolicy,
  resolveDmAllowState,
  resolveDmGroupAccessWithCommandGate,
  resolveDmGroupAccessDecision,
  resolveDmGroupAccessWithLists,
  resolveEffectiveAllowFromLists,
  resolvePinnedMainDmOwnerFromAllowlist,
} from "./dm-policy-shared.js";

describe("security/dm-policy-shared", () => {
  const controlCommand = {
    useAccessGroups: true,
    allowTextCommands: true,
    hasControlCommand: true,
  } as const;

  async function expectStoreReadSkipped(params: {
    provider: string;
    accountId: string;
    dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
    shouldRead?: boolean;
  }) {
    let called = false;
    const storeAllowFrom = await readStoreAllowFromForDmPolicy({
      provider: params.provider,
      accountId: params.accountId,
      ...(params.dmPolicy ? { dmPolicy: params.dmPolicy } : {}),
      ...(params.shouldRead !== undefined ? { shouldRead: params.shouldRead } : {}),
      readStore: async (_provider, _accountId) => {
        called = true;
        return ["should-not-be-read"];
      },
    });
    expect(called).toBe(false);
    expect(storeAllowFrom).toEqual([]);
  }

  function resolveCommandGate(overrides: {
    isGroup: boolean;
    isSenderAllowed: (allowFrom: string[]) => boolean;
    groupPolicy?: "open" | "allowlist" | "disabled";
  }) {
    return resolveDmGroupAccessWithCommandGate({
      dmPolicy: "pairing",
      groupPolicy: overrides.groupPolicy ?? "allowlist",
      allowFrom: ["owner"],
      groupAllowFrom: ["group-owner"],
      storeAllowFrom: ["paired-user"],
      command: controlCommand,
      ...overrides,
    });
  }

  it("normalizes config + store allow entries and counts distinct senders", async () => {
    const state = await resolveDmAllowState({
      provider: "demo-channel-a" as never,
      accountId: "default",
      allowFrom: [" * ", " alice ", "ALICE", "bob"],
      normalizeEntry: (value) => value.toLowerCase(),
      readStore: async (_provider, _accountId) => [" Bob ", "carol", ""],
    });
    expect(state.configAllowFrom).toEqual(["*", "alice", "ALICE", "bob"]);
    expect(state.hasWildcard).toBe(true);
    expect(state.allowCount).toBe(3);
    expect(state.isMultiUserDm).toBe(true);
  });

  it("handles empty allowlists and store failures", async () => {
    const state = await resolveDmAllowState({
      provider: "demo-channel-b" as never,
      accountId: "default",
      allowFrom: undefined,
      readStore: async (_provider, _accountId) => {
        throw new Error("offline");
      },
    });
    expect(state.configAllowFrom).toEqual([]);
    expect(state.hasWildcard).toBe(false);
    expect(state.allowCount).toBe(0);
    expect(state.isMultiUserDm).toBe(false);
  });

  it.each([
    {
      name: "dmPolicy is allowlist",
      params: {
        provider: "demo-channel-a",
        accountId: "default",
        dmPolicy: "allowlist" as const,
      },
    },
    {
      name: "shouldRead=false",
      params: {
        provider: "demo-channel-b",
        accountId: "default",
        shouldRead: false,
      },
    },
  ] as const)("skips pairing-store reads when $name", async ({ params }) => {
    await expectStoreReadSkipped(params);
  });

  it("builds effective DM/group allowlists from config + pairing store", () => {
    const lists = resolveEffectiveAllowFromLists({
      allowFrom: [" owner ", "", "owner2"],
      groupAllowFrom: ["group:abc"],
      storeAllowFrom: [" owner3 ", ""],
    });
    expect(lists.effectiveAllowFrom).toEqual(["owner", "owner2", "owner3"]);
    expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc"]);
  });

  it("falls back to DM allowlist for groups when groupAllowFrom is empty", () => {
    const lists = resolveEffectiveAllowFromLists({
      allowFrom: [" owner "],
      groupAllowFrom: [],
      storeAllowFrom: [" owner2 "],
    });
    expect(lists.effectiveAllowFrom).toEqual(["owner", "owner2"]);
    expect(lists.effectiveGroupAllowFrom).toEqual(["owner"]);
  });

  it("can keep group allowlist empty when fallback is disabled", () => {
    const lists = resolveEffectiveAllowFromLists({
      allowFrom: ["owner"],
      groupAllowFrom: [],
      storeAllowFrom: ["paired-user"],
      groupAllowFromFallbackToAllowFrom: false,
    });
    expect(lists.effectiveAllowFrom).toEqual(["owner", "paired-user"]);
    expect(lists.effectiveGroupAllowFrom).toEqual([]);
  });

  it("infers pinned main DM owner from a single configured allowlist entry", () => {
    const pinnedOwner = resolvePinnedMainDmOwnerFromAllowlist({
      dmScope: "main",
      allowFrom: [" line:user:U123 "],
      normalizeEntry: (entry) =>
        entry
          .trim()
          .toLowerCase()
          .replace(/^line:(?:user:)?/, ""),
    });
    expect(pinnedOwner).toBe("u123");
  });

  it.each([
    {
      name: "wildcard allowlist",
      dmScope: "main" as const,
      allowFrom: ["*"],
    },
    {
      name: "multi-owner allowlist",
      dmScope: "main" as const,
      allowFrom: ["u123", "u456"],
    },
    {
      name: "non-main scope",
      dmScope: "per-channel-peer" as const,
      allowFrom: ["u123"],
    },
  ] as const)("does not infer pinned owner for $name", ({ dmScope, allowFrom }) => {
    expect(
      resolvePinnedMainDmOwnerFromAllowlist({
        dmScope,
        allowFrom: [...allowFrom],
        normalizeEntry: (entry) => entry.trim(),
      }),
    ).toBeNull();
  });

  it("excludes storeAllowFrom when dmPolicy is allowlist", () => {
    const lists = resolveEffectiveAllowFromLists({
      allowFrom: ["+1111"],
      groupAllowFrom: ["group:abc"],
      storeAllowFrom: ["+2222", "+3333"],
      dmPolicy: "allowlist",
    });
    expect(lists.effectiveAllowFrom).toEqual(["+1111"]);
    expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc"]);
  });

  it("keeps group allowlist explicit when dmPolicy is pairing", () => {
    const lists = resolveEffectiveAllowFromLists({
      allowFrom: ["+1111"],
      groupAllowFrom: [],
      storeAllowFrom: ["+2222"],
      dmPolicy: "pairing",
    });
    expect(lists.effectiveAllowFrom).toEqual(["+1111", "+2222"]);
    expect(lists.effectiveGroupAllowFrom).toEqual(["+1111"]);
  });

  it("resolves access + effective allowlists in one shared call", () => {
    const resolved = resolveDmGroupAccessWithLists({
      isGroup: false,
      dmPolicy: "pairing",
      groupPolicy: "allowlist",
      allowFrom: ["owner"],
      groupAllowFrom: ["group:room"],
      storeAllowFrom: ["paired-user"],
      isSenderAllowed: (allowFrom) => allowFrom.includes("paired-user"),
    });
    expect(resolved.decision).toBe("allow");
    expect(resolved.reasonCode).toBe(DM_GROUP_ACCESS_REASON.DM_POLICY_ALLOWLISTED);
    expect(resolved.reason).toBe("dmPolicy=pairing (allowlisted)");
    expect(resolved.effectiveAllowFrom).toEqual(["owner", "paired-user"]);
    expect(resolved.effectiveGroupAllowFrom).toEqual(["group:room"]);
  });

  it("resolves command gate with dm/group parity for groups", () => {
    const resolved = resolveCommandGate({
      isGroup: true,
      isSenderAllowed: (allowFrom) => allowFrom.includes("paired-user"),
    });
    expect(resolved.decision).toBe("block");
    expect(resolved.reason).toBe("groupPolicy=allowlist (not allowlisted)");
    expect(resolved.commandAuthorized).toBe(false);
    expect(resolved.shouldBlockControlCommand).toBe(true);
  });

  it("keeps configured dm allowlist usable for group command auth", () => {
    const resolved = resolveDmGroupAccessWithCommandGate({
      isGroup: true,
      dmPolicy: "pairing",
      groupPolicy: "open",
      allowFrom: ["owner"],
      groupAllowFrom: [],
      storeAllowFrom: ["paired-user"],
      isSenderAllowed: (allowFrom) => allowFrom.includes("owner"),
      command: controlCommand,
    });
    expect(resolved.commandAuthorized).toBe(true);
    expect(resolved.shouldBlockControlCommand).toBe(false);
  });

  it("treats dm command authorization as dm access result", () => {
    const resolved = resolveCommandGate({
      isGroup: false,
      isSenderAllowed: (allowFrom) => allowFrom.includes("paired-user"),
    });
    expect(resolved.decision).toBe("allow");
    expect(resolved.commandAuthorized).toBe(true);
    expect(resolved.shouldBlockControlCommand).toBe(false);
  });

  it("does not auto-authorize dm commands in open mode without explicit allowlists", () => {
    const resolved = resolveDmGroupAccessWithCommandGate({
      isGroup: false,
      dmPolicy: "open",
      groupPolicy: "allowlist",
      allowFrom: [],
      groupAllowFrom: [],
      storeAllowFrom: [],
      isSenderAllowed: () => false,
      command: controlCommand,
    });
    expect(resolved.decision).toBe("allow");
    expect(resolved.commandAuthorized).toBe(false);
    expect(resolved.shouldBlockControlCommand).toBe(false);
  });

  it("keeps allowlist mode strict in shared resolver (no pairing-store fallback)", () => {
    const resolved = resolveDmGroupAccessWithLists({
      isGroup: false,
      dmPolicy: "allowlist",
      groupPolicy: "allowlist",
      allowFrom: ["owner"],
      groupAllowFrom: [],
      storeAllowFrom: ["paired-user"],
      isSenderAllowed: () => false,
    });
    expect(resolved.decision).toBe("block");
    expect(resolved.reasonCode).toBe(DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED);
    expect(resolved.reason).toBe("dmPolicy=allowlist (not allowlisted)");
    expect(resolved.effectiveAllowFrom).toEqual(["owner"]);
  });

  const channels = [
    "bluebubbles",
    "imessage",
    "signal",
    "telegram",
    "whatsapp",
    "msteams",
    "matrix",
    "zalo",
  ] as const;

  type ParityCase = {
    name: string;
    isGroup: boolean;
    dmPolicy: "open" | "allowlist" | "pairing" | "disabled";
    groupPolicy: "open" | "allowlist" | "disabled";
    allowFrom: string[];
    groupAllowFrom: string[];
    storeAllowFrom: string[];
    isSenderAllowed: (allowFrom: string[]) => boolean;
    expectedDecision: "allow" | "block" | "pairing";
    expectedReactionAllowed: boolean;
  };

  type DecisionCase = {
    name: string;
    input: Parameters<typeof resolveDmGroupAccessDecision>[0];
    expected:
      | ReturnType<typeof resolveDmGroupAccessDecision>
      | Pick<ReturnType<typeof resolveDmGroupAccessDecision>, "decision">;
  };

  function createParityCase({
    name,
    ...overrides
  }: Partial<ParityCase> & Pick<ParityCase, "name">): ParityCase {
    return {
      name,
      isGroup: false,
      dmPolicy: "open",
      groupPolicy: "allowlist",
      allowFrom: [],
      groupAllowFrom: [],
      storeAllowFrom: [],
      isSenderAllowed: () => false,
      expectedDecision: "allow",
      expectedReactionAllowed: true,
      ...overrides,
    };
  }

  function expectParityCase(channel: (typeof channels)[number], testCase: ParityCase) {
    const access = resolveDmGroupAccessWithLists({
      isGroup: testCase.isGroup,
      dmPolicy: testCase.dmPolicy,
      groupPolicy: testCase.groupPolicy,
      allowFrom: testCase.allowFrom,
      groupAllowFrom: testCase.groupAllowFrom,
      storeAllowFrom: testCase.storeAllowFrom,
      isSenderAllowed: testCase.isSenderAllowed,
    });
    const reactionAllowed = access.decision === "allow";
    expect(access.decision, `[${channel}] ${testCase.name}`).toBe(testCase.expectedDecision);
    expect(reactionAllowed, `[${channel}] ${testCase.name} reaction`).toBe(
      testCase.expectedReactionAllowed,
    );
  }

  it.each(
    channels.flatMap((channel) =>
      [
        createParityCase({
          name: "dmPolicy=open",
          dmPolicy: "open",
          expectedDecision: "allow",
          expectedReactionAllowed: true,
        }),
        createParityCase({
          name: "dmPolicy=disabled",
          dmPolicy: "disabled",
          expectedDecision: "block",
          expectedReactionAllowed: false,
        }),
        createParityCase({
          name: "dmPolicy=allowlist unauthorized",
          dmPolicy: "allowlist",
          allowFrom: ["owner"],
          isSenderAllowed: () => false,
          expectedDecision: "block",
          expectedReactionAllowed: false,
        }),
        createParityCase({
          name: "dmPolicy=allowlist authorized",
          dmPolicy: "allowlist",
          allowFrom: ["owner"],
          isSenderAllowed: () => true,
          expectedDecision: "allow",
          expectedReactionAllowed: true,
        }),
        createParityCase({
          name: "dmPolicy=pairing unauthorized",
          dmPolicy: "pairing",
          isSenderAllowed: () => false,
          expectedDecision: "pairing",
          expectedReactionAllowed: false,
        }),
        createParityCase({
          name: "groupPolicy=allowlist rejects DM-paired sender not in explicit group list",
          isGroup: true,
          dmPolicy: "pairing",
          allowFrom: ["owner"],
          groupAllowFrom: ["group-owner"],
          storeAllowFrom: ["paired-user"],
          isSenderAllowed: (allowFrom: string[]) => allowFrom.includes("paired-user"),
          expectedDecision: "block",
          expectedReactionAllowed: false,
        }),
      ].map((testCase) => ({
        channel,
        testCase,
      })),
    ),
  )(
    "keeps message/reaction policy parity table across channels: [$channel] $testCase.name",
    ({ channel, testCase }) => {
      expectParityCase(channel, testCase);
    },
  );

  const decisionCases: DecisionCase[] = [
    {
      name: "blocks groups when group allowlist is empty",
      input: {
        isGroup: true,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
        effectiveAllowFrom: ["owner"],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => false,
      },
      expected: {
        decision: "block",
        reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST,
        reason: "groupPolicy=allowlist (empty allowlist)",
      },
    },
    {
      name: "allows groups when group policy is open",
      input: {
        isGroup: true,
        dmPolicy: "pairing",
        groupPolicy: "open",
        effectiveAllowFrom: ["owner"],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => false,
      },
      expected: {
        decision: "allow",
        reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_ALLOWED,
        reason: "groupPolicy=open",
      },
    },
    {
      name: "blocks DM allowlist mode when allowlist is empty",
      input: {
        isGroup: false,
        dmPolicy: "allowlist",
        groupPolicy: "allowlist",
        effectiveAllowFrom: [],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => false,
      },
      expected: {
        decision: "block",
        reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
        reason: "dmPolicy=allowlist (not allowlisted)",
      },
    },
    {
      name: "uses pairing flow when DM sender is not allowlisted",
      input: {
        isGroup: false,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
        effectiveAllowFrom: [],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => false,
      },
      expected: {
        decision: "pairing",
        reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_PAIRING_REQUIRED,
        reason: "dmPolicy=pairing (not allowlisted)",
      },
    },
    {
      name: "allows DM sender when allowlisted",
      input: {
        isGroup: false,
        dmPolicy: "allowlist",
        groupPolicy: "allowlist",
        effectiveAllowFrom: ["owner"],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => true,
      },
      expected: {
        decision: "allow",
      },
    },
    {
      name: "blocks group allowlist mode when sender/group is not allowlisted",
      input: {
        isGroup: true,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
        effectiveAllowFrom: ["owner"],
        effectiveGroupAllowFrom: ["group:abc"],
        isSenderAllowed: () => false,
      },
      expected: {
        decision: "block",
        reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
        reason: "groupPolicy=allowlist (not allowlisted)",
      },
    },
  ];

  it.each(
    channels.flatMap((channel) =>
      decisionCases.map((testCase) => ({
        channel,
        testCase,
      })),
    ),
  )("[$channel] $testCase.name", ({ testCase }) => {
    const decision = resolveDmGroupAccessDecision(testCase.input);
    if ("reasonCode" in testCase.expected && "reason" in testCase.expected) {
      expect(decision).toEqual(testCase.expected);
      return;
    }
    expect(decision).toMatchObject(testCase.expected);
  });
});
