import 'dart:async';

import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/domain/chat_timeline.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/state/messages_cubit.dart';
import 'package:tane/ui/chat_screen.dart';
import 'package:tane/ui/peer_avatar.dart';

/// In-memory [MessageTransport]: lets a test push inbox messages. (Same shape
/// as the one in messages_cubit_test.dart.)
class _FakeMessageTransport implements MessageTransport {
  final StreamController<PrivateMessage> _inbox =
      StreamController<PrivateMessage>.broadcast();

  void receive(PrivateMessage message) => _inbox.add(message);

  @override
  Future<void> send({required String toPubkey, required String text}) async {}

  @override
  Stream<PrivateMessage> inbox() => _inbox.stream;

  @override
  Future<void> close() async => _inbox.close();
}

void main() {
  const peer = 'aa';
  const me = 'bb';

  PrivateMessage msg(String from, String text, int minute) => PrivateMessage(
    fromPubkey: from,
    text: text,
    at: DateTime(2026, 1, 1, 0, minute),
  );

  Widget host(MessagesCubit cubit) {
    LocaleSettings.setLocaleSync(AppLocale.en);
    return TranslationProvider(
      child: MaterialApp(
        locale: AppLocale.en.flutterLocale,
        supportedLocales: AppLocaleUtils.supportedLocales,
        localizationsDelegates: const [
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
        ],
        home: Scaffold(
          body: BlocProvider.value(
            value: cubit,
            child: const ChatMessageList(),
          ),
        ),
      ),
    );
  }

  testWidgets('renders oldest at top, newest at the bottom of the viewport', (
    tester,
  ) async {
    final transport = _FakeMessageTransport();
    final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me)
      ..start();
    addTearDown(cubit.close);

    transport.receive(msg(peer, 'first', 0));
    transport.receive(msg(peer, 'second', 1));
    transport.receive(msg(peer, 'third', 2));
    await tester.pumpWidget(host(cubit));
    await tester.pump(); // let the stream events flush into the cubit

    // Bottom-anchored: the newest ('third') sits lower than the oldest.
    final firstY = tester.getTopLeft(find.text('first')).dy;
    final thirdY = tester.getTopLeft(find.text('third')).dy;
    expect(thirdY, greaterThan(firstY));

    // One day separator (all three are the same day) and a per-bubble time.
    expect(find.byKey(const Key('chat.daySeparator')), findsOneWidget);
    expect(
      find.text(chatBubbleTime(DateTime(2026, 1, 1, 0, 0), 'en')),
      findsOneWidget,
    );
  });

  testWidgets('a separator is inserted per calendar day', (tester) async {
    final transport = _FakeMessageTransport();
    final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me)
      ..start();
    addTearDown(cubit.close);

    transport.receive(
      PrivateMessage(
        fromPubkey: peer,
        text: 'day one',
        at: DateTime(2026, 1, 1, 9),
      ),
    );
    transport.receive(
      PrivateMessage(
        fromPubkey: peer,
        text: 'day two',
        at: DateTime(2026, 1, 2, 9),
      ),
    );
    await tester.pumpWidget(host(cubit));
    await tester.pump();

    expect(find.byKey(const Key('chat.daySeparator')), findsNWidgets(2));
  });

  testWidgets('each sender gets a distinctly coloured avatar', (tester) async {
    final transport = _FakeMessageTransport();
    final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me)
      ..start();
    addTearDown(cubit.close);

    transport.receive(msg(peer, 'from peer', 0));
    await cubit.send('from me'); // optimistically mine (fromPubkey == me)
    await tester.pumpWidget(host(cubit));
    await tester.pump();

    // Both sides carry an avatar, and the two people are different colours —
    // so you can tell who said what at a glance.
    expect(find.byType(PeerAvatar), findsNWidgets(2));
    final discs = tester
        .widgetList<CircleAvatar>(find.byType(CircleAvatar))
        .toList();
    expect(discs, hasLength(2));
    expect(discs.first.backgroundColor, isNot(discs.last.backgroundColor));
  });

  testWidgets('a new message lands in view at the bottom, not below the fold', (
    tester,
  ) async {
    final transport = _FakeMessageTransport();
    final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me)
      ..start();
    addTearDown(cubit.close);

    // Fill past a single screen so a naive top-anchored list would push the
    // newest message off-screen.
    for (var i = 0; i < 40; i++) {
      transport.receive(msg(peer, 'line $i', i));
    }
    await tester.pumpWidget(host(cubit));
    await tester.pump();

    transport.receive(msg(peer, 'brand new', 41));
    await tester.runAsync(() => Future<void>.delayed(Duration.zero));
    await tester.pump();

    // The just-arrived message is visible without any manual scroll.
    expect(find.text('brand new'), findsOneWidget);
  });

  testWidgets('shows the empty note when there are no messages', (
    tester,
  ) async {
    final cubit = MessagesCubit(null, peerPubkey: peer, selfPubkey: me);
    addTearDown(cubit.close);
    await tester.pumpWidget(host(cubit));
    await tester.pump();
    expect(find.byType(ListView), findsNothing);
  });
}
