import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { MockAIService } from '../mock/MockAIService' import type { ParsedNeedCriteria } from '../../../domain/needBuilder' import type { TradeOffInput } from '../IAIService' // Fast-forward simulated delays so tests don't take seconds beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) async function run(promise: Promise): Promise { // Advance timers while waiting — handles setTimeout-based delays const p = promise vi.runAllTimers() return p } // ── generateFollowUpQuestions ───────────────────────────────────────────────── describe('MockAIService.generateFollowUpQuestions', () => { it('asks about assetType when missing', async () => { const criteria: ParsedNeedCriteria = {} const result = await run(MockAIService.generateFollowUpQuestions(criteria)) expect(result.data.some(q => q.targetField === 'assetType')).toBe(true) }) it('asks about areaRange when missing', async () => { const criteria: ParsedNeedCriteria = { assetType: 'OFFICE' } const result = await run(MockAIService.generateFollowUpQuestions(criteria)) expect(result.data.some(q => q.targetField === 'areaRange')).toBe(true) }) it('asks area ambiguity question when max/min > 8', async () => { const criteria: ParsedNeedCriteria = { assetType: 'OFFICE', areaRange: { min: 100, max: 1000 }, // ratio = 10 > 8 preferredLocations: ['Zürich'], } const result = await run(MockAIService.generateFollowUpQuestions(criteria)) const areaQ = result.data.find(q => q.targetField === 'areaRange') expect(areaQ).toBeDefined() expect(areaQ?.importance).toBe('required') expect(areaQ?.questionText).toContain('weit gefasst') }) it('treats zero min or max as ambiguous', async () => { const criteria: ParsedNeedCriteria = { assetType: 'OFFICE', areaRange: { min: 0, max: 500 }, preferredLocations: ['Zürich'], } const result = await run(MockAIService.generateFollowUpQuestions(criteria)) expect(result.data.some(q => q.targetField === 'areaRange')).toBe(true) }) it('does not ask about areaRange when ratio <= 8', async () => { const criteria: ParsedNeedCriteria = { assetType: 'OFFICE', areaRange: { min: 200, max: 600 }, // ratio = 3 preferredLocations: ['Zürich'], } const result = await run(MockAIService.generateFollowUpQuestions(criteria)) expect(result.data.some(q => q.targetField === 'areaRange')).toBe(false) }) it('returns at most 3 questions', async () => { const criteria: ParsedNeedCriteria = {} // everything missing const result = await run(MockAIService.generateFollowUpQuestions(criteria)) expect(result.data.length).toBeLessThanOrEqual(3) }) it('returns 0 questions when all fields are present', async () => { const criteria: ParsedNeedCriteria = { assetType: 'OFFICE', areaRange: { min: 300, max: 600 }, preferredLocations: ['Zürich'], budgetRange: { maxPerSqm: 400 }, timing: { earliestMoveIn: '2025-09-01' }, mustHaveCriteria: ['ÖV-Anbindung'], } const result = await run(MockAIService.generateFollowUpQuestions(criteria)) expect(result.data).toHaveLength(0) }) it('priorities required questions first', async () => { const criteria: ParsedNeedCriteria = { preferredLocations: [] } // missing assetType (required), budgetRange (recommended) const result = await run(MockAIService.generateFollowUpQuestions(criteria)) if (result.data.length >= 2) { expect(result.data[0].importance).toBe('required') } }) it('question ids are unique', async () => { const criteria: ParsedNeedCriteria = {} const result = await run(MockAIService.generateFollowUpQuestions(criteria)) const ids = result.data.map(q => q.id) expect(new Set(ids).size).toBe(ids.length) }) }) // ── provenance ──────────────────────────────────────────────────────────────── describe('MockAIService provenance', () => { it('returns mock provenance on generateFollowUpQuestions', async () => { const result = await run(MockAIService.generateFollowUpQuestions({})) expect(result.provenance.provider).toBe('mock') expect(result.provenance.source).toBe('mock') expect(result.provenance.fallbackUsed).toBe(false) expect(result.provenance.validationPassed).toBe(true) expect(typeof result.provenance.traceId).toBe('string') expect(typeof result.provenance.generatedAt).toBe('string') }) }) // ── summarizeTradeOffs ──────────────────────────────────────────────────────── describe('MockAIService.summarizeTradeOffs', () => { it('returns LOW risk when no HIGH severity trade-offs', async () => { const tradeoffs: TradeOffInput[] = [ { concern: 'Lage', severity: 'LOW', mitigation: 'Umgebung gut erschlossen' }, ] const result = await run(MockAIService.summarizeTradeOffs(tradeoffs)) expect(result.data.overallRisk).toBe('LOW') }) it('returns MEDIUM risk when exactly 1 HIGH severity trade-off', async () => { const tradeoffs: TradeOffInput[] = [ { concern: 'Fläche zu klein', severity: 'HIGH', mitigation: '' }, ] const result = await run(MockAIService.summarizeTradeOffs(tradeoffs)) expect(result.data.overallRisk).toBe('MEDIUM') }) it('returns HIGH risk when 2+ HIGH severity trade-offs', async () => { const tradeoffs: TradeOffInput[] = [ { concern: 'Fläche zu klein', severity: 'HIGH', mitigation: '' }, { concern: 'Budget weit überschritten', severity: 'HIGH', mitigation: '' }, ] const result = await run(MockAIService.summarizeTradeOffs(tradeoffs)) expect(result.data.overallRisk).toBe('HIGH') }) it('returns zero-trade-off headline when list is empty', async () => { const result = await run(MockAIService.summarizeTradeOffs([])) expect(result.data.headline).toContain('Keine') expect(result.data.items).toHaveLength(0) }) }) // ── generateMatchExplanation ────────────────────────────────────────────────── describe('MockAIService.generateMatchExplanation', () => { const base = { propertyId: 'prop-1', propertyTitle: 'Bürofläche Zollstrasse', propertyCity: 'Zürich', positiveFactors: [{ criterion: 'Lage', explanation: 'Zentrale Lage', weight: 0.3 }], negativeFactors: [{ criterion: 'Preis', explanation: 'Etwas über Budget', weight: 0.2 }], } it('generates STARK headline for score >= 78', async () => { const result = await run(MockAIService.generateMatchExplanation({ ...base, matchScore: 85 })) expect(result.data.headline.toLowerCase()).toContain('stark') }) it('generates MEDIUM headline for score 52–77', async () => { const result = await run(MockAIService.generateMatchExplanation({ ...base, matchScore: 65 })) expect(result.data.headline.toLowerCase()).toContain('gut') }) it('generates WEAK headline for score < 52', async () => { const result = await run(MockAIService.generateMatchExplanation({ ...base, matchScore: 40 })) expect(result.data.headline.toLowerCase()).toContain('schwach') }) it('includes property title in summary', async () => { const result = await run(MockAIService.generateMatchExplanation({ ...base, matchScore: 80 })) expect(result.data.summary).toContain('Bürofläche Zollstrasse') }) })