import test from 'node:test'; import assert from 'node:assert/strict'; import JSZip from 'jszip'; import { createProjectDossierPrintHtml, createProjectDossierReport, dossierReportSections, generateProjectDossierDocx, generateProjectDossierPackage, classifyDossierAttachment, validateDossierAttachments, } from '../../resources/js/lib/project-dossier-report.ts'; import { serializeBusinessPlanSnapshot, forecastBusinessPlan, } from '../../resources/js/lib/business-financial-plan.ts'; const allSections = new Set(dossierReportSections.map((section) => section.id)); function simplePlan() { return { currency: 'MZN', openingCash: '0', products: [ { label: 'Pão', monthlyUnits: '100', unitPrice: '50', unitVariableCost: '20', }, ], monthlyCollections: '100', monthlyVariableCostPayments: '100', monthlyOperatingExpenses: '500', monthlyOperatingPayments: '500', monthlyPayrollExpense: '0', monthlyPayrollPayments: '0', assetInvestment: '0', inventory: '0', receivables: '0', payables: '0', equity: '10000', shareholderLoans: '0', bankLoanProceeds: '0', grants: '0', assetDepreciation: '0', depreciationLifeMonths: 12, monthlyInterestExpense: '0', monthlyInterestPayment: '0', monthlyPrincipalRepayment: '0', periods: 12, }; } void test('incomplete dossier identifies unknowns without inventing zeroes', () => { const report = createProjectDossierReport( {}, { selectedSections: allSections, generatedAt: '2026-10-04T10:00:00.000Z', }, ); assert.equal(report.title, 'Não informado'); assert.equal( report.sections.find((section) => section.id === 'summary').rows[0] .value, 'Não informado', ); assert.match( report.sections.find((section) => section.id === 'resources').rows[0] .value, /Informação insuficiente/, ); assert.doesNotMatch(JSON.stringify(report), /"value":"0(\.00)?"/); }); void test('report sections are selected explicitly and preserve declared versus estimated provenance', () => { const selected = new Set(['summary', 'market', 'sources']); const report = createProjectDossierReport( { project_title: 'Moagem', market_assumption: 'procura crescente', market_source: 'entrevista própria; 2026-09-01', }, { selectedSections: selected, version: 'Revisão 3' }, ); assert.deepEqual( report.sections.map((section) => section.id), ['summary', 'market', 'sources'], ); const market = report.sections.find((section) => section.id === 'market'); assert.ok( market.rows.some( (row) => row.value === 'procura crescente' && row.classification === 'Informação declarada, não verificada', ), ); assert.equal(report.version, 'Revisão 3'); }); void test('business plan values in the dossier equal the existing simulator forecast', () => { const plan = simplePlan(); const expected = forecastBusinessPlan(plan); const snapshot = serializeBusinessPlanSnapshot(plan); const report = createProjectDossierReport( { project_title: 'Padaria', currency: 'MZN', business_plan_snapshot: snapshot, }, { selectedSections: allSections }, ); const finance = report.sections.find((section) => section.id === 'finance'); const row = (field) => finance.rows.find((item) => item.field === field).value; assert.equal( row('Facturação no horizonte'), `${expected.totalRevenue} MZN`, ); assert.equal( row('Resultado antes de impostos'), `${expected.totalProfitBeforeTax} MZN`, ); assert.equal(row('Caixa final projectada'), `${expected.endingCash} MZN`); assert.equal(row('Reconciliação do financiamento'), expected.fundingStatus); }); void test('an attached simulator execution keeps its exact snapshot result, explanation and version', () => { const report = createProjectDossierReport( { project_title: 'Pequena indústria' }, { selectedSections: allSections, currentCalculation: { status: 'completed', version: 4, updatedAt: '2026-10-03T12:00:00Z', reportChecksum: 'a'.repeat(64), snapshot: { contract_version: 1, indicative_total: '1250.75', formula_version: 'v2', base: '2000', rate_percent: '75', reduction_percent: '16.6', currency: 'MZN', context: { reference_date: '2026-10-03', engine_version: 'engine-4', scenario_version: 4, }, result: { status: 'calculated', value: '1250.75', unit: null, explanation: 'Cálculo indicativo guardado.', missing_inputs: [], source_ids: [], }, }, }, }, ); const finance = report.sections.find((section) => section.id === 'finance'); assert.equal( finance.rows.find((row) => row.field === 'Resultado indicativo').value, '1250.75', ); assert.equal( finance.rows.find((row) => row.field === 'Explicação e dados em falta') .value, 'Cálculo indicativo guardado.', ); assert.equal( finance.rows.find((row) => row.field === 'Revisão do resultado').value, '4', ); assert.equal( finance.rows.find( (row) => row.field === 'Hash SHA-256 do relatório associado', ).value, 'a'.repeat(64), ); }); void test('DOCX is a readable OpenXML package with Portuguese accents, tables and page fields', async () => { const report = createProjectDossierReport( { project_title: 'Produção de café', project_summary: 'Ideia com acção e localização.', }, { selectedSections: allSections, version: 'Revisão 2' }, ); const blob = await generateProjectDossierDocx(report, [], true); const bytes = new Uint8Array(await blob.arrayBuffer()); assert.deepEqual(Array.from(bytes.subarray(0, 2)), [0x50, 0x4b]); const zip = await JSZip.loadAsync(bytes); const documentXml = await zip.file('word/document.xml').async('string'); const footerXml = await zip.file('word/footer1.xml').async('string'); assert.match(documentXml, /Produção de café/); assert.match(documentXml, /Classificação/); assert.match(documentXml, /projecto/); assert.match(footerXml, /PAGE/); assert.ok(zip.file('[Content_Types].xml')); }); void test('CSV and plain-text annexes are embedded, while other originals get a ZIP index', async () => { const csv = new File(['Produto;Quantidade\r\nCafé;12'], 'tabela.csv', { type: 'text/csv', }); const pdf = new File(['%PDF-1.7 example'], '../apoio.pdf', { type: 'application/pdf', }); assert.equal(classifyDossierAttachment(csv), 'csv'); assert.equal(classifyDossierAttachment(pdf), 'original-only'); const attachments = [ { file: csv, include: true, caption: '' }, { file: pdf, include: true, caption: '' }, ]; const report = createProjectDossierReport( { project_title: 'Projecto de café' }, { selectedSections: allSections }, ); const docx = await generateProjectDossierDocx(report, attachments, true); const docxZip = await JSZip.loadAsync(await docx.arrayBuffer()); const xml = await docxZip.file('word/document.xml').async('string'); assert.match(xml, /Café/); assert.match(xml, /Este formato não foi incorporado/); const bundle = await generateProjectDossierPackage(docx, attachments, true); const bundleZip = await JSZip.loadAsync(await bundle.arrayBuffer()); assert.ok(bundleZip.file('Anexos/tabela.csv')); assert.ok(bundleZip.file('Anexos/apoio.pdf')); const index = await bundleZip.file('indice-anexos.txt').async('string'); assert.match(index, /Conteúdo não incorporado/); assert.match(index, /Conteúdo não incorporado; original preservado/); }); void test('PNG photographs are embedded in the Word package with an image relationship and caption', async () => { const pngBytes = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/aZkAAAAASUVORK5CYII=', 'base64', ); const photo = new File([pngBytes], 'local.jpg', { type: 'image/png' }); const report = createProjectDossierReport( { project_title: 'Projecto com fotografia' }, { selectedSections: allSections }, ); const docx = await generateProjectDossierDocx( report, [{ file: photo, include: true, caption: 'Localização declarada' }], true, ); const zip = await JSZip.loadAsync(await docx.arrayBuffer()); const mediaPath = Object.keys(zip.files).find((path) => /^word\/media\/.+\.png$/.test(path), ); assert.ok(mediaPath); assert.match( await zip.file('word/document.xml').async('string'), /Localização declarada/, ); assert.match( await zip.file('word/_rels/document.xml.rels').async('string'), new RegExp(`Target="media/${mediaPath.split('/').at(-1)}"`), ); }); void test('print/PDF HTML escapes user content and explains omitted annexes', async () => { const report = createProjectDossierReport( { project_title: '', project_summary: 'Acentuação: localização.', }, { selectedSections: new Set(['summary', 'annexes']) }, ); const pdf = await createProjectDossierPrintHtml(report, [], false); assert.doesNotMatch(pdf, /