import 'dart:async'; import 'package:commons_core/commons_core.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/services/message_store.dart'; import 'package:tane/state/messages_cubit.dart'; import '../support/test_support.dart'; /// In-memory [MessageTransport]: records sends, lets a test push inbox messages. class FakeMessageTransport implements MessageTransport { final List<({String to, String text})> sent = []; final StreamController _inbox = StreamController.broadcast(); void receive(PrivateMessage message) => _inbox.add(message); @override Future send({required String toPubkey, required String text}) async => sent.add((to: toPubkey, text: text)); @override Stream inbox() => _inbox.stream; @override Future close() async => _inbox.close(); } /// A transport whose [send] always fails (e.g. no relay reachable). class _FailingSendTransport implements MessageTransport { @override Future send({required String toPubkey, required String text}) async => throw StateError('no relay'); @override Stream inbox() => const Stream.empty(); @override Future close() async {} } void main() { const peer = 'aa'; const me = 'bb'; PrivateMessage msg(String from, String text) => PrivateMessage(fromPubkey: from, text: text, at: DateTime(2026)); test('receives messages from the peer only', () async { final transport = FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me) ..start(); transport.receive(msg(peer, 'hola')); transport.receive(msg('cc', 'not for this chat')); // other conversation await pumpEventQueue(); expect(cubit.state.messages, hasLength(1)); expect(cubit.state.messages.single.text, 'hola'); expect(cubit.isMine(cubit.state.messages.single), isFalse); await cubit.close(); }); test('send delivers to the peer and appends our own message', () async { final transport = FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me); await cubit.send(' ¿cambiamos? '); expect(transport.sent, [(to: peer, text: '¿cambiamos?')]); expect(cubit.state.messages, hasLength(1)); expect(cubit.isMine(cubit.state.messages.single), isTrue); expect(cubit.state.sending, isFalse); await cubit.close(); }); test('a full exchange keeps order and sides', () async { final transport = FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me) ..start(); await cubit.send('hi'); transport.receive(msg(peer, 'hello')); await pumpEventQueue(); expect(cubit.state.messages.map((m) => m.text), ['hi', 'hello']); expect(cubit.state.messages.map(cubit.isMine), [true, false]); await cubit.close(); }); test( 'a failed send surfaces an error and drops the optimistic message', () async { final transport = _FailingSendTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me); await cubit.send('hi'); expect(cubit.state.error, isNotNull); expect(cubit.state.sending, isFalse); expect(cubit.state.messages, isEmpty); await cubit.close(); }, ); test('a message containing a link is not sent', () async { final transport = FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me); await cubit.send('check https://evil.example/login'); expect(transport.sent, isEmpty); expect(cubit.state.messages, isEmpty); await cubit.close(); }); test('empty/whitespace text is not sent', () async { final transport = FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me); await cubit.send(' '); expect(transport.sent, isEmpty); expect(cubit.state.messages, isEmpty); await cubit.close(); }); test( 'loads saved history on start and persists across a new cubit', () async { // History is ordered by each message's own timestamp, so use realistic // monotonic times: 'earlier' predates the sent 'hi' (stamped now() by the // cubit), and 'reply' comes after it. PrivateMessage at(String from, String text, DateTime when) => PrivateMessage(fromPubkey: from, text: text, at: when); final store = MessageStore(newTestChatDatabase()); await store.append( peer, at(peer, 'earlier', DateTime(2020)), ); // from a previous session final transport = FakeMessageTransport(); final cubit = MessagesCubit( transport, peerPubkey: peer, selfPubkey: me, store: store, ); await cubit.start(); expect(cubit.state.messages.map((m) => m.text), ['earlier']); await cubit.send('hi'); // persisted, stamped now() transport.receive(at(peer, 'reply', DateTime(2100))); // persisted, later await pumpEventQueue(); expect(cubit.state.messages.map((m) => m.text), [ 'earlier', 'hi', 'reply', ]); await cubit.close(); // A fresh cubit (even offline) sees the saved conversation. final reopened = MessagesCubit( null, peerPubkey: peer, selfPubkey: me, store: store, ); await reopened.start(); expect(reopened.state.messages.map((m) => m.text), [ 'earlier', 'hi', 'reply', ]); await reopened.close(); }, ); test( 'a re-delivered wrap is shown once (relay resends stored events)', () async { final transport = FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me) ..start(); final same = msg(peer, 'hola'); // identical sender+timestamp+text transport.receive(same); transport.receive(same); // relay re-delivery on the same subscription await pumpEventQueue(); expect(cubit.state.messages, hasLength(1)); await cubit.close(); }, ); test('history already surfaced live is not shown twice', () async { // The stored message is ALSO handed back by the live subscription on open. final store = MessageStore(newTestChatDatabase()); final m = msg(peer, 'hola'); await store.append(peer, m); final transport = FakeMessageTransport(); final cubit = MessagesCubit( transport, peerPubkey: peer, selfPubkey: me, store: store, ); unawaited(cubit.start()); transport.receive(m); // live redelivery races the history load await pumpEventQueue(); expect(cubit.state.messages.map((m) => m.text), ['hola']); await cubit.close(); }); test('offline (no transport) never throws', () async { final cubit = MessagesCubit(null, peerPubkey: peer, selfPubkey: me) ..start(); expect(cubit.isOnline, isFalse); await cubit.send('hi'); expect(cubit.state.messages, isEmpty); await cubit.close(); }); }