import { describe, it, expect, vi, beforeEach } from 'vitest'; import { NutritionTargetService } from './nutrition-target.service.js'; import type { NutritionTargetRepository } from './nutrition-target.repository.js'; describe(NutritionTargetService.name, () => { let service: NutritionTargetService; let mockRepo: { [K in keyof NutritionTargetRepository]: ReturnType }; beforeEach(() => { mockRepo = { findByUser: vi.fn(), findAllByUser: vi.fn(), create: vi.fn(), deactivateAllForUser: vi.fn(), update: vi.fn(), } as never; service = new NutritionTargetService({ nutritionTargetRepository: mockRepo as unknown as NutritionTargetRepository, }); }); describe('getActiveByUser', () => { it('delegates to repository', async () => { const mockTarget = { dailyCalories: 2000, isActive: true }; mockRepo.findByUser.mockResolvedValue(mockTarget); const result = await service.getActiveByUser('u1', 'h1'); expect(mockRepo.findByUser).toHaveBeenCalledWith('u1', 'h1'); expect(result).toEqual(mockTarget); }); }); describe('getAllByUser', () => { it('delegates to repository', async () => { const mockTargets = [{ dailyCalories: 2000 }, { dailyCalories: 1800 }]; mockRepo.findAllByUser.mockResolvedValue(mockTargets); const result = await service.getAllByUser('u1', 'h1'); expect(mockRepo.findAllByUser).toHaveBeenCalledWith('u1', 'h1'); expect(result).toEqual(mockTargets); }); }); describe('setTarget', () => { it('deactivates existing targets before creating an active one', async () => { const mockInput = { dailyCalories: 2000, proteinG: 100, carbsG: 200, fatG: 50, isActive: true }; const createdTarget = { ...mockInput, _id: 'new-id', userId: 'u1', householdId: 'h1' }; mockRepo.create.mockResolvedValue(createdTarget); const result = await service.setTarget('u1', 'h1', mockInput); expect(mockRepo.deactivateAllForUser).toHaveBeenCalledWith('u1', 'h1'); expect(mockRepo.create).toHaveBeenCalledWith({ ...mockInput, userId: 'u1', householdId: 'h1', }); expect(result).toEqual(createdTarget); }); it('does NOT deactivate others if isActive is explicitly false', async () => { const mockInput = { dailyCalories: 2000, proteinG: 100, carbsG: 200, fatG: 50, isActive: false }; await service.setTarget('u1', 'h1', mockInput); expect(mockRepo.deactivateAllForUser).not.toHaveBeenCalled(); expect(mockRepo.create).toHaveBeenCalled(); }); }); describe('calculatePreset', () => { it('calculates macros correctly for maintenance (30p / 40c / 30f)', () => { const result = service.calculatePreset(2000, 'maintenance'); // Math: // Protein: (2000 * 0.3) / 4 = 600 / 4 = 150 // Carbs: (2000 * 0.4) / 4 = 800 / 4 = 200 // Fat: (2000 * 0.3) / 9 = 600 / 9 = 66.66 => 67 expect(result).toEqual({ dailyCalories: 2000, proteinG: 150, carbsG: 200, fatG: 67, isActive: true, }); }); it('calculates macros correctly for loss (40p / 30c / 30f)', () => { const result = service.calculatePreset(2000, 'loss'); // Math: // Protein: (2000 * 0.4) / 4 = 800 / 4 = 200 // Carbs: (2000 * 0.3) / 4 = 600 / 4 = 150 // Fat: (2000 * 0.3) / 9 = 600 / 9 = 66.66 => 67 expect(result).toEqual({ dailyCalories: 2000, proteinG: 200, carbsG: 150, fatG: 67, isActive: true, }); }); it('calculates macros correctly for gain (25p / 50c / 25f)', () => { const result = service.calculatePreset(2000, 'gain'); // Math: // Protein: (2000 * 0.25) / 4 = 500 / 4 = 125 // Carbs: (2000 * 0.5) / 4 = 1000 / 4 = 250 // Fat: (2000 * 0.25) / 9 = 500 / 9 = 55.55 => 56 expect(result).toEqual({ dailyCalories: 2000, proteinG: 125, carbsG: 250, fatG: 56, isActive: true, }); }); }); });