import Foundation @main struct WorkspacePresentationTests { static func material(_ id: String, amount: Double, date: String, merchant: String = "测试商户") -> Material { var ocr = OCRData() ocr.status = "success" ocr.amounts = [String(format: "%.2f", amount)] ocr.merchants = [merchant] return Material(id: id, name: id + ".pdf", path: "", type: "invoice", size: 0, matched: false, previewPath: "", ocr: ocr, displayAmount: amount, sortDate: date, issueDate: date) } static func main() throws { let a = material("a", amount: 10, date: "2026-09-02", merchant: "铁路") let b = material("b", amount: 100, date: "2026-09-01") let c = material("c", amount: 20, date: "2026-09-03") let items = [a, b, c] precondition(WorkspaceFilter.materials(items, query: "", order: .amount).map(\.id) == ["a", "c", "b"]) precondition(WorkspaceFilter.materials(items, query: "", order: .date).map(\.id) == ["b", "a", "c"]) precondition(WorkspaceFilter.materials(items, query: "铁路 10.00", order: .name).map(\.id) == ["a"]) precondition(WorkspaceFilter.materials(items, query: "B.PDF", order: .name).map(\.id) == ["b"]) precondition(WorkspaceFilter.materials(items, query: "2026-09-03", order: .name).map(\.id) == ["c"]) precondition(WorkspaceFilter.materials(items, query: "", order: .amount, descending: true).map(\.id) == ["b", "c", "a"]) var automatic = MatchGroup(id: "auto", invoices: [a], payments: [], category: "交通", matchType: "auto", score: 100, reasons: []) let manual = MatchGroup(id: "manual", invoices: [b], payments: [], category: "其他", matchType: "manual", score: 100, reasons: []) precondition(automatic.needsAttention && !manual.needsAttention) automatic.explanation = try JSONDecoder().decode(MatchExplanation.self, from: Data(""" {"version":2,"title":"test","rawScore":55,"checks":[{"title":"amount","points":55,"maximum":100,"detail":"","issue":""}],"note":""} """.utf8)) precondition(automatic.needsAttention) var failed = manual failed.id = "failed" failed.invoices[0].ocr.status = "failed" precondition(failed.needsAttention) let matches = [automatic, manual, failed] precondition(WorkspaceFilter.matches(matches, query: "", category: "全部", attentionOnly: true, order: .date).count == 2) let visible = WorkspaceFilter.matches(matches, query: "铁路", category: "交通", attentionOnly: false, order: .date) precondition(visible.map(\.id) == ["auto"]) var selection = MatchedPPTSelection(ids: ["manual"]) selection.setVisible(visible, selected: true) precondition(selection.ids == ["manual", "auto"]) selection.setVisible(visible, selected: false) precondition(selection.ids == ["manual"]) var workspace = Workspace() workspace.matches = matches let presentation = WorkspacePresentation(workspace) precondition(presentation.matchNumbers.count == 3) precondition(presentation.categoryCounts["其他"] == 2 && presentation.attentionCount == 2) precondition(workspace.matches.map(\.id) == ["auto", "manual", "failed"]) let columns = presentation.categoryColumns(for: matches) precondition(columns.map(\.category) == ["交通", "其他"]) precondition(columns.allSatisfy { !$0.matches.isEmpty }) let traffic = columns.first(where: { $0.category == "交通" })! let other = columns.first(where: { $0.category == "其他" })! precondition(traffic.matches.map(\.id) == ["auto"] && traffic.invoiceTotal == 10) precondition(other.matches.map(\.id) == ["manual", "failed"] && other.invoiceTotal == 200) precondition(columns.flatMap(\.matches).count == matches.count) selection.setVisible(traffic.matches, selected: true) selection.setVisible(other.matches, selected: true) selection.setVisible(traffic.matches, selected: false) precondition(selection.ids == ["manual", "failed"]) let filteredColumns = presentation.categoryColumns(for: visible) precondition(filteredColumns.map(\.id) == ["交通"]) precondition(selection.ids == ["manual", "failed"]) precondition(presentation.categoryColumns(for: matches).map(\.id) == ["交通", "其他"]) let sorted = WorkspaceFilter.matches(matches, query: "", category: "全部", attentionOnly: false, order: .date) precondition(presentation.categoryColumns(for: sorted).first(where: { $0.category == "其他" })!.matches.map(\.id) == ["manual", "failed"]) workspace.matches[1].category = "住宿" var updated = WorkspacePresentation(workspace) precondition(updated.categoryColumns(for: workspace.matches).first(where: { $0.category == "住宿" })!.matches.map(\.id) == ["manual"]) precondition(selection.ids == ["manual", "failed"]) let exported = try selection.exportWorkspace(from: workspace) precondition(exported.matches.map(\.id) == ["manual", "failed"]) workspace.matches[2].category = "住宿" updated = WorkspacePresentation(workspace) precondition(updated.categoryColumns(for: workspace.matches).map(\.id) == ["住宿", "交通"]) workspace.matches.removeAll { $0.category == "住宿" } precondition(WorkspacePresentation(workspace).categoryColumns(for: workspace.matches).map(\.id) == ["交通"]) workspace.matches = matches workspace.matches[1].category = "住宿" workspace.matches[0].category = "历史类型" workspace.matches[2].category = "" updated = WorkspacePresentation(workspace) let extra = updated.categoryColumns(for: workspace.matches) precondition(extra.map(\.id) == ["住宿", "", "历史类型"]) precondition(extra.first(where: { $0.category == "" })!.title == "未分类") precondition(extra.first(where: { $0.category == "历史类型" })!.matches.map(\.id) == ["auto"]) precondition(updated.categoryColumns(for: []).isEmpty) precondition(WorkspacePresentation().categoryColumns(for: []).isEmpty) var noisy = manual noisy.invoices[0].ocr.merchants = ["载次数", "名称:测试购买方有限公司", "全称", "一站式企业出行与商旅平台"] let originalCandidates = noisy.invoices[0].ocr.merchants precondition(WorkspaceFilter.matches([noisy], query: "测试购买方", category: "全部", attentionOnly: false, order: .date).map(\.id) == [manual.id]) precondition(noisy.invoices[0].ocr.merchants == originalCandidates) precondition(MatchedBoardLayout.columnWidth(available: 900, count: 7) == 132) let desktopWidth = MatchedBoardLayout.columnWidth(available: 1100, count: 7) precondition(abs(desktopWidth * 7 + MatchedBoardLayout.gap * 6 - 1100) < 0.001) precondition(MatchedBoardLayout.columnWidth(available: 700, count: 1) == 700) precondition(MatchedBoardLayout.columnWidth(available: 0, count: 0).isFinite) print("Original OCR search preservation and compact category layout sizing passed") print("Search, numeric sorting, attention filtering, cached summaries and hidden selection retention passed") print("Automatic empty-column removal, category reappearance, filtering, selection and unchanged export order passed") } }