import { BadRequestException } from '@nestjs/common';
import { AttendanceService } from './attendance.service';
import { AttendancePeriodConfig } from '../entities/attendance-period-config.entity';
import { ClassSchedule } from '../entities/class-schedule.entity';

// ── saveAttendancePeriodConfigs ──

describe('AttendanceService — saveAttendancePeriodConfigs boundaries', () => {
  function createService(periodConfigRepoOverrides?: {
    clear?: jest.Mock;
    save?: jest.Mock;
    create?: jest.Mock;
    find?: jest.Mock;
    count?: jest.Mock;
  }) {
    const periodConfigRepo = {
      clear: jest.fn().mockResolvedValue(undefined),
      save: jest.fn().mockImplementation((entities: unknown) => Promise.resolve(entities)),
      create: jest.fn().mockImplementation((data: Partial<AttendancePeriodConfig>) => ({
        id: 1,
        ...data,
      } as AttendancePeriodConfig)),
      find: jest.fn().mockResolvedValue([]),
      count: jest.fn().mockResolvedValue(0),
      ...periodConfigRepoOverrides,
    };

    return new AttendanceService(
      {} as never, // attendanceRepo
      {} as never, // dingRawRepo
      {} as never, // classRepo
      {} as never, // studentRepo
      {} as never, // scheduleRepo
      {} as never, // classStudentRepo
      {} as never, // studentDingMappingRepo
      {} as never, // classTeacherRepo
      {} as never, // attendanceSessionRepo
      {} as never, // attendanceDeviceRepo
      periodConfigRepo as never,
      {} as never, // dataSource
    );
  }

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('detects overlap when sorted by startTime even if sortOrder is inconsistent', async () => {
    // A: sortOrder=1 but startTime="14:00" (later in the day)
    // B: sortOrder=2 but startTime="09:00" (earlier in the day)
    // When sorted by sortOrder, A comes first, then B.
    // The overlap check sorts by startTime, so B (09:00–15:00) comes first,
    // and A (14:00–17:00) is detected as overlapping B.
    const savedPeriods: AttendancePeriodConfig[] = [];
    const service = createService({
      save: jest.fn().mockImplementation((entities: AttendancePeriodConfig[]) => {
        savedPeriods.push(...entities);
        return Promise.resolve(entities);
      }),
      find: jest.fn().mockResolvedValue([]),
    });

    const dto = {
      periods: [
        { periodKey: 'afternoon', label: '晚课', startTime: '14:00', endTime: '17:00', sortOrder: 1, enabled: true },
        { periodKey: 'morning', label: '早课', startTime: '09:00', endTime: '15:00', sortOrder: 2, enabled: true },
      ],
    };

    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow(BadRequestException);
    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow('时间段不能重叠');
  });

  it('accepts valid non-overlapping periods sorted by sortOrder', async () => {
    const savedPeriods: AttendancePeriodConfig[] = [];
    const expectedPeriods: AttendancePeriodConfig[] = [
      { id: 1, periodKey: 'morning_reading', label: '早自习', startTime: '07:30', endTime: '08:30', sortOrder: 1, enabled: true, createdAt: expect.any(Date) as any, updatedAt: expect.any(Date) as any },
      { id: 1, periodKey: 'morning', label: '早课', startTime: '09:00', endTime: '12:00', sortOrder: 2, enabled: true, createdAt: expect.any(Date) as any, updatedAt: expect.any(Date) as any },
      { id: 1, periodKey: 'afternoon', label: '晚课', startTime: '14:00', endTime: '17:00', sortOrder: 3, enabled: true, createdAt: expect.any(Date) as any, updatedAt: expect.any(Date) as any },
      { id: 1, periodKey: 'evening_study', label: '晚自习', startTime: '18:30', endTime: '21:00', sortOrder: 4, enabled: true, createdAt: expect.any(Date) as any, updatedAt: expect.any(Date) as any },
    ];

    // count returns > 0 so ensureAttendancePeriodConfigs does not re-seed defaults
    const service = createService({
      count: jest.fn().mockResolvedValue(4),
      save: jest.fn().mockImplementation((entities: AttendancePeriodConfig[]) => {
        savedPeriods.push(...entities);
        return Promise.resolve(entities);
      }),
      find: jest.fn().mockResolvedValue(expectedPeriods),
    });

    const dto = {
      periods: [
        { periodKey: 'morning_reading', label: '早自习', startTime: '07:30', endTime: '08:30', sortOrder: 1, enabled: true },
        { periodKey: 'morning', label: '早课', startTime: '09:00', endTime: '12:00', sortOrder: 2, enabled: true },
        { periodKey: 'afternoon', label: '晚课', startTime: '14:00', endTime: '17:00', sortOrder: 3, enabled: true },
        { periodKey: 'evening_study', label: '晚自习', startTime: '18:30', endTime: '21:00', sortOrder: 4, enabled: true },
      ],
    };

    const result = await service.saveAttendancePeriodConfigs(dto as any);

    expect(result).toEqual(expectedPeriods);
    expect(savedPeriods).toHaveLength(4);
    // Verify they were saved in sortOrder order (ascending)
    expect(savedPeriods[0].periodKey).toBe('morning_reading');
    expect(savedPeriods[1].periodKey).toBe('morning');
    expect(savedPeriods[2].periodKey).toBe('afternoon');
    expect(savedPeriods[3].periodKey).toBe('evening_study');
  });

  it('rejects periods where endTime is not after startTime', async () => {
    const service = createService();

    const dto = {
      periods: [
        { periodKey: 'bad', label: 'Bad Period', startTime: '10:00', endTime: '09:00', sortOrder: 1, enabled: true },
      ],
    };

    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow(BadRequestException);
    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow('结束时间必须晚于开始时间');
  });

  it('rejects duplicate periodKey', async () => {
    const service = createService();

    const dto = {
      periods: [
        { periodKey: 'morning', label: '早课', startTime: '09:00', endTime: '12:00', sortOrder: 1, enabled: true },
        { periodKey: 'morning', label: '早课2', startTime: '14:00', endTime: '17:00', sortOrder: 2, enabled: true },
      ],
    };

    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow(BadRequestException);
    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow('重复');
  });

  it('rejects empty period key or label', async () => {
    const service = createService();

    const dto = {
      periods: [
        { periodKey: '   ', label: 'Valid Label', startTime: '09:00', endTime: '12:00', sortOrder: 1, enabled: true },
      ],
    };

    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow(BadRequestException);
    await expect(service.saveAttendancePeriodConfigs(dto as any)).rejects.toThrow('时段标识和名称不能为空');
  });

  it('disabled periods are ignored during overlap check', async () => {
    const savedPeriods: AttendancePeriodConfig[] = [];
    const service = createService({
      count: jest.fn().mockResolvedValue(2),
      save: jest.fn().mockImplementation((entities: AttendancePeriodConfig[]) => {
        savedPeriods.push(...entities);
        return Promise.resolve(entities);
      }),
      find: jest.fn().mockResolvedValue([]),
    });

    // A (enabled, startTime=09:00, endTime=17:00) and B (disabled, startTime=10:00, endTime=12:00, which would overlap A)
    // The disabled period should be skipped in overlap check
    const dto = {
      periods: [
        { periodKey: 'day', label: '全天', startTime: '09:00', endTime: '17:00', sortOrder: 1, enabled: true },
        { periodKey: 'break', label: '休息', startTime: '10:00', endTime: '12:00', sortOrder: 2, enabled: false },
      ],
    };

    // Should not throw — the disabled period is ignored
    const result = await service.saveAttendancePeriodConfigs(dto as any);
    expect(result).toBeDefined();
    expect(savedPeriods).toHaveLength(2);
  });
});

// ── getScheduleOptionsForAttendance ──

describe('AttendanceService — getScheduleOptionsForAttendance boundaries', () => {
  it('returns null teacher when schedule has no matching raw entry (teacher not found)', async () => {
    const entity = {
      id: 42,
      classId: 8,
      subject: '数学',
      startTime: '09:00',
      endTime: '12:00',
      weekDay: 1,
      startDate: '2026-01-01',
      endDate: '2026-12-31',
    } as ClassSchedule;

    // Simulate the case where getRawAndEntities returns the entity
    // but `raw` is empty (no teacher row). This happens when the
    // schedule has no teacher assignment.
    const qb = {
      leftJoin: jest.fn().mockReturnThis(),
      addSelect: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      addOrderBy: jest.fn().mockReturnThis(),
      getRawAndEntities: jest.fn().mockResolvedValue({
        entities: [entity],
        raw: [], // empty raw → no teacher info
      }),
    };

    const scheduleRepo = {
      createQueryBuilder: jest.fn().mockReturnValue(qb),
    };

    const service = new AttendanceService(
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      scheduleRepo as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
    );

    const result = await service.getScheduleOptionsForAttendance(8, '2026-07-20');

    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      id: 42,
      teacherName: null,
      teacherUsername: null,
    });
  });

  it('returns teacher info when raw contains a matching schedule entry', async () => {
    const entity = {
      id: 42,
      classId: 8,
      subject: '数学',
      startTime: '09:00',
      endTime: '12:00',
      weekDay: 1,
      startDate: '2026-01-01',
      endDate: '2026-12-31',
    } as ClassSchedule;

    const qb = {
      leftJoin: jest.fn().mockReturnThis(),
      addSelect: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      addOrderBy: jest.fn().mockReturnThis(),
      getRawAndEntities: jest.fn().mockResolvedValue({
        entities: [entity],
        raw: [
          {
            scheduleIdForTeacherMap: 42,
            teacherName: '张老师',
            teacherUsername: 'zhang',
          },
        ],
      }),
    };

    const scheduleRepo = {
      createQueryBuilder: jest.fn().mockReturnValue(qb),
    };

    const service = new AttendanceService(
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      scheduleRepo as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
    );

    const result = await service.getScheduleOptionsForAttendance(8, '2026-07-20');

    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      id: 42,
      teacherName: '张老师',
      teacherUsername: 'zhang',
    });
  });

  it('returns null teacher when raw has a different schedule id than the entity', async () => {
    const entity = {
      id: 42,
      classId: 8,
      subject: '数学',
      startTime: '09:00',
      endTime: '12:00',
      weekDay: 1,
      startDate: '2026-01-01',
      endDate: '2026-12-31',
    } as ClassSchedule;

    // raw contains teacher info but for a DIFFERENT schedule id (99 ≠ 42)
    // The fallback `??` should kick in for schedule 42
    const qb = {
      leftJoin: jest.fn().mockReturnThis(),
      addSelect: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      addOrderBy: jest.fn().mockReturnThis(),
      getRawAndEntities: jest.fn().mockResolvedValue({
        entities: [entity],
        raw: [
          {
            scheduleIdForTeacherMap: 99,
            teacherName: '张老师',
            teacherUsername: 'zhang',
          },
        ],
      }),
    };

    const scheduleRepo = {
      createQueryBuilder: jest.fn().mockReturnValue(qb),
    };

    const service = new AttendanceService(
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      scheduleRepo as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
    );

    const result = await service.getScheduleOptionsForAttendance(8, '2026-07-20');

    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      id: 42,
      teacherName: null,
      teacherUsername: null,
    });
  });
});
