import 'package:flutter_test/flutter_test.dart'; import 'package:votianlt_app/services/ack_tracker.dart'; void main() { group('AckTracker', () { late AckTracker tracker; late List> retryCalls; late List> timeoutCalls; setUp(() { retryCalls = []; timeoutCalls = []; tracker = AckTracker( maxRetries: 4, onRetry: (topic, payload) async { retryCalls.add({'topic': topic, 'payload': payload}); return true; }, onTimeout: (messageId, topic) { timeoutCalls.add({'messageId': messageId, 'topic': topic}); }, ); }); group('track', () { test('adds message to pending', () { tracker.track('msg-1', '/server/user/message', '{"data": "test"}'); expect(tracker.isPending('msg-1'), true); expect(tracker.pendingCount, 1); }); test('can track multiple messages', () { tracker.track('msg-1', '/topic1', 'payload1'); tracker.track('msg-2', '/topic2', 'payload2'); tracker.track('msg-3', '/topic3', 'payload3'); expect(tracker.pendingCount, 3); expect(tracker.isPending('msg-1'), true); expect(tracker.isPending('msg-2'), true); expect(tracker.isPending('msg-3'), true); }); test('stores correct topic and payload', () { tracker.track('msg-1', '/server/user/message', '{"key": "value"}'); final pending = tracker.getPendingMessage('msg-1'); expect(pending, isNotNull); expect(pending!.topic, '/server/user/message'); expect(pending.jsonPayload, '{"key": "value"}'); expect(pending.retryCount, 0); }); test('overwrites existing message with same ID', () { tracker.track('msg-1', '/old/topic', 'old payload'); tracker.track('msg-1', '/new/topic', 'new payload'); final pending = tracker.getPendingMessage('msg-1'); expect(pending!.topic, '/new/topic'); expect(pending.jsonPayload, 'new payload'); expect(tracker.pendingCount, 1); }); }); group('acknowledge', () { test('removes message from pending', () { tracker.track('msg-1', '/topic', 'payload'); expect(tracker.isPending('msg-1'), true); tracker.acknowledge('msg-1'); expect(tracker.isPending('msg-1'), false); expect(tracker.pendingCount, 0); }); test('does nothing for unknown message ID', () { tracker.track('msg-1', '/topic', 'payload'); tracker.acknowledge('unknown-msg'); expect(tracker.pendingCount, 1); expect(tracker.isPending('msg-1'), true); }); test('only removes specified message', () { tracker.track('msg-1', '/topic1', 'payload1'); tracker.track('msg-2', '/topic2', 'payload2'); tracker.acknowledge('msg-1'); expect(tracker.isPending('msg-1'), false); expect(tracker.isPending('msg-2'), true); expect(tracker.pendingCount, 1); }); }); group('isPending', () { test('returns true for tracked message', () { tracker.track('msg-1', '/topic', 'payload'); expect(tracker.isPending('msg-1'), true); }); test('returns false for untracked message', () { expect(tracker.isPending('unknown'), false); }); test('returns false after acknowledge', () { tracker.track('msg-1', '/topic', 'payload'); tracker.acknowledge('msg-1'); expect(tracker.isPending('msg-1'), false); }); }); group('pendingMessageIds', () { test('returns empty list when no messages pending', () { expect(tracker.pendingMessageIds, isEmpty); }); test('returns all pending message IDs', () { tracker.track('msg-1', '/topic1', 'payload1'); tracker.track('msg-2', '/topic2', 'payload2'); final ids = tracker.pendingMessageIds; expect(ids, containsAll(['msg-1', 'msg-2'])); expect(ids.length, 2); }); test('returns unmodifiable list', () { tracker.track('msg-1', '/topic', 'payload'); final ids = tracker.pendingMessageIds; expect(() => ids.add('new-id'), throwsUnsupportedError); }); }); group('processRetries', () { test('increments retryCount on each call', () async { tracker.track('msg-1', '/topic', 'payload'); await tracker.processRetries(); expect(tracker.getPendingMessage('msg-1')!.retryCount, 1); await tracker.processRetries(); expect(tracker.getPendingMessage('msg-1')!.retryCount, 2); await tracker.processRetries(); expect(tracker.getPendingMessage('msg-1')!.retryCount, 3); }); test('calls onRetry callback with correct parameters', () async { tracker.track('msg-1', '/server/user/message', '{"data": "test"}'); await tracker.processRetries(); expect(retryCalls.length, 1); expect(retryCalls[0]['topic'], '/server/user/message'); expect(retryCalls[0]['payload'], '{"data": "test"}'); }); test('calls onRetry for each pending message', () async { tracker.track('msg-1', '/topic1', 'payload1'); tracker.track('msg-2', '/topic2', 'payload2'); await tracker.processRetries(); expect(retryCalls.length, 2); }); test('calls onTimeout after maxRetries exceeded', () async { tracker.track('msg-1', '/timeout/topic', 'payload'); // Process until maxRetries reached for (var i = 0; i < 4; i++) { await tracker.processRetries(); } expect(timeoutCalls, isEmpty); expect(tracker.isPending('msg-1'), true); // One more retry should trigger timeout await tracker.processRetries(); expect(timeoutCalls.length, 1); expect(timeoutCalls[0]['messageId'], 'msg-1'); expect(timeoutCalls[0]['topic'], '/timeout/topic'); }); test('removes message after timeout', () async { tracker.track('msg-1', '/topic', 'payload'); // Process until timeout for (var i = 0; i <= 4; i++) { await tracker.processRetries(); } expect(tracker.isPending('msg-1'), false); expect(tracker.pendingCount, 0); }); test('does not retry when isConnected is false', () async { tracker.track('msg-1', '/topic', 'payload'); await tracker.processRetries(isConnected: false); expect(retryCalls, isEmpty); expect(tracker.getPendingMessage('msg-1')!.retryCount, 0); }); test('still times out when disconnected after max retries', () async { tracker.track('msg-1', '/topic', 'payload'); // Manually set retry count to max (simulating previous retries) final pending = tracker.getPendingMessage('msg-1')!; pending.retryCount = 4; await tracker.processRetries(isConnected: false); expect(timeoutCalls.length, 1); expect(tracker.isPending('msg-1'), false); }); test('does nothing when no messages pending', () async { await tracker.processRetries(); expect(retryCalls, isEmpty); expect(timeoutCalls, isEmpty); }); }); group('clearAll', () { test('removes all pending messages', () { tracker.track('msg-1', '/topic1', 'payload1'); tracker.track('msg-2', '/topic2', 'payload2'); tracker.clearAll(); expect(tracker.pendingCount, 0); expect(tracker.isPending('msg-1'), false); expect(tracker.isPending('msg-2'), false); }); }); group('clearForTopic', () { test('removes only messages for matching topic', () { tracker.track('msg-1', '/server/login', 'login1'); tracker.track('msg-2', '/server/login', 'login2'); tracker.track('msg-3', '/server/user/message', 'message'); tracker.clearForTopic('/server/login'); expect(tracker.isPending('msg-1'), false); expect(tracker.isPending('msg-2'), false); expect(tracker.isPending('msg-3'), true); expect(tracker.pendingCount, 1); }); test('does nothing when no matching topic', () { tracker.track('msg-1', '/server/user/message', 'payload'); tracker.clearForTopic('/server/login'); expect(tracker.pendingCount, 1); expect(tracker.isPending('msg-1'), true); }); }); group('without callbacks', () { test('processRetries works without onRetry callback', () async { final noCallbackTracker = AckTracker(maxRetries: 2); noCallbackTracker.track('msg-1', '/topic', 'payload'); // Should not throw await noCallbackTracker.processRetries(); expect(noCallbackTracker.getPendingMessage('msg-1')!.retryCount, 1); }); test('processRetries works without onTimeout callback', () async { final noCallbackTracker = AckTracker(maxRetries: 1); noCallbackTracker.track('msg-1', '/topic', 'payload'); // Process until timeout await noCallbackTracker.processRetries(); await noCallbackTracker.processRetries(); // Should be removed even without callback expect(noCallbackTracker.isPending('msg-1'), false); }); }); group('PendingMessage', () { test('stores sentAt timestamp', () { final before = DateTime.now(); tracker.track('msg-1', '/topic', 'payload'); final after = DateTime.now(); final pending = tracker.getPendingMessage('msg-1')!; expect(pending.sentAt.isAfter(before) || pending.sentAt == before, true); expect(pending.sentAt.isBefore(after) || pending.sentAt == after, true); }); test('initializes retryCount to 0', () { tracker.track('msg-1', '/topic', 'payload'); expect(tracker.getPendingMessage('msg-1')!.retryCount, 0); }); }); }); }