import {
  OutlookSyncService,
  RECONCILE_SERIES_SNAPSHOT_BACKFILL_LIMIT,
  shouldUseStrictSeriesMasterFilter,
  STRICT_SERIES_FILTER_FRESHNESS_MS,
} from '@/modules/meeting-attendance/services/outlook-sync.service';

describe('OutlookSyncService seriesMaster strict filter', () => {
  it('游标不存在时不启用严格系列过滤', () => {
    const now = new Date('2026-03-04T10:00:00.000Z');

    expect(shouldUseStrictSeriesMasterFilter(null, now)).toBe(false);
    expect(shouldUseStrictSeriesMasterFilter(undefined, now)).toBe(false);
  });

  it('游标过旧时不启用严格系列过滤', () => {
    const now = new Date('2026-03-04T10:00:00.000Z');
    const staleSyncedAt = new Date(now.getTime() - STRICT_SERIES_FILTER_FRESHNESS_MS - 1);

    expect(shouldUseStrictSeriesMasterFilter(staleSyncedAt, now)).toBe(false);
  });

  it('游标在 10 分钟窗口内时启用严格系列过滤', () => {
    const now = new Date('2026-03-04T10:00:00.000Z');
    const freshSyncedAt = new Date(now.getTime() - STRICT_SERIES_FILTER_FRESHNESS_MS);

    expect(shouldUseStrictSeriesMasterFilter(freshSyncedAt, now)).toBe(true);
  });
});

describe('OutlookSyncService ended seriesMaster filter', () => {
  it('已结束 seriesMaster 不应出现在候选结果中', async () => {
    const repository = {} as any;
    const prisma = {
      outlookSyncCursor: {
        findUnique: jest.fn().mockResolvedValue({ lastSyncedAt: new Date('2026-03-07T10:00:00.000Z') }),
      },
      outlookEventSnapshot: {
        groupBy: jest.fn().mockResolvedValue([]),
        findMany: jest
          .fn()
          .mockResolvedValueOnce([
            {
              graphEventId: 'ended-series',
              rawPayload: {
                id: 'ended-series',
                type: 'seriesMaster',
                recurrence: {
                  range: {
                    endDate: '2026-02-24',
                    recurrenceTimeZone: 'Pacific Standard Time',
                  },
                },
              },
            },
          ])
          .mockResolvedValueOnce([]),
        count: jest.fn().mockResolvedValue(0),
      },
      outlookMeetingBinding: {
        findMany: jest.fn().mockResolvedValue([]),
      },
    } as any;
    const service = new OutlookSyncService({} as any, prisma, repository);

    const result = await (service as any).loadCandidateSnapshots({
      mailboxId: 'mailbox-1',
      actorId: 'actor-1',
      startDate: new Date('2026-02-28T00:00:00.000Z'),
      endDate: new Date('2026-09-03T00:00:00.000Z'),
      includeCancelled: false,
      includePast: false,
      onlyUnmanaged: false,
      page: 1,
      pageSize: 20,
    });

    expect(result).toEqual({ total: 0, items: [] });
  });
});

describe('OutlookSyncService listCandidateSeriesChildren', () => {
  it('快照已存在但被筛选过滤时不回退请求 Outlook', async () => {
    const repository = {
      getCursorByMailboxId: jest.fn().mockResolvedValue({ lastSyncedAt: new Date('2026-03-04T10:00:00.000Z') }),
    } as any;
    const prisma = {
      outlookEventSnapshot: {
        findUnique: jest.fn().mockResolvedValue({
          rawPayload: {
            id: 'series-master-1',
            type: 'seriesMaster',
            recurrence: {
              range: {
                endDate: '2026-12-31',
                recurrenceTimeZone: 'Pacific Standard Time',
              },
            },
          },
        }),
        count: jest.fn().mockResolvedValue(1),
      },
    } as any;
    const service = new OutlookSyncService({} as any, prisma, repository);
    const mailbox = { id: 'mailbox-1', mailboxEmail: 'admin@example.com', mailboxType: 'PERSONAL' };

    jest.spyOn(service as any, 'resolveCandidateMailbox').mockResolvedValue(mailbox);
    jest.spyOn(service as any, 'parseBooleanLike').mockReturnValue(false);
    jest.spyOn(service as any, 'loadCandidateSeriesChildrenFromSnapshots').mockResolvedValue([]);
    const fetchSeriesInstanceEventsSpy = jest
      .spyOn(service as any, 'fetchSeriesInstanceEvents')
      .mockResolvedValue([]);
    const enqueueMailboxSyncIfStaleSeriesSpy = jest
      .spyOn(service as any, 'enqueueMailboxSyncIfStaleSeries')
      .mockImplementation(() => {});

    const result = await service.listCandidateSeriesChildren(
      'series-master-1',
      {
        mailboxId: 'mailbox-1',
      } as any,
      { id: 'actor-1', email: 'admin@example.com' },
    );

    expect(fetchSeriesInstanceEventsSpy).not.toHaveBeenCalled();
    expect(enqueueMailboxSyncIfStaleSeriesSpy).toHaveBeenCalled();
    expect(result.items).toEqual([]);
  });

  it('已结束系列不返回候选子项', async () => {
    const repository = {
      getCursorByMailboxId: jest.fn(),
    } as any;
    const prisma = {
      outlookEventSnapshot: {
        findUnique: jest.fn().mockResolvedValue({
          rawPayload: {
            id: 'series-master-1',
            type: 'seriesMaster',
            recurrence: {
              range: {
                endDate: '2026-02-24',
                recurrenceTimeZone: 'Pacific Standard Time',
              },
            },
          },
        }),
      },
    } as any;
    const service = new OutlookSyncService({} as any, prisma, repository);
    const mailbox = { id: 'mailbox-1', mailboxEmail: 'admin@example.com', mailboxType: 'PERSONAL' };

    jest.spyOn(service as any, 'resolveCandidateMailbox').mockResolvedValue(mailbox);
    jest.spyOn(service as any, 'parseBooleanLike').mockReturnValue(false);
    const loadCandidateSeriesChildrenFromSnapshotsSpy = jest
      .spyOn(service as any, 'loadCandidateSeriesChildrenFromSnapshots')
      .mockResolvedValue([]);
    const fetchSeriesInstanceEventsSpy = jest
      .spyOn(service as any, 'fetchSeriesInstanceEvents')
      .mockResolvedValue([]);

    const result = await service.listCandidateSeriesChildren(
      'series-master-1',
      {
        mailboxId: 'mailbox-1',
      } as any,
      { id: 'actor-1', email: 'admin@example.com' },
    );

    expect(loadCandidateSeriesChildrenFromSnapshotsSpy).not.toHaveBeenCalled();
    expect(fetchSeriesInstanceEventsSpy).not.toHaveBeenCalled();
    expect(result.items).toEqual([]);
  });
});

describe('OutlookSyncService bootstrapSeriesOccurrences', () => {
  it('系列补齐优先使用 instances 接口', async () => {
    const repository = {
      getOrCreateSettings: jest.fn().mockResolvedValue({
        lookbackDays: 7,
        lookaheadDays: 180,
        deltaBatchSize: 100,
      }),
      isSeriesOccurrenceExcluded: jest.fn().mockResolvedValue(false),
    } as any;
    const prisma = {} as any;
    const service = new OutlookSyncService({} as any, prisma, repository);

    const seriesMasterId = 'series-master-1';
    const event = {
      id: 'occ-1',
      type: 'occurrence',
      seriesMasterId,
      start: { dateTime: '2026-03-10T10:00:00Z', timeZone: 'UTC' },
      end: { dateTime: '2026-03-10T11:00:00Z', timeZone: 'UTC' },
    };

    const fetchInstancesSpy = jest
      .spyOn(service as any, 'fetchSeriesInstanceEvents')
      .mockResolvedValue([event]);
    const fetchCalendarViewSpy = jest
      .spyOn(service as any, 'fetchCalendarViewEvents')
      .mockResolvedValue([]);
    jest.spyOn(service as any, 'upsertEventSnapshot').mockResolvedValue(undefined);
    jest.spyOn(service as any, 'isEventInSyncWindow').mockReturnValue(false);

    const result = await (service as any).bootstrapSeriesOccurrences(
      { id: 'mailbox-1', mailboxEmail: 'admin@example.com' },
      { id: seriesMasterId, type: 'seriesMaster' },
      { id: 'binding-1', graphEventType: 'seriesMaster', graphEventId: seriesMasterId, primaryMailboxId: 'mailbox-1', meetingSeriesId: 'series-1' },
      'actor-1',
    );

    expect(fetchInstancesSpy).toHaveBeenCalled();
    expect(fetchCalendarViewSpy).not.toHaveBeenCalled();
    expect(result).toMatchObject({
      source: 'instances',
      totalCandidates: 1,
      syncedCount: 0,
    });
  });

  it('instances 接口失败时回退 calendarView', async () => {
    const repository = {
      getOrCreateSettings: jest.fn().mockResolvedValue({
        lookbackDays: 7,
        lookaheadDays: 180,
        deltaBatchSize: 100,
      }),
      isSeriesOccurrenceExcluded: jest.fn().mockResolvedValue(false),
    } as any;
    const prisma = {} as any;
    const service = new OutlookSyncService({} as any, prisma, repository);

    const seriesMasterId = 'series-master-2';
    const event = {
      id: 'occ-2',
      type: 'occurrence',
      seriesMasterId,
      start: { dateTime: '2026-03-11T10:00:00Z', timeZone: 'UTC' },
      end: { dateTime: '2026-03-11T11:00:00Z', timeZone: 'UTC' },
    };

    const fetchInstancesSpy = jest
      .spyOn(service as any, 'fetchSeriesInstanceEvents')
      .mockRejectedValue(new Error('graph failed'));
    const fetchCalendarViewSpy = jest
      .spyOn(service as any, 'fetchCalendarViewEvents')
      .mockResolvedValue([event]);
    jest.spyOn(service as any, 'upsertEventSnapshot').mockResolvedValue(undefined);
    jest.spyOn(service as any, 'isEventInSyncWindow').mockReturnValue(false);

    const result = await (service as any).bootstrapSeriesOccurrences(
      { id: 'mailbox-1', mailboxEmail: 'admin@example.com' },
      { id: seriesMasterId, type: 'seriesMaster' },
      { id: 'binding-1', graphEventType: 'seriesMaster', graphEventId: seriesMasterId, primaryMailboxId: 'mailbox-1', meetingSeriesId: 'series-1' },
      'actor-1',
    );

    expect(fetchInstancesSpy).toHaveBeenCalled();
    expect(fetchCalendarViewSpy).toHaveBeenCalled();
    expect(result).toMatchObject({
      source: 'calendarViewFallback',
      totalCandidates: 1,
      syncedCount: 0,
    });
  });
});

describe('OutlookSyncService reconcile series snapshot backfill', () => {
  it('仅补齐未结束且缺少未来子快照的系列', async () => {
    const repository = {
      getOrCreateSettings: jest.fn().mockResolvedValue({
        lookbackDays: 7,
        lookaheadDays: 180,
      }),
    } as any;
    const prisma = {
      outlookEventSnapshot: {
        findMany: jest.fn().mockResolvedValue([
          {
            graphEventId: 'missing-series',
            rawPayload: {
              id: 'missing-series',
              type: 'seriesMaster',
              recurrence: { range: { endDate: '2026-12-31', recurrenceTimeZone: 'Pacific Standard Time' } },
            },
          },
          {
            graphEventId: 'existing-series',
            rawPayload: {
              id: 'existing-series',
              type: 'seriesMaster',
              recurrence: { range: { endDate: '2026-12-31', recurrenceTimeZone: 'Pacific Standard Time' } },
            },
          },
          {
            graphEventId: 'ended-series',
            rawPayload: {
              id: 'ended-series',
              type: 'seriesMaster',
              recurrence: { range: { endDate: '2026-02-24', recurrenceTimeZone: 'Pacific Standard Time' } },
            },
          },
        ]),
        groupBy: jest.fn().mockResolvedValue([
          { seriesMasterId: 'existing-series' },
        ]),
      },
    } as any;
    const service = new OutlookSyncService({} as any, prisma, repository);

    const fetchSeriesInstanceEventsSpy = jest
      .spyOn(service as any, 'fetchSeriesInstanceEvents')
      .mockResolvedValue([
        {
          id: 'occ-1',
          type: 'occurrence',
          seriesMasterId: 'missing-series',
          start: { dateTime: '2026-03-12T10:00:00Z', timeZone: 'UTC' },
          end: { dateTime: '2026-03-12T11:00:00Z', timeZone: 'UTC' },
        },
      ]);
    const upsertEventSnapshotSpy = jest
      .spyOn(service as any, 'upsertEventSnapshot')
      .mockResolvedValue(undefined);

    await (service as any).backfillCandidateSeriesChildrenSnapshotsForReconcile(
      { id: 'mailbox-1', mailboxEmail: 'admin@example.com' },
      new Date('2026-03-09T10:00:00.000Z'),
    );

    expect(fetchSeriesInstanceEventsSpy).toHaveBeenCalledTimes(1);
    expect(fetchSeriesInstanceEventsSpy).toHaveBeenCalledWith(
      'admin@example.com',
      'missing-series',
      expect.any(Date),
      expect.any(Date),
    );
    expect(upsertEventSnapshotSpy).toHaveBeenCalledWith('mailbox-1', expect.objectContaining({
      id: 'occ-1',
      seriesMasterId: 'missing-series',
    }));
  });

  it('单次对账补齐数量受上限约束', async () => {
    const seriesMasters = Array.from({ length: RECONCILE_SERIES_SNAPSHOT_BACKFILL_LIMIT + 5 }, (_, index) => ({
      graphEventId: `series-${index + 1}`,
      rawPayload: {
        id: `series-${index + 1}`,
        type: 'seriesMaster',
        recurrence: { range: { endDate: '2026-12-31', recurrenceTimeZone: 'Pacific Standard Time' } },
      },
    }));
    const repository = {
      getOrCreateSettings: jest.fn().mockResolvedValue({
        lookbackDays: 7,
        lookaheadDays: 180,
      }),
    } as any;
    const prisma = {
      outlookEventSnapshot: {
        findMany: jest.fn().mockResolvedValue(seriesMasters),
        groupBy: jest.fn().mockResolvedValue([]),
      },
    } as any;
    const service = new OutlookSyncService({} as any, prisma, repository);

    const fetchSeriesInstanceEventsSpy = jest
      .spyOn(service as any, 'fetchSeriesInstanceEvents')
      .mockResolvedValue([]);
    jest.spyOn(service as any, 'upsertEventSnapshot').mockResolvedValue(undefined);

    await (service as any).backfillCandidateSeriesChildrenSnapshotsForReconcile(
      { id: 'mailbox-1', mailboxEmail: 'admin@example.com' },
      new Date('2026-03-09T10:00:00.000Z'),
    );

    expect(fetchSeriesInstanceEventsSpy).toHaveBeenCalledTimes(RECONCILE_SERIES_SNAPSHOT_BACKFILL_LIMIT);
  });
});

describe('OutlookSyncService manageBinding', () => {
  it('已结束系列禁止纳管', async () => {
    const repository = {
      findMailboxById: jest.fn().mockResolvedValue({ id: 'mailbox-1', mailboxEmail: 'admin@example.com', isEnabled: true }),
    } as any;
    const service = new OutlookSyncService({} as any, {} as any, repository);

    jest.spyOn(service as any, 'fetchEventById').mockResolvedValue({ id: 'raw-1', type: 'seriesMaster' });
    jest.spyOn(service as any, 'resolveManageRootEvent').mockResolvedValue({
      id: 'series-master-1',
      type: 'seriesMaster',
      isCancelled: false,
      recurrence: {
        range: {
          endDate: '2026-02-24',
          recurrenceTimeZone: 'Pacific Standard Time',
        },
      },
    });

    await expect(service.manageBinding({
      action: 'MANAGE',
      mailboxId: 'mailbox-1',
      graphEventId: 'series-master-1',
      iCalUId: 'ical-1',
    } as any, { id: 'actor-1', email: 'admin@example.com' })).rejects.toThrow('Ended Outlook series cannot be managed');
  });
});
