import assert from "node:assert/strict"; import test from "node:test"; import { CatalogFood, Recipe, amountToGrams, buildShoppingItemsFromPlan, buildHealthStatsSummary, buildWeeklyBudgetSummary, calcFoodForAmount, calcRecipe, classifyRecipe, defaultHellthData, describeIngredientAmount, parseNutritionLabel, recipeMacroDistribution, redistributeWeeklyCalories, startOfWeek, unitLabel, weekDates, } from "./hellth-data"; const oliveOil: CatalogFood = { id: "oil", name: "Olivenöl", category: "Öl & Fett", kcalPer100g: 884, proteinPer100g: 0, carbsPer100g: 0, fatPer100g: 100, servingG: 15, preferredUnit: "tbsp", densityGPerMl: 0.91, portions: [ { id: "oil-tsp", label: "1 TL", unit: "tsp", amount: 1, gramsEquivalent: 4.6 }, { id: "oil-tbsp", label: "1 EL", unit: "tbsp", amount: 1, gramsEquivalent: 13.7 }, ], source: "manual", }; test("amountToGrams converts metric, liquid and portion units", () => { assert.equal(amountToGrams(oliveOil, 10, "g"), 10); assert.equal(amountToGrams(oliveOil, 100, "ml"), 91); assert.equal(Math.round(amountToGrams(oliveOil, 2, "tsp") * 10) / 10, 9.1); assert.equal(amountToGrams(oliveOil, 2, "tbsp", "oil-tbsp"), 27.4); }); test("unitLabel and describeIngredientAmount keep user-facing German labels", () => { assert.equal(unitLabel("piece"), "Stück"); assert.equal(unitLabel("tbsp"), "EL"); assert.equal(describeIngredientAmount({ foodId: "oil", amountG: 13.7, amount: 1, unit: "tbsp" }), "1 EL"); assert.equal(describeIngredientAmount({ foodId: "oil", amountG: 13.7 }), "14 g"); }); test("calcFoodForAmount and calcRecipe calculate calories and macros per portion", () => { const skyr: CatalogFood = { id: "skyr", name: "Skyr", category: "Milchprodukte", kcalPer100g: 64, proteinPer100g: 11, carbsPer100g: 4, fatPer100g: 0.2, servingG: 500, source: "manual", }; const oats: CatalogFood = { id: "oats", name: "Haferflocken", category: "Getreide", kcalPer100g: 372, proteinPer100g: 13.5, carbsPer100g: 58.7, fatPer100g: 7, servingG: 50, source: "manual", }; const recipe: Recipe = { id: "recipe", name: "Protein-Frühstück", portions: 2, minutes: 5, ingredients: [ { foodId: "skyr", amountG: 500 }, { foodId: "oats", amountG: 100 }, ], }; assert.deepEqual(calcFoodForAmount(skyr, 250), { kcal: 160, protein: 27.5, carbs: 10, fat: 0.5 }); assert.deepEqual(calcRecipe(recipe, [skyr, oats]), { kcal: 346, protein: 34.3, carbs: 39.4, fat: 4, }); }); test("classifyRecipe detects vegetarian vegan and macro criteria", () => { const oats: CatalogFood = { id: "oats", name: "Haferflocken", category: "Getreide", kcalPer100g: 372, proteinPer100g: 13.5, carbsPer100g: 58.7, fatPer100g: 7, servingG: 50, source: "manual", }; const tofu: CatalogFood = { id: "tofu", name: "Tofu natur", category: "Protein", kcalPer100g: 140, proteinPer100g: 16, carbsPer100g: 2, fatPer100g: 8, servingG: 200, source: "manual", }; const chicken: CatalogFood = { id: "chicken", name: "Hähnchenbrust", category: "Fleisch", kcalPer100g: 110, proteinPer100g: 23, carbsPer100g: 0, fatPer100g: 1.5, servingG: 180, source: "manual", }; const veganRecipe: Recipe = { id: "vegan", name: "Tofu Bowl", portions: 1, ingredients: [ { foodId: "tofu", amountG: 200 }, { foodId: "oats", amountG: 50 }, ], }; const chickenRecipe: Recipe = { id: "chicken", name: "Hähnchen Bowl", portions: 1, ingredients: [{ foodId: "chicken", amountG: 180 }], }; const vegan = classifyRecipe(veganRecipe, [oats, tofu, chicken]); const meat = classifyRecipe(chickenRecipe, [oats, tofu, chicken]); assert.equal(vegan.vegetarian, true); assert.equal(vegan.vegan, true); assert.ok(vegan.tags.includes("vegan")); assert.ok(vegan.criteria.some((item) => item.label === "Protein sinnvoll" && item.ok)); assert.equal(meat.vegetarian, false); assert.equal(recipeMacroDistribution({ protein: 25, carbs: 50, fat: 10 }).carbs, 51); }); test("parseNutritionLabel extracts German nutrition labels with comma decimals", () => { const parsed = parseNutritionLabel(` Durchschnittliche Nährwerte je 100 g Energie 57 kcal Fett 0,1 g Kohlenhydrate 3,8 g Eiweiß 10,0 g `); assert.equal(parsed.kcalPer100g, 57); assert.equal(parsed.fatPer100g, 0.1); assert.equal(parsed.carbsPer100g, 3.8); assert.equal(parsed.proteinPer100g, 10); }); test("week helpers start on Monday and build seven dates", () => { assert.equal(startOfWeek("2026-05-31"), "2026-05-25"); assert.deepEqual(weekDates("2026-05-31"), [ "2026-05-25", "2026-05-26", "2026-05-27", "2026-05-28", "2026-05-29", "2026-05-30", "2026-05-31", ]); }); test("weekly budget includes planned meals and activity calories", () => { const data = defaultHellthData(); data.goals.calories = 2000; data.weeklyCaloriePlans = [{ date: "2026-05-30", plannedCalories: 2800, note: "Restaurant" }]; data.activityLogs = [{ id: "run", date: "2026-05-25", name: "Laufen", minutes: 60, points: 100, calories: 550 }]; data.plannedMeals = [ { id: "meal", date: "2026-05-25", mealType: "Frühstück", recipeId: "recipe-eggs", name: "Eier-Muffins", portions: 1 }, ]; const summary = buildWeeklyBudgetSummary(data, "2026-05-25"); assert.equal(summary.baseBudget, 14000); assert.equal(summary.plannedBudget, 14800); assert.equal(summary.activityCalories, 550); assert.ok(summary.eatenCalories > 0); assert.equal(summary.days.find((day) => day.date === "2026-05-30")?.plannedCalories, 2800); }); test("redistributeWeeklyCalories raises one day and lowers the remaining week gently", () => { const data = defaultHellthData(); data.goals.calories = 2000; const plans = redistributeWeeklyCalories(data, "2026-05-30", 800, "2026-05-25"); const saturday = plans.find((plan) => plan.date === "2026-05-30"); const monday = plans.find((plan) => plan.date === "2026-05-25"); assert.equal(saturday?.plannedCalories, 2800); assert.equal(monday?.plannedCalories, 1866); assert.equal(saturday?.note, "bewusst höher geplant"); }); test("buildShoppingItemsFromPlan aggregates recipe ingredients and preserves manual items", () => { const data = defaultHellthData(); data.shoppingItems = [ { id: "manual", name: "Küchenrolle", category: "Manuell", amountG: 0, purchaseAmount: "1 Packung" }, { id: "old-egg", foodId: "food-egg", name: "Ei", category: "Protein", amountG: 10, checked: true }, ]; data.plannedMeals = [ { id: "plan-a", date: "2026-05-26", mealType: "Frühstück", recipeId: "recipe-eggs", name: "Eier-Muffins", portions: 1 }, { id: "plan-b", date: "2026-05-27", mealType: "Frühstück", recipeId: "recipe-eggs", name: "Eier-Muffins", portions: 2 }, ]; const items = buildShoppingItemsFromPlan(data, ["2026-05-26"]); const egg = items.find((item) => item.foodId === "food-egg"); assert.equal(egg?.amountG, 180); assert.equal(egg?.checked, true); assert.deepEqual(egg?.recipeIds, ["recipe-eggs"]); assert.ok(items.some((item) => item.id === "manual")); }); test("buildHealthStatsSummary creates motivating averages, streaks and weight projection", () => { const data = defaultHellthData(); data.goals.drinkMl = 2000; data.goals.protein = 120; data.goals.weightKg = 100; data.healthProfile = { completedAt: "2026-05-01T00:00:00.000Z", goal: "lose_weight", age: 40, heightCm: 180, currentWeightKg: 110, targetWeightKg: 100, activityLevel: "moderate", pace: "normal", mealsPerDay: 3, dietStyle: "mixed", cookingTime: "medium", budgetFocus: false, excludedFoods: "", biggestChallenge: "", medicalNote: false, }; data.activityLogs = [ { id: "a1", date: "2026-05-25", name: "Spaziergang", minutes: 30, points: 60, calories: 140 }, { id: "a2", date: "2026-05-26", name: "Laufen", minutes: 40, points: 90, calories: 380 }, ]; const summary = buildHealthStatsSummary( data, { days: { "2026-05-13": { weightKg: 110 }, "2026-05-19": { weightKg: 109 }, "2026-05-26": { weightKg: 108, drinks: [{ amountMl: 1000 }, { amountMl: 1000 }], foods: [{ calories: 1800, protein: 100 }], }, }, }, "2026-05-26", ); assert.equal(summary.averages.activityCalories > 0, true); assert.equal(summary.streaks.waterDays, 1); assert.equal(summary.streaks.proteinDays, 1); assert.equal(summary.projection?.kind, "weight"); assert.equal(summary.projection?.targetDate !== undefined, true); });