import { describe, it, expect } from 'vitest' import { matchStrengthFromScore, rankMatches, buildFullMatch } from '../rankingEngine' import { MatchStrength, ResultType } from '../../../domain/enums' import { makeNeed, makeProperty } from './fixtures' // ── matchStrengthFromScore ──────────────────────────────────────────────────── describe('matchStrengthFromScore', () => { it('classifies score >= 78 as STRONG', () => { expect(matchStrengthFromScore(78)).toBe(MatchStrength.STRONG) expect(matchStrengthFromScore(100)).toBe(MatchStrength.STRONG) expect(matchStrengthFromScore(90)).toBe(MatchStrength.STRONG) }) it('classifies score 52–77 as MODERATE', () => { expect(matchStrengthFromScore(77)).toBe(MatchStrength.MODERATE) expect(matchStrengthFromScore(52)).toBe(MatchStrength.MODERATE) expect(matchStrengthFromScore(65)).toBe(MatchStrength.MODERATE) }) it('classifies score < 52 as WEAK', () => { expect(matchStrengthFromScore(51)).toBe(MatchStrength.WEAK) expect(matchStrengthFromScore(0)).toBe(MatchStrength.WEAK) expect(matchStrengthFromScore(30)).toBe(MatchStrength.WEAK) }) }) // ── rankMatches ─────────────────────────────────────────────────────────────── describe('rankMatches', () => { const need = makeNeed() it('sorts matches by matchScore descending', () => { const high = buildFullMatch(need, makeProperty({ id: 'high', rentPricePerSqm: 40 })) const low = buildFullMatch(need, makeProperty({ id: 'low', rentPricePerSqm: 65, location: { city: 'Basel', country: 'CH' } })) const ranked = rankMatches([low, high]) expect(ranked[0].matchScore).toBeGreaterThanOrEqual(ranked[1].matchScore) expect(ranked[0].propertyId).toBe('high') }) it('breaks ties by result type: VERIFIED_PORTFOLIO and MAISON_WORK ranked above FUTURE_AVAILABILITY', () => { // Build one of each type but force identical score by using same property body const prop = makeProperty() const verified = buildFullMatch(need, { ...prop, id: 'v', resultType: ResultType.VERIFIED_PORTFOLIO }) const future = buildFullMatch(need, { ...prop, id: 'f', resultType: ResultType.FUTURE_AVAILABILITY, confidenceScore: 0.85 }) // Force same score for a clean tiebreak test verified.matchScore = 75 future.matchScore = 75 const ranked = rankMatches([future, verified]) expect(ranked[0].resultType).toBe(ResultType.VERIFIED_PORTFOLIO) }) it('breaks ties among same type by confidence descending', () => { const highConf = buildFullMatch(need, makeProperty({ id: 'hc', confidenceScore: 0.90 })) const lowConf = buildFullMatch(need, makeProperty({ id: 'lc', confidenceScore: 0.60 })) // Force same score and same type highConf.matchScore = 75 lowConf.matchScore = 75 const ranked = rankMatches([lowConf, highConf]) expect(ranked[0].confidenceLevel).toBeGreaterThan(ranked[1].confidenceLevel) }) }) // ── buildFullMatch ──────────────────────────────────────────────────────────── describe('buildFullMatch', () => { it('returns a Match with all required explainability fields', () => { const match = buildFullMatch(makeNeed(), makeProperty()) expect(match.id).toContain('match-') expect(match.scoreBreakdown).toBeDefined() expect(match.scoreBreakdown.hardMatchScore).toBeGreaterThan(0) expect(match.scoreBreakdown.softFactorScore).toBeGreaterThan(0) expect(match.scoreBreakdown.totalScore).toBe(match.matchScore) expect(match.matchStrength).toBeDefined() expect(match.explainabilitySummary).toBeTruthy() }) it('generates shortlist and contact actions for a strong match', () => { const match = buildFullMatch(makeNeed(), makeProperty()) // Perfect fixture should score >= 78 (STRONG) and generate SHORTLIST + CONTACT actions if (match.matchScore >= 78) { const types = match.nextBestActions?.map(a => a.actionType) ?? [] expect(types).toContain('SHORTLIST') expect(types).toContain('CONTACT') } }) it('adds a SCHEDULE action for future availability properties', () => { const prop = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY }) const match = buildFullMatch(makeNeed(), prop) const types = match.nextBestActions?.map(a => a.actionType) ?? [] expect(types).toContain('SCHEDULE') }) it('uncertainty indicators include a note for future availability', () => { const prop = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY }) const match = buildFullMatch(makeNeed(), prop) expect(match.uncertaintyIndicators?.some(s => s.includes('Signal'))).toBe(true) }) })