import { describe, it, expect } from "vitest";
import type { BudgetVsActualRow } from "../../shared/types";
import { computeResults, sumLeaves } from "./budgetVsActualResults";

// Minimal leaf factory. Amounts arrive from getBudgetVsActualData already in
// the per-type sign convention: expense actual/budget are negative (raw,
// unsigned SUM(transactions.amount) — expenses are stored negative — and the
// user-entered budget negated to match via signFor); income/transfer stay
// however they naturally sum (positive for income, signed net for transfer).
function leaf(
  type: "expense" | "income" | "transfer",
  monthActual: number,
  monthBudget: number,
  ytdActual = monthActual,
  ytdBudget = monthBudget,
): BudgetVsActualRow {
  return {
    category_id: 1,
    category_name: type,
    category_color: "#000",
    category_type: type,
    parent_id: null,
    is_parent: false,
    monthActual,
    monthBudget,
    monthVariation: monthActual - monthBudget,
    monthVariationPct: monthBudget !== 0 ? (monthActual - monthBudget) / Math.abs(monthBudget) : null,
    ytdActual,
    ytdBudget,
    ytdVariation: ytdActual - ytdBudget,
    ytdVariationPct: ytdBudget !== 0 ? (ytdActual - ytdBudget) / Math.abs(ytdBudget) : null,
  };
}

describe("computeResults — income-statement roll-up (Issue #277)", () => {
  it("resultBefore = income + expense (expense already signed negative)", () => {
    const r = computeResults([
      leaf("income", 3000, 2800),
      leaf("expense", -1800, -2000), // spent 1800, budgeted to spend 2000
      leaf("transfer", 0, 0),
    ]);
    // Operating result: 3000 + (-1800) = 1200 actual; 2800 + (-2000) = 800 budgeted.
    expect(r.resultBefore.monthActual).toBe(1200);
    expect(r.resultBefore.monthBudget).toBe(800);
    expect(r.resultBefore.monthVariation).toBe(400);
    // Balanced transfer (0) → net equals the operating result.
    expect(r.resultNet.monthActual).toBe(1200);
    expect(r.hasTransfers).toBe(true);
  });

  it("a non-zero transfer moves resultNet away from resultBefore", () => {
    const r = computeResults([
      leaf("income", 4000, 4000),
      leaf("expense", -3000, -3000),
      leaf("transfer", -200, 0), // a single categorised leg
    ]);
    expect(r.resultBefore.monthActual).toBe(1000);
    // Net folds the residual transfer in: 1000 + (−200) = 800.
    expect(r.resultNet.monthActual).toBe(800);
  });

  it("a deficit yields a negative net result and no transfer line", () => {
    const r = computeResults([leaf("income", 2000, 2000), leaf("expense", -2500, -2400)]);
    expect(r.resultNet.monthActual).toBe(-500);
    expect(r.hasTransfers).toBe(false);
  });

  it("ignores subtotal (is_parent) rows so a group is not double-counted", () => {
    const parent = { ...leaf("expense", -999, -999), is_parent: true };
    const r = computeResults([parent, leaf("expense", -100, -80)]);
    expect(r.expense.monthActual).toBe(-100);
  });

  it("carries the YTD figures through the same arithmetic", () => {
    const r = computeResults([
      leaf("income", 100, 100, 1200, 1150),
      leaf("expense", -60, -60, -700, -650),
    ]);
    expect(r.resultBefore.ytdActual).toBe(500);
    expect(r.resultBefore.ytdBudget).toBe(500);
  });

  it("sumLeaves adds only leaves", () => {
    const t = sumLeaves([
      leaf("expense", -100, -50),
      { ...leaf("expense", -999, -999), is_parent: true },
    ]);
    expect(t.monthActual).toBe(-100);
    expect(t.monthBudget).toBe(-50);
  });
});
