import { test } from 'node:test' import assert from 'node:assert/strict' import { createInitialPedigreeState, buildPedigreePayload, serializeManualAncestor, parseManualAncestor, formatManualAncestorDisplay, formatManualAncestorDetails, pedigreeStateFromScan, uncertainScanSlots, } from './pedigree.js' test('manual ancestor serialize/parse round-trips', () => { const json = serializeManualAncestor({ name: 'Old Bob', farm: 'Sunnyside', ear_number: 'B-12', weight_lbs: 4.5, dob: 'Unknown', }) const parsed = parseManualAncestor(json) assert.equal(parsed.name, 'Old Bob') assert.equal(parsed.farm, 'Sunnyside') assert.equal(parsed.ear_number, 'B-12') assert.equal(parsed.weight_lbs, 4.5) assert.equal(parsed.dob, 'Unknown') }) test('parseManualAncestor accepts legacy plain-text names', () => { const parsed = parseManualAncestor('Just A Name') assert.equal(parsed.name, 'Just A Name') assert.equal(parsed.farm, '') assert.equal(parsed.ear_number, '') }) test('formatManualAncestorDisplay composes name, farm, ear number', () => { const json = serializeManualAncestor({ name: 'Old Bob', farm: 'Sunnyside', ear_number: 'B-12' }) assert.equal(formatManualAncestorDisplay(json), 'Old Bob (Sunnyside) • Ear #B-12') assert.equal(formatManualAncestorDisplay(serializeManualAncestor({ name: 'Solo' })), 'Solo') }) test('buildPedigreePayload: empty tree -> all null', () => { const payload = buildPedigreePayload(createInitialPedigreeState(), new Map()) assert.equal(payload.sire_id, null) assert.equal(payload.dam_id, null) assert.equal(payload.paternal_grandsire_name_text, null) }) test('buildPedigreePayload: a linked parent writes the FK and locks (nulls) its ancestors', () => { const state = createInitialPedigreeState() state.sire = { mode: 'linked', id: 5, name: 'Jimmy' } // Cascade cache holds Jimmy's own (empty) ancestry subtree. const cache = new Map([[5, { id: 5, name: 'Jimmy', sire: null, dam: null }]]) const payload = buildPedigreePayload(state, cache) assert.equal(payload.sire_id, 5) assert.equal(payload.sire_name_text, null) // Gen2 paternal slots are chain-derived from the linked sire, so they must // stay null on the subject rather than being written as freeform text. assert.equal(payload.paternal_grandsire_name_text, null) assert.equal(payload.paternal_granddam_name_text, null) }) test('buildPedigreePayload: a manual parent leaves deeper slots editable and persisted', () => { const state = createInitialPedigreeState() state.sire = { mode: 'manual', id: null, name: 'Bob' } state.paternal_grandsire = { mode: 'manual', id: null, name: 'Old Bob' } const payload = buildPedigreePayload(state, new Map()) assert.equal(payload.sire_id, null) assert.equal(payload.sire_name_text, 'Bob') assert.equal(payload.paternal_grandsire_name_text, 'Old Bob') }) test('pedigreeStateFromScan links matched parents and makes named ancestors manual', () => { const state = pedigreeStateFromScan({ ancestors: { sire: { name: 'Hoppin-pop', farm: "Peachy Buns'", ear_number: 'HP1', dob: null, weight_lbs: null, uncertain: [] }, dam: { name: 'Black Betty', farm: "Cody's", ear_number: 'BB2', dob: '2024-03-04', weight_lbs: 4.25, uncertain: ['dob'] }, paternal_grandsire: { name: null, farm: null, ear_number: null, dob: null, weight_lbs: null, uncertain: [] }, }, matches: { sire: { id: 7, name: 'Hoppin-pop' }, dam: null }, }) assert.deepEqual(state.sire, { mode: 'linked', id: 7, name: 'Hoppin-pop' }) assert.equal(state.dam.mode, 'manual') const dam = parseManualAncestor(state.dam.name) assert.equal(dam.name, 'Black Betty') assert.equal(dam.farm, "Cody's") assert.equal(dam.dob, '2024-03-04') assert.equal(dam.weight_lbs, 4.25) // Unnamed or missing ancestors stay at the empty default. assert.equal(state.paternal_grandsire.mode, 'unknown') assert.equal(state.mm_granddam.mode, 'unknown') }) test('uncertainScanSlots skips matched parents and certain ancestors', () => { const slots = uncertainScanSlots({ ancestors: { sire: { name: 'A', uncertain: ['ear_number'] }, dam: { name: 'B', uncertain: ['dob'] }, maternal_grandsire: { name: 'C', uncertain: [] }, }, matches: { sire: { id: 1, name: 'A' } }, }) assert.deepEqual([...slots], ['dam']) }) test('manual ancestors default to 3 lbs and Unknown DOB, including legacy 0-weight entries', () => { const fresh = parseManualAncestor(serializeManualAncestor({ name: 'No Info', farm: 'X', ear_number: 'A1' })) assert.equal(fresh.weight_lbs, 3) assert.equal(fresh.dob, 'Unknown') const legacy = parseManualAncestor(JSON.stringify({ name: 'Old', farm: '', ear_number: '', weight_lbs: 0, dob: '' })) assert.equal(legacy.weight_lbs, 3) assert.equal(legacy.dob, 'Unknown') assert.equal(parseManualAncestor('Plain Name').weight_lbs, 3) const real = parseManualAncestor(serializeManualAncestor({ name: 'Real', weight_lbs: '4.5', dob: '2024-03-04' })) assert.equal(real.weight_lbs, 4.5) assert.equal(real.dob, '2024-03-04') }) test('formatManualAncestorDetails shows weight and DOB', () => { const fmt = (w) => `${w} lbs` assert.equal(formatManualAncestorDetails(serializeManualAncestor({ name: 'A' }), fmt), '3 lbs · DOB Unknown') assert.equal( formatManualAncestorDetails(serializeManualAncestor({ name: 'B', weight_lbs: 5, dob: '2024-03-04' }), fmt), '5 lbs · DOB 3/4/2024' ) })