import 'package:halkyon_interop/halkyon_interop.dart'; import 'package:test/test.dart'; void main() { group('TaskShape', () { final fixedTime = DateTime.utc(2026, 1, 15, 12); final deepLink = Uri.parse('halkyon-learn://review/spanish-vocab'); TaskShape build({ String sourceAppId = 'eu.halkyon.learn', String externalId = 'review-12', String title = 'Review 12 cards', String? subtitle = 'Spanish vocabulary', DateTime? dueAt, Uri? linkOverride, }) => TaskShape( sourceAppId: sourceAppId, externalId: externalId, title: title, subtitle: subtitle, dueAt: dueAt ?? fixedTime, deepLink: linkOverride ?? deepLink, ); test('exposes provided fields', () { final task = build(); expect(task.sourceAppId, 'eu.halkyon.learn'); expect(task.externalId, 'review-12'); expect(task.title, 'Review 12 cards'); expect(task.subtitle, 'Spanish vocabulary'); expect(task.dueAt, fixedTime); expect(task.deepLink, deepLink); }); test('allows null for optional fields', () { final task = TaskShape( sourceAppId: 'eu.halkyon.learn', externalId: 'review-12', title: 'Review', deepLink: deepLink, ); expect(task.subtitle, isNull); expect(task.dueAt, isNull); }); test('two instances with identical fields are equal', () { expect(build(), equals(build())); expect(build().hashCode, equals(build().hashCode)); }); test('differs on any field change', () { expect(build(), isNot(equals(build(title: 'Different')))); expect(build(), isNot(equals(build(externalId: 'different-id')))); expect(build(), isNot(equals(build(sourceAppId: 'eu.halkyon.todo')))); expect(build(), isNot(equals(build(dueAt: DateTime.utc(2026, 2))))); expect( build(), isNot(equals(build(linkOverride: Uri.parse('halkyon-todo://x')))), ); }); test('toString includes identifying fields', () { final str = build().toString(); expect(str, contains('eu.halkyon.learn')); expect(str, contains('review-12')); expect(str, contains('Review 12 cards')); }); }); group('HalkyonEvent / ReviewDueEvent', () { final fixedTime = DateTime.utc(2026, 1, 15, 12); test('ReviewDueEvent is a HalkyonEvent', () { final event = ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 12, ); expect(event, isA()); }); test('exposes provided fields', () { final event = ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 12, ); expect(event.occurredAt, fixedTime); expect(event.studysetId, 'spanish-vocab'); expect(event.dueCount, 12); }); test('events with identical fields are equal', () { final a = ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 12, ); final b = ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 12, ); expect(a, equals(b)); expect(a.hashCode, equals(b.hashCode)); }); test('differs on field changes', () { final base = ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 12, ); expect( base, isNot( equals( ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 13, ), ), ), ); expect( base, isNot( equals( ReviewDueEvent( occurredAt: fixedTime, studysetId: 'french-vocab', dueCount: 12, ), ), ), ); }); test('switch on sealed type is exhaustive', () { final HalkyonEvent event = ReviewDueEvent( occurredAt: fixedTime, studysetId: 'spanish-vocab', dueCount: 12, ); // If a new HalkyonEvent subtype is added without updating this switch, // the analyzer/compiler will flag it. That's the value of `sealed`. final description = switch (event) { ReviewDueEvent(:final studysetId, :final dueCount) => '$dueCount cards due in $studysetId', }; expect(description, '12 cards due in spanish-vocab'); }); }); group('DormantPublisher', () { test('accepts publish calls without throwing', () async { const publisher = DormantPublisher(); final event = ReviewDueEvent( occurredAt: DateTime.utc(2026, 1, 15), studysetId: 'spanish-vocab', dueCount: 5, ); await expectLater(publisher.publish(event), completes); }); test('implements EventPublisher', () { const publisher = DormantPublisher(); expect(publisher, isA()); }); }); }