import { describe, it, expect } from "vitest";
import { computeOverTimeResults, OVER_TIME_TYPE_ORDER } from "./overTimeResults";
import type { CategoryOverTimeData, OverTimeRow } from "../../shared/types";

/** Builds an id-keyed leaf row; `monthly` is index-aligned to the months list. */
function leaf(
  categoryId: number | null,
  categoryName: string,
  monthly: number[],
  category_type: "income" | "expense" | "transfer",
  extra: Partial<OverTimeRow> = {},
): OverTimeRow {
  return {
    categoryId,
    categoryName,
    categoryColor: "#000",
    monthly,
    total: monthly.reduce((s, v) => s + v, 0),
    parent_id: null,
    is_parent: false,
    depth: 0,
    category_type,
    ...extra,
  };
}

/** Wraps a month list + id-keyed tree into a CategoryOverTimeData. The pivot
 * fields are irrelevant here — computeOverTimeResults reads `data.tree`, using
 * `data.data` only for the (ordered) month list. */
function makeData(months: string[], tree: OverTimeRow[]): CategoryOverTimeData {
  return {
    categories: [],
    data: months.map((month) => ({ month })),
    colors: {},
    categoryIds: {},
    types: {},
    tree,
  };
}

describe("OVER_TIME_TYPE_ORDER", () => {
  it("is income-first (income → expense → transfer)", () => {
    expect(OVER_TIME_TYPE_ORDER).toEqual({ income: 0, expense: 1, transfer: 2 });
  });
});

describe("computeOverTimeResults", () => {
  it("groups leaves into ordered sections with per-month + total subtotals", () => {
    const data = makeData(
      ["2025-01", "2025-02"],
      [
        leaf(2, "Rent", [1000, 1000], "expense"),
        leaf(4, "Savings", [200, 200], "transfer"),
        leaf(1, "Salary", [3000, 3000], "income"),
        leaf(3, "Groceries", [500, 700], "expense"),
      ],
    );

    const r = computeOverTimeResults(data);

    expect(r.months).toEqual(["2025-01", "2025-02"]);
    // Income-first ordering, regardless of the input row order.
    expect(r.sections.map((s) => s.type)).toEqual(["income", "expense", "transfer"]);

    const income = r.sections.find((s) => s.type === "income")!;
    const expense = r.sections.find((s) => s.type === "expense")!;
    const transfer = r.sections.find((s) => s.type === "transfer")!;

    expect(income.monthly).toEqual({ "2025-01": 3000, "2025-02": 3000 });
    expect(income.total).toBe(6000);
    expect(expense.rows.map((row) => row.categoryName)).toEqual(["Rent", "Groceries"]);
    expect(expense.monthly).toEqual({ "2025-01": 1500, "2025-02": 1700 });
    expect(expense.total).toBe(3200);
    expect(transfer.monthly).toEqual({ "2025-01": 200, "2025-02": 200 });
    expect(transfer.total).toBe(400);
  });

  it("computes net (income − expense + transfer) and before-transfers per month", () => {
    const data = makeData(
      ["2025-01", "2025-02"],
      [
        leaf(1, "Salary", [3000, 3000], "income"),
        leaf(2, "Rent", [1000, 1000], "expense"),
        leaf(3, "Groceries", [500, 700], "expense"),
        leaf(4, "Savings", [200, 200], "transfer"),
      ],
    );

    const r = computeOverTimeResults(data);

    expect(r.hasTransfers).toBe(true);
    // before = income − expense
    expect(r.beforeTransfers.monthly).toEqual({ "2025-01": 1500, "2025-02": 1300 });
    expect(r.beforeTransfers.total).toBe(2800);
    // net = before + transfer
    expect(r.net.monthly).toEqual({ "2025-01": 1700, "2025-02": 1500 });
    expect(r.net.total).toBe(3200);
  });

  it("collapses net to before-transfers when there are no transfers", () => {
    const data = makeData(
      ["2025-01"],
      [leaf(1, "Salary", [2000], "income"), leaf(2, "Rent", [2500], "expense")],
    );

    const r = computeOverTimeResults(data);

    expect(r.hasTransfers).toBe(false);
    expect(r.sections.map((s) => s.type)).toEqual(["income", "expense"]);
    // Deficit: 2000 − 2500 = −500, identical for net and before-transfers.
    expect(r.beforeTransfers.monthly["2025-01"]).toBe(-500);
    expect(r.net.monthly["2025-01"]).toBe(-500);
    expect(r.net.total).toBe(-500);
  });

  it("defaults a leaf with an unknown type to the expense section (defensive)", () => {
    // The tree normally stamps a valid type on every leaf; the reducer still
    // guards against a stray value by folding it into expenses.
    const untyped = {
      ...leaf(9, "Other", [300], "expense"),
      category_type: undefined as unknown as "expense",
    };
    const data = makeData(["2025-01"], [leaf(1, "Salary", [1000], "income"), untyped]);

    const r = computeOverTimeResults(data);

    const expense = r.sections.find((s) => s.type === "expense")!;
    expect(expense.rows.map((row) => row.categoryName)).toEqual(["Other"]);
    expect(expense.total).toBe(300);
    expect(r.net.monthly["2025-01"]).toBe(700); // 1000 − 300
  });

  it("drops empty sections and still nets a lone transfer section", () => {
    const data = makeData(["2025-01"], [leaf(1, "Move", [100], "transfer")]);

    const r = computeOverTimeResults(data);

    expect(r.sections.map((s) => s.type)).toEqual(["transfer"]);
    expect(r.hasTransfers).toBe(true);
    expect(r.beforeTransfers.monthly["2025-01"]).toBe(0);
    expect(r.net.monthly["2025-01"]).toBe(100);
  });

  it("keeps homonym categories of different types apart (Résultat net correct) — Issue #264", () => {
    // Two DIFFERENT categories both named "Divers": one income, one expense.
    // A name-keyed pivot would merge them into a single cell and a single type;
    // the id-keyed tree keeps them distinct, so the net stays correct.
    const data = makeData(
      ["2025-01"],
      [leaf(1, "Divers", [1000], "income"), leaf(2, "Divers", [300], "expense")],
    );

    const r = computeOverTimeResults(data);

    const income = r.sections.find((s) => s.type === "income")!;
    const expense = r.sections.find((s) => s.type === "expense")!;
    expect(income.total).toBe(1000);
    expect(expense.total).toBe(300);
    // Net = 1000 − 300 = 700, NOT 0 (which a name collision would produce).
    expect(r.net.monthly["2025-01"]).toBe(700);
  });

  it("sums leaves only — a parent subtotal row is never double-counted — Issue #264", () => {
    // The tree carries both a parent subtotal (is_parent) and its leaves. The
    // reducer must add up leaves only, or the parent's 800 would be counted twice.
    const parent = { ...leaf(10, "Dépenses", [800], "expense"), is_parent: true };
    const child1 = { ...leaf(11, "Épicerie", [500], "expense"), parent_id: 10, depth: 1 };
    const child2 = { ...leaf(12, "Restaurant", [300], "expense"), parent_id: 10, depth: 1 };
    const income = leaf(1, "Salary", [2000], "income");
    const data = makeData(["2025-01"], [income, parent, child1, child2]);

    const r = computeOverTimeResults(data);

    const expense = r.sections.find((s) => s.type === "expense")!;
    // 500 + 300 = 800 (the parent's own 800 subtotal is excluded).
    expect(expense.total).toBe(800);
    expect(expense.rows.map((row) => row.categoryName)).toEqual(["Épicerie", "Restaurant"]);
    // Net = 2000 − 800 = 1200, not 2000 − 1600.
    expect(r.net.monthly["2025-01"]).toBe(1200);
  });

  it("treats a category absent from a month as zero", () => {
    const data = makeData(
      ["2025-01", "2025-02"],
      [leaf(1, "Salary", [1000, 1000], "income"), leaf(2, "Bonus", [0, 500], "income")],
    );

    const r = computeOverTimeResults(data);

    const income = r.sections.find((s) => s.type === "income")!;
    expect(income.monthly).toEqual({ "2025-01": 1000, "2025-02": 1500 });
    expect(r.net.monthly).toEqual({ "2025-01": 1000, "2025-02": 1500 });
    expect(r.net.total).toBe(2500);
  });
});
