import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/push/chat_thread_store.dart';

ThreadMessage _msg(int nid, {String sender = 'Max', String text = 'hi'}) =>
    ThreadMessage(nid: nid, sender: sender, text: text, timestampMs: nid);

void main() {
  group('appendThreadMessage', () {
    test('appends in order', () {
      var messages = <ThreadMessage>[];
      messages = appendThreadMessage(messages, _msg(1));
      messages = appendThreadMessage(messages, _msg(2));
      expect(messages.map((m) => m.nid), [1, 2]);
    });

    test('caps the history, dropping the oldest', () {
      var messages = <ThreadMessage>[];
      for (var nid = 1; nid <= kChatThreadCap + 3; nid++) {
        messages = appendThreadMessage(messages, _msg(nid));
      }
      expect(messages, hasLength(kChatThreadCap));
      expect(messages.first.nid, 4);
      expect(messages.last.nid, kChatThreadCap + 3);
    });

    test('a redelivered nid replaces the old entry instead of duplicating', () {
      var messages = [_msg(1), _msg(2)];
      messages = appendThreadMessage(messages, _msg(1, text: 'edited'));
      expect(messages.map((m) => m.nid), [2, 1]);
      expect(messages.last.text, 'edited');
    });
  });

  group('threadAfterIncoming (dismiss/read reset)', () {
    final existing = [_msg(1), _msg(2)];
    final incoming = _msg(3);

    test('active notification → new message stacks onto history', () {
      final result = threadAfterIncoming(existing, incoming, true);
      expect(result.map((m) => m.nid), [1, 2, 3]);
    });

    test('inactive notification → thread restarts with only the new one', () {
      final result = threadAfterIncoming(existing, incoming, false);
      expect(result.map((m) => m.nid), [3]);
    });

    test('unknown active state (probe failed) → stacks defensively', () {
      final result = threadAfterIncoming(existing, incoming, null);
      expect(result.map((m) => m.nid), [1, 2, 3]);
    });

    test('reset still honours the cap for a burst of messages', () {
      final result = threadAfterIncoming(existing, incoming, false, cap: 1);
      expect(result.map((m) => m.nid), [3]);
    });
  });

  group('removeThreadNid', () {
    test('removes only the matching message', () {
      final remaining = removeThreadNid([_msg(1), _msg(2), _msg(3)], 2);
      expect(remaining.map((m) => m.nid), [1, 3]);
    });

    test('deleting the last message empties the thread (caller cancels)', () {
      final remaining = removeThreadNid([_msg(7)], 7);
      expect(remaining, isEmpty);
    });

    test('unknown nid leaves the thread untouched', () {
      final remaining = removeThreadNid([_msg(1)], 99);
      expect(remaining.map((m) => m.nid), [1]);
    });
  });

  group('conversationHeader', () {
    test('1:1 chat (single sender, no room) gets no title', () {
      final header = conversationHeader([
        _msg(1, sender: 'Max'),
        _msg(2, sender: 'Max'),
      ]);
      expect(header.conversationTitle, isNull);
      expect(header.groupConversation, isFalse);
    });

    test('a known room name becomes the title exactly once', () {
      final header = conversationHeader([
        ThreadMessage(
          nid: 1,
          sender: 'Max',
          text: 'hi',
          timestampMs: 1,
          roomName: 'Projektraum',
        ),
        _msg(2, sender: 'Max'),
      ]);
      expect(header.conversationTitle, 'Projektraum');
      expect(header.groupConversation, isTrue);
    });

    test('multiple senders without room name fall back to last sender', () {
      final header = conversationHeader([
        _msg(1, sender: 'Max'),
        _msg(2, sender: 'Anna'),
      ]);
      expect(header.conversationTitle, 'Anna');
      expect(header.groupConversation, isTrue);
    });
  });

  group('ThreadMessage roomName migration', () {
    test('entries without roomName field read as null', () {
      final restored = ThreadMessage.fromJson({
        'nid': 1,
        'sender': 'Max',
        'text': 'hi',
        'timestampMs': 5,
      });
      expect(restored.roomName, isNull);
    });

    test('roomName round-trips when present', () {
      final restored = ThreadMessage.fromJson(
        const ThreadMessage(
          nid: 1,
          sender: 'Max',
          text: 'hi',
          timestampMs: 5,
          roomName: 'Raum',
        ).toJson(),
      );
      expect(restored.roomName, 'Raum');
    });
  });

  group('stable notification identity', () {
    test('same token always yields the same 31-bit id', () {
      final a = stableChatNotificationId('abc123');
      expect(a, stableChatNotificationId('abc123'));
      expect(a, greaterThanOrEqualTo(0));
      expect(a, lessThanOrEqualTo(0x7fffffff));
    });

    test('different tokens yield different ids and tags', () {
      expect(
        stableChatNotificationId('abc123'),
        isNot(stableChatNotificationId('xyz789')),
      );
      expect(chatNotificationTag('abc123'), 'talk_abc123');
    });
  });

  group('ThreadMessage json', () {
    test('round-trips', () {
      final restored = ThreadMessage.fromJson(
        ThreadMessage(
          nid: 5,
          sender: 'Max',
          text: 'Hallo',
          timestampMs: 1234,
        ).toJson(),
      );
      expect(restored.nid, 5);
      expect(restored.sender, 'Max');
      expect(restored.text, 'Hallo');
      expect(restored.timestampMs, 1234);
    });
  });

  group('ChatThreadStore.docIdForToken', () {
    test('url-safe tokens are used verbatim, others encoded', () {
      expect(ChatThreadStore.docIdForToken('abc_1-2'), 'abc_1-2');
      final encoded = ChatThreadStore.docIdForToken('a/b');
      expect(encoded, isNot(contains('/')));
      expect(encoded, ChatThreadStore.docIdForToken('a/b'));
    });
  });
}
