import { describe, it, expect } from "vitest"; import { estimate, totalActiveDays, activeDaysConsumed } from "./engine"; import type { Config, Selections } from "./types"; import { parseISODate } from "./dates"; function d(iso: string): Date { const parsed = parseISODate(iso); if (!parsed) throw new Error(`bad test date ${iso}`); return parsed; } // A simple 100-day window (Jan 1 .. Apr 10, 2026 is 100 inclusive days). const config: Config = { mealStartDate: d("2026-01-01"), mealEndDate: d("2026-04-10"), plans: [ { id: "green", displayName: "Green", totalMealBlocks: 200, totalFlexDollars: 500 }, ], missingPeriods: [ { id: "break", displayName: "Break", startDate: d("2026-02-01"), endDate: d("2026-02-10") }, // 10 days ], }; function sel(overrides: Partial = {}): Selections { return { selectedPlanId: "green", targetDate: d("2026-04-10"), activeMissingPeriodIds: new Set(), extraExcludedDays: 0, actualMealBlocks: null, actualFlexDollars: null, ...overrides, }; } describe("totalActiveDays", () => { it("counts the full inclusive window with no missing periods", () => { expect(totalActiveDays(config, new Set())).toBe(100); }); it("subtracts an active missing period", () => { expect(totalActiveDays(config, new Set(["break"]))).toBe(90); }); }); describe("activeDaysConsumed", () => { it("is the full window at the end date", () => { expect(activeDaysConsumed(config, sel())).toBe(100); }); it("clamps a target date before the start to zero", () => { expect(activeDaysConsumed(config, sel({ targetDate: d("2025-12-01") }))).toBe(0); }); it("subtracts extra excluded days", () => { expect(activeDaysConsumed(config, sel({ extraExcludedDays: 5 }))).toBe(95); }); }); describe("estimate", () => { it("conserves the budget at the end date with no exclusions", () => { const r = estimate(config, sel()); expect(r.consumedMealBlocks).toBeCloseTo(200); expect(r.remainingMealBlocks).toBeCloseTo(0); expect(r.consumedFlexDollars).toBeCloseTo(500); expect(r.remainingFlexDollars).toBeCloseTo(0); }); it("consumed + remaining always equals the plan total", () => { const r = estimate(config, sel({ targetDate: d("2026-02-15"), activeMissingPeriodIds: new Set(["break"]) })); expect(r.consumedMealBlocks + r.remainingMealBlocks).toBeCloseTo(200); expect(r.consumedFlexDollars + r.remainingFlexDollars).toBeCloseTo(500); }); it("computes a per-week rate of dailyRate * 7", () => { const r = estimate(config, sel()); // 200 blocks / 100 days = 2/day => 14/week expect(r.perWeekMealBlocks).toBeCloseTo(14); expect(r.perWeekFlexDollars).toBeCloseTo(35); // 500/100*7 }); it("never lets excluding days increase consumption (monotonicity)", () => { const base = estimate(config, sel({ targetDate: d("2026-03-01") })); const more = estimate(config, sel({ targetDate: d("2026-03-01"), extraExcludedDays: 10 })); expect(more.consumedMealBlocks).toBeLessThanOrEqual(base.consumedMealBlocks); }); it("handles zero active days without dividing by zero", () => { const allExcluded: Config = { ...config, missingPeriods: [ { id: "all", displayName: "All", startDate: d("2026-01-01"), endDate: d("2026-04-10") }, ], }; const r = estimate(allExcluded, sel({ activeMissingPeriodIds: new Set(["all"]) })); expect(r.perWeekMealBlocks).toBe(0); expect(r.consumedMealBlocks).toBe(0); expect(Number.isFinite(r.remainingMealBlocks)).toBe(true); }); it("reports activeDaysRemaining as total minus consumed", () => { // Halfway: Jan 1 .. Feb 19 is 50 inclusive days of a 100-day window. const r = estimate(config, sel({ targetDate: d("2026-02-19") })); expect(r.activeDaysConsumed).toBe(50); expect(r.activeDaysRemaining).toBe(50); }); it("returns null comparisons when no actual balance is entered", () => { const r = estimate(config, sel()); expect(r.blockComparison).toBeNull(); expect(r.flexComparison).toBeNull(); }); }); describe("balance comparison", () => { // Halfway through the 100-day window: expected remaining = 100 blocks / 250 flex. const halfway = () => sel({ targetDate: d("2026-02-19") }); it("flags a higher-than-expected balance as ahead", () => { const r = estimate(config, { ...halfway(), actualMealBlocks: 130 }); expect(r.blockComparison?.status).toBe("ahead"); expect(r.blockComparison?.difference).toBeCloseTo(30); }); it("flags a lower-than-expected balance as behind", () => { const r = estimate(config, { ...halfway(), actualMealBlocks: 70 }); expect(r.blockComparison?.status).toBe("behind"); expect(r.blockComparison?.difference).toBeCloseTo(-30); }); it("flags a matching balance as on track", () => { const r = estimate(config, { ...halfway(), actualMealBlocks: 100 }); expect(r.blockComparison?.status).toBe("onTrack"); }); it("suggests a go-forward per-week rate from the actual balance", () => { // 130 blocks over 50 remaining days => 2.6/day => 18.2/week. const r = estimate(config, { ...halfway(), actualMealBlocks: 130 }); expect(r.blockComparison?.suggestedPerWeek).toBeCloseTo(18.2, 1); }); it("suggests null per-week when no active days remain", () => { const r = estimate(config, { ...sel(), actualMealBlocks: 0 }); expect(r.activeDaysRemaining).toBe(0); expect(r.blockComparison?.suggestedPerWeek).toBeNull(); }); });