import 'dart:async';

import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nostr/nostr.dart';
import 'package:tane/services/social_connection.dart';
import 'package:tane/services/social_service.dart';
import 'package:tane/services/social_settings.dart';

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

/// A no-op [NostrChannel] that only tracks whether it was closed — enough to
/// build a [SocialSession] and assert the connection's lifecycle.
class FakeChannel implements NostrChannel {
  bool closed = false;

  @override
  String get privateKeyHex => '00' * 32;
  @override
  String get publicKeyHex => 'ab' * 32;
  @override
  Future<({bool accepted, String message})> publish(Event event) async =>
      (accepted: true, message: '');
  @override
  Stream<Event> subscribe(Filter filter) => const Stream.empty();
  @override
  Future<List<Event>> reqOnce(Filter filter) async => const [];
  @override
  Future<void> close() async => closed = true;
}

void main() {
  const seedHex =
      '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f';

  late SocialService social;
  late SocialSettings settings; // relayUrls() falls back to defaults (non-empty)

  setUp(() async {
    social = await SocialService.fromRootSeedHex(seedHex);
    settings = SocialSettings(InMemorySecretStore());
  });

  SocialConnection make({
    required List<FakeChannel> opened,
    Stream<bool>? online,
    bool Function()? fail,
  }) =>
      SocialConnection(
        social: social,
        settings: settings,
        online: online,
        open: (_) async {
          if (fail?.call() ?? false) throw StateError('unreachable');
          final ch = FakeChannel();
          opened.add(ch);
          return SocialSession(ch);
        },
      );

  test('connects once and reuses the shared session', () async {
    final opened = <FakeChannel>[];
    final conn = make(opened: opened);
    final a = await conn.session();
    final b = await conn.session();
    expect(a, isNotNull);
    expect(identical(a, b), isTrue); // same shared instance
    expect(opened, hasLength(1)); // only one connection opened
    await conn.dispose();
  });

  test('concurrent callers share a single connect', () async {
    final opened = <FakeChannel>[];
    final conn = make(opened: opened);
    final results = await Future.wait([conn.session(), conn.session()]);
    expect(identical(results[0], results[1]), isTrue);
    expect(opened, hasLength(1));
    await conn.dispose();
  });

  test('drops when offline and reconnects when back online', () async {
    final opened = <FakeChannel>[];
    final online = StreamController<bool>.broadcast();
    final conn = make(opened: opened, online: online.stream);
    final emitted = <SocialSession?>[];
    conn.sessions.listen(emitted.add);

    conn.start(); // watch connectivity + initial connect
    final first = await conn.session();
    expect(first, isNotNull);
    expect(opened, hasLength(1));

    online.add(false); // network lost
    await Future<void>.delayed(Duration.zero);
    expect(conn.current, isNull);
    expect(opened.first.closed, isTrue); // old session closed
    expect(emitted.last, isNull); // announced the drop

    online.add(true); // network back
    await Future<void>.delayed(Duration.zero);
    expect(conn.current, isNotNull);
    expect(opened, hasLength(2)); // reconnected
    expect(identical(emitted.last, conn.current), isTrue);

    await conn.dispose();
    await online.close();
  });

  test('returns null when the relay is unreachable, and retries later',
      () async {
    final opened = <FakeChannel>[];
    var down = true;
    final conn = make(opened: opened, fail: () => down);
    expect(await conn.session(), isNull); // unreachable now
    down = false;
    expect(await conn.session(), isNotNull); // succeeds on retry
    await conn.dispose();
  });
}
