import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  CabinetEventType,
  CabinetEventSourceType,
  OrganizerFillStatus,
  DosageFrequency,
} from '@meshitrack/shared';

const { mockSession } = vi.hoisted(() => ({
  mockSession: {
    startTransaction: vi.fn(),
    commitTransaction: vi.fn(),
    abortTransaction: vi.fn(),
    endSession: vi.fn(),
  },
}));

vi.mock('mongoose', () => {
  return { default: { startSession: vi.fn().mockResolvedValue(mockSession) } };
});

import { OrganizerService } from '../../../src/modules/organizer/organizer.service.js';

describe(OrganizerService.name, () => {
  const mockOrganizerRepo = {
    findByHousehold: vi.fn(),
    findById: vi.fn(),
    create: vi.fn(),
    updateStatus: vi.fn(),
  };

  const mockRegimensService = {
    list: vi.fn(),
    getById: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
    getActiveByUser: vi.fn(),
    calculateBurnRates: vi.fn(),
  };

  const mockCabinetRepo = {
    findByHousehold: vi.fn(),
    findById: vi.fn(),
    getAggregateSummary: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    adjustQuantity: vi.fn(),
    findExpiringSoon: vi.fn(),
    softDelete: vi.fn(),
    countByMedicineId: vi.fn(),
    discard: vi.fn(),
    findActiveByMedicineForFEFO: vi.fn(),
  };

  const mockCabinetEventsService = {
    logEvent: vi.fn(),
    logEvents: vi.fn(),
    listEvents: vi.fn(),
    getEventsByItem: vi.fn(),
    getSpendingSummary: vi.fn(),
    getAvgUnitPrices: vi.fn(),
  };

  let service: OrganizerService;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new OrganizerService({
      organizerRepository: mockOrganizerRepo as never,
      regimensService: mockRegimensService as never,
      cabinetRepository: mockCabinetRepo as never,
      cabinetEventsService: mockCabinetEventsService as never,
    });
  });

  describe('listFills', () => {
    it('delegates to repository', async () => {
      const result = { data: [], pagination: { cursor: null, hasMore: false } };
      mockOrganizerRepo.findByHousehold.mockResolvedValue(result);

      const response = await service.listFills('hh1', 'user-1', { limit: 20 });

      expect(response).toEqual(result);
      expect(mockOrganizerRepo.findByHousehold).toHaveBeenCalledWith('hh1', 'user-1', {
        limit: 20,
      });
    });
  });

  describe('getFillById', () => {
    it('returns fill when found', async () => {
      const fill = { _id: 'fill-1', regimenName: 'Morning' };
      mockOrganizerRepo.findById.mockResolvedValue(fill);

      const result = await service.getFillById('fill-1', 'hh1');

      expect(result).toEqual(fill);
    });

    it('throws NotFoundError when not found', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(null);

      await expect(service.getFillById('fill-missing', 'hh1')).rejects.toThrow(
        'Organizer fill not found',
      );
    });
  });

  describe('preview', () => {
    const makeRegimen = (overrides = {}) => ({
      _id: 'reg-1',
      name: 'Morning Routine',
      isActive: true,
      medications: [
        {
          medicineId: 'med-1',
          medicineName: 'Metformin',
          dosage: 1,
          frequency: DosageFrequency.DAILY,
          customFrequencyPerDay: null,
        },
      ],
      ...overrides,
    });

    it('returns preview with no shortages when stock is sufficient', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: new Date('2027-01-01') },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      expect(result.regimenName).toBe('Morning Routine');
      expect(result.numberOfDays).toBe(7);
      expect(result.canFillCompletely).toBe(true);
      expect(result.hasShortages).toBe(false);
      expect(result.items).toHaveLength(1);
      expect(result.items[0].medicineId).toBe('med-1');
      expect(result.items[0].quantityNeeded).toBe(7);
      expect(result.items[0].quantityAvailable).toBe(30);
      expect(result.items[0].isShort).toBe(false);
      expect(result.items[0].shortage).toBe(0);
      expect(result.items[0].cabinetBreakdown).toHaveLength(1);
      expect(result.items[0].cabinetBreakdown[0].quantityToTake).toBe(7);
      expect(result.items[0].cabinetBreakdown[0].quantityBefore).toBe(30);
    });

    it('returns preview with shortages when stock is insufficient', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 3, expirationDate: null },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      expect(result.canFillCompletely).toBe(false);
      expect(result.hasShortages).toBe(true);
      expect(result.items[0].isShort).toBe(true);
      expect(result.items[0].shortage).toBe(4);
      expect(result.items[0].quantityAvailable).toBe(3);
    });

    it('handles FEFO allocation across multiple cabinet items', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 3, expirationDate: new Date('2026-06-01') },
        { _id: { toString: () => 'ci-2' }, quantity: 5, expirationDate: new Date('2026-12-01') },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      expect(result.canFillCompletely).toBe(true);
      expect(result.items[0].cabinetBreakdown).toHaveLength(2);
      expect(result.items[0].cabinetBreakdown[0].cabinetItemId).toBe('ci-1');
      expect(result.items[0].cabinetBreakdown[0].quantityToTake).toBe(3);
      expect(result.items[0].cabinetBreakdown[1].cabinetItemId).toBe('ci-2');
      expect(result.items[0].cabinetBreakdown[1].quantityToTake).toBe(4);
    });

    it('skips AS_NEEDED medications', async () => {
      mockRegimensService.getById.mockResolvedValue(
        makeRegimen({
          medications: [
            {
              medicineId: 'med-1',
              medicineName: 'Metformin',
              dosage: 1,
              frequency: DosageFrequency.AS_NEEDED,
              customFrequencyPerDay: null,
            },
            {
              medicineId: 'med-2',
              medicineName: 'Ibuprofen',
              dosage: 2,
              frequency: DosageFrequency.DAILY,
              customFrequencyPerDay: null,
            },
          ],
        }),
      );
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-2' }, quantity: 20, expirationDate: null },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      expect(result.items).toHaveLength(1);
      expect(result.items[0].medicineId).toBe('med-2');
    });

    it('throws BadRequestError when regimen is not active', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen({ isActive: false }));

      await expect(service.preview('hh1', 'user-1', 'reg-1', 7)).rejects.toThrow(
        'Regimen is not active',
      );
    });

    it('handles null expirationDate in cabinet items', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 10, expirationDate: null },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      expect(result.items[0].cabinetBreakdown[0].expirationDate).toBeNull();
    });

    it('handles empty cabinet (no items available)', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      expect(result.canFillCompletely).toBe(false);
      expect(result.hasShortages).toBe(true);
      expect(result.items[0].quantityAvailable).toBe(0);
      expect(result.items[0].shortage).toBe(7);
      expect(result.items[0].cabinetBreakdown).toHaveLength(0);
    });

    it('uses customFrequencyPerDay when present', async () => {
      mockRegimensService.getById.mockResolvedValue(
        makeRegimen({
          medications: [
            {
              medicineId: 'med-1',
              medicineName: 'Custom Med',
              dosage: 2,
              frequency: DosageFrequency.CUSTOM,
              customFrequencyPerDay: 3,
            },
          ],
        }),
      );
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 100, expirationDate: null },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      // dosage(2) * customFrequencyPerDay(3) * numberOfDays(7) = 42
      expect(result.items[0].quantityNeeded).toBe(42);
    });

    it('stops taking from cabinet items once remaining is zero', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 7, expirationDate: null },
        { _id: { toString: () => 'ci-2' }, quantity: 10, expirationDate: null },
      ]);

      const result = await service.preview('hh1', 'user-1', 'reg-1', 7);

      // Needs 7, first item has 7 -- second item should not be touched
      expect(result.items[0].cabinetBreakdown).toHaveLength(1);
      expect(result.items[0].cabinetBreakdown[0].quantityToTake).toBe(7);
    });
  });

  describe('fill', () => {
    const fillInput = {
      regimenId: 'reg-1',
      numberOfDays: 7,
      allowPartial: false,
      notes: 'Weekly fill',
    };

    const makeRegimen = () => ({
      _id: 'reg-1',
      name: 'Morning Routine',
      isActive: true,
      medications: [
        {
          medicineId: 'med-1',
          medicineName: 'Metformin',
          dosage: 1,
          frequency: DosageFrequency.DAILY,
          customFrequencyPerDay: null,
        },
      ],
    });

    it('executes fill successfully with no shortages', async () => {
      // preview dependencies
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: new Date('2027-01-01') },
      ]);
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockOrganizerRepo.create.mockResolvedValue({
        _id: { toString: () => 'fill-1' },
        householdId: 'hh1',
        userId: 'user-1',
        regimenId: 'reg-1',
        regimenName: 'Morning Routine',
        status: OrganizerFillStatus.COMPLETED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      const result = await service.fill('hh1', 'user-1', fillInput);

      expect(result.status).toBe(OrganizerFillStatus.COMPLETED);
      expect(mockSession.startTransaction).toHaveBeenCalled();
      expect(mockSession.commitTransaction).toHaveBeenCalled();
      expect(mockSession.endSession).toHaveBeenCalled();
      expect(mockCabinetRepo.adjustQuantity).toHaveBeenCalledWith('ci-1', 'hh1', -7);
      expect(mockCabinetEventsService.logEvents).toHaveBeenCalled();
    });

    it('throws BadRequestError when shortages exist and allowPartial is false', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 3, expirationDate: null },
      ]);

      await expect(service.fill('hh1', 'user-1', fillInput)).rejects.toThrow(
        'Not enough stock to fill completely',
      );
    });

    it('allows partial fill when allowPartial is true', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 3, expirationDate: null },
      ]);
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 0 });
      mockOrganizerRepo.create.mockResolvedValue({
        _id: { toString: () => 'fill-1' },
        status: OrganizerFillStatus.PARTIAL,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      const result = await service.fill('hh1', 'user-1', { ...fillInput, allowPartial: true });

      expect(result.status).toBe(OrganizerFillStatus.PARTIAL);
      expect(mockOrganizerRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          status: OrganizerFillStatus.PARTIAL,
        }),
      );
    });

    it('creates CONSUMED events for each deduction', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: null },
      ]);
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockOrganizerRepo.create.mockResolvedValue({
        _id: { toString: () => 'fill-1' },
        status: OrganizerFillStatus.COMPLETED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      await service.fill('hh1', 'user-1', fillInput);

      const events = mockCabinetEventsService.logEvents.mock.calls[0][0];
      expect(events).toHaveLength(1);
      expect(events[0]).toEqual(
        expect.objectContaining({
          householdId: 'hh1',
          userId: 'user-1',
          cabinetItemId: 'ci-1',
          medicineId: 'med-1',
          medicineName: 'Metformin',
          eventType: CabinetEventType.CONSUMED,
          quantity: -7,
          quantityBefore: 30,
          quantityAfter: 23,
          sourceType: CabinetEventSourceType.ORGANIZER_FILL,
          sourceId: 'fill-1',
        }),
      );
    });

    it('handles adjustQuantity returning null (skips deduction)', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: null },
      ]);
      mockCabinetRepo.adjustQuantity.mockResolvedValue(null);
      mockOrganizerRepo.create.mockResolvedValue({
        _id: { toString: () => 'fill-1' },
        status: OrganizerFillStatus.PARTIAL,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      const result = await service.fill('hh1', 'user-1', { ...fillInput, allowPartial: true });

      expect(result).toBeTruthy();
      // No events logged since adjustQuantity returned null
      const events = mockCabinetEventsService.logEvents.mock.calls[0][0];
      expect(events).toHaveLength(0);
    });

    it('aborts transaction and rethrows on error', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: null },
      ]);
      mockCabinetRepo.adjustQuantity.mockRejectedValue(new Error('DB failure'));

      await expect(service.fill('hh1', 'user-1', fillInput)).rejects.toThrow('DB failure');

      expect(mockSession.abortTransaction).toHaveBeenCalled();
      expect(mockSession.endSession).toHaveBeenCalled();
    });

    it('creates fill with notes when provided', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: null },
      ]);
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockOrganizerRepo.create.mockResolvedValue({
        _id: { toString: () => 'fill-1' },
        status: OrganizerFillStatus.COMPLETED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      await service.fill('hh1', 'user-1', fillInput);

      expect(mockOrganizerRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          notes: 'Weekly fill',
        }),
      );
    });

    it('sets COMPLETED status when no items have shortages', async () => {
      mockRegimensService.getById.mockResolvedValue(makeRegimen());
      mockCabinetRepo.findActiveByMedicineForFEFO.mockResolvedValue([
        { _id: { toString: () => 'ci-1' }, quantity: 30, expirationDate: null },
      ]);
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockOrganizerRepo.create.mockResolvedValue({
        _id: { toString: () => 'fill-1' },
        status: OrganizerFillStatus.COMPLETED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      await service.fill('hh1', 'user-1', fillInput);

      expect(mockOrganizerRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          status: OrganizerFillStatus.COMPLETED,
        }),
      );
    });
  });

  describe('undoFill', () => {
    const makeFill = (overrides = {}) => ({
      _id: 'fill-1',
      householdId: 'hh1',
      userId: 'user-1',
      status: OrganizerFillStatus.COMPLETED,
      items: [
        {
          medicineId: 'med-1',
          medicineName: 'Metformin',
          quantityNeeded: 7,
          quantityTaken: 7,
          wasShort: false,
          shortage: 0,
          deductions: [{ cabinetItemId: 'ci-1', quantityTaken: 7 }],
        },
      ],
      ...overrides,
    });

    it('reverses fill and restores quantities', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(makeFill());
      mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 30 });
      mockOrganizerRepo.updateStatus.mockResolvedValue({
        ...makeFill(),
        status: OrganizerFillStatus.REVERSED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      const result = await service.undoFill('hh1', 'fill-1', 'user-1');

      expect(result.status).toBe(OrganizerFillStatus.REVERSED);
      expect(mockCabinetRepo.adjustQuantity).toHaveBeenCalledWith('ci-1', 'hh1', 7);
      expect(mockOrganizerRepo.updateStatus).toHaveBeenCalledWith(
        'fill-1',
        'hh1',
        OrganizerFillStatus.REVERSED,
      );
      expect(mockSession.startTransaction).toHaveBeenCalled();
      expect(mockSession.commitTransaction).toHaveBeenCalled();
      expect(mockSession.endSession).toHaveBeenCalled();
    });

    it('creates RESTORED events for each deduction', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(makeFill());
      mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 30 });
      mockOrganizerRepo.updateStatus.mockResolvedValue({
        ...makeFill(),
        status: OrganizerFillStatus.REVERSED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      await service.undoFill('hh1', 'fill-1', 'user-1');

      const events = mockCabinetEventsService.logEvents.mock.calls[0][0];
      expect(events).toHaveLength(1);
      expect(events[0]).toEqual(
        expect.objectContaining({
          householdId: 'hh1',
          userId: 'user-1',
          cabinetItemId: 'ci-1',
          medicineId: 'med-1',
          medicineName: 'Metformin',
          eventType: CabinetEventType.RESTORED,
          quantity: 7,
          quantityBefore: 23,
          quantityAfter: 30,
          sourceType: CabinetEventSourceType.ORGANIZER_UNDO,
          sourceId: 'fill-1',
        }),
      );
    });

    it('throws BadRequestError when fill is already reversed', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(
        makeFill({ status: OrganizerFillStatus.REVERSED }),
      );

      await expect(service.undoFill('hh1', 'fill-1', 'user-1')).rejects.toThrow(
        'Fill has already been reversed',
      );
    });

    it('throws NotFoundError when fill does not exist', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(null);

      await expect(service.undoFill('hh1', 'fill-missing', 'user-1')).rejects.toThrow(
        'Organizer fill not found',
      );
    });

    it('aborts transaction and rethrows on error', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(makeFill());
      mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 23 });
      mockCabinetRepo.adjustQuantity.mockRejectedValue(new Error('DB failure'));

      await expect(service.undoFill('hh1', 'fill-1', 'user-1')).rejects.toThrow('DB failure');

      expect(mockSession.abortTransaction).toHaveBeenCalled();
      expect(mockSession.endSession).toHaveBeenCalled();
    });

    it('handles cabinet item not found when restoring (uses 0 as quantityBefore)', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(makeFill());
      mockCabinetRepo.findById.mockResolvedValue(null);
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 7 });
      mockOrganizerRepo.updateStatus.mockResolvedValue({
        ...makeFill(),
        status: OrganizerFillStatus.REVERSED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      await service.undoFill('hh1', 'fill-1', 'user-1');

      const events = mockCabinetEventsService.logEvents.mock.calls[0][0];
      expect(events[0].quantityBefore).toBe(0);
      expect(events[0].quantityAfter).toBe(7);
    });

    it('throws NotFoundError when updateStatus returns null', async () => {
      mockOrganizerRepo.findById.mockResolvedValue(makeFill({ items: [] }));
      mockOrganizerRepo.updateStatus.mockResolvedValue(null);

      await expect(service.undoFill('hh1', 'fill-1', 'user-1')).rejects.toThrow(
        'Organizer fill not found',
      );
    });

    it('restores multiple deductions across items', async () => {
      const fill = makeFill({
        items: [
          {
            medicineId: 'med-1',
            medicineName: 'Metformin',
            quantityNeeded: 7,
            quantityTaken: 7,
            wasShort: false,
            shortage: 0,
            deductions: [
              { cabinetItemId: 'ci-1', quantityTaken: 4 },
              { cabinetItemId: 'ci-2', quantityTaken: 3 },
            ],
          },
          {
            medicineId: 'med-2',
            medicineName: 'Aspirin',
            quantityNeeded: 14,
            quantityTaken: 14,
            wasShort: false,
            shortage: 0,
            deductions: [{ cabinetItemId: 'ci-3', quantityTaken: 14 }],
          },
        ],
      });
      mockOrganizerRepo.findById.mockResolvedValue(fill);
      mockCabinetRepo.findById.mockResolvedValue({ quantity: 10 });
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ quantity: 20 });
      mockOrganizerRepo.updateStatus.mockResolvedValue({
        ...fill,
        status: OrganizerFillStatus.REVERSED,
      });
      mockCabinetEventsService.logEvents.mockResolvedValue([]);

      await service.undoFill('hh1', 'fill-1', 'user-1');

      expect(mockCabinetRepo.adjustQuantity).toHaveBeenCalledTimes(3);
      expect(mockCabinetRepo.findById).toHaveBeenCalledTimes(3);
      const events = mockCabinetEventsService.logEvents.mock.calls[0][0];
      expect(events).toHaveLength(3);
    });
  });
});
