import { DingTalkService } from './dingtalk.service';

describe('DingTalkService — queryShifts', () => {
  const originalAppKey = process.env.DINGTALK_APP_KEY;
  const originalAppSecret = process.env.DINGTALK_APP_SECRET;
  let service: DingTalkService;

  beforeEach(() => {
    process.env.DINGTALK_APP_KEY = 'test-app-key';
    process.env.DINGTALK_APP_SECRET = 'test-app-secret';

    service = new DingTalkService({} as never, {} as never);
    Object.assign(service, {
      accessToken: 'test-token',
      accessTokenCredentialKey: 'test-app-key:test-app-secret',
      tokenExpiresAt: Date.now() + 3_600_000,
    });
    jest.spyOn(service, 'rateLimit').mockResolvedValue(undefined);
  });

  afterEach(() => {
    jest.restoreAllMocks();
    global.fetch = undefined as unknown as typeof fetch;
  });

  afterAll(() => {
    if (originalAppKey === undefined) delete process.env.DINGTALK_APP_KEY;
    else process.env.DINGTALK_APP_KEY = originalAppKey;
    if (originalAppSecret === undefined) delete process.env.DINGTALK_APP_SECRET;
    else process.env.DINGTALK_APP_SECRET = originalAppSecret;
  });

  it('unwraps the paged result object returned by DingTalk', async () => {
    global.fetch = jest.fn().mockResolvedValue({
      json: jest.fn().mockResolvedValue({
        errcode: 0,
        errmsg: 'ok',
        result: {
          cursor: 678215070,
          has_more: false,
          result: [
            { id: 677995086, name: 'A' },
            { id: 678215070, name: 'B' },
          ],
        },
      }),
    }) as jest.MockedFunction<typeof fetch>;

    await expect(service.queryShifts('manager')).resolves.toEqual([
      { id: 677995086, name: 'A' },
      { id: 678215070, name: 'B' },
    ]);
  });

  it('requests subsequent pages using the cursor returned by DingTalk', async () => {
    global.fetch = jest
      .fn()
      .mockResolvedValueOnce({
        json: jest.fn().mockResolvedValue({
          errcode: 0,
          errmsg: 'ok',
          result: {
            cursor: 200,
            has_more: true,
            result: [{ id: 100, name: '早班' }],
          },
        }),
      })
      .mockResolvedValueOnce({
        json: jest.fn().mockResolvedValue({
          errcode: 0,
          errmsg: 'ok',
          result: {
            cursor: 300,
            has_more: false,
            result: [{ id: 200, name: '晚班' }],
          },
        }),
      }) as jest.MockedFunction<typeof fetch>;

    await expect(service.queryShifts('manager')).resolves.toEqual([
      { id: 100, name: '早班' },
      { id: 200, name: '晚班' },
    ]);

    expect(global.fetch).toHaveBeenCalledTimes(2);
    expect(JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body)).toEqual({
      op_user_id: 'manager',
      cursor: 0,
    });
    expect(JSON.parse((global.fetch as jest.Mock).mock.calls[1][1].body)).toEqual({
      op_user_id: 'manager',
      cursor: 200,
    });
  });
});

describe('DingTalkService — attendance machine only group', () => {
  const originalAppKey = process.env.DINGTALK_APP_KEY;
  const originalAppSecret = process.env.DINGTALK_APP_SECRET;
  let service: DingTalkService;

  beforeEach(() => {
    process.env.DINGTALK_APP_KEY = 'test-app-key';
    process.env.DINGTALK_APP_SECRET = 'test-app-secret';
    service = new DingTalkService({} as never, {} as never);
    Object.assign(service, {
      accessToken: 'test-token',
      accessTokenCredentialKey: 'test-app-key:test-app-secret',
      tokenExpiresAt: Date.now() + 3_600_000,
    });
    jest.spyOn(service, 'rateLimit').mockResolvedValue(undefined);
  });

  afterEach(() => {
    jest.restoreAllMocks();
    global.fetch = undefined as unknown as typeof fetch;
  });

  afterAll(() => {
    if (originalAppKey === undefined) delete process.env.DINGTALK_APP_KEY;
    else process.env.DINGTALK_APP_KEY = originalAppKey;
    if (originalAppSecret === undefined) delete process.env.DINGTALK_APP_SECRET;
    else process.env.DINGTALK_APP_SECRET = originalAppSecret;
  });

  it('accepts the success envelope returned when updating an attendance group', async () => {
    global.fetch = jest.fn().mockResolvedValue({
      json: jest.fn().mockResolvedValue({
        success: true,
        result: { id: 123, name: '排课_冲刺班' },
        request_id: 'request-1',
      }),
    }) as jest.MockedFunction<typeof fetch>;

    await expect(
      service.updateAttendanceGroup({
        id: 123,
        name: '排课_冲刺班',
        type: 'TURN',
        owner: 'manager',
        members: [{ role: 'Attendance', type: 'StaffMember', user_id: 'student-1' }],
        shift_ids: [456],
      }),
    ).resolves.toBeUndefined();
  });

  it('disables mobile-oriented punching when creating a machine-only group', async () => {
    global.fetch = jest.fn().mockResolvedValue({
      json: jest.fn().mockResolvedValue({
        errcode: 0,
        errmsg: 'ok',
        result: { id: 123 },
      }),
    }) as jest.MockedFunction<typeof fetch>;

    await service.createAttendanceGroup({
      name: '排课_冲刺班',
      type: 'TURN',
      owner: 'manager',
      members: [{ role: 'Attendance', type: 'StaffMember', user_id: 'student-1' }],
      shift_ids: [456],
      attendance_machine_only: true,
    });

    const body = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body);
    expect(body.top_group).toEqual(expect.objectContaining({
      enable_emp_select_class: false,
      disable_check_without_schedule: true,
      enable_outside_check: false,
      enable_position_ble: false,
      positions: [],
      wifis: [],
    }));
  });
});

describe('DingTalkService — department user pagination boundaries', () => {
  type PrivateDingTalkService = {
    getDeptUsers(token: string, deptId: number): Promise<unknown[]>;
  };
  afterEach(() => {
    jest.restoreAllMocks();
    global.fetch = undefined as unknown as typeof fetch;
  });

  it('fails when DingTalk says there is another page but omits the next cursor', async () => {
    const service = new DingTalkService({} as never, {} as never);
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({
        errcode: 0,
        errmsg: 'ok',
        result: {
          list: [{ userid: 'u1', name: 'Alice', mobile: '', dept_id_list: [1] }],
          has_more: true,
        },
      }),
    }) as jest.MockedFunction<typeof fetch>;

    await expect((service as unknown as PrivateDingTalkService).getDeptUsers('token', 1)).rejects.toThrow('分页游标未前进');
    expect(global.fetch).toHaveBeenCalledTimes(1);
  });

  it('fails when the next cursor repeats the current cursor', async () => {
    const service = new DingTalkService({} as never, {} as never);
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({
        errcode: 0,
        errmsg: 'ok',
        result: { list: [], has_more: true, next_cursor: 0 },
      }),
    }) as jest.MockedFunction<typeof fetch>;

    await expect((service as unknown as PrivateDingTalkService).getDeptUsers('token', 1)).rejects.toThrow('分页游标未前进');
    expect(global.fetch).toHaveBeenCalledTimes(1);
  });

  it('fails on a DingTalk API error instead of returning a partial user list', async () => {
    const service = new DingTalkService({} as never, {} as never);
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({ errcode: 40035, errmsg: 'invalid department' }),
    }) as jest.MockedFunction<typeof fetch>;

    await expect((service as unknown as PrivateDingTalkService).getDeptUsers('token', 9)).rejects.toThrow('invalid department');
  });

  it('keeps a multi-department user visible in the selected subtree', async () => {
    const service = new DingTalkService({} as never, {} as never);
    const privateService = service as unknown as {
      isConfigured(): Promise<boolean>;
      getAccessToken(): Promise<string>;
      getDeptInfo(token: string, deptId: number): Promise<{ name: string; parent_id: number }>;
      buildDeptNode(token: string, deptId: number, name: string, parentId: number): Promise<{
        id: number;
        name: string;
        parentId: number;
        children: [];
      }>;
      getDeptUsers(token: string, deptId: number): Promise<Array<{
        userid: string;
        name: string;
        mobile: string;
        dept_id_list: number[];
      }>>;
    };
    jest.spyOn(privateService, 'isConfigured').mockResolvedValue(true);
    jest.spyOn(privateService, 'getAccessToken').mockResolvedValue('token');
    jest.spyOn(privateService, 'getDeptInfo').mockResolvedValue({ name: '子部门', parent_id: 1 });
    jest.spyOn(privateService, 'buildDeptNode').mockResolvedValue({
      id: 2,
      name: '子部门',
      parentId: 1,
      children: [],
    });
    jest.spyOn(privateService, 'getDeptUsers').mockResolvedValue([
      { userid: 'u1', name: 'Alice', mobile: '', dept_id_list: [2, 99] },
    ]);

    const tree = await service.fetchOrgTreeWithUsers(2);

    expect(tree[0].users).toEqual([
      expect.objectContaining({ userid: 'u1', deptIds: [2, 99] }),
    ]);
  });
});
