import { BadRequestException, NotFoundException } from '@nestjs/common';
import { BillsService } from './bills.service';
import { Bill } from '../entities/bill.entity';

function queryBuilder() {
  return {
    update: jest.fn().mockReturnThis(),
    set: jest.fn().mockReturnThis(),
    where: jest.fn().mockReturnThis(),
    execute: jest.fn().mockResolvedValue({ affected: 1 }),
  };
}

function createService(bills: Partial<Bill>[] = []) {
  const billRepo = {
    find: jest.fn().mockResolvedValue(bills),
    findOne: jest.fn().mockResolvedValue(bills[0] ?? null),
    save: jest.fn(async (value) => value),
    update: jest.fn(),
    createQueryBuilder: jest.fn(() => queryBuilder()),
  };
  const manager = {
    delete: jest.fn(),
    update: jest.fn(),
  };
  const dataSource = { transaction: jest.fn(async (callback) => callback(manager)) };
  const service = new BillsService(
    billRepo as any,
    { delete: jest.fn() } as any,
    {} as any,
    { update: jest.fn() } as any,
    {} as any,
    {} as any,
    dataSource as any,
    {} as any,
  );
  return { service, billRepo, dataSource, manager };
}

describe('BillsService state and batch boundaries', () => {
  it('rejects an empty batch status update', async () => {
    const { service, billRepo } = createService();
    await expect(service.batchUpdateStatus([], 'paid')).rejects.toBeInstanceOf(BadRequestException);
    expect(billRepo.find).not.toHaveBeenCalled();
  });

  it('rejects a batch status update when some ids do not exist', async () => {
    const { service, billRepo } = createService([{ id: 1, paidAmount: 0, outstandingAmount: 10 }]);
    await expect(service.batchUpdateStatus([1, 2], 'unpaid')).rejects.toBeInstanceOf(NotFoundException);
    expect(billRepo.createQueryBuilder).not.toHaveBeenCalled();
  });

  it('rejects marking a partially paid bill unpaid', async () => {
    const { service, billRepo } = createService([{ id: 1, paidAmount: 10, outstandingAmount: 90, status: 'partially_paid' }]);
    await expect(service.updateStatus(1, { status: 'unpaid' })).rejects.toBeInstanceOf(BadRequestException);
    expect(billRepo.save).not.toHaveBeenCalled();
  });

  it('rejects an empty batch archive', async () => {
    const { service, dataSource } = createService();
    await expect(service.batchRemove([])).rejects.toBeInstanceOf(BadRequestException);
    expect(dataSource.transaction).not.toHaveBeenCalled();
  });

  it('rejects a batch archive when some ids do not exist', async () => {
    const { service, dataSource } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]);
    await expect(service.batchRemove([1, 2])).rejects.toBeInstanceOf(NotFoundException);
    expect(dataSource.transaction).not.toHaveBeenCalled();
  });

  it('archives an unpaid bill without deleting rows', async () => {
    const { service, billRepo, dataSource, manager } = createService([{ id: 1, paidAmount: 0, status: 'unpaid' }]);
    await expect(service.remove(1)).resolves.toEqual({ message: '账单已归档' });
    expect(billRepo.save).not.toHaveBeenCalled();
    expect(billRepo.createQueryBuilder).not.toHaveBeenCalled();
    expect(billRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
    expect((billRepo as any).update).toHaveBeenCalledWith(1, expect.objectContaining({ status: 'cancelled' }));
    expect(dataSource.transaction).not.toHaveBeenCalled();
    expect(manager.delete).not.toHaveBeenCalled();
  });
});
