import 'package:flutter_test/flutter_test.dart'; import 'package:life_helper/domain/models/habit.dart'; import 'package:life_helper/domain/recommend/recommend_variant.dart'; HabitModel _habit(List variants) => HabitModel( id: 'hb_x', userId: 'u_local_default', type: HabitType.build, status: HabitStatus.active, title: 't', protocolId: 'p', frameLevel: FrameLevel.l2, frameFramedText: 't', startedAt: '2026-06-01', doseVariants: variants, ); HabitDoseVariantModel _v({ required String id, required int sort, bool isMin = false, List ctx = const [], List cond = const [], }) => HabitDoseVariantModel( variantId: id, habitId: 'hb_x', label: id, doseText: '1x', contextTags: ctx, conditionTags: cond, isMinimum: isMin, sortOrder: sort, ); void main() { test('empty variants → null', () { expect(recommendVariant(_habit(const []), const CheckInContext()), isNull); }); test('exact match wins (location + condition, score 4)', () { final habit = _habit([ _v(id: 'A', sort: 0, ctx: ['집']), _v(id: 'B', sort: 1, ctx: ['회사'], cond: ['좋음']), _v(id: 'C', sort: 2, isMin: true), ]); final pick = recommendVariant( habit, const CheckInContext(location: '회사', condition: '좋음')); expect(pick!.variant.variantId, 'B'); expect(pick.reason, RecommendReason.exactMatch); expect(pick.score, 4); }); test('partial match (only location)', () { final habit = _habit([ _v(id: 'A', sort: 0, ctx: ['집']), _v(id: 'B', sort: 1, ctx: ['회사']), ]); final pick = recommendVariant(habit, const CheckInContext(location: '집')); expect(pick!.variant.variantId, 'A'); expect(pick.reason, RecommendReason.partial); expect(pick.score, 2); }); test('fallback to minimum when nothing matches', () { final habit = _habit([ _v(id: 'A', sort: 0, ctx: ['집']), _v(id: 'B', sort: 1, isMin: true), ]); final pick = recommendVariant( habit, const CheckInContext(location: '카페', condition: '나쁨')); // is_minimum + condition=='나쁨' → score 1 → partial pick, not fallback. expect(pick!.variant.variantId, 'B'); expect(pick.reason, RecommendReason.partial); }); test('fallback to first by sortOrder when no minimum, no match', () { final habit = _habit([ _v(id: 'A', sort: 5, ctx: ['집']), _v(id: 'B', sort: 2, ctx: ['회사']), ]); final pick = recommendVariant(habit, const CheckInContext(location: '카페')); expect(pick!.variant.variantId, 'B'); expect(pick.reason, RecommendReason.fallbackFirst); expect(pick.score, 0); }); test('tie-break: same score → smaller sortOrder wins', () { final habit = _habit([ _v(id: 'A', sort: 10, ctx: ['집']), _v(id: 'B', sort: 1, ctx: ['집']), ]); final pick = recommendVariant(habit, const CheckInContext(location: '집')); expect(pick!.variant.variantId, 'B'); }); }