import { describe, it, expect } from "vitest";
import type { BudgetYearRow, Category, BudgetEntry } from "../shared/types";
import { buildBudgetYearRows } from "./useBudget";
import { sumLeavesForType } from "../components/budget/budgetTableResults";
import {
  type CollapseAccessors,
  visibleRows,
  isCollapsedFor,
} from "../utils/collapsibleRows";

// The Budget grid's rows are built by `useBudget` (the "3rd builder" the
// abandoned v1 collapse plan ignored). Its final sort is LEVEL-ORDER (depth
// ascending / BFS), not depth-first. These tests pin that invariant: the
// shipped ancestor-walk collapse (issue #288) is order-INDEPENDENT, but the v1
// depth-cursor algorithm assumed DFS and would have broken exactly here.

function cat(
  id: number,
  name: string,
  type: Category["type"],
  opts: { parent_id?: number; is_inputable?: boolean; sort_order?: number } = {},
): Category {
  return {
    id,
    name,
    type,
    parent_id: opts.parent_id,
    color: "#000",
    is_active: true,
    is_inputable: opts.is_inputable ?? true,
    sort_order: opts.sort_order ?? 0,
    created_at: "",
  };
}

// income leaf + a 3-level expense group:
//   Housing (root, non-inputable)
//     ├─ Rent            (depth-1 leaf)
//     └─ Utilities       (depth-1 intermediate parent)
//          ├─ Hydro      (depth-2 leaf)
//          └─ Internet   (depth-2 leaf)
const CATEGORIES: Category[] = [
  cat(10, "Salary", "income", { sort_order: 0 }),
  cat(1, "Housing", "expense", { is_inputable: false, sort_order: 1 }),
  cat(2, "Rent", "expense", { parent_id: 1, sort_order: 0 }),
  cat(3, "Utilities", "expense", { parent_id: 1, is_inputable: false, sort_order: 1 }),
  cat(4, "Hydro", "expense", { parent_id: 3, sort_order: 0 }),
  cat(5, "Internet", "expense", { parent_id: 3, sort_order: 1 }),
];

function entry(category_id: number, amount: number, month = 1): BudgetEntry {
  return { id: 0, category_id, year: 2026, month, amount, created_at: "", updated_at: "" };
}

// Same accessors BudgetTable declares inline — duplicated here (they are 4 trivial
// lambdas the issue mandates stay inline; a .tsx import into a node-env test would
// pull in JSX/lucide/react-i18next needlessly).
const ACCESSORS: CollapseAccessors<BudgetYearRow> = {
  keyOf: (row) => `p:${row.category_id}`,
  parentKeyOf: (row) => (row.parent_id != null ? `p:${row.parent_id}` : null),
  depthOf: (row) => row.depth ?? 0,
  isParent: (row) => row.is_parent,
};

describe("buildBudgetYearRows — level-order emission (issue #289 invariant)", () => {
  const rows = buildBudgetYearRows(CATEGORIES, [], []);

  it("emits income before expense, then each group in LEVEL order (BFS), not DFS", () => {
    // Salary (income) first; then the Housing expense group depth-ascending:
    // root subtotal, then the depth-1 rows (parent Utilities before leaf Rent),
    // then the depth-2 grandchildren.
    expect(rows.map((r) => r.category_id)).toEqual([10, 1, 3, 2, 4, 5]);
  });

  it("keeps depth non-decreasing within a top group — the property that would have killed the v1 (DFS-assuming) collapse algorithm", () => {
    const expenseGroup = rows.filter((r) => r.category_type === "expense");
    const depths = expenseGroup.map((r) => r.depth ?? 0);
    for (let i = 1; i < depths.length; i++) {
      expect(depths[i]).toBeGreaterThanOrEqual(depths[i - 1]);
    }

    // Explicit DFS-killer: the depth-2 grandchildren (Hydro/Internet) come AFTER
    // the depth-1 leaf sibling (Rent). A depth-first emission would interleave
    // them directly under Utilities, i.e. BEFORE Rent.
    const leafIdx = (id: number) => rows.findIndex((r) => r.category_id === id && !r.is_parent);
    expect(leafIdx(4)).toBeGreaterThan(leafIdx(2)); // Hydro after Rent
    expect(leafIdx(5)).toBeGreaterThan(leafIdx(2)); // Internet after Rent
  });
});

describe("buildBudgetYearRows + multi-level collapse (issue #289 end-to-end)", () => {
  const rows = buildBudgetYearRows(CATEGORIES, [], []);

  it("collapsed-by-default (empty set) shows only the roots", () => {
    const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(new Set<string>(), ACCESSORS.keyOf(r), false);
    const visible = visibleRows(rows, ACCESSORS, isCollapsed);
    // Salary (root leaf) + Housing (root subtotal); nothing beneath Housing.
    expect(visible.map((r) => r.category_id)).toEqual([10, 1]);
  });

  it("expanding a root reveals its DIRECT children only — grandchildren stay folded under the still-collapsed intermediate", () => {
    const flipped = new Set<string>(["p:1"]); // Housing expanded
    const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(flipped, ACCESSORS.keyOf(r), false);
    const visible = visibleRows(rows, ACCESSORS, isCollapsed);
    // Housing's direct children: Utilities subtotal + Rent leaf. Hydro/Internet
    // remain hidden under the collapsed Utilities.
    expect(visible.map((r) => r.category_id)).toEqual([10, 1, 3, 2]);
  });
});

describe("BudgetTable section subtotals stay on RAW rows (collapse is purely visual, issue #289)", () => {
  const ENTRIES = [entry(2, 1000), entry(4, 100), entry(5, 50)];
  const rows = buildBudgetYearRows(CATEGORIES, ENTRIES, []);

  it("sums every expense LEAF regardless of collapse — folding a parent never moves a total", () => {
    const rawExpense = sumLeavesForType(rows, "expense");
    expect(rawExpense.annual).toBe(-1150); // -(Rent 1000 + Hydro 100 + Internet 50)

    // If the grid mistakenly summed the collapse-visible rows, a fully-collapsed
    // grid (only roots visible, and the expense root is a parent → excluded)
    // would report 0. The grid feeds the RAW group to `sumLeavesForType`, so the
    // section total is invariant under collapse; this asserts the two differ,
    // which is exactly why raw rows must be used.
    const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(new Set<string>(), ACCESSORS.keyOf(r), false);
    const collapsedVisible = visibleRows(rows, ACCESSORS, isCollapsed);
    const visibleExpense = sumLeavesForType(collapsedVisible, "expense");
    expect(visibleExpense.annual).toBe(0);
    expect(rawExpense.annual).not.toBe(visibleExpense.annual);
  });
});
