import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CabinetService } from '../../../src/modules/cabinet/cabinet.service.js';
import { CabinetEventType, CabinetEventSourceType } from '@meshitrack/shared';

describe(CabinetService.name, () => {
  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 mockMedicinesRepo = {
    findById: vi.fn(),
    findByHousehold: vi.fn(),
    findDuplicate: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    softDelete: vi.fn(),
  };

  const mockProductsRepo = {
    findById: vi.fn(),
    findByMedicine: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    softDelete: vi.fn(),
    countByMedicineId: vi.fn(),
  };

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

  let service: CabinetService;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new CabinetService({
      cabinetRepository: mockCabinetRepo as never,
      medicinesRepository: mockMedicinesRepo as never,
      medicineProductsRepository: mockProductsRepo as never,
      cabinetEventsService: mockCabinetEventsService as never,
    });
  });

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

      const response = await service.list('hh1', { limit: 20 });

      expect(response).toEqual(result);
      expect(mockCabinetRepo.findByHousehold).toHaveBeenCalledWith('hh1', { limit: 20 });
    });
  });

  describe('getById', () => {
    it('returns item when found', async () => {
      const item = { _id: 'ci-1', quantity: 30 };
      mockCabinetRepo.findById.mockResolvedValue(item);

      const result = await service.getById('ci-1', 'hh1');

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

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

      await expect(service.getById('ci-missing', 'hh1')).rejects.toThrow('Cabinet item not found');
    });
  });

  describe('getSummary', () => {
    it('returns aggregated summary with formatted dates', async () => {
      const expiryDate = new Date('2026-06-01T00:00:00.000Z');
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([
        {
          _id: 'med-1',
          medicineName: 'Metformin',
          medicineStrength: 500,
          medicineStrengthUnit: 'mg',
          medicineForm: 'tablet',
          totalQuantity: 60,
          unit: 'tablet',
          earliestExpiry: expiryDate,
          itemCount: 2,
        },
      ]);

      const result = await service.getSummary('hh1');

      expect(result).toHaveLength(1);
      expect(result[0].medicineId).toBe('med-1');
      expect(result[0].earliestExpiry).toBe('2026-06-01T00:00:00.000Z');
    });

    it('handles null expiry dates', async () => {
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([
        {
          _id: 'med-1',
          medicineName: 'Test',
          medicineStrength: 10,
          medicineStrengthUnit: 'mg',
          medicineForm: 'tablet',
          totalQuantity: 30,
          unit: 'tablet',
          earliestExpiry: null,
          itemCount: 1,
        },
      ]);

      const result = await service.getSummary('hh1');

      expect(result[0].earliestExpiry).toBeNull();
    });
  });

  describe('addItem', () => {
    const createInput = {
      medicineId: 'med-1',
      quantity: 30,
      unit: 'tablet' as const,
    };

    it('creates item with denormalized medicine fields', async () => {
      mockMedicinesRepo.findById.mockResolvedValue({
        _id: 'med-1',
        name: 'Metformin',
        strength: 500,
        strengthUnit: 'mg',
        form: 'tablet',
      });
      const created = { _id: 'ci-1', ...createInput, medicineName: 'Metformin' };
      mockCabinetRepo.create.mockResolvedValue(created);

      const result = await service.addItem(createInput, 'hh1', 'user-1');

      expect(result).toEqual(created);
      expect(mockCabinetRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          medicineName: 'Metformin',
          medicineStrength: 500,
          medicineStrengthUnit: 'mg',
          medicineForm: 'tablet',
        }),
        'hh1',
        'user-1',
      );
    });

    it('logs PURCHASED event after creation', async () => {
      mockMedicinesRepo.findById.mockResolvedValue({
        _id: 'med-1',
        name: 'Metformin',
        strength: 500,
        strengthUnit: 'mg',
        form: 'tablet',
      });
      mockCabinetRepo.create.mockResolvedValue({ _id: 'ci-1' });

      await service.addItem(createInput, 'hh1', 'user-1');

      expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          householdId: 'hh1',
          userId: 'user-1',
          cabinetItemId: 'ci-1',
          medicineId: 'med-1',
          eventType: CabinetEventType.PURCHASED,
          quantity: 30,
          quantityBefore: 0,
          quantityAfter: 30,
          sourceType: CabinetEventSourceType.MANUAL,
        }),
      );
    });

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

      await expect(service.addItem(createInput, 'hh1', 'user-1')).rejects.toThrow(
        'Medicine not found',
      );
    });

    it('denormalizes product brand when medicineProductId given', async () => {
      mockMedicinesRepo.findById.mockResolvedValue({
        _id: 'med-1',
        name: 'Metformin',
        strength: 500,
        strengthUnit: 'mg',
        form: 'tablet',
      });
      mockProductsRepo.findById.mockResolvedValue({
        _id: 'prod-1',
        brand: 'Glucophage',
      });
      mockCabinetRepo.create.mockResolvedValue({ _id: 'ci-1' });

      await service.addItem({ ...createInput, medicineProductId: 'prod-1' }, 'hh1', 'user-1');

      expect(mockCabinetRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ medicineProductBrand: 'Glucophage' }),
        'hh1',
        'user-1',
      );
    });

    it('throws NotFoundError when product not found', async () => {
      mockMedicinesRepo.findById.mockResolvedValue({
        _id: 'med-1',
        name: 'Metformin',
        strength: 500,
        strengthUnit: 'mg',
        form: 'tablet',
      });
      mockProductsRepo.findById.mockResolvedValue(null);

      await expect(
        service.addItem({ ...createInput, medicineProductId: 'prod-missing' }, 'hh1', 'user-1'),
      ).rejects.toThrow('Medicine product not found');
    });
  });

  describe('update', () => {
    it('updates and returns item', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      const updated = { _id: 'ci-1', quantity: 25 };
      mockCabinetRepo.update.mockResolvedValue(updated);

      const result = await service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1');

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

    it('logs ADJUSTED event when quantity changes', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.update.mockResolvedValue({ _id: 'ci-1', quantity: 25 });

      await service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1');

      expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: CabinetEventType.ADJUSTED,
          quantity: -5,
          quantityBefore: 30,
          quantityAfter: 25,
        }),
      );
    });

    it('does not log event when quantity unchanged', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.update.mockResolvedValue({ _id: 'ci-1', quantity: 30 });

      await service.update('ci-1', 'hh1', { notes: 'updated' }, 'user-1');

      expect(mockCabinetEventsService.logEvent).not.toHaveBeenCalled();
    });

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

      await expect(service.update('ci-missing', 'hh1', { quantity: 25 }, 'user-1')).rejects.toThrow(
        'Cabinet item not found',
      );
    });

    it('throws NotFoundError when update returns null', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.update.mockResolvedValue(null);

      await expect(service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1')).rejects.toThrow(
        'Cabinet item not found',
      );
    });
  });

  describe('adjustQuantity', () => {
    it('adjusts quantity and returns item', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      const updated = { _id: 'ci-1', quantity: 27 };
      mockCabinetRepo.adjustQuantity.mockResolvedValue(updated);

      const result = await service.adjustQuantity('ci-1', 'hh1', -3, 'user-1');

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

    it('logs ADJUSTED event', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 27 });

      await service.adjustQuantity('ci-1', 'hh1', -3, 'user-1', 'took some');

      expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: CabinetEventType.ADJUSTED,
          quantity: -3,
          quantityBefore: 30,
          quantityAfter: 27,
          reason: 'took some',
        }),
      );
    });

    it('throws BadRequestError when delta is 0', async () => {
      await expect(service.adjustQuantity('ci-1', 'hh1', 0, 'user-1')).rejects.toThrow(
        'Delta must be non-zero',
      );
    });

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

      await expect(service.adjustQuantity('ci-missing', 'hh1', 5, 'user-1')).rejects.toThrow(
        'Cabinet item not found',
      );
    });

    it('throws NotFoundError when adjust returns null', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 30,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.adjustQuantity.mockResolvedValue(null);

      await expect(service.adjustQuantity('ci-1', 'hh1', 5, 'user-1')).rejects.toThrow(
        'Cabinet item not found',
      );
    });
  });

  describe('getExpiringSoon', () => {
    it('delegates to repository', async () => {
      const items = [{ _id: 'ci-1' }];
      mockCabinetRepo.findExpiringSoon.mockResolvedValue(items);

      const result = await service.getExpiringSoon('hh1', 30);

      expect(result).toEqual(items);
      expect(mockCabinetRepo.findExpiringSoon).toHaveBeenCalledWith('hh1', 30);
    });
  });

  describe('delete', () => {
    it('soft deletes item and logs DELETED event', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 10,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.softDelete.mockResolvedValue({ _id: 'ci-1', isDeleted: true });

      const result = await service.delete('ci-1', 'hh1', 'user-1');

      expect(result.isDeleted).toBe(true);
      expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: CabinetEventType.DELETED,
          quantity: -10,
          quantityBefore: 10,
          quantityAfter: 0,
        }),
      );
    });

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

      await expect(service.delete('ci-missing', 'hh1', 'user-1')).rejects.toThrow(
        'Cabinet item not found',
      );
    });

    it('throws NotFoundError when softDelete returns null', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 5,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.softDelete.mockResolvedValue(null);

      await expect(service.delete('ci-1', 'hh1', 'user-1')).rejects.toThrow(
        'Cabinet item not found',
      );
    });
  });

  describe('discard', () => {
    it('discards item and logs DISCARDED event', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 20,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.discard.mockResolvedValue({ _id: 'ci-1', quantity: 0, isDeleted: true });

      const result = await service.discard('ci-1', 'hh1', 'user-1', 'expired', 'smelled off');

      expect(result.quantity).toBe(0);
      expect(result.isDeleted).toBe(true);
      expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          eventType: CabinetEventType.DISCARDED,
          quantity: -20,
          quantityBefore: 20,
          quantityAfter: 0,
          reason: 'expired',
          notes: 'smelled off',
        }),
      );
    });

    it('throws BadRequestError when quantity is zero', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 0,
        medicineId: 'med-1',
        medicineName: 'Test',
      });

      await expect(service.discard('ci-1', 'hh1', 'user-1', 'expired')).rejects.toThrow(
        'Cannot discard an item with zero quantity',
      );
    });

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

      await expect(service.discard('ci-missing', 'hh1', 'user-1', 'expired')).rejects.toThrow(
        'Cabinet item not found',
      );
    });

    it('throws NotFoundError when discard returns null', async () => {
      mockCabinetRepo.findById.mockResolvedValue({
        _id: 'ci-1',
        quantity: 10,
        medicineId: 'med-1',
        medicineName: 'Test',
      });
      mockCabinetRepo.discard.mockResolvedValue(null);

      await expect(service.discard('ci-1', 'hh1', 'user-1', 'expired')).rejects.toThrow(
        'Cabinet item not found',
      );
    });
  });
});
