import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MedicineProductsService } from '../../../src/modules/medicine-products/medicine-products.service.js';
import { NotFoundError } from '../../../src/common/errors.js';
import { DosageUnit, MedicineProductSource } from '@meshitrack/shared';

describe(MedicineProductsService.name, () => {
  const mockProductsRepo = {
    findByMedicine: vi.fn(),
    findById: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    softDelete: vi.fn(),
  };

  const mockMedicinesRepo = {
    findById: vi.fn(),
  };

  let service: MedicineProductsService;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new MedicineProductsService({
      medicineProductsRepository: mockProductsRepo as never,
      medicinesRepository: mockMedicinesRepo as never,
    });
  });

  describe('listByMedicine', () => {
    it('delegates to repository', async () => {
      const expected = { data: [], pagination: { cursor: null, hasMore: false } };
      mockProductsRepo.findByMedicine.mockResolvedValue(expected);

      const result = await service.listByMedicine('hh1', 'med-1', { limit: 20 });

      expect(mockProductsRepo.findByMedicine).toHaveBeenCalledWith('hh1', 'med-1', { limit: 20 });
      expect(result).toEqual(expected);
    });
  });

  describe('getById', () => {
    it('returns product when found', async () => {
      const product = { _id: 'mp-1', brand: 'CVS' };
      mockProductsRepo.findById.mockResolvedValue(product);

      const result = await service.getById('mp-1', 'hh1');
      expect(result).toEqual(product);
    });

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

      await expect(service.getById('missing', 'hh1')).rejects.toThrow(NotFoundError);
    });
  });

  describe('create', () => {
    const data = {
      brand: 'CVS Health',
      packageSize: 90,
      packageUnit: DosageUnit.TABLET,
      source: MedicineProductSource.MANUAL,
    };

    it('creates when parent medicine exists', async () => {
      mockMedicinesRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Metformin' });
      mockProductsRepo.create.mockResolvedValue({
        _id: 'mp-1',
        ...data,
        medicineName: 'Metformin',
      });

      const result = await service.create(data, 'hh1', 'med-1', 'kc-1');

      expect(mockMedicinesRepo.findById).toHaveBeenCalledWith('med-1', 'hh1');
      expect(mockProductsRepo.create).toHaveBeenCalledWith(
        data,
        'hh1',
        'med-1',
        'Metformin',
        'kc-1',
      );
      expect(result._id).toBe('mp-1');
    });

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

      await expect(service.create(data, 'hh1', 'missing', 'kc-1')).rejects.toThrow(NotFoundError);
      expect(mockProductsRepo.create).not.toHaveBeenCalled();
    });
  });

  describe('update', () => {
    it('updates a product', async () => {
      mockProductsRepo.findById.mockResolvedValue({ _id: 'mp-1', brand: 'CVS' });
      mockProductsRepo.update.mockResolvedValue({ _id: 'mp-1', brand: 'Updated' });

      const result = await service.update('mp-1', 'hh1', { brand: 'Updated' });
      expect(result.brand).toBe('Updated');
    });

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

      await expect(service.update('missing', 'hh1', { brand: 'X' })).rejects.toThrow(NotFoundError);
    });

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

      await expect(service.update('mp-1', 'hh1', { brand: 'X' })).rejects.toThrow(NotFoundError);
    });
  });

  describe('delete', () => {
    it('soft deletes a product', async () => {
      mockProductsRepo.findById.mockResolvedValue({ _id: 'mp-1' });
      mockProductsRepo.softDelete.mockResolvedValue({ _id: 'mp-1', isDeleted: true });

      await service.delete('mp-1', 'hh1');

      expect(mockProductsRepo.softDelete).toHaveBeenCalledWith('mp-1', 'hh1');
    });

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

      await expect(service.delete('missing', 'hh1')).rejects.toThrow(NotFoundError);
    });

    it('throws NotFoundError when softDelete returns null', async () => {
      mockProductsRepo.findById.mockResolvedValue({ _id: 'mp-1' });
      mockProductsRepo.softDelete.mockResolvedValue(null);

      await expect(service.delete('mp-1', 'hh1')).rejects.toThrow(NotFoundError);
    });
  });
});
