import { describe, expect, it } from 'vitest' import { createMunicipalityProfile, createMunicipalityContact, deleteMunicipalityContact, fetchAvailableJurisdictions, fetchMunicipalityContacts, fetchMunicipalityProfiles, fetchPriorCycleDefaults, MunicipalityContactValidationError, MunicipalityValidationError, updateMunicipalityContact, updateMunicipalityProfile, type MunicipalityContact, type MunicipalityProfile, type PriorCycleDefaults, } from './municipalityContracts' const makeProfile = (overrides: Partial = {}): MunicipalityProfile => ({ profileId: 'abc123', jCode: 'FAIR01', displayName: 'Fairview Borough', updatedAt: '2026-05-06T12:00:00Z', updatedBy: 'user@test.com', legacyName: 'Fairview Borough', legacyMailingAddress: '100 Main St', legacyCityStateZip: 'Fairview, PA 16415', ...overrides, }) const makeContact = (overrides: Partial = {}): MunicipalityContact => ({ contactId: 'contact-1', profileId: 'abc123', contactType: 'Primary', name: 'Ada Clerk', roleTitle: 'Town Clerk', phone: '555-0100', email: 'ada@example.test', createdAt: '2026-05-07T12:00:00Z', createdBy: 'creator@example.test', updatedAt: '2026-05-07T12:00:00Z', updatedBy: 'actor@example.test', ...overrides, }) const makeDefaults = (overrides: Partial = {}): PriorCycleDefaults => ({ profileId: 'abc123', hasPriorCycles: true, selectedCycleId: 'cycle-2026-primary', emptyStateMessage: '', cycles: [ { cycleId: 'cycle-2026-primary', cycleName: '2026 Primary', completedAt: '2026-05-01T00:00:00Z', services: [ { serviceType: 'Addressing', summary: 'Standard addressing run', values: { Quantity: '1200', Tracking: 'Yes' }, }, ], }, ], ...overrides, }) // ── fetchMunicipalityProfiles ───────────────────────────────────────────────── describe('fetchMunicipalityProfiles', () => { it('returns profiles on 200', async () => { const stub = async () => new Response(JSON.stringify([makeProfile()]), { status: 200 }) const result = await fetchMunicipalityProfiles(stub) expect(result).toHaveLength(1) expect(result[0].jCode).toBe('FAIR01') expect(result[0].legacyName).toBe('Fairview Borough') }) it('throws on non-200', async () => { const stub = async () => new Response('{}', { status: 500 }) await expect(fetchMunicipalityProfiles(stub)).rejects.toThrow('500') }) }) // -- municipality contacts --------------------------------------------------- describe('municipality contact contracts', () => { it('fetchMunicipalityContacts returns primary and secondary contacts on 200', async () => { const stub = async () => new Response(JSON.stringify([ makeContact({ contactType: 'Primary', name: 'Main Clerk' }), makeContact({ contactId: 'contact-2', contactType: 'Secondary', name: 'Backup Clerk' }), ]), { status: 200 }) const result = await fetchMunicipalityContacts('abc123', stub) expect(result.map((c) => c.contactType)).toEqual(['Primary', 'Secondary']) expect(result[0].name).toBe('Main Clerk') }) it('createMunicipalityContact posts required and optional fields', async () => { let postedBody: unknown const stub = async (_input: RequestInfo | URL, init?: RequestInit) => { postedBody = JSON.parse(String(init?.body)) return new Response(JSON.stringify(makeContact()), { status: 201 }) } const result = await createMunicipalityContact( 'abc123', { contactType: 'Primary', name: 'Ada Clerk', roleTitle: 'Town Clerk', phone: '555-0100', email: 'ada@example.test', }, stub, ) expect(result.contactType).toBe('Primary') expect(postedBody).toEqual({ contactType: 'Primary', name: 'Ada Clerk', roleTitle: 'Town Clerk', phone: '555-0100', email: 'ada@example.test', }) }) it('updateMunicipalityContact throws validation error for 422', async () => { const stub = async () => new Response(JSON.stringify({ error: 'Name is required.' }), { status: 422 }) await expect( updateMunicipalityContact('abc123', 'contact-1', { contactType: 'Primary', name: '' }, stub), ).rejects.toSatisfy( (e) => e instanceof MunicipalityContactValidationError && e.message.includes('Name'), ) }) it('deleteMunicipalityContact succeeds on 204 and throws on failure', async () => { const okStub = async () => new Response(null, { status: 204 }) const failStub = async () => new Response('{}', { status: 500 }) await expect(deleteMunicipalityContact('abc123', 'contact-1', okStub)).resolves.toBeUndefined() await expect(deleteMunicipalityContact('abc123', 'contact-1', failStub)).rejects.toThrow('500') }) }) describe('fetchPriorCycleDefaults', () => { it('returns read-only prior cycle defaults on 200', async () => { const stub = async () => new Response(JSON.stringify(makeDefaults()), { status: 200 }) const result = await fetchPriorCycleDefaults('abc123', stub) expect(result.hasPriorCycles).toBe(true) expect(result.selectedCycleId).toBe('cycle-2026-primary') expect(result.cycles[0].services[0].values.Quantity).toBe('1200') }) it('returns empty state payload when no prior cycles exist', async () => { const stub = async () => new Response(JSON.stringify(makeDefaults({ hasPriorCycles: false, selectedCycleId: null, emptyStateMessage: 'No prior cycle defaults available.', cycles: [], })), { status: 200 }) const result = await fetchPriorCycleDefaults('abc123', stub) expect(result.hasPriorCycles).toBe(false) expect(result.emptyStateMessage).toContain('No prior cycle') expect(result.cycles).toEqual([]) }) it('throws on non-200', async () => { const stub = async () => new Response('{}', { status: 404 }) await expect(fetchPriorCycleDefaults('missing', stub)).rejects.toThrow('404') }) }) // ── createMunicipalityProfile ───────────────────────────────────────────────── describe('createMunicipalityProfile', () => { it('returns profile on 200', async () => { const stub = async () => new Response(JSON.stringify(makeProfile()), { status: 200 }) const result = await createMunicipalityProfile('FAIR01', 'Fairview', stub) expect(result.profileId).toBe('abc123') expect(result.jCode).toBe('FAIR01') }) it('throws MunicipalityValidationError on 422 with descriptive message', async () => { const stub = async () => new Response(JSON.stringify({ error: "No legacy jurisdiction found for JCode 'NOPE'." }), { status: 422, }) await expect(createMunicipalityProfile('NOPE', null, stub)).rejects.toSatisfy( (e) => e instanceof MunicipalityValidationError && e.message.includes('NOPE'), ) }) it('throws generic Error on other non-200 status', async () => { const stub = async () => new Response('{}', { status: 500 }) await expect(createMunicipalityProfile('FAIR01', null, stub)).rejects.toThrow('500') await expect(createMunicipalityProfile('FAIR01', null, stub)).rejects.not.toSatisfy( (e) => e instanceof MunicipalityValidationError, ) }) }) // ── updateMunicipalityProfile ───────────────────────────────────────────────── describe('updateMunicipalityProfile', () => { it('returns updated profile on 200', async () => { const stub = async () => new Response(JSON.stringify(makeProfile({ displayName: 'New Name' })), { status: 200 }) const result = await updateMunicipalityProfile('abc123', 'New Name', stub) expect(result.displayName).toBe('New Name') }) it('throws MunicipalityValidationError on 422', async () => { const stub = async () => new Response(JSON.stringify({ error: 'Profile not found.' }), { status: 422 }) await expect(updateMunicipalityProfile('ghost', 'X', stub)).rejects.toSatisfy( (e) => e instanceof MunicipalityValidationError, ) }) }) // ── fetchAvailableJurisdictions ─────────────────────────────────────────────── describe('fetchAvailableJurisdictions', () => { it('returns jurisdictions on 200', async () => { const stub = async () => new Response( JSON.stringify([ { jCode: 'FAIR01', name: 'Fairview Borough' }, { jCode: 'LAKE02', name: null }, ]), { status: 200 }, ) const result = await fetchAvailableJurisdictions(stub) expect(result).toHaveLength(2) expect(result[0].jCode).toBe('FAIR01') expect(result[0].name).toBe('Fairview Borough') expect(result[1].name).toBeNull() }) it('throws on non-200', async () => { const stub = async () => new Response('{}', { status: 503 }) await expect(fetchAvailableJurisdictions(stub)).rejects.toThrow('503') }) }) // ── MunicipalityValidationError ─────────────────────────────────────────────── describe('MunicipalityValidationError', () => { it('has correct name and message', () => { const err = new MunicipalityValidationError('JCode not found') expect(err.name).toBe('MunicipalityValidationError') expect(err.message).toBe('JCode not found') expect(err).toBeInstanceOf(Error) }) })