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: "telegram",
      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: "slack",
      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("skips pairing-store reads when dmPolicy is allowlist", async () => {
    await expectStoreReadSkipped({
      provider: "telegram",
      accountId: "default",
      dmPolicy: "allowlist",
    });
  });

  it("skips pairing-store reads when shouldRead=false", async () => {
    await expectStoreReadSkipped({
      provider: "slack",
      accountId: "default",
      shouldRead: false,
    });
  });

  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("does not infer pinned owner for wildcard/multi-owner/non-main scope", () => {
    expect(
      resolvePinnedMainDmOwnerFromAllowlist({
        dmScope: "main",
        allowFrom: ["*"],
        normalizeEntry: (entry) => entry.trim(),
      }),
    ).toBeNull();
    expect(
      resolvePinnedMainDmOwnerFromAllowlist({
        dmScope: "main",
        allowFrom: ["u123", "u456"],
        normalizeEntry: (entry) => entry.trim(),
      }),
    ).toBeNull();
    expect(
      resolvePinnedMainDmOwnerFromAllowlist({
        dmScope: "per-channel-peer",
        allowFrom: ["u123"],
        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;
  };

  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,
    };
  }

  it("keeps message/reaction policy parity table across channels", () => {
    const cases = [
      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,
      }),
    ];

    for (const channel of channels) {
      for (const testCase of cases) {
        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,
        );
      }
    }
  });

  for (const channel of channels) {
    it(`[${channel}] blocks DM allowlist mode when allowlist is empty`, () => {
      const decision = resolveDmGroupAccessDecision({
        isGroup: false,
        dmPolicy: "allowlist",
        groupPolicy: "allowlist",
        effectiveAllowFrom: [],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => false,
      });
      expect(decision).toEqual({
        decision: "block",
        reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
        reason: "dmPolicy=allowlist (not allowlisted)",
      });
    });

    it(`[${channel}] uses pairing flow when DM sender is not allowlisted`, () => {
      const decision = resolveDmGroupAccessDecision({
        isGroup: false,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
        effectiveAllowFrom: [],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => false,
      });
      expect(decision).toEqual({
        decision: "pairing",
        reasonCode: DM_GROUP_ACCESS_REASON.DM_POLICY_PAIRING_REQUIRED,
        reason: "dmPolicy=pairing (not allowlisted)",
      });
    });

    it(`[${channel}] allows DM sender when allowlisted`, () => {
      const decision = resolveDmGroupAccessDecision({
        isGroup: false,
        dmPolicy: "allowlist",
        groupPolicy: "allowlist",
        effectiveAllowFrom: ["owner"],
        effectiveGroupAllowFrom: [],
        isSenderAllowed: () => true,
      });
      expect(decision.decision).toBe("allow");
    });

    it(`[${channel}] blocks group allowlist mode when sender/group is not allowlisted`, () => {
      const decision = resolveDmGroupAccessDecision({
        isGroup: true,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
        effectiveAllowFrom: ["owner"],
        effectiveGroupAllowFrom: ["group:abc"],
        isSenderAllowed: () => false,
      });
      expect(decision).toEqual({
        decision: "block",
        reasonCode: DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
        reason: "groupPolicy=allowlist (not allowlisted)",
      });
    });
  }
});
