import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/services/inbox_service.dart';
import 'package:tane/services/message_store.dart';
import 'package:tane/services/notification_service.dart';
import 'package:tane/services/social_connection.dart';
import 'package:tane/services/social_service.dart';
import 'package:tane/services/social_settings.dart';
import 'package:tane/services/unread_service.dart';

import '../support/test_support.dart';

/// Records notification calls instead of touching the OS plugin.
class _RecordingNotifications extends NotificationService {
  _RecordingNotifications() : super(supported: false);

  final titles = <String>[];
  final peers = <String>[];

  @override
  Future<void> showMessage({
    required String peerPubkey,
    required String title,
  }) async {
    peers.add(peerPubkey);
    titles.add(title);
  }
}

/// The app-wide inbox listener persists incoming messages, updates unread and
/// notifies — even when the specific chat isn't open. Driven through the
/// [InboxService.ingest] seam so no relay/network is involved.
void main() {
  const seedHex =
      '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f';

  // A connection that never actually opens — ingest doesn't use it.
  Future<SocialConnection> offlineConnection() async => SocialConnection(
        social: await SocialService.fromRootSeedHex(seedHex),
        settings: SocialSettings(InMemorySecretStore()),
        open: (_) async => throw StateError('unused in ingest tests'),
        online: const Stream.empty(),
      );

  late MessageStore store;
  late InboxService inbox;

  setUp(() async {
    store = MessageStore(newTestChatDatabase());
    inbox = InboxService(
      connection: await offlineConnection(),
      selfPubkey: 'me',
      store: store,
    );
  });

  tearDown(() => inbox.stop());

  PrivateMessage msg(String from, String text, int atMs) => PrivateMessage(
      fromPubkey: from,
      text: text,
      at: DateTime.fromMillisecondsSinceEpoch(atMs));

  test('an incoming message is persisted into its conversation', () async {
    await inbox.ingest(msg('alice', 'got seeds?', 1000));
    final convos = await store.conversations();
    expect(convos.single.peerPubkey, 'alice');
    expect(convos.single.lastText, 'got seeds?');
  });

  test('a new message announces a change; a duplicate stays silent', () async {
    final changes = <void>[];
    final sub = inbox.changes.listen(changes.add);

    await inbox.ingest(msg('alice', 'hola', 1000));
    await inbox.ingest(msg('alice', 'hola', 1000)); // relay re-delivery
    await Future<void>.delayed(Duration.zero); // let the broadcast flush

    expect(changes, hasLength(1)); // only the first, new one fired
    expect(await store.history('alice'), hasLength(1));
    await sub.cancel();
  });

  group('unread + notification hooks', () {
    late UnreadService unread;
    late _RecordingNotifications notifications;
    const myPubkey = 'me';

    setUp(() async {
      unread = UnreadService(store, InMemorySecretStore());
      notifications = _RecordingNotifications();
      inbox = InboxService(
        connection: await offlineConnection(),
        selfPubkey: myPubkey,
        store: store,
        unread: unread,
        notifications: notifications,
      );
    });

    tearDown(() => unread.dispose());

    test('a new peer message updates unread and notifies', () async {
      await inbox.ingest(msg('alice', 'hola', 1000));
      expect(await unread.unreadCount('alice'), 1);
      expect(notifications.peers, ['alice']);
    });

    test('a duplicate re-delivery neither counts nor notifies', () async {
      await inbox.ingest(msg('alice', 'hola', 1000));
      await inbox.ingest(msg('alice', 'hola', 1000));
      expect(await unread.unreadCount('alice'), 1);
      expect(notifications.peers, hasLength(1));
    });

    test('a message authored by me is never notified', () async {
      // NIP-17 doesn't loop your own gift wrap back; the guard is defensive.
      await inbox.ingest(msg(myPubkey, 'echo', 1000));
      expect(notifications.peers, isEmpty);
    });

    test('the open chat is not notified and stays read', () async {
      unread.activePeer = 'alice';
      await inbox.ingest(msg('alice', 'while open', 1000));
      expect(await unread.unreadCount('alice'), 0);
      expect(notifications.peers, isEmpty);
    });
  });
}
