import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { BadRequestException, ConflictException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { SchedulesService } from './schedules.service';
import { ClassSchedule, ScheduleType } from '../entities/class-schedule.entity';
import { ClassroomRental } from '../entities/classroom-rental.entity';
import { Class } from '../entities/class.entity';
import { ClassTeacher } from '../entities/class-teacher.entity';
import { AttendanceSession } from '../entities/attendance-session.entity';
import { Classroom } from '../entities/classroom.entity';

/** Build a mock query-builder where each chain method returns `this`. */
function mockQueryBuilder<T>(results: T[] = []) {
  const qb = {
    where: jest.fn().mockReturnThis(),
    andWhere: jest.fn().mockReturnThis(),
    orderBy: jest.fn().mockReturnThis(),
    addOrderBy: jest.fn().mockReturnThis(),
    getMany: jest.fn().mockResolvedValue(results),
  };
  return qb;
}

describe('SchedulesService — getLookups', () => {
  it('includes active classrooms that have never been scheduled', async () => {
    const classroom = { id: 7, name: '新教室', building: 'A座' } as Classroom;
    const classroomRepo = { find: jest.fn().mockResolvedValue([classroom]) };
    const scheduleQb = {
      select: jest.fn().mockReturnThis(),
      addSelect: jest.fn().mockReturnThis(),
      innerJoin: jest.fn().mockReturnThis(),
      distinct: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      addOrderBy: jest.fn().mockReturnThis(),
      getRawMany: jest.fn().mockResolvedValue([]),
    };
    const module = await Test.createTestingModule({
      providers: [
        SchedulesService,
        {
          provide: getRepositoryToken(ClassSchedule),
          useValue: { createQueryBuilder: jest.fn().mockReturnValue(scheduleQb) },
        },
        { provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
        { provide: getRepositoryToken(Classroom), useValue: classroomRepo },
        { provide: getRepositoryToken(ClassroomRental), useValue: {} },
        { provide: getRepositoryToken(ClassTeacher), useValue: {} },
        { provide: getRepositoryToken(AttendanceSession), useValue: {} },
      ],
    }).compile();

    const service = module.get(SchedulesService);

    await expect(service.getLookups([1])).resolves.toMatchObject({ classrooms: [classroom] });
    expect(classroomRepo.find).toHaveBeenCalledWith({
      where: expect.any(Object),
      select: ['id', 'name', 'building', 'floor', 'roomType'],
      order: { building: 'ASC', name: 'ASC' },
    });
  });
});

describe('SchedulesService — checkConflict', () => {
  let service: SchedulesService;
  let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'createQueryBuilder'>>;
  let rentalRepo: jest.Mocked<Pick<Repository<ClassroomRental>, 'createQueryBuilder'>>;

  beforeEach(async () => {
    const mockRepo = {
      createQueryBuilder: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SchedulesService,
        { provide: getRepositoryToken(ClassSchedule), useValue: mockRepo },
        { provide: getRepositoryToken(Classroom), useValue: { find: jest.fn().mockResolvedValue([]) } },
        { provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
        {
          provide: getRepositoryToken(ClassroomRental),
          useValue: { createQueryBuilder: jest.fn() },
        },
        {
          provide: getRepositoryToken(ClassTeacher),
          useValue: { find: jest.fn().mockResolvedValue([]) },
        },
        {
          provide: getRepositoryToken(AttendanceSession),
          useValue: { count: jest.fn().mockResolvedValue(0) },
        },
      ],
    }).compile();

    service = module.get<SchedulesService>(SchedulesService);
    scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
    rentalRepo = module.get(getRepositoryToken(ClassroomRental));
  });

  it('same classroom + same weekday + overlapping times → ConflictException', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([
      { id: 1, subject: '数学', startTime: '08:00', endTime: '10:00' } as ClassSchedule,
    ]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
    ).rejects.toThrow(ConflictException);
  });

  it('rejects schedules separated by less than 10 minutes', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([
      { id: 1, subject: '数学', startTime: '08:00', endTime: '10:00' } as ClassSchedule,
    ]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(1, 3, '10:09', '12:00', '2026-03-01', '2026-06-30'),
    ).rejects.toThrow('排课之间必须至少间隔 10 分钟');
    expect(qb.andWhere).toHaveBeenCalledWith('cs.endTime > :bufferedStartTime', {
      bufferedStartTime: '09:59',
    });
  });

  it('allows adjacent schedules when there is exactly a 10-minute gap', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(1, 3, '10:10', '12:00', '2026-03-01', '2026-06-30'),
    ).resolves.toEqual([]);
    expect(qb.andWhere).toHaveBeenCalledWith('cs.endTime > :bufferedStartTime', {
      bufferedStartTime: '10:00',
    });
  });

  it('reserves 10 minutes after the new schedule when checking the next schedule', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await service.checkConflict(1, 3, '08:00', '09:50', '2026-03-01', '2026-06-30');

    expect(qb.andWhere).toHaveBeenCalledWith('cs.startTime < :bufferedEndTime', {
      bufferedEndTime: '10:00',
    });
  });

  it('same classroom + same weekday + overlapping times but disjoint date ranges → no conflict', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(1, 3, '09:00', '11:00', '2026-07-01', '2026-08-31'),
    ).resolves.toEqual([]);
  });

  it('different classroom → no conflict', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(2, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
    ).resolves.toEqual([]);
  });

  it('excludes the given schedule id from conflict check', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30', 42);

    expect(qb.andWhere).toHaveBeenCalledWith('cs.id != :excludeId', { excludeId: 42 });
  });

  it('overlapping classroom rental → ConflictException', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([
      {
        id: 10,
        startDate: '2026-03-01',
        endDate: '2026-03-31',
        status: 'active',
      } as ClassroomRental,
    ]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
    ).rejects.toThrow(ConflictException);
  });

  it('cancelled rental does not conflict', async () => {
    const qb = mockQueryBuilder<ClassSchedule>([]);
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);

    await expect(
      service.checkConflict(1, 3, '09:00', '11:00', '2026-03-01', '2026-06-30'),
    ).resolves.toEqual([]);
  });
});

describe('SchedulesService — getClassroomOccupancy', () => {
  let service: SchedulesService;
  let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'createQueryBuilder'>>;
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SchedulesService,
        { provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } },
        { provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
        { provide: getRepositoryToken(Classroom), useValue: { find: jest.fn().mockResolvedValue([]) } },
        {
          provide: getRepositoryToken(ClassroomRental),
          useValue: { createQueryBuilder: jest.fn() },
        },
        {
          provide: getRepositoryToken(ClassTeacher),
          useValue: { find: jest.fn().mockResolvedValue([]) },
        },
        {
          provide: getRepositoryToken(AttendanceSession),
          useValue: { count: jest.fn().mockResolvedValue(0) },
        },
      ],
    }).compile();

    service = module.get<SchedulesService>(SchedulesService);
    scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
  });

  it('returns both INTERNAL and RENTAL schedules', async () => {
    const internalSchedule = {
      id: 1,
      scheduleType: ScheduleType.INTERNAL,
      subject: '数学',
    } as ClassSchedule;
    const rentalSchedule = {
      id: 2,
      scheduleType: ScheduleType.RENTAL,
      subject: 'Organization A 租赁',
    } as ClassSchedule;
    const qb = mockQueryBuilder<ClassSchedule>([internalSchedule, rentalSchedule]);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);

    const result = await service.getClassroomOccupancy(1, '2026-03-15');

    expect(result).toHaveLength(2);
    expect(result.map((s) => s.scheduleType)).toContain(ScheduleType.INTERNAL);
    expect(result.map((s) => s.scheduleType)).toContain(ScheduleType.RENTAL);
    expect(qb.where).toHaveBeenCalledWith('cs.classroomId = :classroomId', { classroomId: 1 });
    expect(qb.andWhere).toHaveBeenCalledWith('cs.status = :status', { status: 'active' });
    expect(qb.andWhere).toHaveBeenCalledWith('cs.scheduleType IN (:...scheduleTypes)', {
      scheduleTypes: [ScheduleType.INTERNAL, ScheduleType.RENTAL],
    });
    expect(qb.andWhere).toHaveBeenCalledWith('cs.startDate <= :date', { date: '2026-03-15' });
    expect(qb.andWhere).toHaveBeenCalledWith('cs.endDate >= :date', { date: '2026-03-15' });
  });
});

describe('SchedulesService — remove/update status', () => {
  let service: SchedulesService;
  let scheduleRepo: jest.Mocked<
    Pick<Repository<ClassSchedule>, 'findOne' | 'remove' | 'update' | 'createQueryBuilder'>
  >;
  let classroomRepo: jest.Mocked<Pick<Repository<Classroom>, 'find' | 'findOne'>>;
  let classTeacherRepo: jest.Mocked<Pick<Repository<ClassTeacher>, 'find' | 'findOne'>>;
  let rentalRepo: jest.Mocked<Pick<Repository<ClassroomRental>, 'createQueryBuilder'>>;
  let attendanceSessionRepo: jest.Mocked<Pick<Repository<AttendanceSession>, 'count'>>;

  beforeEach(async () => {
    const scheduleRepoMock = {
      findOne: jest.fn(),
      remove: jest.fn(),
      update: jest.fn(),
      createQueryBuilder: jest.fn(),
    };
    const classroomRepoMock = {
      find: jest.fn().mockResolvedValue([]),
      findOne: jest.fn().mockResolvedValue({ id: 1, status: 'available' }),
    };
    const classTeacherRepoMock = {
      find: jest.fn().mockResolvedValue([]),
      findOne: jest.fn().mockResolvedValue({ id: 1 }),
    };
    const rentalRepoMock = { createQueryBuilder: jest.fn() };
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SchedulesService,
        {
          provide: getRepositoryToken(ClassSchedule),
          useValue: scheduleRepoMock,
        },
        { provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } },
        { provide: getRepositoryToken(Classroom), useValue: classroomRepoMock },
        {
          provide: getRepositoryToken(ClassroomRental),
          useValue: rentalRepoMock,
        },
        {
          provide: getRepositoryToken(ClassTeacher),
          useValue: classTeacherRepoMock,
        },
        {
          provide: getRepositoryToken(AttendanceSession),
          useValue: { count: jest.fn() },
        },
      ],
    }).compile();

    service = module.get<SchedulesService>(SchedulesService);
    scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
    classroomRepo = module.get(getRepositoryToken(Classroom));
    classTeacherRepo = module.get(getRepositoryToken(ClassTeacher));
    rentalRepo = module.get(getRepositoryToken(ClassroomRental));
    attendanceSessionRepo = module.get(getRepositoryToken(AttendanceSession));
  });

  it('disables a schedule instead of deleting it', async () => {
    const schedule = { id: 1, subject: '数学', status: 'active' } as ClassSchedule;
    (scheduleRepo.findOne as jest.Mock).mockResolvedValue(schedule);
    (scheduleRepo.update as jest.Mock).mockResolvedValue({ affected: 1 });

    const result = await service.remove(1);

    expect(result).toEqual({ success: true, message: '排课已停用（历史考勤记录已保留）' });
    expect(scheduleRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
    expect(scheduleRepo.update).toHaveBeenCalledWith(1, { status: 'inactive' });
    expect(scheduleRepo.remove).not.toHaveBeenCalled();
    expect(attendanceSessionRepo.count).not.toHaveBeenCalled();
  });

  it('rejects disabling an already inactive schedule', async () => {
    const schedule = { id: 2, subject: '英语', status: 'inactive' } as ClassSchedule;
    (scheduleRepo.findOne as jest.Mock).mockResolvedValue(schedule);

    await expect(service.remove(2)).rejects.toThrow('排课已停用');
    expect(scheduleRepo.update).not.toHaveBeenCalled();
    expect(scheduleRepo.remove).not.toHaveBeenCalled();
  });

  it('throws NotFoundException for non-existent schedule', async () => {
    (scheduleRepo.findOne as jest.Mock).mockResolvedValue(null);

    await expect(service.remove(999)).rejects.toThrow('排课记录不存在');
    expect(scheduleRepo.remove).not.toHaveBeenCalled();
  });


  it('disables a schedule without checking conflicts or deleting history', async () => {
    const schedule = {
      id: 3,
      classId: 1,
      classroomId: 10,
      weekDay: 2,
      startTime: '09:00',
      endTime: '10:00',
      startDate: '2026-07-01',
      endDate: '2026-07-31',
      subject: '英语',
      teacherId: 5,
      status: 'active',
    } as ClassSchedule;
    (scheduleRepo.findOne as jest.Mock)
      .mockResolvedValueOnce(schedule)
      .mockResolvedValueOnce({ ...schedule, status: 'inactive' });

    const result = await service.update(3, { status: 'inactive' });

    expect(result).toMatchObject({ id: 3, status: 'inactive' });
    expect(scheduleRepo.update).toHaveBeenCalledWith(3, { status: 'inactive' });
    expect(scheduleRepo.createQueryBuilder).not.toHaveBeenCalled();
    expect(rentalRepo.createQueryBuilder).not.toHaveBeenCalled();
    expect(classroomRepo.findOne).not.toHaveBeenCalled();
    expect(classTeacherRepo.findOne).not.toHaveBeenCalled();
  });

  it('checks classroom availability and conflicts when reactivating a schedule', async () => {
    const schedule = {
      id: 4,
      classId: 1,
      classroomId: 10,
      weekDay: 2,
      startTime: '09:00',
      endTime: '10:00',
      startDate: '2026-07-01',
      endDate: '2026-07-31',
      subject: '英语',
      teacherId: 5,
      status: 'inactive',
    } as ClassSchedule;
    (scheduleRepo.findOne as jest.Mock)
      .mockResolvedValueOnce(schedule)
      .mockResolvedValueOnce({ ...schedule, status: 'active' });
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(mockQueryBuilder<ClassSchedule>([]));
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(mockQueryBuilder<ClassroomRental>([]));

    await expect(service.update(4, { status: 'active' })).resolves.toMatchObject({
      id: 4,
      status: 'active',
    });

    expect(classroomRepo.findOne).toHaveBeenCalledWith({ where: { id: 10 } });
    expect(scheduleRepo.createQueryBuilder).toHaveBeenCalled();
    expect(scheduleRepo.update).toHaveBeenCalledWith(4, { status: 'active' });
  });

  it('rejects invalid schedule statuses', async () => {
    const schedule = {
      id: 5,
      classId: 1,
      classroomId: 10,
      weekDay: 2,
      startTime: '09:00',
      endTime: '10:00',
      startDate: '2026-07-01',
      endDate: '2026-07-31',
      subject: '英语',
      status: 'active',
    } as ClassSchedule;
    (scheduleRepo.findOne as jest.Mock).mockResolvedValue(schedule);

    await expect(service.update(5, { status: 'paused' })).rejects.toThrow(BadRequestException);
    expect(scheduleRepo.update).not.toHaveBeenCalled();
  });
});

describe('SchedulesService — range boundaries', () => {
  const makeService = () => {
    const scheduleRepo = { create: jest.fn() };
    return {
      service: new SchedulesService(
        scheduleRepo as never,
        {} as never,
        {} as never,
        {} as never,
        {} as never,
        {} as never,
      ),
      scheduleRepo,
    };
  };
  const valid = {
    classId: 1, classroomId: 2, weekDay: 1,
    startTime: '09:00', endTime: '10:00',
    startDate: '2026-07-01', endDate: '2026-07-31', subject: '数学',
  };

  it('rejects zero-duration schedules before repository access', async () => {
    const { service, scheduleRepo } = makeService();
    await expect(service.create({ ...valid, endTime: '09:00' })).rejects.toThrow(
      '上课时间和下课时间不能相同',
    );
    expect(scheduleRepo.create).not.toHaveBeenCalled();
  });

  it('rejects reversed date ranges before repository access', async () => {
    const { service, scheduleRepo } = makeService();
    await expect(
      service.create({ ...valid, startDate: '2026-08-01', endDate: '2026-07-31' }),
    ).rejects.toThrow('排课结束日期不能早于开始日期');
    expect(scheduleRepo.create).not.toHaveBeenCalled();
  });
});
