import { BadRequestException } from '@nestjs/common';
import { AttendanceImportService } from './attendance-import.service';
import { DingTalkService } from '../integration/dingtalk.service';
import { AttendanceService } from './attendance.service';

describe('AttendanceImportService', () => {
  const dingRawRepo = {
    find: jest.fn(),
    findOne: jest.fn(),
    save: jest.fn(),
  };
  const studentRepo = { findOne: jest.fn() };
  const studentDingMappingRepo = { findOne: jest.fn() };
  const dingTalkService = {
    fetchAttendanceResults: jest.fn(),
  };
  const attendanceService = {
    autoMatchDingRecords: jest.fn(),
  };

  let service: AttendanceImportService;

  beforeEach(() => {
    jest.clearAllMocks();
    service = new AttendanceImportService(
      dingRawRepo as never,
      studentRepo as never,
      studentDingMappingRepo as never,
      dingTalkService as unknown as DingTalkService,
      attendanceService as unknown as AttendanceService,
    );
  });

  it('splits DingTalk requests by at most 50 users and 7 calendar days without offset pagination', async () => {
    const userIds = Array.from({ length: 51 }, (_, index) => `user-${index + 1}`);
    dingTalkService.fetchAttendanceResults.mockResolvedValue([]);

    await (service as any).fetchAllPages({
      startDate: '2026-07-01',
      endDate: '2026-07-10',
      userIds,
    });

    expect(dingTalkService.fetchAttendanceResults).toHaveBeenCalledTimes(4);
    expect(dingTalkService.fetchAttendanceResults.mock.calls.map(([params]) => params)).toEqual([
      {
        startDate: '2026-07-01',
        endDate: '2026-07-07',
        userIds: userIds.slice(0, 50),
      },
      {
        startDate: '2026-07-01',
        endDate: '2026-07-07',
        userIds: userIds.slice(50),
      },
      {
        startDate: '2026-07-08',
        endDate: '2026-07-10',
        userIds: userIds.slice(0, 50),
      },
      {
        startDate: '2026-07-08',
        endDate: '2026-07-10',
        userIds: userIds.slice(50),
      },
    ]);
  });

  it('rejects an attendance import without DingTalk user IDs', async () => {
    await expect(
      (service as any).fetchAllPages({
        startDate: '2026-07-01',
        endDate: '2026-07-01',
        userIds: [],
      }),
    ).rejects.toBeInstanceOf(BadRequestException);

    expect(dingTalkService.fetchAttendanceResults).not.toHaveBeenCalled();
  });
  it('stores the DingTalk user name returned with the attendance record', async () => {
    const entity = await (service as any).mapToEntity({
      userId: 'ding-1',
      userName: '张三',
      workDate: '2026-07-01',
      timeResult: 'Normal',
      locationResult: '',
      planCheckTime: '',
      actualCheckTime: '2026-07-01T08:00:00.000Z',
      checkId: 'check-1',
      checkType: 'OnDuty',
    });

    expect(entity.userName).toBe('张三');
  });

  it('stores DingTalk punch source and attendance machine metadata', async () => {
    const entity = await (service as any).mapToEntity({
      userId: 'ding-1',
      userName: '张三',
      workDate: '2026-07-01',
      timeResult: 'Normal',
      locationResult: '',
      planCheckTime: '',
      actualCheckTime: '2026-07-01T08:00:00.000Z',
      checkId: 'check-1',
      checkType: 'OnDuty',
      sourceType: 'ATM',
      deviceName: '东门考勤机',
      deviceId: 'ATM-01',
    });

    expect(entity).toEqual(
      expect.objectContaining({
        punchSource: 'ATM',
        punchDeviceName: '东门考勤机',
        punchDeviceId: 'ATM-01',
      }),
    );
  });

  it('fills the student name from the DingTalk mapping when saving an imported record', async () => {
    dingTalkService.fetchAttendanceResults.mockResolvedValue([
      {
        userId: 'ding-1',
        userName: '',
        workDate: '2026-07-01',
        timeResult: 'Normal',
        locationResult: '',
        planCheckTime: '',
        actualCheckTime: '2026-07-01T08:00:00.000Z',
        checkId: 'check-1',
        checkType: 'OnDuty',
      },
    ]);
    dingRawRepo.find.mockResolvedValue([]);
    studentDingMappingRepo.findOne.mockResolvedValue({ studentId: 3 });
    studentRepo.findOne.mockResolvedValue({ id: 3, name: '张三' });
    dingRawRepo.save.mockImplementation(async (entities) => entities);

    await service.importFromDingTalk({
      startDate: '2026-07-01',
      endDate: '2026-07-01',
      userIds: ['ding-1'],
      autoMatch: false,
    });

    expect(dingRawRepo.save).toHaveBeenCalledWith(
      [expect.objectContaining({ userName: '张三' })],
      { chunk: 50 },
    );
  });


  it('auto-matches previously imported duplicate records', async () => {
    dingTalkService.fetchAttendanceResults.mockResolvedValue([
      {
        userId: 'ding-1',
        userName: '张三',
        workDate: '2026-07-01',
        timeResult: 'Normal',
        locationResult: '',
        planCheckTime: '',
        actualCheckTime: '2026-07-01T08:00:00.000Z',
        checkId: 'check-1',
        checkType: 'OnDuty',
        sourceType: 'ATM',
        deviceName: '东门考勤机',
        deviceId: 'ATM-01',
      },
    ]);
    dingRawRepo.find.mockResolvedValue([{
      dingId: 'check-1',
      punchSource: null,
      punchDeviceName: null,
      punchDeviceId: null,
      rawData: '',
    }]);
    dingRawRepo.save.mockImplementation(async (entities) => entities);
    attendanceService.autoMatchDingRecords.mockResolvedValue({ matched: 1, total: 1 });

    const result = await service.importFromDingTalk({
      startDate: '2026-07-01',
      endDate: '2026-07-01',
      userIds: ['ding-1'],
      autoMatch: true,
    });

    expect(dingRawRepo.save).toHaveBeenCalledWith(
      [expect.objectContaining({
        dingId: 'check-1',
        punchSource: 'ATM',
        punchDeviceName: '东门考勤机',
        punchDeviceId: 'ATM-01',
      })],
      { chunk: 50 },
    );
    expect(attendanceService.autoMatchDingRecords).toHaveBeenCalled();
    expect(result.matched).toBe(1);
  });

  it('preserves existing device metadata when a duplicate response omits it', async () => {
    const existing = {
      dingId: 'check-keep-device',
      punchSource: 'ATM',
      punchDeviceName: '东门考勤机',
      punchDeviceId: 'ATM-01',
      rawData: '{}',
    };
    dingTalkService.fetchAttendanceResults.mockResolvedValue([{
      userId: 'ding-1',
      userName: '张三',
      workDate: '2026-07-01',
      timeResult: 'Normal',
      locationResult: '',
      planCheckTime: '',
      actualCheckTime: '2026-07-01T08:00:00.000Z',
      checkId: 'check-keep-device',
      checkType: 'OnDuty',
      sourceType: '',
    }]);
    dingRawRepo.find.mockResolvedValue([existing]);
    attendanceService.autoMatchDingRecords.mockResolvedValue({ matched: 0, total: 1 });

    await service.importFromDingTalk({
      startDate: '2026-07-01',
      endDate: '2026-07-01',
      userIds: ['ding-1'],
      autoMatch: true,
    });

    expect(existing).toEqual(expect.objectContaining({
      punchSource: 'ATM',
      punchDeviceName: '东门考勤机',
      punchDeviceId: 'ATM-01',
    }));
    expect(dingRawRepo.save).not.toHaveBeenCalled();
  });

  it('scopes SSE progress events to the importing user', async () => {
    dingTalkService.fetchAttendanceResults.mockResolvedValue([
      {
        userId: 'ding-1',
        userName: '李四',
        workDate: '2026-07-01',
        timeResult: 'Normal',
        locationResult: '',
        planCheckTime: '',
        actualCheckTime: '2026-07-01T09:00:00.000Z',
        checkId: 'check-2',
        checkType: 'OnDuty',
      },
    ]);

    const events: Array<{ phase: string; userId?: number }> = [];
    const sub = service.progress$.subscribe((event) => {
      events.push({ phase: event.phase, userId: event.userId });
    });

    await service.importFromDingTalk({
      startDate: '2026-07-01',
      endDate: '2026-07-01',
      userIds: ['ding-1'],
      userId: 42,
    });

    sub.unsubscribe();

    expect(events.length).toBeGreaterThan(0);
    for (const event of events) {
      expect(event.userId).toBe(42);
    }
  });

  it('emits userId undefined when import has no HTTP user', async () => {
    dingTalkService.fetchAttendanceResults.mockResolvedValue([
      {
        userId: 'ding-2',
        userName: '王五',
        workDate: '2026-07-02',
        timeResult: 'Normal',
        locationResult: '',
        planCheckTime: '',
        actualCheckTime: '2026-07-02T10:00:00.000Z',
        checkId: 'check-3',
        checkType: 'OnDuty',
      },
    ]);

    const events: Array<{ phase: string; userId?: number }> = [];
    const sub = service.progress$.subscribe((event) => {
      events.push({ phase: event.phase, userId: event.userId });
    });

    await service.importFromDingTalk({
      startDate: '2026-07-02',
      endDate: '2026-07-02',
      userIds: ['ding-2'],
    });

    sub.unsubscribe();

    expect(events.length).toBeGreaterThan(0);
    for (const event of events) {
      expect(event.userId).toBeUndefined();
    }
  });
});
