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

describe(PurchasesService.name, () => {
  const mockPurchasesRepo = {
    create: vi.fn(),
    findByHousehold: vi.fn(),
    findById: vi.fn(),
    update: vi.fn(),
    receiveAll: vi.fn(),
    softDelete: vi.fn(),
    getPendingMedicineStock: vi.fn(),
  };
  const mockCabinetService = {
    addItem: vi.fn(),
  };
  const mockStoresRepo = {
    findById: vi.fn(),
  };
  const mockProductsRepo = {
    findById: vi.fn(),
  };
  const mockPricesRepo = {
    create: vi.fn(),
  };

  let service: PurchasesService;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new PurchasesService({
      purchasesRepository: mockPurchasesRepo as never,
      cabinetService: mockCabinetService as never,
      storesRepository: mockStoresRepo as never,
      medicineProductsRepository: mockProductsRepo as never,
      medicinePricesRepository: mockPricesRepo as never,
    });
  });

  const fakeStore = { _id: 'st-1', name: 'CVS' };
  const fakeProduct = {
    _id: 'mp-1',
    medicineId: 'med-1',
    medicineName: 'Ibuprofen',
    brand: 'Advil',
  };

  describe('create', () => {
    const validInput = {
      storeId: 'st-1',
      status: 'in_cabinet' as const,
      items: [{ name: 'Advil', quantity: 30, unit: 'tablet', medicineProductId: 'mp-1' }],
    };

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

      await expect(service.create(validInput, 'hh1', 'user-1')).rejects.toThrow('Store not found');
    });

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

      await expect(service.create(validInput, 'hh1', 'user-1')).rejects.toThrow(
        'Medicine product not found: mp-1',
      );
    });

    it('creates purchase with in_cabinet status and adds items to cabinet', async () => {
      mockStoresRepo.findById.mockResolvedValue(fakeStore);
      mockProductsRepo.findById.mockResolvedValue(fakeProduct);
      mockCabinetService.addItem.mockResolvedValue({});
      const purchase = { _id: 'p-1', status: 'in_cabinet' };
      mockPurchasesRepo.create.mockResolvedValue(purchase);

      const result = await service.create(validInput, 'hh1', 'user-1');

      expect(mockCabinetService.addItem).toHaveBeenCalledOnce();
      expect(mockPurchasesRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ status: 'in_cabinet', storeName: 'CVS' }),
      );
      expect(result).toEqual(purchase);
    });

    it('records price when actualPrice is set and status is in_cabinet', async () => {
      const inputWithPrice = {
        ...validInput,
        items: [{ ...validInput.items[0], actualPrice: 9.99, currency: 'USD' }],
      };
      mockStoresRepo.findById.mockResolvedValue(fakeStore);
      mockProductsRepo.findById.mockResolvedValue(fakeProduct);
      mockCabinetService.addItem.mockResolvedValue({});
      mockPricesRepo.create.mockResolvedValue({});
      mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' });

      await service.create(inputWithPrice, 'hh1', 'user-1');

      expect(mockPricesRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          price: 9.99,
          medicineName: 'Ibuprofen',
          storeName: 'CVS',
          pricePerUnit: expect.closeTo(0.333, 2),
        }),
      );
    });

    it('does not add to cabinet when status is ordered', async () => {
      const orderedInput = { ...validInput, status: 'ordered' as const };
      mockStoresRepo.findById.mockResolvedValue(fakeStore);
      mockProductsRepo.findById.mockResolvedValue(fakeProduct);
      mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1', status: 'ordered' });

      await service.create(orderedInput, 'hh1', 'user-1');

      expect(mockCabinetService.addItem).not.toHaveBeenCalled();
      expect(mockPricesRepo.create).not.toHaveBeenCalled();
    });

    it('handles item without medicineProductId for in_cabinet', async () => {
      const noProductInput = {
        storeId: 'st-1',
        status: 'in_cabinet' as const,
        items: [{ name: 'Generic OTC', quantity: 1, unit: 'tablet' }],
      };
      mockStoresRepo.findById.mockResolvedValue(fakeStore);
      mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' });

      await service.create(noProductInput, 'hh1', 'user-1');

      expect(mockCabinetService.addItem).not.toHaveBeenCalled();
      expect(mockPurchasesRepo.create).toHaveBeenCalled();
    });

    it('uses purchasedAt from input when provided', async () => {
      const inputWithDate = { ...validInput, purchasedAt: '2026-01-15T00:00:00.000Z' };
      mockStoresRepo.findById.mockResolvedValue(fakeStore);
      mockProductsRepo.findById.mockResolvedValue(fakeProduct);
      mockCabinetService.addItem.mockResolvedValue({});
      mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' });

      await service.create(inputWithDate, 'hh1', 'user-1');

      expect(mockPurchasesRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ purchasedAt: new Date('2026-01-15T00:00:00.000Z') }),
      );
    });

    it('uses medicineName as brand fallback when brand is undefined', async () => {
      mockStoresRepo.findById.mockResolvedValue(fakeStore);
      mockProductsRepo.findById.mockResolvedValue({ ...fakeProduct, brand: undefined });
      mockCabinetService.addItem.mockResolvedValue({});
      mockPricesRepo.create.mockResolvedValue({});
      const inputWithPrice = {
        ...validInput,
        items: [{ ...validInput.items[0], actualPrice: 5 }],
      };
      mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' });

      await service.create(inputWithPrice, 'hh1', 'user-1');

      expect(mockPricesRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ medicineProductBrand: 'Ibuprofen' }),
      );
    });
  });

  describe('receive', () => {
    it('throws NotFoundError when purchase not found', async () => {
      mockPurchasesRepo.findById.mockResolvedValue(null);

      await expect(service.receive('missing', 'hh1', 'user-1')).rejects.toThrow(
        'Purchase not found',
      );
    });

    it('throws BadRequestError when status is not ordered', async () => {
      mockPurchasesRepo.findById.mockResolvedValue({ _id: 'p-1', status: 'in_cabinet', items: [] });

      await expect(service.receive('p-1', 'hh1', 'user-1')).rejects.toThrow(
        'Purchase is not in ordered status',
      );
    });

    it('adds medicine items to cabinet and calls receiveAll', async () => {
      const purchase = {
        _id: 'p-1',
        status: 'ordered',
        storeId: 'st-1',
        storeName: 'CVS',
        purchasedAt: new Date('2026-01-01T00:00:00.000Z'),
        items: [
          {
            medicineProductId: 'mp-1',
            medicineId: 'med-1',
            name: 'Advil',
            quantity: 30,
            unit: 'tablet',
            addedToCabinet: false,
          },
        ],
      };
      mockPurchasesRepo.findById.mockResolvedValue(purchase);
      mockCabinetService.addItem.mockResolvedValue({});
      mockPurchasesRepo.receiveAll.mockResolvedValue({ _id: 'p-1', status: 'in_cabinet' });

      const result = await service.receive('p-1', 'hh1', 'user-1');

      expect(mockCabinetService.addItem).toHaveBeenCalledOnce();
      expect(mockPurchasesRepo.receiveAll).toHaveBeenCalledWith('p-1', 'hh1');
      expect(result.addedCount).toBe(1);
      expect(result.priceRecordsCreated).toBe(0);
    });

    it('creates price record when actualPrice is set on item', async () => {
      const purchase = {
        _id: 'p-1',
        status: 'ordered',
        storeId: 'st-1',
        storeName: 'CVS',
        purchasedAt: new Date('2026-01-01T00:00:00.000Z'),
        items: [
          {
            medicineProductId: 'mp-1',
            medicineId: 'med-1',
            name: 'Advil',
            quantity: 30,
            unit: 'tablet',
            actualPrice: 9.99,
            currency: 'USD',
            addedToCabinet: false,
          },
        ],
      };
      mockPurchasesRepo.findById.mockResolvedValue(purchase);
      mockCabinetService.addItem.mockResolvedValue({});
      mockProductsRepo.findById.mockResolvedValue(fakeProduct);
      mockPricesRepo.create.mockResolvedValue({});
      mockPurchasesRepo.receiveAll.mockResolvedValue({});

      const result = await service.receive('p-1', 'hh1', 'user-1');

      expect(mockPricesRepo.create).toHaveBeenCalledOnce();
      expect(result.priceRecordsCreated).toBe(1);
    });

    it('uses medicineName as brand fallback in price record when brand is undefined', async () => {
      const purchase = {
        _id: 'p-1',
        status: 'ordered',
        storeId: 'st-1',
        storeName: 'CVS',
        purchasedAt: new Date('2026-01-01T00:00:00.000Z'),
        items: [
          {
            medicineProductId: 'mp-1',
            medicineId: 'med-1',
            name: 'Ibuprofen',
            quantity: 30,
            unit: 'tablet',
            actualPrice: 9.99,
            currency: 'USD',
            addedToCabinet: false,
          },
        ],
      };
      mockPurchasesRepo.findById.mockResolvedValue(purchase);
      mockCabinetService.addItem.mockResolvedValue({});
      mockProductsRepo.findById.mockResolvedValue({ ...fakeProduct, brand: undefined });
      mockPricesRepo.create.mockResolvedValue({});
      mockPurchasesRepo.receiveAll.mockResolvedValue({});

      await service.receive('p-1', 'hh1', 'user-1');

      expect(mockPricesRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ medicineProductBrand: 'Ibuprofen' }),
      );
    });

    it('skips price record creation when product not found in receive', async () => {
      const purchase = {
        _id: 'p-1',
        status: 'ordered',
        storeId: 'st-1',
        storeName: 'CVS',
        purchasedAt: new Date(),
        items: [
          {
            medicineProductId: 'mp-1',
            medicineId: 'med-1',
            name: 'Advil',
            quantity: 30,
            unit: 'tablet',
            actualPrice: 9.99,
            addedToCabinet: false,
          },
        ],
      };
      mockPurchasesRepo.findById.mockResolvedValue(purchase);
      mockCabinetService.addItem.mockResolvedValue({});
      mockProductsRepo.findById.mockResolvedValue(null);
      mockPurchasesRepo.receiveAll.mockResolvedValue({});

      const result = await service.receive('p-1', 'hh1', 'user-1');

      expect(mockPricesRepo.create).not.toHaveBeenCalled();
      expect(result.priceRecordsCreated).toBe(0);
    });

    it('skips items already added to cabinet', async () => {
      const purchase = {
        _id: 'p-1',
        status: 'ordered',
        storeId: 'st-1',
        storeName: 'CVS',
        purchasedAt: new Date(),
        items: [
          {
            medicineProductId: 'mp-1',
            medicineId: 'med-1',
            name: 'X',
            quantity: 10,
            unit: 'tablet',
            addedToCabinet: true,
          },
        ],
      };
      mockPurchasesRepo.findById.mockResolvedValue(purchase);
      mockPurchasesRepo.receiveAll.mockResolvedValue({});

      const result = await service.receive('p-1', 'hh1', 'user-1');

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

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

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

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

  describe('getById', () => {
    it('returns purchase when found', async () => {
      const purchase = { _id: 'p-1', status: 'in_cabinet' };
      mockPurchasesRepo.findById.mockResolvedValue(purchase);

      expect(await service.getById('p-1', 'hh1')).toEqual(purchase);
    });

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

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

  describe('update', () => {
    it('updates and returns purchase', async () => {
      mockPurchasesRepo.findById.mockResolvedValue({ _id: 'p-1' });
      const updated = { _id: 'p-1', notes: 'updated' };
      mockPurchasesRepo.update.mockResolvedValue(updated);

      const result = await service.update('p-1', 'hh1', { notes: 'updated' });

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

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

      await expect(service.update('missing', 'hh1', {})).rejects.toThrow('Purchase not found');
    });

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

      await expect(service.update('p-1', 'hh1', {})).rejects.toThrow('Purchase not found');
    });
  });

  describe('delete', () => {
    it('soft-deletes and returns purchase', async () => {
      const deleted = { _id: 'p-1', isDeleted: true };
      mockPurchasesRepo.softDelete.mockResolvedValue(deleted);

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

      expect(result).toEqual(deleted);
      expect(mockPurchasesRepo.softDelete).toHaveBeenCalledWith('p-1', 'hh1');
    });

    it('throws NotFoundError when purchase not found', async () => {
      mockPurchasesRepo.softDelete.mockResolvedValue(null);

      await expect(service.delete('missing', 'hh1')).rejects.toThrow(
        'Purchase not found or cannot be deleted',
      );
    });
  });

  describe('getPendingStockByMedicine', () => {
    it('returns map of medicineId to totalUnits', async () => {
      mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([
        { medicineId: 'med-1', totalUnits: 60 },
        { medicineId: 'med-2', totalUnits: 30 },
      ]);

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

      expect(result.get('med-1')).toBe(60);
      expect(result.get('med-2')).toBe(30);
    });

    it('returns empty map when no pending stock', async () => {
      mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([]);

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

      expect(result.size).toBe(0);
    });
  });
});
