import 'package:flutter_test/flutter_test.dart'; import 'package:meshcore_open/helpers/reaction_helper.dart'; import 'package:meshcore_open/widgets/emoji_picker.dart'; void main() { group('ReactionHelper', () { group('reactionEmojis', () { test('should contain all emoji categories', () { final emojis = ReactionHelper.reactionEmojis; // Should contain quickEmojis for (final emoji in EmojiPicker.quickEmojis) { expect( emojis.contains(emoji), isTrue, reason: 'Missing quick emoji: $emoji', ); } // Should contain smileys for (final emoji in EmojiPicker.smileys) { expect( emojis.contains(emoji), isTrue, reason: 'Missing smiley: $emoji', ); } // Should contain gestures for (final emoji in EmojiPicker.gestures) { expect( emojis.contains(emoji), isTrue, reason: 'Missing gesture: $emoji', ); } // Should contain hearts for (final emoji in EmojiPicker.hearts) { expect( emojis.contains(emoji), isTrue, reason: 'Missing heart: $emoji', ); } // Should contain objects for (final emoji in EmojiPicker.objects) { expect( emojis.contains(emoji), isTrue, reason: 'Missing object: $emoji', ); } }); test('should fit in 1 byte (max 256 emojis)', () { expect(ReactionHelper.reactionEmojis.length, lessThanOrEqualTo(256)); }); }); group('emojiToIndex', () { test('should return 2-char hex for valid emoji', () { // First emoji (thumbs up) should be index 0 expect(ReactionHelper.emojiToIndex('👍'), equals('00')); // Second emoji (heart) should be index 1 expect(ReactionHelper.emojiToIndex('❤️'), equals('01')); }); test('should return null for unknown emoji', () { expect(ReactionHelper.emojiToIndex('🦄'), isNull); // Not in list expect(ReactionHelper.emojiToIndex('invalid'), isNull); expect(ReactionHelper.emojiToIndex(''), isNull); }); test('should return lowercase hex', () { final index = ReactionHelper.emojiToIndex('👍'); expect(index, matches(RegExp(r'^[0-9a-f]{2}$'))); }); }); group('indexToEmoji', () { test('should return emoji for valid index', () { expect(ReactionHelper.indexToEmoji('00'), equals('👍')); expect(ReactionHelper.indexToEmoji('01'), equals('❤️')); }); test('should return null for invalid index', () { expect( ReactionHelper.indexToEmoji('ff'), isNull, ); // Index 255, out of range expect(ReactionHelper.indexToEmoji('zz'), isNull); // Invalid hex expect(ReactionHelper.indexToEmoji(''), isNull); // Empty string // Note: indexToEmoji parses any valid hex; length validation is done by parseReaction's regex }); test('should handle case insensitivity', () { // Both uppercase and lowercase should work expect(ReactionHelper.indexToEmoji('0a'), isNotNull); expect(ReactionHelper.indexToEmoji('0A'), isNotNull); }); }); group('emoji round-trip', () { test('all emojis should round-trip correctly', () { for (int i = 0; i < ReactionHelper.reactionEmojis.length; i++) { final emoji = ReactionHelper.reactionEmojis[i]; final index = ReactionHelper.emojiToIndex(emoji); expect(index, isNotNull, reason: 'emojiToIndex failed for $emoji'); final decoded = ReactionHelper.indexToEmoji(index!); expect( decoded, equals(emoji), reason: 'Round-trip failed for $emoji (index $index)', ); } }); }); group('computeReactionHash', () { test('should return 4-char hex hash', () { final hash = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello world', ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); test('should be deterministic', () { final hash1 = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello', ); final hash2 = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello', ); expect(hash1, equals(hash2)); }); test('should differ for different inputs', () { final hash1 = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello', ); final hash2 = ReactionHelper.computeReactionHash( 1234567890, 'Bob', 'Hello', ); final hash3 = ReactionHelper.computeReactionHash( 1234567891, 'Alice', 'Hello', ); final hash4 = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'World', ); expect(hash1, isNot(equals(hash2))); // Different sender expect(hash1, isNot(equals(hash3))); // Different timestamp expect(hash1, isNot(equals(hash4))); // Different text }); test('should use first 5 chars of text', () { final hash1 = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello world', ); final hash2 = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello there', ); expect(hash1, equals(hash2)); // Same first 5 chars }); test('should handle short text', () { final hash = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hi', ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); test('should handle empty text', () { final hash = ReactionHelper.computeReactionHash( 1234567890, 'Alice', '', ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); }); group('computeReactionHash with null sender (1:1 chats)', () { test('should return 4-char hex hash', () { final hash = ReactionHelper.computeReactionHash( 1234567890, null, 'Hello world', ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); test('should be deterministic', () { final hash1 = ReactionHelper.computeReactionHash( 1234567890, null, 'Hello', ); final hash2 = ReactionHelper.computeReactionHash( 1234567890, null, 'Hello', ); expect(hash1, equals(hash2)); }); test('should differ for different inputs', () { final hash1 = ReactionHelper.computeReactionHash( 1234567890, null, 'Hello', ); final hash2 = ReactionHelper.computeReactionHash( 1234567891, null, 'Hello', ); final hash3 = ReactionHelper.computeReactionHash( 1234567890, null, 'World', ); expect(hash1, isNot(equals(hash2))); // Different timestamp expect(hash1, isNot(equals(hash3))); // Different text }); test('should differ from hash with sender name', () { // Null sender hash doesn't include sender, so should differ final nullSenderHash = ReactionHelper.computeReactionHash( 1234567890, null, 'Hello', ); final withSenderHash = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello', ); expect(nullSenderHash, isNot(equals(withSenderHash))); }); test('1:1 chat flow: sender and receiver compute same hash', () { // Alice sends "Hello" at timestamp 1234567890 // Bob receives it and wants to react // Bob computes hash the same way Alice's app will match it const timestamp = 1234567890; const messageText = 'Hello there!'; // Bob (sender of reaction) computes hash with null sender final bobHash = ReactionHelper.computeReactionHash( timestamp, null, messageText, ); // Alice (receiver of reaction) computes hash for her outgoing message final aliceHash = ReactionHelper.computeReactionHash( timestamp, null, messageText, ); expect(bobHash, equals(aliceHash)); }); }); group('parseReaction', () { test('should parse valid reaction format', () { final info = ReactionHelper.parseReaction('r:a1b2:00'); expect(info, isNotNull); expect(info!.targetHash, equals('a1b2')); expect(info.emoji, equals('👍')); }); test('should return null for invalid format', () { expect(ReactionHelper.parseReaction('invalid'), isNull); expect( ReactionHelper.parseReaction('r:abc:00'), isNull, ); // Hash too short expect( ReactionHelper.parseReaction('r:abcde:00'), isNull, ); // Hash too long expect( ReactionHelper.parseReaction('r:a1b2:0'), isNull, ); // Index too short expect( ReactionHelper.parseReaction('r:a1b2:000'), isNull, ); // Index too long expect( ReactionHelper.parseReaction('R:a1b2:00'), isNull, ); // Uppercase R expect( ReactionHelper.parseReaction('r:A1B2:00'), isNull, ); // Uppercase hash expect(ReactionHelper.parseReaction(''), isNull); }); test('should return null for invalid emoji index', () { // Index ff (255) is likely out of range expect(ReactionHelper.parseReaction('r:a1b2:ff'), isNull); }); test('should decode emoji correctly', () { // Encode thumbs up and verify decode final index = ReactionHelper.emojiToIndex('👍'); final info = ReactionHelper.parseReaction('r:dead:$index'); expect(info, isNotNull); expect(info!.emoji, equals('👍')); }); }); group('full reaction flow', () { test('should encode and decode reaction correctly', () { // Simulate sending a reaction const timestamp = 1234567890; const senderName = 'Alice'; const messageText = 'Hello world!'; const emoji = '🎉'; // Compute hash (sender side) final hash = ReactionHelper.computeReactionHash( timestamp, senderName, messageText, ); // Encode emoji (sender side) final emojiIndex = ReactionHelper.emojiToIndex(emoji); expect(emojiIndex, isNotNull); // Build reaction text (sender side) final reactionText = 'r:$hash:$emojiIndex'; // Parse reaction (receiver side) final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); expect(info!.targetHash, equals(hash)); expect(info.emoji, equals(emoji)); // Verify receiver can match the hash final receiverHash = ReactionHelper.computeReactionHash( timestamp, senderName, messageText, ); expect(receiverHash, equals(info.targetHash)); }); test('reaction text should be 9 bytes', () { final hash = ReactionHelper.computeReactionHash( 1234567890, 'Alice', 'Hello', ); final index = ReactionHelper.emojiToIndex('👍')!; final reactionText = 'r:$hash:$index'; // r: (2) + hash (4) + : (1) + index (2) = 9 bytes expect(reactionText.length, equals(9)); }); test('1:1 chat: Bob reacts to Alice message', () { // Alice sends "Hello" to Bob at timestamp 1234567890 const timestamp = 1234567890; const aliceName = 'Alice'; const messageText = 'Hello'; const emoji = '👍'; // On Bob's device: message.isOutgoing = false, so senderName = contact.name = Alice final bobSideHash = ReactionHelper.computeReactionHash( timestamp, aliceName, messageText, ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$bobSideHash:$emojiIndex'; // Alice receives the reaction final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); // On Alice's device: message.isOutgoing = true, so senderName = selfName = Alice final aliceSideHash = ReactionHelper.computeReactionHash( timestamp, aliceName, messageText, ); // Hashes should match! expect(info!.targetHash, equals(aliceSideHash)); expect(info.emoji, equals(emoji)); }); test('1:1 chat: Alice reacts to Bob message', () { // Bob sends "Hi there" to Alice at timestamp 9876543210 const timestamp = 9876543210; const bobName = 'Bob'; const messageText = 'Hi there'; const emoji = '❤️'; // On Alice's device: message.isOutgoing = false, so senderName = contact.name = Bob final aliceSideHash = ReactionHelper.computeReactionHash( timestamp, bobName, messageText, ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$aliceSideHash:$emojiIndex'; // Bob receives the reaction final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); // On Bob's device: message.isOutgoing = true, so senderName = selfName = Bob final bobSideHash = ReactionHelper.computeReactionHash( timestamp, bobName, messageText, ); // Hashes should match! expect(info!.targetHash, equals(bobSideHash)); expect(info.emoji, equals(emoji)); }); test('room server: user reacts to message from another user', () { // In a room server, Charlie sends "Hello room" at timestamp 1111111111 // Alice wants to react to it const timestamp = 1111111111; const charlieName = 'Charlie'; const messageText = 'Hello room'; const emoji = '🎉'; // Alice computes hash including sender name (room servers are multi-user) final aliceHash = ReactionHelper.computeReactionHash( timestamp, charlieName, messageText, ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$aliceHash:$emojiIndex'; // Verify format expect(reactionText.length, equals(9)); expect(reactionText, matches(RegExp(r'^r:[0-9a-f]{4}:[0-9a-f]{2}$'))); // Bob (another user in the room) receives the reaction final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); // Bob computes hash for Charlie's message the same way final bobHash = ReactionHelper.computeReactionHash( timestamp, charlieName, messageText, ); // Hashes should match! expect(info!.targetHash, equals(bobHash)); expect(info.emoji, equals(emoji)); }); test( 'room server: hash differs from 1:1 hash for same message content', () { // Same timestamp and text, but room server includes sender name const timestamp = 1234567890; const senderName = 'Dave'; const messageText = 'Hello'; // Room server hash (with sender name) final roomHash = ReactionHelper.computeReactionHash( timestamp, senderName, messageText, ); // 1:1 hash (without sender name) final directHash = ReactionHelper.computeReactionHash( timestamp, null, messageText, ); // They should be different! expect(roomHash, isNot(equals(directHash))); }, ); test('room server: different senders produce different hashes', () { // Two users send the exact same message at the same time in a room const timestamp = 1234567890; const messageText = 'Hello'; final aliceHash = ReactionHelper.computeReactionHash( timestamp, 'Alice', messageText, ); final bobHash = ReactionHelper.computeReactionHash( timestamp, 'Bob', messageText, ); // Different senders = different hashes (even with same content) expect(aliceHash, isNot(equals(bobHash))); }); test('room server: self message reaction works', () { // Alice sends "My message" at timestamp 2222222222 // Bob wants to react to it const timestamp = 2222222222; const aliceName = 'Alice'; const messageText = 'My message'; const emoji = '👍'; // Bob computes hash for Alice's message final bobHash = ReactionHelper.computeReactionHash( timestamp, aliceName, messageText, ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$bobHash:$emojiIndex'; // Alice receives the reaction and matches against her outgoing message final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); // Alice computes hash using her selfName final aliceHash = ReactionHelper.computeReactionHash( timestamp, aliceName, messageText, ); // Hashes should match! expect(info!.targetHash, equals(aliceHash)); }); test('channel: same logic as room server', () { // Channel messages also use sender name in hash const timestamp = 3333333333; const senderName = 'Eve'; const messageText = 'Channel msg'; const emoji = '🔥'; // Compute hash with sender name final hash = ReactionHelper.computeReactionHash( timestamp, senderName, messageText, ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$hash:$emojiIndex'; // Parse and verify final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); expect(info!.emoji, equals(emoji)); // Another user computes the same hash final otherUserHash = ReactionHelper.computeReactionHash( timestamp, senderName, messageText, ); expect(info.targetHash, equals(otherUserHash)); }); }); }); }