import { describe, it, expect } from "vitest"; import { fyStartYear, fyLabel, fyMonthIndex, buildAccountIndex, costCentreRows, accountNodeSpend, accountLevelRows, topAccountsForCostCentre, costCentresForAccount, childBreakdown, applyScope, parseScope, resolveFy, type ReportDataset, type AccountNode, } from "@/lib/reports"; const ACCOUNTS: AccountNode[] = [ { id: "H", code: "5000", name: "Operating", parentId: null, tier: "Heading" }, { id: "S", code: "5100", name: "Vessel Running", parentId: "H", tier: "Sub-heading" }, { id: "L1", code: "5110", name: "Fuel", parentId: "S", tier: "Leaf" }, { id: "L2", code: "5120", name: "Spares", parentId: "S", tier: "Leaf" }, ]; // fys ascending: [2024, 2025] const DS: ReportDataset = { vessels: [ { id: "v1", code: "V1", name: "MV One" }, { id: "v2", code: "V2", name: "MV Two" }, ], accounts: ACCOUNTS, fys: [2024, 2025], rows: [ { vesselId: "v1", accountId: "L1", amount: 100, fy: 2025, month: 0 }, { vesselId: "v1", accountId: "L1", amount: 50, fy: 2025, month: 1 }, { vesselId: "v1", accountId: "L2", amount: 30, fy: 2025, month: 0 }, { vesselId: "v2", accountId: "L1", amount: 200, fy: 2024, month: 5 }, { vesselId: "v2", accountId: "L2", amount: 70, fy: 2025, month: 11 }, ], }; describe("financial-year helpers", () => { it("maps Apr–Mar to the Indian FY start year", () => { expect(fyStartYear(new Date(2025, 3, 1))).toBe(2025); // Apr expect(fyStartYear(new Date(2025, 0, 15))).toBe(2024); // Jan → prior FY expect(fyStartYear(new Date(2025, 2, 31))).toBe(2024); // Mar → prior FY }); it("labels and indexes months within the FY", () => { expect(fyLabel(2025)).toBe("FY 2025–26"); expect(fyMonthIndex(new Date(2025, 3, 1))).toBe(0); // Apr expect(fyMonthIndex(new Date(2026, 2, 1))).toBe(11); // Mar }); }); describe("costCentreRows", () => { it("totals the selected FY by vessel with a 12-month series and PO count", () => { const rows = costCentreRows(DS, 2025); const v1 = rows.find((r) => r.id === "v1")!; expect(v1.total).toBe(180); expect(v1.months[0]).toBe(130); // 100 + 30 expect(v1.months[1]).toBe(50); expect(v1.poCount).toBe(3); expect(v1.fyTotals).toEqual([0, 180]); // [2024, 2025] const v2 = rows.find((r) => r.id === "v2")!; expect(v2.total).toBe(70); expect(v2.fyTotals).toEqual([200, 70]); }); }); describe("accounting-code rollup", () => { const idx = buildAccountIndex(ACCOUNTS); it("rolls leaf spend up to the heading", () => { expect(accountNodeSpend(DS, idx, "H", 2025).total).toBe(250); // 100+50+30+70 expect(accountNodeSpend(DS, idx, "L1", 2025).total).toBe(150); }); it("lists the children to compare at a drill level", () => { const top = accountLevelRows(DS, idx, null, 2025); // headings expect(top.map((r) => r.node.id)).toEqual(["H"]); const subs = accountLevelRows(DS, idx, "H", 2025); expect(subs.map((r) => r.node.id)).toEqual(["S"]); }); it("leaf detection and leaf set", () => { expect(idx.isLeaf("L1")).toBe(true); expect(idx.isLeaf("H")).toBe(false); expect([...idx.leavesUnder("H")].sort()).toEqual(["L1", "L2"]); }); }); describe("breakdowns", () => { const idx = buildAccountIndex(ACCOUNTS); it("top accounting codes for a cost centre (by tier)", () => { const bd = topAccountsForCostCentre(DS, idx, "v1", 2025, "Leaf"); expect(bd.map((b) => [b.id, b.value])).toEqual([ ["L1", 150], ["L2", 30], ]); }); it("cost centres for an account node", () => { const bd = costCentresForAccount(DS, idx, "H", 2025); expect(bd.map((b) => [b.id, b.value])).toEqual([ ["v1", 180], ["v2", 70], ]); }); it("child breakdown of a non-leaf node", () => { const bd = childBreakdown(DS, idx, "H", 2025); expect(bd).toEqual([{ id: "S", label: "5100 · Vessel Running", value: 250 }]); }); }); describe("scope + param parsing", () => { it("applies Top/Bottom-N to a descending list", () => { const sorted = [5, 4, 3, 2, 1]; expect(applyScope(sorted, "top5")).toEqual([5, 4, 3, 2, 1]); expect(applyScope([9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1], "top10")).toHaveLength(10); expect(applyScope(sorted, "bottom5")).toEqual([1, 2, 3, 4, 5]); expect(applyScope(sorted, "all")).toEqual(sorted); }); it("parses scope + resolves FY with sensible defaults", () => { expect(parseScope("top10")).toBe("top10"); expect(parseScope("garbage")).toBe("top5"); expect(resolveFy(DS, "2024")).toBe(2024); expect(resolveFy(DS, undefined)).toBe(2025); // latest expect(resolveFy(DS, "1999")).toBe(2025); // out of range → latest }); });