import { AttendanceSettlementService } from './attendance-settlement.service';

const schedule = {
  id: 2,
  classId: 8,
  teacherId: 21,
  weekDay: 1,
  startTime: '09:00',
  endTime: '10:00',
  startDate: '2026-07-01',
  endDate: '2026-07-31',
  scheduleType: 'INTERNAL',
  status: 'active',
};

const createService = () => {
  const scheduleRepo = { find: jest.fn() };
  const sessionRepo = {
    find: jest.fn(),
    update: jest.fn().mockResolvedValue({ affected: 1 }),
  };
  const attendanceService = {
    getLessonAttendanceImportDateRange: jest.fn().mockImplementation((targetSchedule, lessonDate: string) => {
      if (targetSchedule.endTime > targetSchedule.startTime) {
        return { startDate: lessonDate, endDate: lessonDate };
      }
      const next = new Date(`${lessonDate}T00:00:00.000Z`);
      next.setUTCDate(next.getUTCDate() + 1);
      return { startDate: lessonDate, endDate: next.toISOString().slice(0, 10) };
    }),
    getTeacherClassDingUserIds: jest.fn().mockResolvedValue(['ding-1']),
    createLessonAttendanceFromDingTalk: jest.fn().mockImplementation(
      async (_scheduleId: number, lessonDate: string, userId: number, finalize: boolean) => ({
        session: { id: 90, lessonDate, startedBy: userId, status: finalize ? 'completed' : 'in_progress' },
      }),
    ),
  };
  const importService = {
    importFromDingTalk: jest.fn().mockResolvedValue({ success: true, errors: [] }),
  };
  const service = new AttendanceSettlementService(
    scheduleRepo as never,
    sessionRepo as never,
    attendanceService as never,
    importService as never,
  );
  return { service, scheduleRepo, sessionRepo, attendanceService, importService };
};

describe('AttendanceSettlementService', () => {
  it('pulls and finalizes an ended lesson once', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService, importService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule]);
    sessionRepo.find.mockResolvedValue([]);

    await service.settleEndedLessons(new Date('2026-07-13T10:01:00+08:00'));

    expect(importService.importFromDingTalk).toHaveBeenCalledWith({
      startDate: '2026-07-13',
      endDate: '2026-07-13',
      userIds: ['ding-1'],
      autoMatch: true,
      userId: 21,
    });
    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenNthCalledWith(
      1, 2, '2026-07-13', 21, false,
    );
    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenNthCalledWith(
      2, 2, '2026-07-13', 21, true,
    );
  });

  it('does not settle a lesson before its end time', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule]);
    sessionRepo.find.mockResolvedValue([]);

    await service.settleEndedLessons(new Date('2026-07-13T09:30:00+08:00'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalled();
  });

  it('does not settle an in-progress lesson before its end time', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService, importService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule]);
    sessionRepo.find.mockResolvedValue([
      {
        id: 90,
        scheduleId: 2,
        lessonDate: '2026-07-13',
        status: 'in_progress',
        schedule,
      },
    ]);

    await service.settleEndedLessons(new Date('2026-07-13T09:30:00+08:00'));

    expect(importService.importFromDingTalk).not.toHaveBeenCalled();
    expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalled();
  });

  it('settles an in-progress lesson when its end time is reached', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule]);
    sessionRepo.find.mockResolvedValue([
      {
        id: 90,
        scheduleId: 2,
        lessonDate: '2026-07-13',
        status: 'in_progress',
        schedule,
      },
    ]);

    await service.settleEndedLessons(new Date('2026-07-13T10:00:00+08:00'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledTimes(1);
    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith(
      2,
      '2026-07-13',
      21,
      true,
    );
  });

  it('continues with the next lesson when one settlement fails', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService, importService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule, { ...schedule, id: 3 }]);
    sessionRepo.find.mockResolvedValue([]);
    importService.importFromDingTalk
      .mockRejectedValueOnce(new Error('DingTalk unavailable'))
      .mockResolvedValueOnce({ success: true, errors: [] });

    await service.settleEndedLessons(new Date('2026-07-13T10:01:00+08:00'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenNthCalledWith(
      2,
      3,
      '2026-07-13',
      21,
      false,
    );
    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenLastCalledWith(
      3,
      '2026-07-13',
      21,
      true,
    );
  });

  it('does not finalize when an import reports partial errors', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService, importService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule]);
    sessionRepo.find.mockResolvedValue([]);
    importService.importFromDingTalk.mockResolvedValue({
      success: true,
      errors: ['one batch failed'],
    });

    await service.settleEndedLessons(new Date('2026-07-13T10:01:00+08:00'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalledWith(
      2, '2026-07-13', 21, true,
    );
  });

  it('retries an uncompleted daytime lesson on a later day', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService } = createService();
    scheduleRepo.find.mockResolvedValue([]);
    sessionRepo.find.mockResolvedValue([
      {
        scheduleId: 2,
        lessonDate: '2026-07-13',
        status: 'in_progress',
        schedule,
      },
    ]);

    await service.settleEndedLessons(new Date('2026-07-15T10:01:00+08:00'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith(
      2,
      '2026-07-13',
      21,
      true,
    );
  });

  it('skips final pull when another worker already claimed the session', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService, importService } = createService();
    scheduleRepo.find.mockResolvedValue([]);
    sessionRepo.find.mockResolvedValue([
      {
        id: 90,
        scheduleId: 2,
        lessonDate: '2026-07-13',
        status: 'in_progress',
        schedule,
      },
    ]);
    sessionRepo.update.mockResolvedValue({ affected: 0 });

    await service.settleEndedLessons(new Date('2026-07-13T10:01:00+08:00'));

    expect(importService.importFromDingTalk).not.toHaveBeenCalled();
    expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalled();
  });

  it('does not settle an in-progress overnight lesson before its next-day end time', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService, importService } = createService();
    const overnightSchedule = {
      ...schedule,
      id: 4,
      weekDay: 7,
      startTime: '22:00',
      endTime: '01:00',
    };
    scheduleRepo.find.mockResolvedValue([]);
    sessionRepo.find.mockResolvedValue([
      {
        id: 91,
        scheduleId: 4,
        lessonDate: '2026-07-12',
        status: 'in_progress',
        schedule: overnightSchedule,
      },
    ]);

    await service.settleEndedLessons(new Date('2026-07-13T00:30:00+08:00'));

    expect(importService.importFromDingTalk).not.toHaveBeenCalled();
    expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalled();
  });

  it('settles an overnight lesson after its next-day end time', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService } = createService();
    scheduleRepo.find.mockResolvedValue([
      { ...schedule, id: 4, weekDay: 7, startTime: '22:00', endTime: '01:00' },
    ]);
    sessionRepo.find.mockResolvedValue([]);

    await service.settleEndedLessons(new Date('2026-07-13T01:01:00+08:00'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith(
      4,
      '2026-07-12',
      21,
      true,
    );
  });

  it('pulls an overnight lesson through its next calendar date', async () => {
    const { service, scheduleRepo, sessionRepo, importService } = createService();
    scheduleRepo.find.mockResolvedValue([
      { ...schedule, id: 4, weekDay: 7, startTime: '22:00', endTime: '01:00' },
    ]);
    sessionRepo.find.mockResolvedValue([]);

    await service.settleEndedLessons(new Date('2026-07-13T01:01:00+08:00'));

    expect(importService.importFromDingTalk).toHaveBeenCalledWith(
      expect.objectContaining({ startDate: '2026-07-12', endDate: '2026-07-13' }),
    );
  });
});

describe('attendance settlement timezone', () => {
  it('uses Asia/Shanghai course time when the server runs in UTC', async () => {
    const { service, scheduleRepo, sessionRepo, attendanceService } = createService();
    scheduleRepo.find.mockResolvedValue([schedule]);
    sessionRepo.find.mockResolvedValue([]);

    await service.settleEndedLessons(new Date('2026-07-13T02:01:00.000Z'));

    expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith(
      2,
      '2026-07-13',
      21,
      true,
    );
  });
});
