// @vitest-environment node
import { describe, it, expect } from "vitest";
import { reconcile, type RecAccount, type RecLine, type ReconcileInput } from "./reconcile";

// Synthetic chart of accounts for the §10 test ledgers.
const ACCT: Record<string, RecAccount> = {
  cash: { id: "cash", type: "asset", name: "Operating Cash", is_cash: true },
  ar: { id: "ar", type: "asset", name: "Accounts Receivable" },
  ap: { id: "ap", type: "liability", name: "Accounts Payable" },
  re: { id: "re", type: "equity", name: "Retained Earnings" },
  rev: { id: "rev", type: "income", name: "Assessment Fees" },
  exp: { id: "exp", type: "expense", name: "Administrative" },
};
const accounts = Object.values(ACCT);

function dr(account_id: string, amount: number, date = "2026-03-15"): RecLine {
  return { account_id, date, debit: amount, credit: 0 };
}
function cr(account_id: string, amount: number, date = "2026-03-15"): RecLine {
  return { account_id, date, debit: 0, credit: amount };
}

const base = (lines: RecLine[], extra: Partial<ReconcileInput> = {}): ReconcileInput => ({
  accounts,
  lines,
  periodStart: "2026-01-01",
  openInvoices: 0,
  openBills: 0,
  ...extra,
});

const resid = (checks: ReturnType<typeof reconcile>, id: string) => checks.find((c) => c.id === id)!;

describe("reconciliation matrix (§9)", () => {
  it("ledger 1: empty ledger — every check passes trivially", () => {
    const checks = reconcile(base([]));
    expect(checks.every((c) => c.pass)).toBe(true);
  });

  it("ledger 2: single balanced entry — R1 and R2 hold", () => {
    // Customer invoiced 1000 (Dr AR / Cr Revenue), then fully paid (Dr Cash / Cr AR)
    const lines = [
      dr("ar", 1000), cr("rev", 1000),
      dr("cash", 1000), cr("ar", 1000),
    ];
    const checks = reconcile(base(lines, { openInvoices: 0 })); // fully paid → no open AR
    expect(resid(checks, "R1").pass).toBe(true);
    expect(resid(checks, "R2").pass).toBe(true);
    expect(resid(checks, "R4").pass).toBe(true);
    expect(resid(checks, "R7").pass).toBe(true);
  });

  it("ledger 3: full period (revenue, expense, AP, partial AR) — R1,R2,R4,R7,R8 pass", () => {
    const lines = [
      // Assessments billed 1000, 600 collected
      dr("ar", 1000), cr("rev", 1000),
      dr("cash", 600), cr("ar", 600),
      // Expense 300 on credit (bill), 100 paid
      dr("exp", 300), cr("ap", 300),
      dr("ap", 100), cr("cash", 100),
    ];
    const checks = reconcile(base(lines, { openInvoices: 400, openBills: 200 }));
    for (const id of ["R1", "R2", "R4", "R7", "R8"]) {
      expect(resid(checks, id).pass, `${id} should pass`).toBe(true);
    }
  });

  it("fault: single-sided entry → R1 fails", () => {
    const checks = reconcile(base([dr("cash", 500)])); // no offsetting credit
    expect(resid(checks, "R1").pass).toBe(false);
    expect(resid(checks, "R1").residual).toBeCloseTo(500, 2);
  });

  it("R2 and R4 are structural in a GL-derived model (mirror R1)", () => {
    // Because the statements derive from one balanced ledger, R2 residual == R1
    // residual == R4 residual: they pass together and fail together. Independent
    // bugs (sub-ledger drift, type misclassification of A/R) surface in R7/R8.
    const balanced = reconcile(base([dr("ar", 1000), cr("rev", 1000)], { openInvoices: 1000 }));
    expect(resid(balanced, "R1").pass && resid(balanced, "R2").pass && resid(balanced, "R4").pass).toBe(true);

    const unbalanced = reconcile(base([dr("cash", 500)]));
    expect(resid(unbalanced, "R1").pass).toBe(false);
    expect(resid(unbalanced, "R2").pass).toBe(false);
    expect(resid(unbalanced, "R4").pass).toBe(false);
  });

  it("fault: payment not applied to invoice → R7 fails (A/R overstated)", () => {
    // Invoice 1000 billed and cash collected in GL, but the invoice still shows
    // open (payment not applied): A/R control 0, openInvoices 1000 → mismatch.
    const lines = [dr("ar", 1000), cr("rev", 1000), dr("cash", 1000), cr("ar", 1000)];
    const checks = reconcile(base(lines, { openInvoices: 1000 }));
    expect(resid(checks, "R7").pass).toBe(false);
    expect(resid(checks, "R7").residual).toBeCloseTo(-1000, 2);
  });

  it("ledger 5: partial settlement — A/R open balance is 600, not 1000 gross (§1.5)", () => {
    // Invoice 1000, payment 400 applied → A/R control 600, openInvoices 600
    const lines = [dr("ar", 1000), cr("rev", 1000), dr("cash", 400), cr("ar", 400)];
    const checks = reconcile(base(lines, { openInvoices: 600 }));
    expect(resid(checks, "R7").pass).toBe(true);
    // and summing gross (1000) would fail by exactly the 400 collected
    const gross = reconcile(base(lines, { openInvoices: 1000 }));
    expect(resid(gross, "R7").residual).toBeCloseTo(-400, 2);
  });

  it("R3: P&L net income matches the raw-GL period net income, fails on a builder drift", () => {
    // Period: revenue 1000, expense 300 → GL period net income 700.
    const lines = [dr("ar", 1000), cr("rev", 1000), dr("exp", 300), cr("cash", 300)];
    // Builder agrees → R3 passes.
    expect(resid(reconcile(base(lines, { reportPLNetIncome: 700 })), "R3").pass).toBe(true);
    // Builder disagrees (e.g. dropped/misclassified an account) → R3 fails by the gap.
    const bad = reconcile(base(lines, { reportPLNetIncome: 650 }));
    expect(resid(bad, "R3").pass).toBe(false);
    expect(resid(bad, "R3").residual).toBeCloseTo(-50, 2);
    // Omitted → R3 not emitted.
    expect(reconcile(base(lines)).find((c) => c.id === "R3")).toBeUndefined();
  });

  it("R5: Movement of Equity ending equity ties to the Balance Sheet total equity", () => {
    const lines = [dr("ar", 1000), cr("rev", 1000)];
    expect(resid(reconcile(base(lines, { sceEndingEquity: 1000, bsTotalEquity: 1000 })), "R5").pass).toBe(true);
    const bad = reconcile(base(lines, { sceEndingEquity: 940, bsTotalEquity: 1000 }));
    expect(resid(bad, "R5").pass).toBe(false);
    expect(resid(bad, "R5").residual).toBeCloseTo(-60, 2);
  });

  it("R7/R8 are omitted for imported-GL companies (arApApplicable=false)", () => {
    const lines = [dr("ar", 1000), cr("rev", 1000)];
    const checks = reconcile(base(lines, { arApApplicable: false, openInvoices: 1000 }));
    expect(checks.find((c) => c.id === "R7")).toBeUndefined();
    expect(checks.find((c) => c.id === "R8")).toBeUndefined();
    expect(resid(checks, "R1").pass).toBe(true);
  });

  it("checks are returned in numeric order", () => {
    const lines = [dr("ar", 1000), cr("rev", 1000)];
    const checks = reconcile(base(lines, { reportPLNetIncome: 1000, sceEndingEquity: 1000, bsTotalEquity: 1000, openInvoices: 1000 }));
    const ids = checks.map((c) => Number(c.id.slice(1)));
    expect(ids).toEqual([...ids].sort((a, b) => a - b));
  });

  it("cash flow is GL-derived: it ties (R4) whenever the ledger balances", () => {
    // Indirect CFO+CFI+CFF == ΔCash is an algebraic identity of double entry, so a
    // balanced ledger always passes R4 — e.g. invoice billed but uncollected:
    // net income +1000, A/R +1000, cash unchanged → CFO 0.
    const balanced = reconcile(base([dr("ar", 1000), cr("rev", 1000)], { openInvoices: 1000 }));
    expect(resid(balanced, "R4").pass).toBe(true);

    // An unbalanced (single-sided) entry breaks the identity → R1 and R4 both fail.
    const broken = reconcile(base([cr("rev", 1000)]));
    expect(resid(broken, "R1").pass).toBe(false);
    expect(resid(broken, "R4").pass).toBe(false);
  });
});
