import { describe, it, expect } from "vitest";
import type { BudgetYearRow } from "../../shared/types";
import { computeBudgetResults, sumLeavesForType, signForBudgetType } from "./budgetTableResults";

// Minimal leaf factory. `months` arrives as the positive-magnitude budgeted
// figure BudgetTable itself flips by type via `signFor` — same convention
// `sumLeavesForType` re-implements. `annual` always equals `sum(months)`
// (useBudget derives it that way; see useBudget.ts's `buildMonths`).
function leaf(
  type: BudgetYearRow["category_type"],
  months: number[],
  previousYearTotal = 0,
  overrides: Partial<BudgetYearRow> = {},
): BudgetYearRow {
  return {
    category_id: 1,
    category_name: type,
    category_color: "#000",
    category_type: type,
    parent_id: null,
    is_parent: false,
    depth: 0,
    months,
    annual: months.reduce((s, v) => s + v, 0),
    previousYearTotal,
    ...overrides,
  };
}

const FLAT12 = (v: number) => Array(12).fill(v) as number[];

describe("computeBudgetResults — income-statement roll-up (Issue #278)", () => {
  it("resultBefore = budgeted income − expense; resultNet adds transfers", () => {
    const r = computeBudgetResults([
      leaf("income", FLAT12(400)), // annual 4800
      leaf("expense", FLAT12(300)), // annual 3600, stored positive, flipped to -300/mo
      leaf("transfer", FLAT12(0)),
    ]);
    expect(r.resultBefore.months[0]).toBe(100); // 400 − 300
    expect(r.resultBefore.annual).toBe(1200); // 4800 − 3600
    expect(r.resultNet.annual).toBe(1200); // balanced (0) transfer → unchanged
    expect(r.hasTransfers).toBe(true);
  });

  it("an unbalanced (one-leg) transfer moves resultNet away from resultBefore", () => {
    const r = computeBudgetResults([
      leaf("income", FLAT12(1000)),
      leaf("expense", FLAT12(700)),
      leaf("transfer", FLAT12(50)), // uncategorized single leg, signed pass-through
    ]);
    expect(r.resultBefore.annual).toBe(3600); // (1000 − 700) × 12
    expect(r.resultNet.annual).toBe(4200); // resultBefore + transfer(50×12)
  });

  it("a deficit yields a negative net result and no transfer line when absent", () => {
    const r = computeBudgetResults([leaf("income", FLAT12(200)), leaf("expense", FLAT12(250))]);
    expect(r.resultNet.months[0]).toBe(-50);
    expect(r.hasTransfers).toBe(false);
  });

  it("ignores subtotal (is_parent) rows so a group is not double-counted", () => {
    const parent = leaf("expense", FLAT12(999), 0, { is_parent: true });
    const r = computeBudgetResults([parent, leaf("expense", FLAT12(100))]);
    expect(r.expense.annual).toBe(-1200); // only the leaf's 100 × 12 (flipped), not 999
  });

  it("carries the previous-year actual through unsigned (already signed in the DB)", () => {
    // Actual totals arrive pre-signed: expense negative, income positive.
    const r = computeBudgetResults([
      leaf("income", FLAT12(100), 1150),
      leaf("expense", FLAT12(60), -650),
    ]);
    expect(r.resultBefore.previousYearTotal).toBe(500); // 1150 + (−650), no re-sign
  });

  it("sumLeavesForType filters by type and excludes parents", () => {
    const t = sumLeavesForType(
      [
        leaf("expense", FLAT12(50)),
        leaf("expense", FLAT12(999), 0, { is_parent: true }),
        leaf("income", FLAT12(10)),
      ],
      "expense",
    );
    expect(t.annual).toBe(-600); // 50 × 12, flipped negative
    expect(t.months[0]).toBe(-50);
  });

  it("signForBudgetType flips expense only", () => {
    expect(signForBudgetType("expense")).toBe(-1);
    expect(signForBudgetType("income")).toBe(1);
    expect(signForBudgetType("transfer")).toBe(1);
  });
});
