jest.mock("../models/group-invites.model", () => ({
  addGroupMember: jest.fn(),
  createGroupAuditLog: jest.fn(),
  createInviteLink: jest.fn(),
  createOrTouchPendingJoinRequest: jest.fn(),
  consumeSingleUseInvite: jest.fn(),
  deleteInviteLink: jest.fn(),
  getGroupById: jest.fn(),
  getGroupSettings: jest.fn(),
  getInviteLinkById: jest.fn(),
  getInviteLinkSummaryByToken: jest.fn(),
  getManageableGroupsForUser: jest.fn(),
  getPendingJoinRequest: jest.fn(),
  getUserGroupRole: jest.fn(),
  isGroupMember: jest.fn(),
  listInviteLinks: jest.fn(),
  revokeInviteLink: jest.fn(),
  reviveInviteLink: jest.fn(),
  upsertGroupSettings: jest.fn(),
  withTransaction: jest.fn(),
}));

const invitesModel = require("../models/group-invites.model");
const invitesService = require("../services/group-invites.service");

function inviteSummary(overrides = {}) {
  return {
    id: 30,
    group_id: 10,
    group_name: "Test Group",
    token: "1234567890abcdef1234567890fedcba",
    policy: "AUTO_ACCEPT",
    current_join_policy: "AUTO_ACCEPT",
    single_use: false,
    expires_at: "2030-01-01T00:00:00.000Z",
    used_at: null,
    revoked_at: null,
    ...overrides,
  };
}

describe("group invites service", () => {
  beforeEach(() => {
    invitesModel.withTransaction.mockImplementation(async (handler) => handler({}));
  });

  test("create link success writes audit with request_id and token last4 only", async () => {
    invitesModel.getGroupById.mockResolvedValue({ id: 1, name: "G1" });
    invitesModel.getUserGroupRole.mockResolvedValue("admin");
    invitesModel.createInviteLink.mockResolvedValue({
      id: 55,
      group_id: 1,
      token: "1234567890abcdef1234567890fedcba",
      policy: "AUTO_ACCEPT",
      single_use: true,
      expires_at: "2030-01-01T00:00:00.000Z",
      created_at: "2026-01-01T00:00:00.000Z",
    });

    const link = await invitesService.createInviteLink(
      7,
      1,
      "AUTO_ACCEPT",
      true,
      "2030-01-01T00:00:00.000Z",
      "req-123",
      "127.0.0.1",
      "ua"
    );

    expect(link.id).toBe(55);
    expect(invitesModel.createGroupAuditLog).toHaveBeenCalledTimes(1);
    const auditPayload = invitesModel.createGroupAuditLog.mock.calls[0][0];
    expect(auditPayload.requestId).toBe("req-123");
    expect(auditPayload.metadata).toEqual({ inviteCodeLast4: "dcba" });
  });

  test("accept auto-accept adds membership", async () => {
    invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(inviteSummary());
    invitesModel.isGroupMember.mockResolvedValue(false);
    invitesModel.getPendingJoinRequest.mockResolvedValue(null);
    invitesModel.getUserGroupRole.mockResolvedValue(null);
    invitesModel.addGroupMember.mockResolvedValue(true);

    const result = await invitesService.acceptInviteLink(
      99,
      "token-1",
      "req-1",
      "127.0.0.1",
      "ua"
    );

    expect(result).toEqual({
      status: "JOINED",
      group: { id: 10, name: "Test Group" },
    });
    expect(invitesModel.addGroupMember).toHaveBeenCalled();
    expect(invitesModel.createGroupAuditLog.mock.calls[0][0].eventType).toBe(
      "GROUP_INVITE_USED"
    );
  });

  test("accept manual policy creates pending request", async () => {
    invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(
      inviteSummary({
        current_join_policy: "APPROVAL_REQUIRED",
        single_use: true,
      })
    );
    invitesModel.isGroupMember.mockResolvedValue(false);
    invitesModel.getPendingJoinRequest.mockResolvedValue(null);
    invitesModel.getUserGroupRole.mockResolvedValue(null);
    invitesModel.createOrTouchPendingJoinRequest.mockResolvedValue({
      id: 1,
      status: "PENDING",
    });

    const result = await invitesService.acceptInviteLink(
      99,
      "token-2",
      "req-2",
      "127.0.0.1",
      "ua"
    );

    expect(result).toEqual({
      status: "PENDING",
      group: { id: 10, name: "Test Group" },
    });
    expect(invitesModel.createOrTouchPendingJoinRequest).toHaveBeenCalled();
    expect(invitesModel.consumeSingleUseInvite).toHaveBeenCalledWith(30, {});
    expect(invitesModel.createGroupAuditLog.mock.calls[0][0].eventType).toBe(
      "GROUP_INVITE_REQUESTED"
    );
  });

  test.each([
    ["INVITE_EXPIRED", inviteSummary({ expires_at: "2020-01-01T00:00:00.000Z" })],
    ["INVITE_REVOKED", inviteSummary({ revoked_at: "2026-01-01T00:00:00.000Z" })],
    [
      "INVITE_USED",
      inviteSummary({ single_use: true, used_at: "2026-01-01T00:00:00.000Z" }),
    ],
  ])("rejects %s links", async (expectedCode, summary) => {
    invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(summary);
    invitesModel.isGroupMember.mockResolvedValue(false);
    invitesModel.getPendingJoinRequest.mockResolvedValue(null);

    await expect(
      invitesService.acceptInviteLink(99, "token-3", "req-3", "127.0.0.1", "ua")
    ).rejects.toMatchObject({ code: expectedCode });
  });

  test("accept returns ALREADY_MEMBER before pending checks", async () => {
    invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(inviteSummary());
    invitesModel.isGroupMember.mockResolvedValue(true);

    const result = await invitesService.acceptInviteLink(
      99,
      "token-4",
      "req-4",
      "127.0.0.1",
      "ua"
    );

    expect(result.status).toBe("ALREADY_MEMBER");
    expect(invitesModel.getPendingJoinRequest).not.toHaveBeenCalled();
  });

  test("accept returns PENDING when request already exists", async () => {
    invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(inviteSummary());
    invitesModel.isGroupMember.mockResolvedValue(false);
    invitesModel.getPendingJoinRequest.mockResolvedValue({
      id: 5,
      status: "PENDING",
    });

    const result = await invitesService.acceptInviteLink(
      99,
      "token-5",
      "req-5",
      "127.0.0.1",
      "ua"
    );

    expect(result.status).toBe("PENDING");
    expect(invitesModel.addGroupMember).not.toHaveBeenCalled();
  });
});
