import { describe, expect, it } from 'vitest' import { creatableImportRows, buildNextEditedRow, canEditResultRow, canEditReviewRow, canRetryResultRow, hasExactSourceRows, initialisePreviewRows, mergeImportResults, mergeValidatedImportRow, mergeValidatedImportRows, processableImportRows, replaceImportRow, resultRepairMode, resultRowMessages, resultSummaryCounts, retryImportRow, reviewSummaryCounts } from '@/lib/postImportSession' import { buildPostImportRow } from '@/test/postImportFactories' describe ('post import row state', () => { it ('separates processable existing rows from creatable rows', () => { const ready = buildPostImportRow ({ sourceRow: 1 }) const existing = buildPostImportRow ({ sourceRow: 2, skipReason: 'existing', existingPostId: 20 }) const invalid = buildPostImportRow ({ sourceRow: 3, status: 'error', validationErrors: { url: ['invalid'] } }) const created = buildPostImportRow ({ sourceRow: 4, importStatus: 'created', createdPostId: 40 }) const rows = [ready, existing, invalid, created] expect (processableImportRows (rows)).toEqual ([ready, existing]) expect (creatableImportRows (rows)).toEqual ([ready]) expect (reviewSummaryCounts (rows)).toEqual ({ total: 4, submittable: 1, skipPlanned: 1 }) }) it ('preserves terminal rows while merging validation results', () => { const created = buildPostImportRow ({ sourceRow: 1, importStatus: 'created', createdPostId: 10, attributes: { title: 'created title' } }) const pending = buildPostImportRow ({ sourceRow: 2, fieldWarnings: { title: ['old warning'] }, provenance: { title: 'manual' }, attributes: { title: 'manual title' } }) const validated = [ buildPostImportRow ({ sourceRow: 1, attributes: { title: 'changed' } }), buildPostImportRow ({ sourceRow: 2, fieldWarnings: { title: ['fetch warning'], tags: ['tag warning'] }, provenance: { title: 'manual' }, attributes: { title: 'manual title' } })] const result = mergeValidatedImportRows ([created, pending], validated) expect (result[0]).toBe (created) expect (result[1]?.fieldWarnings).toEqual ({ tags: ['tag warning'] }) }) it ('updates the reset snapshot only after metadata URL changes', () => { const current = buildPostImportRow ({ attributes: { title: 'manual title' }, metadataUrl: 'https://example.com/old' }) const validated = buildPostImportRow ({ attributes: { title: 'new metadata title' }, metadataUrl: 'https://example.com/new', fieldWarnings: { title: ['warning'] }, baseWarnings: ['base warning'] }) const result = mergeValidatedImportRows ([current], [validated])[0] expect (result?.resetSnapshot).toMatchObject ({ attributes: { title: 'new metadata title' }, fieldWarnings: { title: ['warning'] }, baseWarnings: ['base warning'], metadataUrl: 'https://example.com/new' }) }) it ('merges result states and clears incompatible post identifiers', () => { const created = mergeImportResults ([buildPostImportRow ({ skipReason: 'existing', existingPostId: 2, recoverable: true, importStatus: 'pending' })], [{ sourceRow: 1, status: 'created', post: { id: 3 } }])[0] const skipped = mergeImportResults ([buildPostImportRow ({ createdPostId: 3, importStatus: 'created', recoverable: true })], [{ sourceRow: 1, status: 'skipped', existingPostId: 4 }])[0] const failed = mergeImportResults ([buildPostImportRow ({ skipReason: 'existing', existingPostId: 4 })], [{ sourceRow: 1, status: 'failed', recoverable: true, errors: { base: ['failure'] } }])[0] expect (created).toMatchObject ({ importStatus: 'created', createdPostId: 3, existingPostId: undefined, recoverable: undefined, skipReason: undefined }) expect (skipped).toMatchObject ({ importStatus: 'skipped', existingPostId: 4, createdPostId: undefined, recoverable: undefined, skipReason: 'existing' }) expect (failed).toMatchObject ({ importStatus: 'failed', recoverable: true, createdPostId: undefined, existingPostId: undefined, skipReason: undefined, importErrors: { base: ['failure'] } }) }) it ('retries only the selected failed row and counts results exclusively', () => { const rows = [ buildPostImportRow ({ sourceRow: 1, importStatus: 'created', createdPostId: 1 }), buildPostImportRow ({ sourceRow: 2, importStatus: 'skipped', existingPostId: 2, skipReason: 'existing' }), buildPostImportRow ({ sourceRow: 3, importStatus: 'failed', recoverable: true, importErrors: { base: ['failed'] } }), buildPostImportRow ({ sourceRow: 4, importStatus: 'pending', recoverable: true, validationErrors: { base: ['failed'] } })] expect (resultSummaryCounts (rows)).toEqual ({ created: 1, skipped: 1, failed: 2 }) expect (retryImportRow (rows, 3)[2]).toMatchObject ({ importStatus: 'pending', importErrors: undefined }) expect (retryImportRow (rows, 4)[3]).toBe (rows[3]) }) it ('deduplicates messages and keeps repair mode only for repairable rows', () => { const repairable = buildPostImportRow ({ sourceRow: 1, importStatus: 'pending', validationErrors: { title: ['invalid'], base: ['duplicate'] }, importErrors: { base: ['duplicate'], url: ['network'] } }) const complete = buildPostImportRow ({ sourceRow: 2, importStatus: 'created', createdPostId: 2 }) expect (resultRowMessages (repairable)).toEqual (['invalid', 'duplicate', 'network']) expect (resultRepairMode ([repairable, complete])).toBe ('failed') expect (resultRepairMode ([complete])).toBe ('all') }) it ('classifies editable and retryable rows by terminal and recoverable state', () => { const ready = buildPostImportRow () const skipped = buildPostImportRow ({ importStatus: 'skipped', skipReason: 'existing', existingPostId: 2 }) const hardFailed = buildPostImportRow ({ importStatus: 'failed', importErrors: { base: ['failed'] } }) const pendingInvalid = buildPostImportRow ({ importStatus: 'pending', recoverable: true, validationErrors: { title: ['invalid'] } }) const pendingValid = buildPostImportRow ({ importStatus: 'pending', recoverable: true }) expect (canEditReviewRow (ready)).toBe (true) expect (canEditReviewRow (skipped)).toBe (false) expect (canEditReviewRow (hardFailed)).toBe (false) expect (canEditResultRow (pendingInvalid)).toBe (true) expect (canRetryResultRow (pendingInvalid)).toBe (false) expect (canRetryResultRow (pendingValid)).toBe (true) }) it ('detects missing, duplicate, and extra source rows exactly', () => { expect (hasExactSourceRows ([1, 2], [{ sourceRow: 1 }, { sourceRow: 2 }])).toBe (true) expect (hasExactSourceRows ([1, 2], [{ sourceRow: 1 }])).toBe (false) expect (hasExactSourceRows ([1, 2], [{ sourceRow: 1 }, { sourceRow: 1 }])).toBe (false) expect (hasExactSourceRows ([1, 2], [{ sourceRow: 1 }, { sourceRow: 3 }])).toBe (false) }) it ('merges only the validated source row and preserves other row edits', () => { const edited = buildPostImportRow ({ sourceRow: 1, attributes: { title: 'edited row' }, provenance: { title: 'manual' } }) const other = buildPostImportRow ({ sourceRow: 2, attributes: { title: 'keep me' }, provenance: { title: 'manual' } }) const validated = buildPostImportRow ({ sourceRow: 1, attributes: { title: 'validated row' } }) const result = mergeValidatedImportRow ([edited, other], validated) expect (result[0]?.attributes.title).toBe ('validated row') expect (result[1]?.attributes.title).toBe ('keep me') }) it ('replaces only the targeted source row and preserves the others', () => { const original = buildPostImportRow ({ sourceRow: 1, importStatus: 'failed', recoverable: true, importErrors: { base: ['failed'] } }) const other = buildPostImportRow ({ sourceRow: 2, attributes: { title: 'keep edited row' } }) const restored = buildPostImportRow ({ sourceRow: 1, importStatus: 'failed', recoverable: true, importErrors: { base: ['failed'] } }) const result = replaceImportRow ([original, other], restored) expect (result[0]).toEqual (restored) expect (result[1]?.attributes.title).toBe ('keep edited row') }) it ('copies reset snapshot values instead of sharing mutable records', () => { const row = buildPostImportRow ({ fieldWarnings: { title: ['warning'] } }) const initialised = initialisePreviewRows ([row])[0] expect (initialised).toBeDefined () if (initialised == null) return initialised.attributes.title = 'changed' initialised.fieldWarnings.title?.push ('another') expect (initialised.resetSnapshot.attributes.title).toBe ('') expect (initialised.resetSnapshot.fieldWarnings.title).toEqual (['warning']) }) it ('builds the next edited row with shared repair semantics', () => { const row = buildPostImportRow ({ url: 'https://example.com/original', importStatus: 'failed', recoverable: true, importErrors: { base: ['failed'] }, attributes: { title: 'old title', tags: 'old-tag', duration: '2' }, provenance: { title: 'automatic', tags: 'automatic', url: 'manual' }, tagSources: { automatic: 'old-tag', manual: '' } }) const nextRow = buildNextEditedRow ( row, { url: 'https://example.com/edited', title: 'edited title', thumbnailBase: '', originalCreatedFrom: '', originalCreatedBefore: '', tags: 'edited-tag', parentPostIds: '' }, true) expect (nextRow.importStatus).toBe ('pending') expect (nextRow.importErrors).toBeUndefined () expect (nextRow.url).toBe ('https://example.com/edited') expect (nextRow.attributes.title).toBe ('edited title') expect (nextRow.attributes.duration).toBe ('2') expect (nextRow.attributes.tags).toBe ('edited-tag') expect (nextRow.provenance.title).toBe ('manual') expect (nextRow.provenance.url).toBe ('manual') expect (nextRow.tagSources?.manual).toBe ('edited-tag') }) })