import 'package:flutter_test/flutter_test.dart'; import 'package:votianlt_app/services/message_handler.dart'; void main() { group('MessageHandler', () { late MessageHandler handler; late List ackCallbackCalls; setUp(() { ackCallbackCalls = []; handler = MessageHandler( maxProcessedIds: 100, onAckRequired: (messageId) => ackCallbackCalls.add(messageId), ); }); group('isEnvelopeMessage', () { test('returns true for valid envelope with all required fields', () { final data = { 'messageId': 'test-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'key': 'value'}, }; expect(handler.isEnvelopeMessage(data), true); }); test('returns false when messageId is missing', () { final data = { 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'key': 'value'}, }; expect(handler.isEnvelopeMessage(data), false); }); test('returns false when timestamp is missing', () { final data = { 'messageId': 'test-id', 'topic': '/server/user/message', 'payload': {'key': 'value'}, }; expect(handler.isEnvelopeMessage(data), false); }); test('returns false when topic is missing', () { final data = { 'messageId': 'test-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'payload': {'key': 'value'}, }; expect(handler.isEnvelopeMessage(data), false); }); test('returns false when payload is missing', () { final data = { 'messageId': 'test-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', }; expect(handler.isEnvelopeMessage(data), false); }); test('returns false for non-map data', () { expect(handler.isEnvelopeMessage('string'), false); expect(handler.isEnvelopeMessage(123), false); expect(handler.isEnvelopeMessage(['list']), false); expect(handler.isEnvelopeMessage(null), false); }); test('returns true even if payload is null', () { final data = { 'messageId': 'test-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': null, }; expect(handler.isEnvelopeMessage(data), true); }); }); group('unwrapEnvelope', () { test('extracts payload from valid envelope', () { final data = { 'messageId': 'test-id-123', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'content': 'Hello'}, 'requiresAck': true, }; final result = handler.unwrapEnvelope(data); expect(result, isNotNull); expect(result!.payload, {'content': 'Hello'}); expect(result.messageId, 'test-id-123'); expect(result.requiresAck, true); }); test('returns non-envelope data as-is', () { final plainData = {'content': 'Hello', 'sender': 'user1'}; final result = handler.unwrapEnvelope(plainData); expect(result, isNotNull); expect(result!.payload, plainData); expect(result.messageId, isNull); expect(result.requiresAck, false); }); test('returns null for duplicate messageId', () { final data = { 'messageId': 'duplicate-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'content': 'First'}, 'requiresAck': false, }; // First call should succeed final firstResult = handler.unwrapEnvelope(data); expect(firstResult, isNotNull); // Second call with same messageId should return null final secondResult = handler.unwrapEnvelope(data); expect(secondResult, isNull); }); test('calls onAckRequired for duplicate when original required ACK', () { final data = { 'messageId': 'ack-duplicate-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'content': 'Message'}, 'requiresAck': true, }; // First call handler.unwrapEnvelope(data); expect(ackCallbackCalls, isEmpty); // No ACK callback on first process // Second call (duplicate) - should trigger ACK handler.unwrapEnvelope(data); expect(ackCallbackCalls, ['ack-duplicate-id']); }); test('does not call onAckRequired for duplicate when requiresAck is false', () { final data = { 'messageId': 'no-ack-duplicate', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'content': 'Message'}, 'requiresAck': false, }; handler.unwrapEnvelope(data); handler.unwrapEnvelope(data); expect(ackCallbackCalls, isEmpty); }); test('defaults requiresAck to true when not specified', () { final data = { 'messageId': 'default-ack-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/message', 'payload': {'content': 'Message'}, // requiresAck not specified }; final result = handler.unwrapEnvelope(data); expect(result!.requiresAck, true); }); test('handles list payload', () { final data = { 'messageId': 'list-payload-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/jobs', 'payload': [ {'id': '1'}, {'id': '2'} ], }; final result = handler.unwrapEnvelope(data); expect(result!.payload, isList); expect(result.payload.length, 2); }); test('handles null payload', () { final data = { 'messageId': 'null-payload-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/server/user/ping', 'payload': null, }; final result = handler.unwrapEnvelope(data); expect(result!.payload, isNull); }); }); group('deduplication memory management', () { test('respects maxProcessedIds limit', () { final smallHandler = MessageHandler(maxProcessedIds: 3); // Add 4 messages for (var i = 1; i <= 4; i++) { smallHandler.unwrapEnvelope({ 'messageId': 'msg-$i', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, }); } // Should only have 3 IDs tracked expect(smallHandler.processedCount, 3); // First message should have been evicted (FIFO) expect(smallHandler.wasProcessed('msg-1'), false); expect(smallHandler.wasProcessed('msg-2'), true); expect(smallHandler.wasProcessed('msg-3'), true); expect(smallHandler.wasProcessed('msg-4'), true); }); test('allows reprocessing after eviction', () { final smallHandler = MessageHandler(maxProcessedIds: 2); // Process msg-1 final first = smallHandler.unwrapEnvelope({ 'messageId': 'msg-1', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': {'first': true}, }); expect(first, isNotNull); // Process msg-2 and msg-3 to evict msg-1 smallHandler.unwrapEnvelope({ 'messageId': 'msg-2', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, }); smallHandler.unwrapEnvelope({ 'messageId': 'msg-3', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, }); // msg-1 should be processable again final reprocessed = smallHandler.unwrapEnvelope({ 'messageId': 'msg-1', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': {'reprocessed': true}, }); expect(reprocessed, isNotNull); expect(reprocessed!.payload, {'reprocessed': true}); }); }); group('wasProcessed', () { test('returns true for processed message', () { handler.unwrapEnvelope({ 'messageId': 'processed-id', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, }); expect(handler.wasProcessed('processed-id'), true); }); test('returns false for unprocessed message', () { expect(handler.wasProcessed('unknown-id'), false); }); }); group('clearProcessedIds', () { test('removes all tracked message IDs', () { handler.unwrapEnvelope({ 'messageId': 'msg-1', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, }); handler.unwrapEnvelope({ 'messageId': 'msg-2', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, }); expect(handler.processedCount, 2); handler.clearProcessedIds(); expect(handler.processedCount, 0); expect(handler.wasProcessed('msg-1'), false); expect(handler.wasProcessed('msg-2'), false); }); test('allows reprocessing cleared messages', () { final data = { 'messageId': 'cleared-msg', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': {'value': 1}, }; handler.unwrapEnvelope(data); handler.clearProcessedIds(); final result = handler.unwrapEnvelope(data); expect(result, isNotNull); }); }); group('without onAckRequired callback', () { test('handles duplicate gracefully when no callback set', () { final noCallbackHandler = MessageHandler(); final data = { 'messageId': 'no-callback-msg', 'timestamp': '2024-01-15T10:30:00.000Z', 'topic': '/test', 'payload': null, 'requiresAck': true, }; noCallbackHandler.unwrapEnvelope(data); // Should not throw when processing duplicate expect(() => noCallbackHandler.unwrapEnvelope(data), returnsNormally); }); }); }); }