import { promises as fs } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { PDFDocument } from 'pdf-lib'; import { fillEoiFormFields, loadEoiTemplatePdf } from '@/lib/pdf/fill-eoi-form'; import type { EoiContext } from '@/lib/services/eoi-context'; // ─── Test PDF builder (synthetic source PDF with the same field names) ─────── async function buildSyntheticEoiPdf(): Promise { const doc = await PDFDocument.create(); const page = doc.addPage([600, 800]); const form = doc.getForm(); const textFieldNames = [ 'Name', 'Email', 'Address', 'Yacht Name', 'Length', 'Width', 'Draft', 'Berth Number', ]; textFieldNames.forEach((name, i) => { const f = form.createTextField(name); f.addToPage(page, { x: 50, y: 700 - i * 40, width: 300, height: 24 }); }); for (const name of ['Lease_10', 'Purchase']) { const cb = form.createCheckBox(name); cb.addToPage(page, { x: 400, y: 700 - (name === 'Purchase' ? 0 : 40), width: 12, height: 12 }); } return doc.save(); } function makeContext(overrides: Partial = {}): EoiContext { return { client: { fullName: 'Alice Smith', nationality: 'US', primaryEmail: 'alice@example.com', primaryPhone: '+1-555-0100', address: { street: '123 Main St', city: 'Austin', country: 'USA' }, }, yacht: { name: 'Sea Breeze', lengthFt: '45', widthFt: '14', draftFt: '6', lengthM: null, widthM: null, draftM: null, hullNumber: 'HN-1', flag: 'US', yearBuilt: 2020, }, company: null, owner: { type: 'client', name: 'Alice Smith' }, berth: { mooringNumber: 'A-12', area: 'North', lengthFt: '50', price: '1000', priceCurrency: 'USD', tenureType: 'permanent', }, interest: { stage: 'open', leadCategory: null, dateFirstContact: null, notes: null }, port: { name: 'Port Nimara', defaultCurrency: 'USD' }, date: { today: '2026-04-26', year: '2026' }, ...overrides, }; } // ─── fillEoiFormFields ──────────────────────────────────────────────────────── describe('fillEoiFormFields', () => { it('populates every text field and checkbox using EoiContext', async () => { const sourcePdf = await buildSyntheticEoiPdf(); const filled = await fillEoiFormFields(sourcePdf, makeContext()); const out = await PDFDocument.load(filled); const form = out.getForm(); expect(form.getTextField('Name').getText()).toBe('Alice Smith'); expect(form.getTextField('Email').getText()).toBe('alice@example.com'); expect(form.getTextField('Address').getText()).toBe('123 Main St, Austin, USA'); expect(form.getTextField('Yacht Name').getText()).toBe('Sea Breeze'); expect(form.getTextField('Length').getText()).toBe('45'); expect(form.getTextField('Width').getText()).toBe('14'); expect(form.getTextField('Draft').getText()).toBe('6'); expect(form.getTextField('Berth Number').getText()).toBe('A-12'); expect(form.getCheckBox('Purchase').isChecked()).toBe(true); expect(form.getCheckBox('Lease_10').isChecked()).toBe(false); }); it('handles null primary email and null address gracefully', async () => { const sourcePdf = await buildSyntheticEoiPdf(); const filled = await fillEoiFormFields( sourcePdf, makeContext({ client: { fullName: 'Bob', nationality: null, primaryEmail: null, primaryPhone: null, address: null, }, }), ); const out = await PDFDocument.load(filled); const form = out.getForm(); expect(form.getTextField('Email').getText()).toBe(undefined); expect(form.getTextField('Address').getText()).toBe(undefined); expect(form.getTextField('Name').getText()).toBe('Bob'); }); it('leaves the form interactive (not flattened) so values can be edited', async () => { const sourcePdf = await buildSyntheticEoiPdf(); const filled = await fillEoiFormFields(sourcePdf, makeContext()); const out = await PDFDocument.load(filled); // Field still present and reachable as a TextField → not flattened. expect(() => out.getForm().getTextField('Name')).not.toThrow(); }); it('skips fields silently if the source PDF lacks them', async () => { // Build a PDF with only a subset of fields and ensure no error. const doc = await PDFDocument.create(); const page = doc.addPage([600, 800]); const form = doc.getForm(); const f = form.createTextField('Name'); f.addToPage(page, { x: 50, y: 700, width: 300, height: 24 }); const sparse = await doc.save(); await expect(fillEoiFormFields(sparse, makeContext())).resolves.toBeInstanceOf(Uint8Array); }); }); // ─── loadEoiTemplatePdf ─────────────────────────────────────────────────────── describe('loadEoiTemplatePdf', () => { let tmpFile: string; const originalEnv = process.env.EOI_TEMPLATE_PDF_PATH; beforeAll(async () => { const sourcePdf = await buildSyntheticEoiPdf(); tmpFile = path.join(os.tmpdir(), `eoi-template-${Date.now()}.pdf`); await fs.writeFile(tmpFile, sourcePdf); }); afterAll(async () => { if (originalEnv === undefined) delete process.env.EOI_TEMPLATE_PDF_PATH; else process.env.EOI_TEMPLATE_PDF_PATH = originalEnv; await fs.unlink(tmpFile).catch(() => undefined); }); it('reads the PDF from EOI_TEMPLATE_PDF_PATH override', async () => { process.env.EOI_TEMPLATE_PDF_PATH = tmpFile; const bytes = await loadEoiTemplatePdf(); expect(bytes.byteLength).toBeGreaterThan(100); // Round-trip: should re-load as a valid PDF. await expect(PDFDocument.load(bytes)).resolves.toBeInstanceOf(PDFDocument); }); it('throws a clear error with instructions when the file is missing', async () => { process.env.EOI_TEMPLATE_PDF_PATH = '/nope/does-not-exist.pdf'; await expect(loadEoiTemplatePdf()).rejects.toThrow(/EOI source PDF not found/); }); });