function makeExpensesService(
  a: never, b: never, c: never, d: never, e: never, f: never,
) {
  const operations = new ExpenseOperationsService(a, b, c, d, e, f);
  return new ExpensesService(a, b, c, d, e, f, operations);
}

import { BadRequestException, NotFoundException } from '@nestjs/common';
import { ExpensesService } from '../expenses/expenses.service';
import { ExpenseOperationsService } from '../expenses/expense-operations.service';
import { OccupanciesService } from '../occupancies/occupancies.service';
import { RoomsService } from '../rooms/rooms.service';
import { StudentsService } from '../students/students.service';

function updateQb(affected = 1) {
  const qb = {
    update: jest.fn(),
    set: jest.fn(),
    where: jest.fn(),
    execute: jest.fn().mockResolvedValue({ affected }),
  };
  qb.update.mockReturnValue(qb);
  qb.set.mockReturnValue(qb);
  qb.where.mockReturnValue(qb);
  return qb;
}

function listQb() {
  const qb = {
    leftJoinAndSelect: jest.fn(),
    where: jest.fn(),
    orderBy: jest.fn(),
    andWhere: jest.fn(),
    getMany: jest.fn().mockResolvedValue([]),
  };
  qb.leftJoinAndSelect.mockReturnValue(qb);
  qb.where.mockReturnValue(qb);
  qb.orderBy.mockReturnValue(qb);
  qb.andWhere.mockReturnValue(qb);
  return qb;
}

describe('batch restore service semantics', () => {
  it('rejects empty and invalid ids in every restore service', async () => {
    const students = new StudentsService(
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
      {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    const rooms = new RoomsService(
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    const expenses = makeExpensesService(
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    const occupancies = new OccupanciesService(
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    const calls = [
      (ids: number[]) => students.batchRestore(ids),
      (ids: number[]) => rooms.batchRestore(ids),
      (ids: number[]) => expenses.batchRestoreRoomExpenses(ids),
      (ids: number[]) => expenses.batchRestorePersonalExpenses(ids),
      (ids: number[]) => occupancies.batchRestore(ids),
    ];
    for (const call of calls) {
      await expect(call([])).rejects.toBeInstanceOf(BadRequestException);
      await expect(call([0])).rejects.toBeInstanceOf(BadRequestException);
      await expect(call([1.5])).rejects.toBeInstanceOf(BadRequestException);
    }
  });

  it('deduplicates student ids, restores archived rows, and skips active rows', async () => {
    const qb = updateQb();
    const repo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived' },
        { id: 2, status: 'active' },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const service = new StudentsService(
      repo as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
    );

    await expect(service.batchRestore([1, 1, 2])).resolves.toEqual({
      message: '已批量恢复 1 名学生',
      restored: 1,
      skipped: 1,
    });
    expect(repo.find).toHaveBeenCalledWith({ where: { id: expect.anything() } });
    expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
    expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
  });

  it('rejects missing student ids before updating', async () => {
    const repo = { find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived' }]), createQueryBuilder: jest.fn() };
    const service = new StudentsService(
      repo as never, {} as never, {} as never, {} as never, {} as never, {} as never,
      {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.batchRestore([1, 2])).rejects.toBeInstanceOf(NotFoundException);
    expect(repo.createQueryBuilder).not.toHaveBeenCalled();
  });

  it('restores archived rooms to available and skips non-archived rooms', async () => {
    const qb = updateQb();
    const repo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived' },
        { id: 2, status: 'maintenance' },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const service = new RoomsService(
      repo as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.batchRestore([1, 2])).resolves.toEqual({
      message: '已批量恢复 1 间宿舍', restored: 1, skipped: 1,
    });
    expect(qb.set).toHaveBeenCalledWith({ status: 'available' });
  });

  it('rejects room-expense restore when any selected record is billed', async () => {
    const qb = updateQb();
    const roomExpRepo = {
      find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived' }]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const billItemsRepo = { count: jest.fn().mockResolvedValue(1) };
    const service = makeExpensesService(
      roomExpRepo as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      { getRepository: jest.fn(() => billItemsRepo) } as never,
    );
    await expect(service.batchRestoreRoomExpenses([1])).rejects.toBeInstanceOf(BadRequestException);
    expect(roomExpRepo.createQueryBuilder).not.toHaveBeenCalled();
  });

  it('restores unbilled room expenses and reports active rows as skipped', async () => {
    const qb = updateQb();
    const roomExpRepo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived' },
        { id: 2, status: 'active' },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const service = makeExpensesService(
      roomExpRepo as never, {} as never, {} as never, {} as never, {} as never,
      { getRepository: jest.fn(() => ({ count: jest.fn().mockResolvedValue(0) })) } as never,
    );
    await expect(service.batchRestoreRoomExpenses([1, 2])).resolves.toMatchObject({ restored: 1, skipped: 1 });
    expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
  });

  it('skips an active billed room expense without blocking an archived unbilled expense', async () => {
    const qb = updateQb();
    const count = jest.fn().mockResolvedValue(0);
    const roomExpRepo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived' },
        { id: 2, status: 'active' },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const service = makeExpensesService(
      roomExpRepo as never, {} as never, {} as never, {} as never, {} as never,
      { getRepository: jest.fn(() => ({ count })) } as never,
    );
    await expect(service.batchRestoreRoomExpenses([1, 2])).resolves.toMatchObject({
      restored: 1,
      skipped: 1,
    });
    expect(count).toHaveBeenCalledWith({ where: { roomExpenseId: expect.anything() } });
    expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
  });

  it('rejects personal-expense restore when any selected record has a bill id', async () => {
    const personalExpRepo = {
      find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived', billId: 9 }]),
      createQueryBuilder: jest.fn(),
    };
    const service = makeExpensesService(
      {} as never, personalExpRepo as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.batchRestorePersonalExpenses([1])).rejects.toBeInstanceOf(BadRequestException);
    expect(personalExpRepo.createQueryBuilder).not.toHaveBeenCalled();
  });

  it('restores unbilled personal expenses and skips already active records', async () => {
    const qb = updateQb();
    const personalExpRepo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived', billId: null },
        { id: 2, status: 'active', billId: null },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const service = makeExpensesService(
      {} as never, personalExpRepo as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.batchRestorePersonalExpenses([1, 1, 2])).resolves.toMatchObject({
      restored: 1,
      skipped: 1,
    });
    expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
  });

  it('skips an active billed personal expense without blocking an archived unbilled expense', async () => {
    const qb = updateQb();
    const personalExpRepo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived', billId: null },
        { id: 2, status: 'active', billId: 9 },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const service = makeExpensesService(
      {} as never, personalExpRepo as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.batchRestorePersonalExpenses([1, 2])).resolves.toMatchObject({
      restored: 1,
      skipped: 1,
    });
    expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [1] });
  });

  it('uses archived status when querying expense archive views', async () => {
    const roomQb = listQb();
    const personalRepo = { find: jest.fn().mockResolvedValue([]) };
    const service = makeExpensesService(
      { createQueryBuilder: jest.fn(() => roomQb) } as never,
      personalRepo as never,
      {} as never, {} as never, {} as never, {} as never,
    );
    await service.findRoomExpenses({ status: 'archived' });
    await service.findPersonalExpenses({ status: 'archived' });
    expect(roomQb.where).toHaveBeenCalledWith('e.status = :status', { status: 'archived' });
    expect(personalRepo.find).toHaveBeenCalledWith(expect.objectContaining({ where: { status: 'archived' } }));
  });

  it('rejects invalid expense query status values', async () => {
    const service = makeExpensesService(
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.findRoomExpenses({ status: 'deleted' as never })).rejects.toBeInstanceOf(BadRequestException);
    await expect(service.findPersonalExpenses({ status: 'deleted' as never })).rejects.toBeInstanceOf(BadRequestException);
  });

  it('rejects an archived occupancy without a checkout date before updating', async () => {
    const repo = {
      find: jest.fn().mockResolvedValue([{ id: 1, status: 'archived', checkOutDate: null }]),
      createQueryBuilder: jest.fn(),
    };
    const service = new OccupanciesService(
      repo as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.batchRestore([1])).rejects.toBeInstanceOf(BadRequestException);
    expect(repo.createQueryBuilder).not.toHaveBeenCalled();
  });

  it('restores checked-out occupancies without changing room, bed, or locker state', async () => {
    const qb = updateQb();
    const repo = {
      find: jest.fn().mockResolvedValue([
        { id: 1, status: 'archived', checkOutDate: '2026-07-01' },
        { id: 2, status: 'active', checkOutDate: '2026-07-02' },
      ]),
      createQueryBuilder: jest.fn(() => qb),
    };
    const roomRepo = { update: jest.fn() };
    const bedRepo = { update: jest.fn() };
    const lockerRepo = { update: jest.fn() };
    const service = new OccupanciesService(
      repo as never, roomRepo as never, {} as never, {} as never, bedRepo as never,
      lockerRepo as never, {} as never, {} as never,
    );
    await expect(service.batchRestore([1, 2])).resolves.toMatchObject({ restored: 1, skipped: 1 });
    expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
    expect(roomRepo.update).not.toHaveBeenCalled();
    expect(bedRepo.update).not.toHaveBeenCalled();
    expect(lockerRepo.update).not.toHaveBeenCalled();
  });

  it('uses archived status while preserving active=true as checkout filtering', async () => {
    const qb = listQb();
    const service = new OccupanciesService(
      { createQueryBuilder: jest.fn(() => qb) } as never,
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await service.findAll({ status: 'archived', active: true });
    expect(qb.where).toHaveBeenCalledWith('o.status = :status', { status: 'archived' });
    expect(qb.andWhere).toHaveBeenCalledWith('o.checkOutDate IS NULL');
  });

  it('rejects invalid occupancy query status values', async () => {
    const service = new OccupanciesService(
      {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(service.findAll({ status: 'deleted' as never })).rejects.toBeInstanceOf(BadRequestException);
  });
});
