import { describe, expect, it } from "bun:test"; import { buildStatementTree, resolveStatementSelection, } from "@/lib/financials/statement-view-model"; import type { DetailFinancialRow, SurfaceFinancialRow } from "@/lib/types"; function createSurfaceRow( input: Partial & Pick, ): SurfaceFinancialRow { return { key: input.key, label: input.label, category: input.category ?? "revenue", order: input.order ?? 10, unit: input.unit ?? "currency", values: input.values, sourceConcepts: input.sourceConcepts ?? [input.key], sourceRowKeys: input.sourceRowKeys ?? [input.key], sourceFactIds: input.sourceFactIds ?? [1], formulaKey: input.formulaKey ?? null, hasDimensions: input.hasDimensions ?? false, resolvedSourceRowKeys: input.resolvedSourceRowKeys ?? Object.fromEntries( Object.keys(input.values).map((periodId) => [periodId, input.key]), ), statement: input.statement ?? "income", detailCount: input.detailCount, resolutionMethod: input.resolutionMethod, confidence: input.confidence, warningCodes: input.warningCodes, }; } function createDetailRow( input: Partial & Pick, ): DetailFinancialRow { return { key: input.key, parentSurfaceKey: input.parentSurfaceKey, label: input.label, conceptKey: input.conceptKey ?? input.key, qname: input.qname ?? `us-gaap:${input.key}`, namespaceUri: input.namespaceUri ?? "http://fasb.org/us-gaap/2024", localName: input.localName ?? input.key, unit: input.unit ?? "USD", values: input.values, sourceFactIds: input.sourceFactIds ?? [100], isExtension: input.isExtension ?? false, dimensionsSummary: input.dimensionsSummary ?? [], residualFlag: input.residualFlag ?? false, }; } describe("statement view model", () => { const categories = [{ key: "opex", label: "Operating Expenses", count: 4 }]; it("builds a root-only tree when there are no configured children or details", () => { const model = buildStatementTree({ surfaceKind: "income_statement", rows: [ createSurfaceRow({ key: "revenue", label: "Revenue", category: "revenue", values: { p1: 100 }, }), ], statementDetails: null, categories: [], searchQuery: "", expandedRowKeys: new Set(), }); expect(model.sections).toHaveLength(1); expect(model.sections[0]?.nodes[0]).toMatchObject({ kind: "surface", row: { key: "revenue" }, expandable: false, }); }); it("nests the operating expense child surfaces under the parent row", () => { const model = buildStatementTree({ surfaceKind: "income_statement", rows: [ createSurfaceRow({ key: "operating_expenses", label: "Operating Expenses", category: "opex", order: 10, values: { p1: 40 }, }), createSurfaceRow({ key: "selling_general_and_administrative", label: "SG&A", category: "opex", order: 20, values: { p1: 20 }, }), createSurfaceRow({ key: "research_and_development", label: "Research Expense", category: "opex", order: 30, values: { p1: 12 }, }), createSurfaceRow({ key: "other_operating_expense", label: "Other Expense", category: "opex", order: 40, values: { p1: 8 }, }), ], statementDetails: null, categories, searchQuery: "", expandedRowKeys: new Set(["operating_expenses"]), }); const parent = model.sections[0]?.nodes[0]; expect(parent?.kind).toBe("surface"); const childKeys = parent?.kind === "surface" ? parent.children.map((node) => node.row.key) : []; expect(childKeys).toEqual([ "selling_general_and_administrative", "research_and_development", "other_operating_expense", ]); }); it("nests raw detail rows under the matching child surface row", () => { const model = buildStatementTree({ surfaceKind: "income_statement", rows: [ createSurfaceRow({ key: "operating_expenses", label: "Operating Expenses", category: "opex", order: 10, values: { p1: 40 }, }), createSurfaceRow({ key: "selling_general_and_administrative", label: "SG&A", category: "opex", order: 20, values: { p1: 20 }, }), ], statementDetails: { selling_general_and_administrative: [ createDetailRow({ key: "corporate_sga", label: "Corporate SG&A", parentSurfaceKey: "selling_general_and_administrative", values: { p1: 20 }, }), ], }, categories, searchQuery: "", expandedRowKeys: new Set([ "operating_expenses", "selling_general_and_administrative", ]), }); const child = model.sections[0]?.nodes[0]; expect(child?.kind).toBe("surface"); const sgaNode = child?.kind === "surface" ? child.children[0] : null; expect(sgaNode?.kind).toBe("surface"); const detailNode = sgaNode?.kind === "surface" ? sgaNode.children[0] : null; expect(detailNode).toMatchObject({ kind: "detail", row: { key: "corporate_sga" }, }); }); it("dedupes raw detail rows that are already covered by a child surface concept", () => { const rows = [ createSurfaceRow({ key: "operating_expenses", label: "Operating Expenses", category: "opex", order: 10, values: { p1: 40 }, }), createSurfaceRow({ key: "research_and_development", label: "Research Expense", category: "opex", order: 20, values: { p1: 12 }, sourceConcepts: ["us-gaap:ResearchAndDevelopmentExpense"], }), ]; const statementDetails = { operating_expenses: [ createDetailRow({ key: "rnd_raw", label: "Research And Development Expense", parentSurfaceKey: "operating_expenses", conceptKey: "research_and_development_expense", qname: "us-gaap:ResearchAndDevelopmentExpense", localName: "ResearchAndDevelopmentExpense", values: { p1: 12 }, }), createDetailRow({ key: "general_admin", label: "General And Administrative Expense", parentSurfaceKey: "operating_expenses", conceptKey: "general_and_administrative_expense", qname: "us-gaap:GeneralAndAdministrativeExpense", localName: "GeneralAndAdministrativeExpense", values: { p1: 7 }, }), ], }; const model = buildStatementTree({ surfaceKind: "income_statement", rows, statementDetails, categories, searchQuery: "", expandedRowKeys: new Set(["operating_expenses"]), }); const parent = model.sections[0]?.nodes[0]; expect(parent?.kind).toBe("surface"); if (parent?.kind !== "surface") { throw new Error("expected surface node"); } expect(parent.directDetailCount).toBe(1); expect(parent.children.map((node) => node.row.key)).toEqual([ "research_and_development", "general_admin", ]); expect(parent.children.some((node) => node.row.key === "rnd_raw")).toBe( false, ); expect(model.totalNodeCount).toBe(3); }); it("returns the deduped detail rows for a selected surface", () => { const rows = [ createSurfaceRow({ key: "operating_expenses", label: "Operating Expenses", category: "opex", order: 10, values: { p1: 40 }, }), createSurfaceRow({ key: "research_and_development", label: "Research Expense", category: "opex", order: 20, values: { p1: 12 }, sourceConcepts: ["us-gaap:ResearchAndDevelopmentExpense"], }), ]; const statementDetails = { operating_expenses: [ createDetailRow({ key: "rnd_raw", label: "Research And Development Expense", parentSurfaceKey: "operating_expenses", conceptKey: "research_and_development_expense", qname: "us-gaap:ResearchAndDevelopmentExpense", localName: "ResearchAndDevelopmentExpense", values: { p1: 12 }, }), createDetailRow({ key: "general_admin", label: "General And Administrative Expense", parentSurfaceKey: "operating_expenses", conceptKey: "general_and_administrative_expense", qname: "us-gaap:GeneralAndAdministrativeExpense", localName: "GeneralAndAdministrativeExpense", values: { p1: 7 }, }), ], }; const selection = resolveStatementSelection({ surfaceKind: "income_statement", rows, statementDetails, selection: { kind: "surface", key: "operating_expenses" }, }); expect(selection).toMatchObject({ kind: "surface", detailRows: [{ key: "general_admin" }], }); expect( selection?.kind === "surface" && selection.detailRows.some((row) => row.key === "rnd_raw"), ).toBe(false); }); it("auto-expands the parent chain when search matches a child surface or detail row", () => { const rows = [ createSurfaceRow({ key: "operating_expenses", label: "Operating Expenses", category: "opex", order: 10, values: { p1: 40 }, }), createSurfaceRow({ key: "research_and_development", label: "Research Expense", category: "opex", order: 20, values: { p1: 12 }, }), ]; const childSearch = buildStatementTree({ surfaceKind: "income_statement", rows, statementDetails: null, categories, searchQuery: "research", expandedRowKeys: new Set(), }); expect(childSearch.autoExpandedKeys.has("operating_expenses")).toBe(true); expect( childSearch.sections[0]?.nodes[0]?.kind === "surface" && childSearch.sections[0]?.nodes[0].expanded, ).toBe(true); const detailSearch = buildStatementTree({ surfaceKind: "income_statement", rows, statementDetails: { research_and_development: [ createDetailRow({ key: "ai_lab_expense", label: "AI Lab Expense", parentSurfaceKey: "research_and_development", values: { p1: 12 }, }), ], }, categories, searchQuery: "ai lab", expandedRowKeys: new Set(), }); const parent = detailSearch.sections[0]?.nodes[0]; const child = parent?.kind === "surface" ? parent.children[0] : null; expect(parent?.kind === "surface" && parent.expanded).toBe(true); expect(child?.kind === "surface" && child.expanded).toBe(true); }); it("searches hidden duplicate concepts through the mapped child surface without reintroducing the raw detail row", () => { const rows = [ createSurfaceRow({ key: "operating_expenses", label: "Operating Expenses", category: "opex", order: 10, values: { p1: 40 }, }), createSurfaceRow({ key: "research_and_development", label: "Research Expense", category: "opex", order: 20, values: { p1: 12 }, sourceConcepts: ["us-gaap:ResearchAndDevelopmentExpense"], }), ]; const model = buildStatementTree({ surfaceKind: "income_statement", rows, statementDetails: { operating_expenses: [ createDetailRow({ key: "rnd_raw", label: "Research And Development Expense", parentSurfaceKey: "operating_expenses", conceptKey: "research_and_development_expense", qname: "us-gaap:ResearchAndDevelopmentExpense", localName: "ResearchAndDevelopmentExpense", values: { p1: 12 }, }), ], }, categories, searchQuery: "researchanddevelopmentexpense", expandedRowKeys: new Set(), }); const parent = model.sections[0]?.nodes[0]; expect(parent?.kind === "surface" && parent.expanded).toBe(true); expect( parent?.kind === "surface" && parent.children.map((node) => node.row.key), ).toEqual(["research_and_development"]); }); it("does not throw when legacy surface rows are missing source arrays", () => { const malformedRow = { ...createSurfaceRow({ key: "revenue", label: "Revenue", category: "revenue", values: { p1: 100 }, }), sourceConcepts: undefined, sourceRowKeys: undefined, } as unknown as SurfaceFinancialRow; const model = buildStatementTree({ surfaceKind: "income_statement", rows: [malformedRow], statementDetails: null, categories: [], searchQuery: "revenue", expandedRowKeys: new Set(), }); expect(model.sections[0]?.nodes[0]).toMatchObject({ kind: "surface", row: { key: "revenue" }, }); }); it("keeps not meaningful rows visible and resolves selections for surface and detail nodes", () => { const rows = [ createSurfaceRow({ key: "gross_profit", label: "Gross Profit", category: "profit", values: { p1: null }, resolutionMethod: "not_meaningful", warningCodes: ["gross_profit_not_meaningful_bank_pack"], }), ]; const details = { gross_profit: [ createDetailRow({ key: "gp_unmapped", label: "Gross Profit residual", parentSurfaceKey: "gross_profit", values: { p1: null }, residualFlag: true, }), ], }; const model = buildStatementTree({ surfaceKind: "income_statement", rows, statementDetails: details, categories: [], searchQuery: "", expandedRowKeys: new Set(["gross_profit"]), }); expect(model.sections[0]?.nodes[0]).toMatchObject({ kind: "surface", row: { key: "gross_profit", resolutionMethod: "not_meaningful" }, }); const surfaceSelection = resolveStatementSelection({ surfaceKind: "income_statement", rows, statementDetails: details, selection: { kind: "surface", key: "gross_profit" }, }); expect(surfaceSelection?.kind).toBe("surface"); const detailSelection = resolveStatementSelection({ surfaceKind: "income_statement", rows, statementDetails: details, selection: { kind: "detail", key: "gp_unmapped", parentKey: "gross_profit", }, }); expect(detailSelection).toMatchObject({ kind: "detail", row: { key: "gp_unmapped", residualFlag: true }, }); }); it("renders unmapped detail rows in a dedicated residual section and counts them", () => { const model = buildStatementTree({ surfaceKind: "income_statement", rows: [ createSurfaceRow({ key: "revenue", label: "Revenue", category: "revenue", values: { p1: 100 }, }), ], statementDetails: { unmapped: [ createDetailRow({ key: "unmapped_other_income", label: "Other income residual", parentSurfaceKey: "unmapped", values: { p1: 5 }, residualFlag: true, }), ], }, categories: [], searchQuery: "", expandedRowKeys: new Set(), }); expect(model.sections).toHaveLength(2); expect(model.sections[1]).toMatchObject({ key: "unmapped_residual", label: "Unmapped / Residual", }); expect(model.sections[1]?.nodes[0]).toMatchObject({ kind: "detail", row: { key: "unmapped_other_income", parentSurfaceKey: "unmapped" }, }); expect(model.visibleNodeCount).toBe(2); expect(model.totalNodeCount).toBe(2); }); it("matches search and resolves selection for unmapped detail rows without a real parent surface", () => { const rows = [ createSurfaceRow({ key: "revenue", label: "Revenue", category: "revenue", values: { p1: 100 }, }), ]; const statementDetails = { unmapped: [ createDetailRow({ key: "unmapped_fx_gain", label: "FX gain residual", parentSurfaceKey: "unmapped", values: { p1: 2 }, residualFlag: true, }), ], }; const model = buildStatementTree({ surfaceKind: "income_statement", rows, statementDetails, categories: [], searchQuery: "fx gain", expandedRowKeys: new Set(), }); expect(model.sections).toHaveLength(1); expect(model.sections[0]).toMatchObject({ key: "unmapped_residual", label: "Unmapped / Residual", }); expect(model.visibleNodeCount).toBe(1); expect(model.totalNodeCount).toBe(2); const selection = resolveStatementSelection({ surfaceKind: "income_statement", rows, statementDetails, selection: { kind: "detail", key: "unmapped_fx_gain", parentKey: "unmapped", }, }); expect(selection).toMatchObject({ kind: "detail", row: { key: "unmapped_fx_gain", parentSurfaceKey: "unmapped" }, parentSurfaceRow: null, }); }); });