import { BadRequestException } from '@nestjs/common';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { RoomInspection } from '../entities/room-inspection.entity';
import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { RoomInspectionsService } from './room-inspections.service';

describe('RoomInspectionsService', () => {
  const today = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Asia/Shanghai',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(new Date());

  const createService = (options?: {
    existingInspection?: Partial<RoomInspection> | null;
    existingInspections?: Partial<RoomInspection>[];
    occupancies?: Occupancy[];
  }) => {
    const inspectionRepo = {
      findOne: jest.fn().mockResolvedValue(options?.existingInspection ?? null),
      create: jest.fn((value) => value),
      save: jest.fn(async (value) => ({ id: 91, ...value })),
      find: jest.fn().mockResolvedValue(options?.existingInspections ?? []),
    } as unknown as Repository<RoomInspection>;
    const detailRepo = {
      create: jest.fn((value) => value),
      save: jest.fn(async (value) => value),
      delete: jest.fn().mockResolvedValue(undefined),
    } as unknown as Repository<RoomInspectionDetail>;
    const roomRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 5, roomNumber: '4-102' }),
    } as unknown as Repository<Room>;
    const occupancies = options?.occupancies ?? ([
      {
        id: 11,
        roomId: 5,
        studentId: 21,
        bedId: 31,
        student: { name: '张三' },
        bed: { bedNumber: '1号床' },
      },
      {
        id: 12,
        roomId: 5,
        studentId: 22,
        bedId: 32,
        student: { name: '李四' },
        bed: { bedNumber: '2号床' },
      },
    ] as Occupancy[]);
    const occupancyRepo = {
      find: jest.fn().mockResolvedValue(occupancies),
    } as unknown as Repository<Occupancy>;
    const roomQuery = {
      where: jest.fn().mockReturnThis(),
      getOne: jest.fn().mockResolvedValue({ id: 5, roomNumber: '4-102', status: 'full' }),
      setLock: jest.fn().mockReturnThis(),
    };
    const manager = {
      createQueryBuilder: jest.fn().mockReturnValue(roomQuery),
      getRepository: jest.fn((entity) => {
        if (entity === RoomInspection) return inspectionRepo;
        if (entity === RoomInspectionDetail) return detailRepo;
        if (entity === Occupancy) return occupancyRepo;
        throw new Error(`unexpected repository ${String(entity)}`);
      }),
    } as unknown as EntityManager;
    const dataSource = {
      options: { type: 'better-sqlite3' },
      manager,
      transaction: jest.fn(async (callback) => callback(manager)),
    } as unknown as DataSource;
    const logs = { log: jest.fn().mockResolvedValue(undefined) } as unknown as OperationLogsService;
    const service = new RoomInspectionsService(
      inspectionRepo,
      detailRepo,
      roomRepo,
      occupancyRepo,
      dataSource,
      logs,
    );
    return { service, inspectionRepo, detailRepo, occupancyRepo, roomRepo, logs };
  };

  it('creates present and absent details in one transaction', async () => {
    const { service, detailRepo } = createService();

    const result = await service.submit(5, today, [11], { id: 7, username: 'teacher' });

    expect(detailRepo.delete).toHaveBeenCalledWith({ inspectionId: 91 });
    expect(detailRepo.save).toHaveBeenCalledWith([
      expect.objectContaining({ occupancyId: 11, status: 'present', studentNameSnapshot: '张三' }),
      expect.objectContaining({ occupancyId: 12, status: 'absent', studentNameSnapshot: '李四' }),
    ]);
    expect(result.presentNames).toEqual(['张三']);
    expect(result.absentNames).toEqual(['李四']);
  });

  it('replaces details when the same room is submitted again today', async () => {
    const { service, inspectionRepo, detailRepo } = createService({
      existingInspection: { id: 91, roomId: 5, inspectionDate: today },
    });

    const result = await service.submit(5, today, [11, 12], { id: 8, username: 'reviewer' });

    expect(result.isUpdate).toBe(true);
    expect(inspectionRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ id: 91, inspectorId: 8, source: 'manual' }),
    );
    expect(detailRepo.delete).toHaveBeenCalledWith({ inspectionId: 91 });
  });

  it('rejects historical and future dates', async () => {
    const { service } = createService();
    await expect(service.submit('5' as never, '2000-01-01', [], {})).rejects.toThrow(
      new BadRequestException('历史日期的查寝记录不可更改'),
    );
    await expect(service.submit(5, '2999-01-01', [], {})).rejects.toThrow(
      new BadRequestException('不能提前提交未来日期的查寝记录'),
    );
  });

  it('rejects occupancy ids outside the room and date snapshot', async () => {
    const { service } = createService();
    await expect(service.submit(5, today, [999], { id: 7, username: 'teacher' })).rejects.toThrow(
      '存在不属于该宿舍当日住户的入住记录: 999',
    );
  });

  it('attributes automatic absences to the last manual inspector', async () => {
    const { service, inspectionRepo, logs } = createService();
    (inspectionRepo.findOne as jest.Mock).mockResolvedValueOnce({
      inspectorId: 8,
      inspectorName: 'last-teacher',
      source: 'manual',
    });

    const created = await service.settleDate('2026-07-21');

    expect(created).toBe(1);
    expect(inspectionRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({
        inspectorId: 8,
        inspectorName: 'last-teacher',
        source: 'automatic',
      }),
    );
    expect(logs.log).toHaveBeenCalledWith(
      expect.objectContaining({ username: 'last-teacher', action: '自动补记缺勤' }),
    );
  });

  it('uses the system identity when nobody inspected any room that day', async () => {
    const { service, inspectionRepo, logs } = createService();
    (inspectionRepo.findOne as jest.Mock).mockResolvedValueOnce(null);

    await service.settleDate('2026-07-21');

    expect(inspectionRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({
        inspectorId: null,
        inspectorName: '系统自动判定',
        source: 'automatic',
      }),
    );
    expect(logs.log).toHaveBeenCalledWith(
      expect.objectContaining({ username: '系统自动判定' }),
    );
  });
});
