import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RefillsService } from '../../../src/modules/refills/refills.service.js';

describe(RefillsService.name, () => {
  const mockRepo = {
    create: vi.fn(),
    findByHousehold: vi.fn(),
    findById: vi.fn(),
    update: vi.fn(),
    updateItem: vi.fn(),
    markItemsAddedToCabinet: vi.fn(),
  };
  const mockRegimensService = {
    calculateBurnRates: vi.fn(),
  };
  const mockCabinetRepo = {
    getAggregateSummary: vi.fn(),
  };
  const mockCabinetService = {
    addItem: vi.fn(),
  };
  const mockPricesRepo = {
    getLatestForMedicine: vi.fn(),
    compareStores: vi.fn(),
  };
  const mockPurchasesRepo = {
    getPendingMedicineStock: vi.fn(),
  };

  let service: RefillsService;

  beforeEach(() => {
    vi.clearAllMocks();
    mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([]);
    service = new RefillsService({
      refillsRepository: mockRepo as never,
      regimensService: mockRegimensService as never,
      cabinetRepository: mockCabinetRepo as never,
      cabinetService: mockCabinetService as never,
      medicinePricesRepository: mockPricesRepo as never,
      purchasesRepository: mockPurchasesRepo as never,
    });
  });

  describe('getAlerts', () => {
    it('returns empty array when no medicines are running low', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 1,
          totalInCabinet: 100,
          daysUntilEmpty: 100,
        },
      ]);

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

      expect(result).toEqual([]);
    });

    it('returns alerts for medicines below threshold', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 2,
          totalInCabinet: 10,
          daysUntilEmpty: 5,
        },
      ]);
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([
        { _id: 'med-1', medicineStrength: 500, medicineStrengthUnit: 'mg' },
      ]);
      mockPricesRepo.getLatestForMedicine.mockResolvedValue(null);
      mockPricesRepo.compareStores.mockResolvedValue([]);

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

      expect(result).toHaveLength(1);
      expect(result[0].medicineId).toBe('med-1');
      expect(result[0].daysUntilEmpty).toBe(5);
      expect(result[0].suggestedQuantity).toBe(60); // ceil(2 * 30)
    });

    it('attaches lastKnownPrice when available', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 2,
          totalInCabinet: 5,
          daysUntilEmpty: 2,
        },
      ]);
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([]);
      mockPricesRepo.getLatestForMedicine.mockResolvedValue({
        price: 10,
        pricePerUnit: 0.1,
        storeName: 'Walgreens',
        storeId: 'st-1',
        date: new Date('2026-01-01T00:00:00.000Z'),
      });
      mockPricesRepo.compareStores.mockResolvedValue([]);

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

      expect(result[0].lastKnownPrice).toBeDefined();
      expect(result[0].lastKnownPrice?.storeName).toBe('Walgreens');
    });

    it('attaches cheapestOption from compareStores', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 2,
          totalInCabinet: 5,
          daysUntilEmpty: 2,
        },
      ]);
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([]);
      mockPricesRepo.getLatestForMedicine.mockResolvedValue(null);
      mockPricesRepo.compareStores.mockResolvedValue([
        {
          storeId: 'st-1',
          storeName: 'CVS',
          latestPrice: 8,
          latestPricePerUnit: 0.08,
          currency: 'USD',
          date: new Date(),
        },
      ]);

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

      expect(result[0].cheapestOption).toBeDefined();
      expect(result[0].cheapestOption?.storeName).toBe('CVS');
    });

    it('includes pendingOrderStock and daysUntilEmptyWithOrders from ordered purchases', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 2,
          totalInCabinet: 4,
          daysUntilEmpty: 2,
        },
      ]);
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([]);
      mockPricesRepo.getLatestForMedicine.mockResolvedValue(null);
      mockPricesRepo.compareStores.mockResolvedValue([]);
      mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([
        { medicineId: 'med-1', totalUnits: 60 },
      ]);

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

      expect(result[0].pendingOrderStock).toBe(60);
      expect(result[0].daysUntilEmptyWithOrders).toBe(32); // (4 + 60) / 2
    });

    it('excludes medicines with null daysUntilEmpty', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 2,
          totalInCabinet: 0,
          daysUntilEmpty: null,
        },
      ]);

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

      expect(result).toHaveLength(0);
    });
  });

  describe('createList', () => {
    it('creates list with provided items', async () => {
      const list = { _id: 'rl-1', name: 'My List', items: [], status: 'active' };
      mockRepo.create.mockResolvedValue(list);

      const result = await service.createList(
        {
          name: 'My List',
          fromAlerts: false,
          thresholdDays: 7,
          items: [
            { medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet' as never },
          ],
        },
        'hh1',
        'user-1',
      );

      expect(result).toEqual(list);
      expect(mockRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'My List', householdId: 'hh1' }),
      );
    });

    it('creates list with no items when neither fromAlerts nor items provided', async () => {
      mockRepo.create.mockResolvedValue({ _id: 'rl-1', items: [] });

      await service.createList(
        { name: 'Empty List', fromAlerts: false, thresholdDays: 7 },
        'hh1',
        'user-1',
      );

      expect(mockRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ items: [], totalEstimatedCost: undefined }),
      );
    });

    it('computes totalEstimatedCost from items with estimatedPrice', async () => {
      mockRepo.create.mockResolvedValue({ _id: 'rl-1', items: [] });

      await service.createList(
        {
          name: 'Priced List',
          fromAlerts: false,
          thresholdDays: 7,
          items: [
            {
              medicineId: 'med-1',
              medicineName: 'Aspirin',
              quantity: 30,
              unit: 'tablet' as never,
              estimatedPrice: 10,
            },
            {
              medicineId: 'med-2',
              medicineName: 'Ibuprofen',
              quantity: 20,
              unit: 'tablet' as never,
              estimatedPrice: 8,
            },
          ],
        },
        'hh1',
        'user-1',
      );

      expect(mockRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ totalEstimatedCost: 18 }),
      );
    });

    it('creates list from alerts when fromAlerts is true', async () => {
      mockRegimensService.calculateBurnRates.mockResolvedValue([
        {
          medicineId: 'med-1',
          medicineName: 'Aspirin',
          dailyConsumption: 2,
          totalInCabinet: 5,
          daysUntilEmpty: 2,
        },
      ]);
      mockCabinetRepo.getAggregateSummary.mockResolvedValue([]);
      mockPricesRepo.getLatestForMedicine.mockResolvedValue(null);
      mockPricesRepo.compareStores.mockResolvedValue([]);
      mockRepo.create.mockResolvedValue({ _id: 'rl-1', items: [] });

      await service.createList(
        { name: 'Auto List', fromAlerts: true, thresholdDays: 7 },
        'hh1',
        'user-1',
      );

      expect(mockRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          items: expect.arrayContaining([expect.objectContaining({ medicineId: 'med-1' })]),
        }),
      );
    });
  });

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

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

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

  describe('getById', () => {
    it('returns list when found', async () => {
      const list = { _id: 'rl-1', name: 'My List' };
      mockRepo.findById.mockResolvedValue(list);

      expect(await service.getById('rl-1', 'hh1')).toEqual(list);
    });

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

      await expect(service.getById('missing', 'hh1')).rejects.toThrow('Refill list not found');
    });
  });

  describe('updateList', () => {
    it('updates and returns list', async () => {
      mockRepo.findById.mockResolvedValue({ _id: 'rl-1' });
      const updated = { _id: 'rl-1', name: 'Updated' };
      mockRepo.update.mockResolvedValue(updated);

      const result = await service.updateList('rl-1', 'hh1', { name: 'Updated' });

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

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

      await expect(service.updateList('missing', 'hh1', {})).rejects.toThrow(
        'Refill list not found',
      );
    });

    it('throws NotFoundError when update returns null', async () => {
      mockRepo.findById.mockResolvedValue({ _id: 'rl-1' });
      mockRepo.update.mockResolvedValue(null);

      await expect(service.updateList('rl-1', 'hh1', {})).rejects.toThrow('Refill list not found');
    });
  });

  describe('updateItem', () => {
    it('updates item and returns list', async () => {
      mockRepo.findById.mockResolvedValue({ _id: 'rl-1' });
      const updated = { _id: 'rl-1', items: [{ _id: 'item-1', checked: true }] };
      mockRepo.updateItem.mockResolvedValue(updated);

      const result = await service.updateItem('rl-1', 'hh1', 'item-1', { checked: true });

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

    it('sets checkedAt when checked is true', async () => {
      mockRepo.findById.mockResolvedValue({ _id: 'rl-1' });
      mockRepo.updateItem.mockResolvedValue({ _id: 'rl-1', items: [] });

      await service.updateItem('rl-1', 'hh1', 'item-1', { checked: true });

      expect(mockRepo.updateItem).toHaveBeenCalledWith(
        'rl-1',
        'hh1',
        'item-1',
        expect.objectContaining({ checkedAt: expect.any(Date) }),
      );
    });

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

      await expect(service.updateItem('missing', 'hh1', 'item-1', {})).rejects.toThrow(
        'Refill list not found',
      );
    });

    it('throws NotFoundError when item not found', async () => {
      mockRepo.findById.mockResolvedValue({ _id: 'rl-1' });
      mockRepo.updateItem.mockResolvedValue(null);

      await expect(service.updateItem('rl-1', 'hh1', 'bad-item', {})).rejects.toThrow(
        'Refill list or item not found',
      );
    });
  });

  describe('addToCabinet', () => {
    it('adds checked items to cabinet', async () => {
      mockRepo.findById.mockResolvedValue({
        _id: 'rl-1',
        items: [
          {
            _id: { toString: () => 'item-1' },
            medicineId: 'med-1',
            medicineName: 'Aspirin',
            quantity: 30,
            unit: 'tablet',
            checked: true,
            addedToCabinet: false,
          },
        ],
      });
      mockCabinetService.addItem.mockResolvedValue({ _id: 'ci-1' });
      mockRepo.markItemsAddedToCabinet.mockResolvedValue({ _id: 'rl-1' });

      const result = await service.addToCabinet('rl-1', 'hh1', 'user-1');

      expect(result.addedCount).toBe(1);
      expect(mockCabinetService.addItem).toHaveBeenCalledTimes(1);
    });

    it('computes unitPrice when actualPrice is set', async () => {
      mockRepo.findById.mockResolvedValue({
        _id: 'rl-1',
        items: [
          {
            _id: { toString: () => 'item-1' },
            medicineId: 'med-1',
            medicineName: 'Aspirin',
            quantity: 30,
            unit: 'tablet',
            actualPrice: 9,
            checked: true,
            addedToCabinet: false,
          },
        ],
      });
      mockCabinetService.addItem.mockResolvedValue({ _id: 'ci-1' });
      mockRepo.markItemsAddedToCabinet.mockResolvedValue({ _id: 'rl-1' });

      await service.addToCabinet('rl-1', 'hh1', 'user-1');

      expect(mockCabinetService.addItem).toHaveBeenCalledWith(
        expect.objectContaining({ unitPrice: 0.3, totalPrice: 9 }),
        'hh1',
        'user-1',
      );
    });

    it('returns zero count when no checked items', async () => {
      mockRepo.findById.mockResolvedValue({
        _id: 'rl-1',
        items: [
          {
            _id: 'item-1',
            medicineId: 'med-1',
            medicineName: 'Aspirin',
            quantity: 30,
            unit: 'tablet',
            checked: false,
            addedToCabinet: false,
          },
        ],
      });

      const result = await service.addToCabinet('rl-1', 'hh1', 'user-1');

      expect(result.addedCount).toBe(0);
      expect(mockCabinetService.addItem).not.toHaveBeenCalled();
    });

    it('skips already-added items', async () => {
      mockRepo.findById.mockResolvedValue({
        _id: 'rl-1',
        items: [
          {
            _id: 'item-1',
            medicineId: 'med-1',
            medicineName: 'Aspirin',
            quantity: 30,
            unit: 'tablet',
            checked: true,
            addedToCabinet: true,
          },
        ],
      });

      const result = await service.addToCabinet('rl-1', 'hh1', 'user-1');

      expect(result.addedCount).toBe(0);
      expect(mockCabinetService.addItem).not.toHaveBeenCalled();
    });
  });

  describe('getStoreComparison', () => {
    it('returns store comparisons for list items', async () => {
      mockRepo.findById.mockResolvedValue({
        _id: 'rl-1',
        items: [{ medicineId: 'med-1' }, { medicineId: 'med-2' }],
      });
      mockPricesRepo.compareStores
        .mockResolvedValueOnce([{ storeId: 'st-1', storeName: 'CVS' }])
        .mockResolvedValueOnce([]);

      const result = await service.getStoreComparison('rl-1', 'hh1');

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

    it('deduplicates medicine ids', async () => {
      mockRepo.findById.mockResolvedValue({
        _id: 'rl-1',
        items: [{ medicineId: 'med-1' }, { medicineId: 'med-1' }],
      });
      mockPricesRepo.compareStores.mockResolvedValue([{ storeId: 'st-1', storeName: 'CVS' }]);

      const result = await service.getStoreComparison('rl-1', 'hh1');

      expect(mockPricesRepo.compareStores).toHaveBeenCalledTimes(1);
      expect(result).toHaveLength(1);
    });
  });
});
