import { Repository, DataSource } from 'typeorm';
import { OccupanciesService } from './occupancies.service';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { Student } from '../entities/student.entity';
import { Deposit } from '../entities/deposit.entity';
import { Bed } from '../entities/bed.entity';
import { Locker } from '../entities/locker.entity';
import { Organization } from '../entities/organization.entity';

type QueryBuilderMock<T> = {
  where: jest.Mock;
  andWhere: jest.Mock;
  setLock: jest.Mock;
  getOne: jest.Mock<Promise<T | null>, []>;
};

function createQueryBuilderMock<T>(result: T | null): QueryBuilderMock<T> {
  const qb = {
    where: jest.fn(),
    andWhere: jest.fn(),
    setLock: jest.fn(),
    getOne: jest.fn().mockResolvedValue(result),
  } as QueryBuilderMock<T>;
  qb.where.mockReturnValue(qb);
  qb.andWhere.mockReturnValue(qb);
  qb.setLock.mockReturnValue(qb);
  return qb;
}

function createTransactionDataSource(manager: Record<string, unknown>): DataSource {
  return {
    options: { type: 'sqlite' },
    transaction: jest.fn(
      async (fn: (manager: Record<string, unknown>) => unknown) => fn(manager),
    ),
  } as any as DataSource;
}

function createCheckInManager(options?: {
  existingOccupancy?: Occupancy | null;
  room?: Partial<Room> | null;
  student?: Partial<Student> | null;
  bed?: Partial<Bed> | null;
  locker?: Partial<Locker> | null;
  occupancyCount?: number;
  deposit?: Deposit | null;
}) {
  const manager = {
    createQueryBuilder: jest.fn(),
    count: jest.fn().mockResolvedValue(options?.occupancyCount ?? 0),
    findOne: jest.fn(),
    create: jest.fn((_: unknown, value: unknown) => value),
    save: jest.fn(async (value: any) => ({ ...value, id: value?.id ?? 10 })),
    update: jest.fn(),
  };
  const queryResults = [
    options?.existingOccupancy ?? null,
    options?.room ?? { id: 2, capacity: 4, status: 'available' },
    ...(options?.bed !== undefined ? [options.bed] : []),
    ...(options?.locker !== undefined ? [options.locker] : []),
  ];
  manager.createQueryBuilder.mockImplementation(() =>
    createQueryBuilderMock(queryResults.shift() ?? null),
  );
  manager.findOne.mockImplementation(async (entity: unknown) => {
    if (entity === Student) return options?.student ?? { id: 3, organizationId: 7 };
    if (entity === Deposit) return options?.deposit ?? null;
    return null;
  });
  return manager;
}

function createQueryRunnerDataSource(config: {
  oldOccupancy?: Partial<Occupancy> | null;
  newRoom?: Partial<Room> | null;
  newRoomCount?: number;
}): DataSource {
  const queryResults: (Record<string, unknown> | null)[] = [
    (config.oldOccupancy ?? {
      id: 1,
      roomId: 2,
      bedId: 3,
      lockerId: 4,
      checkInDate: '2026-07-01',
      billingStartDate: '2026-07-01',
      checkOutDate: null,
      stayType: 'long',
      responsibleOrganizationId: 7,
      studentId: 5,
    }) as Record<string, unknown>,
    (config.newRoom !== undefined
      ? config.newRoom
      : { id: 3, capacity: 4, status: 'available' }) as Record<string, unknown> | null,
  ];

  const manager = {
    createQueryBuilder: jest.fn().mockImplementation(() =>
      createQueryBuilderMock(queryResults.shift() ?? null),
    ),
    save: jest.fn(async (value: any) => ({ ...value, id: value?.id ?? 10 })),
    update: jest.fn(),
    create: jest.fn((_: unknown, value: unknown) => value),
    count: jest.fn().mockResolvedValue(config.newRoomCount ?? 0),
  };

  return {
    options: { type: 'sqlite' },
    createQueryRunner: jest.fn().mockReturnValue({
      connect: jest.fn().mockResolvedValue(undefined),
      startTransaction: jest.fn().mockResolvedValue(undefined),
      commitTransaction: jest.fn().mockResolvedValue(undefined),
      rollbackTransaction: jest.fn().mockResolvedValue(undefined),
      release: jest.fn().mockResolvedValue(undefined),
      manager,
    }),
  } as any as DataSource;
}

function createImportTransactionDataSource(repos: {
  occupancyRepo: Repository<Occupancy>;
  roomRepo: Repository<Room>;
  studentRepo: Repository<Student>;
  depositRepo: Repository<Deposit>;
  bedRepo: Repository<Bed>;
  lockerRepo: Repository<Locker>;
  organizationRepo: Repository<any>;
}): DataSource {
  return createTransactionDataSource({
    getRepository: jest.fn((entity: unknown) => {
      if (entity === Occupancy) return repos.occupancyRepo;
      if (entity === Room) return repos.roomRepo;
      if (entity === Student) return repos.studentRepo;
      if (entity === Deposit) return repos.depositRepo;
      if (entity === Bed) return repos.bedRepo;
      if (entity === Locker) return repos.lockerRepo;
      if (entity === Organization) return repos.organizationRepo;
      throw new Error('Unexpected repository');
    }),
  });
}

describe('OccupanciesService — boundary conditions', () => {
  // ── checkIn capacity guards ────────────────────────────────────────────

  it('1. checkIn rejects when capacity is 0 (??0 guard — fail closed)', async () => {
    const manager = createCheckInManager({
      room: { id: 2, capacity: 0, status: 'available' },
      occupancyCount: 0,
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    await expect(
      service.checkIn({ studentId: 3, roomId: 2, checkInDate: '2026-07-10' }),
    ).rejects.toThrow('宿舍已满');
  });

  it('2. checkIn rejects when capacity is undefined (??0 guard — fail closed)', async () => {
    const manager = createCheckInManager({
      room: { id: 2, capacity: undefined, status: 'available' },
      occupancyCount: 0,
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    await expect(
      service.checkIn({ studentId: 3, roomId: 2, checkInDate: '2026-07-10' }),
    ).rejects.toThrow('宿舍已满');
  });

  it('3. checkIn allows when count < capacity (normal path)', async () => {
    const manager = createCheckInManager({
      room: { id: 2, capacity: 4, status: 'available' },
      occupancyCount: 2,
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    const result = await service.checkIn({
      studentId: 3,
      roomId: 2,
      checkInDate: '2026-07-10',
    });

    expect(result).toBeTruthy();
    expect(manager.save).toHaveBeenCalled();
  });

  it('4. checkIn rejects when count >= capacity (normal rejection)', async () => {
    const manager = createCheckInManager({
      room: { id: 2, capacity: 4, status: 'available' },
      occupancyCount: 4,
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    await expect(
      service.checkIn({ studentId: 3, roomId: 2, checkInDate: '2026-07-10' }),
    ).rejects.toThrow('宿舍已满');
  });

  // ── transferRoom capacity guard ─────────────────────────────────────────

  it('5. transferRoom rejects when target room capacity is undefined (??0 guard — fail closed)', async () => {
    const dataSource = createQueryRunnerDataSource({
      oldOccupancy: {
        id: 1,
        roomId: 2,
        bedId: 3,
        lockerId: 4,
        checkInDate: '2026-07-01',
        billingStartDate: '2026-07-01',
        checkOutDate: null,
        stayType: 'long',
        responsibleOrganizationId: 7,
        studentId: 5,
      },
      newRoom: { id: 3, capacity: undefined, status: 'available' },
      newRoomCount: 0,
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      dataSource,
    );

    await expect(
      service.transferRoom(1, { newRoomId: 3, transferDate: '2026-07-15' }),
    ).rejects.toThrow('目标宿舍已满');
  });

  // ── batchImportCheckIn capacity guard ───────────────────────────────────

  it('6. batchImportCheckIn skips row when room capacity is undefined (??0 guard — fail closed)', async () => {
    const occupancyRepo = {
      findOne: jest.fn().mockResolvedValue(null),
      count: jest.fn().mockResolvedValue(0),
      create: jest.fn((value: any) => value),
      save: jest.fn(),
    } as any as Repository<Occupancy>;
    const roomRepo = {
      findOne: jest.fn().mockResolvedValue({
        id: 2,
        roomNumber: '4-102',
        capacity: undefined,
      }),
      update: jest.fn(),
    } as any as Repository<Room>;
    const studentRepo = {
      findOne: jest
        .fn()
        .mockResolvedValue({
          id: 3,
          name: '张三',
          phone: '13800138000',
          organizationId: 7,
        }),
    } as any as Repository<Student>;
    const bedRepo = {
      findOne: jest.fn(),
      count: jest.fn(),
      create: jest.fn(),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Bed>;
    const organizationRepo = {
      findOne: jest.fn(),
    } as any as Repository<any>;

    const service = new OccupanciesService(
      occupancyRepo,
      roomRepo,
      studentRepo,
      { findOne: jest.fn() } as any as Repository<Deposit>,
      bedRepo,
      { findOne: jest.fn() } as any as Repository<Locker>,
      organizationRepo,
      createImportTransactionDataSource({
        occupancyRepo,
        roomRepo,
        studentRepo,
        depositRepo: { findOne: jest.fn() } as any as Repository<Deposit>,
        bedRepo,
        lockerRepo: { findOne: jest.fn() } as any as Repository<Locker>,
        organizationRepo,
      }),
    );

    const result = await service.batchImportCheckIn([
      {
        name: '张三',
        phone: '13800138000',
        roomNumber: '4-102',
        bedNumber: '1号床',
        checkInDate: '2026-07-14',
      },
    ]);

    expect(result.imported).toBe(0);
    expect(result.skipped).toBe(1);
    expect(result.errors).toEqual(
      expect.arrayContaining([expect.stringContaining('已满')]),
    );
    expect(occupancyRepo.save).not.toHaveBeenCalled();
  });
});
