import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart'; import 'package:halkyon_learn_flutter/src/core/profile/public_person.dart'; import 'package:halkyon_learn_flutter/src/feature/discussion/data/discussion_gateway.dart'; import 'package:halkyon_learn_flutter/src/feature/discussion/model/discussion.dart'; /// The instance's discussions in memory, as the person [me] (the /// studyset's author when [author]) sees them; every question on a studyset /// titled [studysetTitle]. Records what was written. class FakeDiscussionGateway({ final PublicPerson me = const PublicPerson(handle: 'me'), final bool author = false, final String studysetTitle = 'Japanese', /// Why the person may not write; null when they may. var DiscussionRefusal? refusal, }) implements DiscussionGateway { /// Thrown by every call while set: the instance can't be reached. Exception? fail; /// The questions, by id, with what is in them. final questions = {}; /// The topics each question is within. final topicsOf = >{}; /// The discussions marked read, in order. final seen = []; /// The handles blocked. final blocked = []; var _next = 0; /// Adds a question by [by] about [target], with [answers]; returns it. Discussion addQuestion( String title, { String publicId = 'pub-1', PublicPerson by = const PublicPerson(handle: 'bob'), DiscussionTarget target = const DiscussionTarget( kind: DiscussionTargetKind.studyset, item: 'root', version: 1, ), Set topics = const {'root'}, List answers = const [], List comments = const [], String? acceptedAnswerId, String text = 'Why?', }) { final id = 'q${_next++}'; final discussion = Discussion( question: DiscussionPost( id: id, kind: DiscussionPostKind.question, createdAt: DateTime.utc(2026, 10, 15, 12), by: by, title: title, content: RichContent.plain(text), mine: by == me, ), publicId: publicId, studysetTitle: studysetTitle, location: 'halkyon.eu/@alice/japanese', target: target, answers: answers, comments: comments, acceptedAnswerId: acceptedAnswerId, asker: by == me, author: author, refusal: refusal, ); questions[id] = discussion; topicsOf[id] = topics; return discussion; } /// An answer by [by]. DiscussionPost answerBy( String text, { PublicPerson by = const PublicPerson(handle: 'carol'), String? id, }) => DiscussionPost( id: id ?? 'a${_next++}', kind: DiscussionPostKind.answer, createdAt: DateTime.utc(2026, 10, 15, 13), by: by, content: RichContent.plain(text), mine: by == me, ); void _failing() { if (fail case final error?) { throw error; } } void _writing() { _failing(); if (refusal case final refusal?) { throw DiscussionRefusedException(refusal); } } DiscussionSummary _summary(Discussion discussion) => DiscussionSummary( questionId: discussion.question.id, publicId: discussion.publicId, studysetTitle: discussion.studysetTitle, title: discussion.question.title, asker: discussion.question.by, askerIsAuthor: discussion.question.fromAuthor, target: discussion.target, createdAt: discussion.question.createdAt, activeAt: discussion.question.createdAt, answers: discussion.answers.length, accepted: discussion.acceptedAnswerId != null, mine: discussion.asker, unseen: !seen.contains(discussion.question.id), ); @override Future> list( String publicId, { String? topic, DiscussionTarget? item, String? query, int page = 0, }) async { _failing(); return [ for (final MapEntry(key: id, value: discussion) in questions.entries) if (discussion.publicId == publicId && (topic == null || topicsOf[id]!.contains(topic)) && (item == null || discussion.target == item) && (query == null || discussion.question.title.toLowerCase().contains( query.toLowerCase(), ))) _summary(discussion), ].reversed.toList(); } @override Future read(String questionId) async { _failing(); return questions[questionId] ?? (throw DiscussionMissing()); } @override Future askRefusal(String publicId) async { _failing(); return refusal; } @override Future ask( String publicId, DiscussionTarget target, String title, RichContent content, ) async { _writing(); return addQuestion( title, publicId: publicId, by: me, target: target, topics: {target.item}, text: content.plainText, ).question.id; } @override Future answer(String questionId, RichContent content) async { _writing(); final discussion = questions[questionId] ?? (throw DiscussionMissing()); final answer = DiscussionPost( id: 'a${_next++}', kind: DiscussionPostKind.answer, createdAt: DateTime.utc(2026, 10, 15, 14), by: me, fromAuthor: author, content: content, mine: true, ); questions[questionId] = _with( discussion, answers: [ ...discussion.answers, answer, ], ); return answer.id; } @override Future comment(String onId, String text) async { _writing(); final MapEntry(key: id, value: discussion) = questions.entries.firstWhere( (entry) => entry.key == onId || entry.value.answers.any((answer) => answer.id == onId), orElse: () => throw DiscussionMissing(), ); final comment = DiscussionPost( id: 'c${_next++}', kind: DiscussionPostKind.comment, createdAt: DateTime.utc(2026, 10, 15, 15), parentId: onId, by: me, text: text, mine: true, ); questions[id] = _with( discussion, comments: [ ...discussion.comments, comment, ], ); return comment.id; } @override Future edit(String postId, RichContent content, {String? title}) async { _writing(); } @override Future editComment(String commentId, String text) async { _writing(); } @override Future delete(String postId) async { _failing(); questions.remove(postId); } @override Future accept(String questionId, String? answerId) async { _failing(); final discussion = questions[questionId] ?? (throw DiscussionMissing()); questions[questionId] = _with( discussion, acceptedAnswerId: answerId, unaccept: answerId == null, answers: [ ...discussion.answers.where((answer) => answer.id == answerId), ...discussion.answers.where((answer) => answer.id != answerId), ], ); } @override Future hide(String postId, {required bool hidden}) async { _failing(); } @override Future> inbox() async { _failing(); return [ for (final discussion in questions.values) if (discussion.asker || discussion.author) _summary(discussion), ]; } @override Future unseen() async => (await inbox()).where((summary) => summary.unseen).length; @override Future markSeen(String questionId) async { _failing(); seen.add(questionId); } @override Future block(String handle) async { _failing(); blocked.add(handle); } Discussion _with( Discussion discussion, { List? answers, List? comments, String? acceptedAnswerId, bool unaccept = false, }) => Discussion( question: discussion.question, publicId: discussion.publicId, studysetTitle: discussion.studysetTitle, location: discussion.location, target: discussion.target, answers: answers ?? discussion.answers, comments: comments ?? discussion.comments, acceptedAnswerId: unaccept ? null : acceptedAnswerId ?? discussion.acceptedAnswerId, asker: discussion.asker, author: discussion.author, refusal: discussion.refusal, ); }