import 'package:flutter_test/flutter_test.dart'; import 'package:friendly_map/modules/app/app.dart'; import 'package:friendly_map/modules/events/events.dart'; import 'package:friendly_map/modules/kinship/kinship.dart'; import 'package:friendly_map/modules/platform/platform.dart'; import 'package:friendly_map/modules/transport/transport.dart'; import 'support/app_device.dart'; class FakeLog implements CallLogProvider { FakeLog(this.entries); final List entries; @override Future> forNumber(String number, {int limit = 200}) async => entries; } class FakePhone implements PhoneActions { final dialed = []; final texted = []; @override Future dial(String number) async { dialed.add(number); return true; } @override Future sms(String number) async { texted.add(number); return true; } } class FakeNotifier implements Notifier { List last = []; @override Future replaceAll(List notices) async => last = notices; } void main() { late AppDevice me; late FakeNotifier notifier; late FakePhone phone; setUp(() async { notifier = FakeNotifier(); phone = FakePhone(); me = await AppDevice.create(MemoryHub(), '010-1111-2222', '+821011112222', '나', notifier: notifier, phone: phone, callLog: FakeLog([ CallLogEntry('+821033334444', DateTime(2026, 10, 1, 19, 10), 600, CallKind.outgoing), CallLogEntry('+821033334444', DateTime(2026, 10, 9, 19, 30), 120, CallKind.incoming), CallLogEntry('+821033334444', DateTime(2026, 10, 12, 9, 0), 0, CallKind.missed), ])); }); group('촌수 알려주기', () { test('번호 없는 친척도 가계도에 넣고, 나와 내 자식 눈으로 본 관계를 알려 준다', () async { final k = me.app.kin; await k.setMe(gender: Gender.male, birthYear: 1990); final father = (await k.addRelative('아버지', gender: Gender.male, birthYear: 1960))!; final uncle = (await k.addRelative('큰아버지', gender: Gender.male, birthYear: 1955))!; final cousin = (await k.addRelative('사촌형', gender: Gender.male, birthYear: 1985))!; final gf = (await k.addRelative('할아버지', gender: Gender.male, birthYear: 1930))!; final son = (await k.addRelative('철수', gender: Gender.male, birthYear: 2020))!; expect(await k.setParent('me', father.id), true); expect(await k.setParent(father.id, gf.id), true); expect(await k.setParent(uncle.id, gf.id), true); expect(await k.setParent(cousin.id, uncle.id), true); expect(await k.setParent(son.id, 'me'), true); final r = await k.relationOf(cousin.id); expect(r.title, '친사촌 형'); expect(r.chon, 4); final views = await k.explain(cousin.id); expect(views.map((v) => v.whose).toList(), ['나', '철수']); expect(views[1].relation.title, '당숙'); expect(views[1].relation.chon, 5); final list = await k.everyone(); final chons = list.map((e) => e.relation.chon ?? 99).toList(); expect(chons.first, 1); // 가까운 촌수가 맨 앞 expect(chons, [...chons]..sort()); }); test('잘못된 번호는 거절, 같은 번호는 한 번만', () async { expect(await me.app.kin.addRelative('x', phone: '12'), isNull); expect(await me.app.kin.addRelative(' '), isNull); expect(await me.app.kin.addRelative('고모', phone: '010-5555-6666'), isNotNull); expect(await me.app.kin.addRelative('고모2', phone: '01055556666'), isNull); }); test('다시 열어도 가계도가 남아 있다', () async { final f = (await me.app.kin.addRelative('엄마', gender: Gender.female))!; await me.app.kin.setParent('me', f.id); expect((await me.app.kin.relationOf(f.id)).title, '어머니'); expect(await me.app.kin.inTree(f.id), true); await me.app.kin.exclude(f.id); expect((await me.app.kin.relationOf(f.id)).related, false); }); }); group('사람 정보와 통화', () { test('통화 요약: 횟수·시간·자주 받는 시간대, 바로 전화·문자', () async { final p = (await me.app.friends.add('+821033334444', '삼촌'))!; final s = await me.app.calls.summary(p.id); expect(s.total, 3); expect(s.missed, 1); expect(s.totalSeconds, 720); expect(s.windows.first.fromHour, 19); expect(await me.app.calls.call(p.id), true); expect(await me.app.calls.sms(p.id), true); expect(phone.dialed, ['+821033334444']); expect(phone.texted, ['+821033334444']); }); test('번호 없는 친척은 걸 수 없고 요약도 비어 있다', () async { final r = (await me.app.kin.addRelative('먼 친척'))!; expect(await me.app.calls.reachable(r.id), false); expect(await me.app.calls.call(r.id), false); expect((await me.app.calls.summary(r.id)).total, 0); }); test('생일·돌아가신 날 기록, 주제·메모', () async { final r = (await me.app.kin.addRelative('할머니'))!; await me.app.facts.save(r.id, const PersonFacts(birthMonth: 5, birthDay: 3, topics: ['안부', '제사'], memo: '저녁에 편함')); var f = await me.app.facts.get(r.id); expect(f.hasBirthday, true); expect(f.deceased, false); expect(f.topics, ['안부', '제사']); await me.app.facts.setDeceased(r.id, year: 2020, month: 2, day: 14); f = await me.app.facts.get(r.id); expect(f.deceased, true); expect(f.deceasedText, '2020.2.14'); expect(f.topics, ['안부', '제사']); // 다른 값은 그대로 await me.app.facts.setDeceased(r.id); // 해제 expect((await me.app.facts.get(r.id)).deceased, false); expect(() => me.app.facts.save(r.id, const PersonFacts(birthMonth: 2, birthDay: 30)), throwsArgumentError); expect(() => me.app.facts.save(r.id, const PersonFacts(birthMonth: 2)), throwsArgumentError); }); }); group('일정 알림', () { test('기본은 하루 전과 당일, 설정에 적은 날짜대로 예약이 바뀐다', () async { final r = (await me.app.kin.addRelative('엄마'))!; final soon = DateTime.now().add(const Duration(days: 10)); await me.app.facts.save(r.id, PersonFacts(birthMonth: soon.month, birthDay: soon.day)); expect(await me.app.events.leadDays(), [1, 0]); expect(await me.app.events.reschedule(), 2); expect(notifier.last.length, 2); expect(notifier.last.first.text, '내일 엄마 생일입니다'); await me.app.events.setLeadDays('1일 전, 2일 전, 7일 전'); expect(await me.app.events.leadDays(), [7, 2, 1]); expect(notifier.last.map((n) => n.text).toList(), ['7일 뒤 엄마 생일입니다', '2일 뒤 엄마 생일입니다', '내일 엄마 생일입니다']); }); test('돌아가신 분은 생일 대신 기일, 표시 설정', () async { final r = (await me.app.kin.addRelative('할아버지'))!; final soon = DateTime.now().add(const Duration(days: 3)); await me.app.facts.save( r.id, PersonFacts(birthMonth: 1, birthDay: 1, deceasedYear: 2015, deceasedMonth: soon.month, deceasedDay: soon.day)); final list = await me.app.events.forPerson(r.id); expect(list.length, 1); expect(list.first.upcoming.event.kind, EventKind.memorial); expect(list.first.text, '할아버지 기일'); expect(list.first.upcoming.years, soon.year - 2015); expect((await me.app.events.soon(withinDays: 5)).length, 1); expect(await me.app.events.showDeceased(), true); await me.app.events.setShowDeceased(false); expect(await me.app.events.showDeceased(), false); }); test('음력 기일: 음력 날짜가 양력으로 바뀌어 알림이 잡힌다', () async { final r = (await me.app.kin.addRelative('할아버지'))!; final target = DateTime.now().add(const Duration(days: 20)); final l = solarToLunar(target)!; await me.app.facts.setDeceased(r.id, year: 2015, month: l.month, day: l.day, lunar: true); final list = await me.app.events.forPerson(r.id, withinDays: 40); expect(list.length, 1); final d = list.first.upcoming.date; expect(DateTime(d.year, d.month, d.day).difference(DateTime(target.year, target.month, target.day)).inDays.abs() <= 1, true); expect(list.first.text, '할아버지 기일(음력)'); expect((await me.app.facts.get(r.id)).deceasedText.startsWith('음력 2015.'), true); expect(() => me.app.facts.save(r.id, const PersonFacts(birthMonth: 13, birthDay: 1, birthLunar: true)), throwsArgumentError); }); test('알림 부품이 없으면 예약 0건', () async { final bare = await AppDevice.create(MemoryHub(), '010-7777-8888', '+821077778888', '무'); expect(bare.app.events.canNotify, false); expect(await bare.app.events.reschedule(), 0); }); }); }