import { describe, it, expect } from 'vitest' import { buildMatchCardViewModel } from '../matchCardAdapter' import { buildFullMatch } from '../rankingEngine' import { ResultType } from '../../../domain/enums' import type { VerifiedPortfolioResult, FutureAvailabilityResult } from '../../../domain/unifiedResult' import type { FutureSignal } from '../../../domain/futureSignal' import { makeNeed, makeProperty } from './fixtures' // ── buildMatchCardViewModel — verified portfolio ────────────────────────────── describe('buildMatchCardViewModel — VERIFIED_PORTFOLIO', () => { const need = makeNeed() const property = makeProperty() const match = buildFullMatch(need, property) const result: VerifiedPortfolioResult = { matchId: 'vm-test-1', needId: need.id, matchScore: match.matchScore, resultType: 'VERIFIED_PORTFOLIO', property, match, } it('sets resultType correctly', () => { const vm = buildMatchCardViewModel(result, []) expect(vm.resultType).toBe('VERIFIED_PORTFOLIO') }) it('uses property title as the card title', () => { const vm = buildMatchCardViewModel(result, []) expect(vm.title).toBe(property.title) }) it('builds locationLabel from city (and district when present)', () => { const vm = buildMatchCardViewModel(result, []) expect(vm.locationLabel).toContain('Zürich') }) it('propagates matchScore from the result', () => { const vm = buildMatchCardViewModel(result, []) expect(vm.matchScore).toBe(match.matchScore) }) it('scoreBreakdown reflects hard and soft components from the engine', () => { const vm = buildMatchCardViewModel(result, []) expect(vm.scoreBreakdown!.hardMatchScore).toBe(match.scoreBreakdown.hardMatchScore) expect(vm.scoreBreakdown!.softFactorScore).toBe(match.scoreBreakdown.softFactorScore) expect(vm.scoreBreakdown!.totalScore).toBe(match.scoreBreakdown.totalScore) }) it('does NOT set a disclaimer for verified portfolio results', () => { const vm = buildMatchCardViewModel(result, []) expect(vm.disclaimer).toBeUndefined() }) it('returns reasons derived from top positive factors', () => { const vm = buildMatchCardViewModel(result, []) // A high-scoring match should have at least one reason expect(vm.reasons.length).toBeGreaterThan(0) vm.reasons.forEach(r => { expect(r.type).toMatch(/HARD_FACT|SOFT_FACTOR/) expect(r.score).toBeGreaterThanOrEqual(0) }) }) it('passes through supplied actions unchanged', () => { const actions = [{ id: 'shortlist', label: 'Shortlist', actionType: 'SAVE_SHORTLIST' as const, primary: true, onClick: () => {} }] const vm = buildMatchCardViewModel(result, actions) expect(vm.actions).toBe(actions) }) }) // ── buildMatchCardViewModel — future availability ───────────────────────────── describe('buildMatchCardViewModel — FUTURE_AVAILABILITY', () => { const need = makeNeed() const futureProperty = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY }) const match = buildFullMatch(need, futureProperty) const minimalSignal = { id: 'sig-1', probability: 0.72, locationHint: 'Zürich Oerlikon', isVerified: false, source: { type: 'AI_SIGNAL', credibility: 0.7 }, } as unknown as FutureSignal const futureResult: FutureAvailabilityResult = { matchId: 'future-test-1', needId: need.id, matchScore: match.matchScore, resultType: 'FUTURE_AVAILABILITY', signal: minimalSignal, property: futureProperty, match, } it('sets resultType to FUTURE_AVAILABILITY', () => { const vm = buildMatchCardViewModel(futureResult, []) expect(vm.resultType).toBe('FUTURE_AVAILABILITY') }) it('always includes a disclaimer for future signals', () => { const vm = buildMatchCardViewModel(futureResult, []) expect(vm.disclaimer).toBeTruthy() expect(typeof vm.disclaimer).toBe('string') }) it('exposes signal probability in the view model', () => { const vm = buildMatchCardViewModel(futureResult, []) expect(vm.signalProbability).toBe(0.72) }) it('classifies signal quality as HIGH when probability >= 0.70', () => { const vm = buildMatchCardViewModel(futureResult, []) expect(vm.signalQuality).toBe('HIGH') }) })