import { describe, it, expect } from 'vitest'; import { computeSubScores, computeWarnings } from './variety'; describe('computeSubScores', () => { it('returns proteinDiversity=10 when no protein repeats', () => { const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 4, medium: 2, hard: 1 }); expect(result.proteinDiversity).toBe(10); }); it('reduces proteinDiversity by 2 per protein repeat', () => { const tagRepeats = [ { tagType: 'protein', tagName: 'Chicken', days: ['MON', 'TUE'] }, { tagType: 'protein', tagName: 'Beef', days: ['WED', 'THU'] } ]; const result = computeSubScores({ tagRepeats, ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 }); // 2 protein repeat entries → 10 - 2*2 = 6 expect(result.proteinDiversity).toBe(6); }); it('clamps proteinDiversity to minimum 0', () => { const tagRepeats = Array.from({ length: 6 }, (_, i) => ({ tagType: 'protein', tagName: `P${i}`, days: ['MON', 'TUE'] })); const result = computeSubScores({ tagRepeats, ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 }); expect(result.proteinDiversity).toBe(0); }); it('returns ingredientOverlap=10 when no overlaps', () => { const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 }); expect(result.ingredientOverlap).toBe(10); }); it('reduces ingredientOverlap by 1.5 per overlap (rounded)', () => { const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON', 'TUE'] }]; const result = computeSubScores({ tagRepeats: [], ingredientOverlaps, easy: 0, medium: 0, hard: 0 }); // 1 overlap → 10 - 1*1.5 = 8.5 → round = 9 (Math.round rounds .5 up) expect(result.ingredientOverlap).toBe(9); }); it('clamps ingredientOverlap to minimum 0', () => { const ingredientOverlaps = Array.from({ length: 8 }, (_, i) => ({ ingredientName: `Ing${i}`, days: ['MON', 'TUE'] })); const result = computeSubScores({ tagRepeats: [], ingredientOverlaps, easy: 0, medium: 0, hard: 0 }); expect(result.ingredientOverlap).toBe(0); }); it('returns effortBalance=10 when no meals (total=0)', () => { const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 }); expect(result.effortBalance).toBe(10); }); it('returns effortBalance=10 when easy and hard are equal', () => { const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 3, medium: 0, hard: 3 }); // |3-3| = 0 → 10 - 0 = 10 expect(result.effortBalance).toBe(10); }); it('reduces effortBalance by 1.5 per unit of easy-hard difference', () => { const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 4, medium: 0, hard: 0 }); // |4-0| = 4 → 10 - 4*1.5 = 4 → round(4) = 4 expect(result.effortBalance).toBe(4); }); it('clamps effortBalance to minimum 0', () => { const result = computeSubScores({ tagRepeats: [], ingredientOverlaps: [], easy: 10, medium: 0, hard: 0 }); // |10-0| = 10 → 10 - 10*1.5 = -5 → clamp to 0 expect(result.effortBalance).toBe(0); }); it('ignores non-protein tag repeats for proteinDiversity', () => { const tagRepeats = [{ tagType: 'category', tagName: 'Pasta', days: ['MON', 'TUE'] }]; const result = computeSubScores({ tagRepeats, ingredientOverlaps: [], easy: 0, medium: 0, hard: 0 }); expect(result.proteinDiversity).toBe(10); }); }); describe('computeWarnings', () => { it('returns empty array when no repeats or overlaps', () => { const result = computeWarnings({ tagRepeats: [], ingredientOverlaps: [], duplicatesInPlan: [] }); expect(result).toHaveLength(0); }); it('generates warning for protein appearing on 2+ days', () => { const tagRepeats = [{ tagType: 'protein', tagName: 'Chicken', days: ['MON', 'TUE'] }]; const result = computeWarnings({ tagRepeats, ingredientOverlaps: [], duplicatesInPlan: [] }); expect(result).toHaveLength(1); expect(result[0].title).toContain('Chicken'); }); it('does not generate warning for protein appearing on only 1 day', () => { const tagRepeats = [{ tagType: 'protein', tagName: 'Chicken', days: ['MON'] }]; const result = computeWarnings({ tagRepeats, ingredientOverlaps: [], duplicatesInPlan: [] }); expect(result).toHaveLength(0); }); it('generates warning for ingredient overlap on 2+ days', () => { const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON', 'WED'] }]; const result = computeWarnings({ tagRepeats: [], ingredientOverlaps, duplicatesInPlan: [] }); expect(result).toHaveLength(1); expect(result[0].title).toContain('Rice'); }); it('does not generate warning for ingredient appearing on only 1 day', () => { const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON'] }]; const result = computeWarnings({ tagRepeats: [], ingredientOverlaps, duplicatesInPlan: [] }); expect(result).toHaveLength(0); }); it('generates warning for each duplicate recipe in plan', () => { const result = computeWarnings({ tagRepeats: [], ingredientOverlaps: [], duplicatesInPlan: ['Pasta Bolognese', 'Risotto'] }); expect(result).toHaveLength(2); expect(result[0].title).toContain('Pasta Bolognese'); expect(result[1].title).toContain('Risotto'); }); it('combines all warning types', () => { const tagRepeats = [{ tagType: 'protein', tagName: 'Chicken', days: ['MON', 'TUE'] }]; const ingredientOverlaps = [{ ingredientName: 'Rice', days: ['MON', 'WED'] }]; const result = computeWarnings({ tagRepeats, ingredientOverlaps, duplicatesInPlan: ['Pasta'] }); expect(result).toHaveLength(3); }); });