import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/services/ts3_server_link.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
import 'package:chanora_flutter/widgets/chat_views.dart';

void main() {
  setUp(() {
    SharedPreferences.setMockInitialValues({});
  });

  ChatEntry entry(rust.BridgeMessageTarget target) {
    return ChatEntry(
      senderId: BigInt.one,
      senderName: 'Sender',
      message: 'Message',
      target: target,
    );
  }

  rust.BridgeChannel channel(BigInt id, String name) {
    return rust.BridgeChannel(
      id: id,
      parent: BigInt.zero,
      name: name,
      order: 0,
      hasPassword: false,
      neededTalkPower: 0,
    );
  }

  rust.BridgeClient client({
    required BigInt id,
    required String name,
    required BigInt channelId,
    bool isServerQuery = false,
  }) {
    return rust.BridgeClient(
      id: id,
      channel: channelId,
      name: name,
      inputMuted: false,
      outputMuted: false,
      isSpeaking: false,
      isServerQuery: isServerQuery,
      talkPower: 0,
      talkPowerGranted: false,
    );
  }

  rust.BridgeSnapshot snapshot({
    required List<rust.BridgeChannel> channels,
    required List<rust.BridgeClient> clients,
  }) {
    return rust.BridgeSnapshot(
      serverName: 'Server',
      welcomeMessage: '',
      platform: '',
      version: '',
      channels: channels,
      clients: clients,
      ownClientId: BigInt.one,
    );
  }

  test('initial chat target opens latest private conversation first', () {
    final target = resolveInitialChatTarget(
      messages: [
        entry(rust.BridgeMessageTarget.client(BigInt.from(1))),
        entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
        entry(rust.BridgeMessageTarget.client(BigInt.from(3))),
      ],
      currentVoiceChannelId: BigInt.from(9),
    );

    expect(target, rust.BridgeMessageTarget.client(BigInt.from(3)));
  });

  test('initial chat target ignores pokes and falls back to channel', () {
    final target = resolveInitialChatTarget(
      messages: [
        entry(const rust.BridgeMessageTarget.server()),
        entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
        entry(rust.BridgeMessageTarget.poke(BigInt.from(4))),
      ],
      currentVoiceChannelId: BigInt.from(9),
    );

    expect(target, const rust.BridgeMessageTarget.channel());
  });

  test('initial chat target falls back to channel when connected to voice', () {
    final target = resolveInitialChatTarget(
      messages: [
        entry(const rust.BridgeMessageTarget.server()),
        entry(const rust.BridgeMessageTarget.channel()),
      ],
      currentVoiceChannelId: BigInt.from(9),
    );

    expect(target, const rust.BridgeMessageTarget.channel());
  });

  test('initial chat target falls back to hub when nothing is active', () {
    final target = resolveInitialChatTarget(
      messages: const [],
      currentVoiceChannelId: null,
    );

    expect(target, isNull);
  });

  test('initial chat target falls back to server activity when present', () {
    final target = resolveInitialChatTarget(
      messages: [
        entry(const rust.BridgeMessageTarget.server()),
        entry(const rust.BridgeMessageTarget.channel()),
      ],
      currentVoiceChannelId: null,
    );

    expect(target, const rust.BridgeMessageTarget.server());
  });

  test('builds server activity entries for join, move, and disconnect', () {
    final lobbyId = BigInt.from(10);
    final quietId = BigInt.from(20);
    final previous = snapshot(
      channels: [
        channel(lobbyId, 'Default Channel'),
        channel(quietId, 'Quiet Zone'),
      ],
      clients: [
        client(id: BigInt.one, name: 'Me', channelId: quietId),
        client(id: BigInt.from(2), name: 'Alice', channelId: lobbyId),
        client(id: BigInt.from(3), name: 'Bob', channelId: lobbyId),
      ],
    );
    final current = snapshot(
      channels: [
        channel(lobbyId, 'Default Channel'),
        channel(quietId, 'Quiet Zone'),
      ],
      clients: [
        client(id: BigInt.one, name: 'Me', channelId: quietId),
        client(id: BigInt.from(2), name: 'Alice', channelId: quietId),
        client(id: BigInt.from(4), name: 'Carol', channelId: lobbyId),
      ],
    );

    final entries = buildServerActivityEntries(
      previous: previous,
      current: current,
      timestamp: DateTime(2026, 5, 24, 11, 40, 39),
    );

    expect(entries.map((entry) => entry.message), [
      '"Alice" switched from channel "Default Channel" to channel "Quiet Zone"',
      '"Carol" connected to channel "Default Channel"',
      '"Bob" disconnected from channel "Default Channel"',
    ]);
    expect(entries.every((entry) => !entry.countsTowardUnread), isTrue);
    expect(
      entries.every(
        (entry) => entry.target == const rust.BridgeMessageTarget.server(),
      ),
      isTrue,
    );
  });

  test(
    'groups chat picker clients by channel and excludes own/query clients',
    () {
      final groups = groupChatClientsForPicker(
        snapshot(
          channels: [channel(BigInt.from(10), 'Lobby')],
          clients: [
            client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
            client(
              id: BigInt.from(2),
              name: 'Zulu',
              channelId: BigInt.from(10),
            ),
            client(
              id: BigInt.from(3),
              name: 'Alpha',
              channelId: BigInt.from(10),
            ),
            client(
              id: BigInt.from(4),
              name: 'Query',
              channelId: BigInt.from(10),
              isServerQuery: true,
            ),
            client(
              id: BigInt.from(5),
              name: 'Orphan',
              channelId: BigInt.from(99),
            ),
          ],
        ),
      );

      expect(
        groups.clientsByChannel[BigInt.from(10)]?.map((client) => client.name),
        ['Alpha', 'Zulu'],
      );
      expect(groups.ungroupedClients.map((client) => client.name), ['Orphan']);
    },
  );

  test('filters chat picker groups by client name case-insensitively', () {
    final groups = filterChatClientPickerGroups(
      channels: [
        channel(BigInt.from(10), 'Lobby'),
        channel(BigInt.from(20), 'Games'),
      ],
      clientsByChannel: {
        BigInt.from(10): [
          client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
        ],
        BigInt.from(20): [
          client(id: BigInt.from(3), name: 'Bravo', channelId: BigInt.from(20)),
        ],
      },
      ungroupedClients: [
        client(id: BigInt.from(4), name: 'Alfred', channelId: BigInt.from(99)),
      ],
      query: 'AL',
    );

    expect(groups.map((group) => group.channelName), ['Lobby', 'Other']);
    expect(
      groups.expand((group) => group.clients).map((client) => client.name),
      ['Alpha', 'Alfred'],
    );
  });

  test('keeps all chat picker groups for blank queries', () {
    final groups = filterChatClientPickerGroups(
      channels: [channel(BigInt.from(10), 'Lobby')],
      clientsByChannel: {
        BigInt.from(10): [
          client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
          client(id: BigInt.from(3), name: 'Bravo', channelId: BigInt.from(10)),
        ],
      },
      ungroupedClients: [
        client(id: BigInt.from(4), name: 'Orphan', channelId: BigInt.from(99)),
      ],
      query: '   ',
    );

    expect(groups.map((group) => group.channelName), ['Lobby', 'Other']);
    expect(groups.first.clients.map((client) => client.name), [
      'Alpha',
      'Bravo',
    ]);
    expect(groups.last.clients.map((client) => client.name), ['Orphan']);
  });

  test('omits empty chat picker groups after filtering', () {
    final groups = filterChatClientPickerGroups(
      channels: [
        channel(BigInt.from(10), 'Lobby'),
        channel(BigInt.from(20), 'Games'),
      ],
      clientsByChannel: {
        BigInt.from(10): [
          client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
        ],
        BigInt.from(20): [
          client(id: BigInt.from(3), name: 'Bravo', channelId: BigInt.from(20)),
        ],
      },
      ungroupedClients: [
        client(id: BigInt.from(4), name: 'Orphan', channelId: BigInt.from(99)),
      ],
      query: 'brav',
    );

    expect(groups, hasLength(1));
    expect(groups.single.channelName, 'Games');
    expect(groups.single.clients.single.name, 'Bravo');
  });

  test('formats spacer channel names for filtered chat picker groups', () {
    final groups = filterChatClientPickerGroups(
      channels: [channel(BigInt.from(10), '[cSpacer]Lobby')],
      clientsByChannel: {
        BigInt.from(10): [
          client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.from(10)),
        ],
      },
      ungroupedClients: const [],
      query: '',
    );

    expect(groups.single.channelName, 'Lobby');
  });

  test('builds server activity entries from snapshot deltas', () {
    final before = snapshot(
      channels: [
        channel(BigInt.from(10), 'Lobby'),
        channel(BigInt.from(20), 'AFK'),
      ],
      clients: [
        client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
        client(id: BigInt.from(2), name: 'Alice', channelId: BigInt.from(10)),
        client(id: BigInt.from(3), name: 'Bob', channelId: BigInt.from(20)),
      ],
    );
    final after = snapshot(
      channels: [
        channel(BigInt.from(10), 'Lobby'),
        channel(BigInt.from(20), 'AFK'),
      ],
      clients: [
        client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
        client(id: BigInt.from(2), name: 'Alice', channelId: BigInt.from(20)),
        client(id: BigInt.from(4), name: 'Carol', channelId: BigInt.from(10)),
      ],
    );

    final entries = buildServerActivityEntries(
      previous: before,
      current: after,
      timestamp: DateTime(2026, 1, 2, 3, 4, 5),
    );

    expect(entries.map((entry) => entry.message), [
      '"Alice" switched from channel "Lobby" to channel "AFK"',
      '"Carol" connected to channel "Lobby"',
      '"Bob" disconnected from channel "AFK"',
    ]);
    expect(
      entries.every(
        (entry) => entry.target == const rust.BridgeMessageTarget.server(),
      ),
      isTrue,
    );
  });

  test('ignores ServerQuery clients in snapshot-driven server activity', () {
    final before = snapshot(
      channels: [channel(BigInt.from(10), 'Lobby')],
      clients: [
        client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
        client(
          id: BigInt.from(2),
          name: 'Query',
          channelId: BigInt.from(10),
          isServerQuery: true,
        ),
      ],
    );
    final after = snapshot(
      channels: [channel(BigInt.from(10), 'Lobby')],
      clients: [client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10))],
    );

    final entries = buildServerActivityEntries(
      previous: before,
      current: after,
      timestamp: DateTime(2026, 1, 2, 3, 4, 5),
    );

    expect(entries, isEmpty);
  });

  test('formats chat target titles', () {
    expect(
      chatTargetTitle(
        const rust.BridgeMessageTarget.server(),
        channelName: '',
        clientName: '',
      ),
      'Server Activity',
    );
    expect(
      chatTargetTitle(
        const rust.BridgeMessageTarget.channel(),
        channelName: 'Lobby',
        clientName: '',
      ),
      '# Lobby',
    );
    expect(
      chatTargetTitle(
        rust.BridgeMessageTarget.client(BigInt.from(2)),
        channelName: '',
        clientName: 'Alpha',
      ),
      'Alpha',
    );
    expect(
      chatTargetTitle(
        rust.BridgeMessageTarget.poke(BigInt.from(2)),
        channelName: '',
        clientName: 'Alpha',
      ),
      'Poke: Alpha',
    );
  });

  test('formats chat empty-state text', () {
    expect(
      chatEmptyTitle(const rust.BridgeMessageTarget.server()),
      'No server activity yet',
    );
    expect(
      chatEmptyBody(
        const rust.BridgeMessageTarget.channel(),
        channelName: 'Lobby',
      ),
      'Start the conversation in #Lobby.',
    );
    expect(
      chatEmptyBody(const rust.BridgeMessageTarget.channel(), channelName: ''),
      'Join a channel to start chatting.',
    );
    expect(
      chatEmptyBody(
        rust.BridgeMessageTarget.client(BigInt.from(2)),
        channelName: '',
      ),
      'Select a user to start a private chat.',
    );
  });

  test('formats chat input placeholders', () {
    expect(
      chatInputPlaceholder(
        const rust.BridgeMessageTarget.channel(),
        channelName: 'Lobby',
        clientName: '',
      ),
      'Message #Lobby...',
    );
    expect(
      chatInputPlaceholder(
        rust.BridgeMessageTarget.client(BigInt.from(2)),
        channelName: '',
        clientName: 'Alpha',
      ),
      'Message Alpha...',
    );
    expect(
      chatInputPlaceholder(
        rust.BridgeMessageTarget.poke(BigInt.from(2)),
        channelName: '',
        clientName: 'Alpha',
      ),
      'Poke message...',
    );
  });

  testWidgets('chat sidebar keeps server and channel fixed above privates', (
    tester,
  ) async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: [
            entry(rust.BridgeMessageTarget.poke(BigInt.from(2))),
            ChatEntry(
              senderId: BigInt.from(2),
              senderName: 'Alpha',
              message: 'Private',
              target: rust.BridgeMessageTarget.client(BigInt.from(2)),
            ),
            entry(const rust.BridgeMessageTarget.server()),
          ],
          snapshot: snapshot(
            channels: const [],
            clients: [
              client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.zero),
            ],
          ),
        ),
      ),
    );

    expect(find.text('Server'), findsWidgets);
    expect(find.text('Channel'), findsOneWidget);
    expect(find.text('Alpha'), findsWidgets);
    expect(find.text('Pokes'), findsNothing);
    expect(find.text('No pokes'), findsNothing);

    final serverTop = tester.getTopLeft(find.text('Server').first).dy;
    final channelTop = tester.getTopLeft(find.text('Channel')).dy;
    final privateTop = tester.getTopLeft(find.text('Alpha').first).dy;
    final serverTileSize = tester.getSize(
      find
          .ancestor(of: find.text('Server').first, matching: find.byType(Ink))
          .first,
    );

    expect(serverTop, lessThan(channelTop));
    expect(channelTop, lessThan(privateTop));
    expect(serverTileSize.width, 92);
    expect(serverTileSize.height, 92);
  });

  testWidgets('chat page uses a compact top rail on phone width', (
    tester,
  ) async {
    tester.view.physicalSize = const Size(390, 844);
    tester.view.devicePixelRatio = 1;
    addTearDown(tester.view.resetPhysicalSize);
    addTearDown(tester.view.resetDevicePixelRatio);

    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: [
            ChatEntry(
              senderId: BigInt.from(2),
              senderName: 'Alpha',
              message: 'Private',
              target: rust.BridgeMessageTarget.client(BigInt.from(2)),
            ),
          ],
          snapshot: snapshot(
            channels: const [],
            clients: [
              client(id: BigInt.from(2), name: 'Alpha', channelId: BigInt.zero),
            ],
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.byType(VerticalDivider), findsNothing);
    final serverTileSize = tester.getSize(
      find
          .ancestor(of: find.text('Server').first, matching: find.byType(Ink))
          .first,
    );
    expect(serverTileSize.width, lessThan(92));
    expect(serverTileSize.height, lessThan(92));
    expect(tester.getTopLeft(find.text('Private').first).dy, greaterThan(92));
  });

  testWidgets(
    'plus opens user picker and close removes selected private chat',
    (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          localizationsDelegates: AppL10n.localizationsDelegates,
          supportedLocales: AppL10n.supportedLocales,
          home: ChatPage(
            messages: [
              ChatEntry(
                senderId: BigInt.from(2),
                senderName: 'Alpha',
                message: 'Private',
                target: rust.BridgeMessageTarget.client(BigInt.from(2)),
              ),
            ],
            snapshot: snapshot(
              channels: const [],
              clients: [
                client(
                  id: BigInt.from(2),
                  name: 'Alpha',
                  channelId: BigInt.zero,
                ),
                client(
                  id: BigInt.from(3),
                  name: 'Bravo',
                  channelId: BigInt.zero,
                ),
              ],
            ),
            initialTarget: rust.BridgeMessageTarget.client(BigInt.from(2)),
            initialClientName: 'Alpha',
          ),
        ),
      );

      expect(find.text('Alpha'), findsWidgets);
      expect(find.byTooltip('Close chat'), findsOneWidget);

      await tester.tap(find.byTooltip('Close chat'));
      await tester.pump();

      expect(find.text('Alpha'), findsNothing);
      expect(find.text('Server Activity'), findsOneWidget);

      await tester.tap(find.byTooltip('New private chat'));
      await tester.pumpAndSettle();

      expect(find.text('Search clients...'), findsOneWidget);
      expect(find.text('Bravo'), findsOneWidget);
    },
  );

  testWidgets('channel chat displays localized poke history in English', (
    tester,
  ) async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: [
            ChatEntry(
              senderId: BigInt.from(2),
              senderName: 'EdisonJwa',
              message: '111',
              target: rust.BridgeMessageTarget.poke(BigInt.from(2)),
              timestamp: DateTime(2026, 5, 23, 5, 11, 17),
            ),
          ],
          snapshot: snapshot(channels: const [], clients: const []),
          initialTarget: const rust.BridgeMessageTarget.channel(),
        ),
      ),
    );

    expect(find.text('<05:11:17> "EdisonJwa" pokes you: 111'), findsOneWidget);
  });

  testWidgets('channel chat displays localized poke history in Chinese', (
    tester,
  ) async {
    await tester.pumpWidget(
      MaterialApp(
        locale: const Locale('zh'),
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: [
            ChatEntry(
              senderId: BigInt.from(2),
              senderName: 'EdisonJwa',
              message: '',
              target: rust.BridgeMessageTarget.poke(BigInt.from(2)),
              timestamp: DateTime(2026, 5, 23, 5, 10, 37),
            ),
          ],
          snapshot: snapshot(channels: const [], clients: const []),
          initialTarget: const rust.BridgeMessageTarget.channel(),
        ),
      ),
    );

    expect(find.text('<05:10:37> “EdisonJwa”戳了你一下'), findsOneWidget);
  });

  testWidgets('server activity renders as timestamped styled row', (
    tester,
  ) async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: [
            ChatEntry(
              senderId: BigInt.zero,
              senderName: 'Server',
              message: '"Alice" connected to channel "Lobby"',
              target: const rust.BridgeMessageTarget.server(),
              timestamp: DateTime(2026, 5, 24, 11, 40, 39),
              countsTowardUnread: false,
            ),
          ],
          snapshot: snapshot(channels: const [], clients: const []),
          initialTarget: const rust.BridgeMessageTarget.server(),
        ),
      ),
    );

    expect(find.textContaining('<11:40:39>'), findsOneWidget);
    expect(find.textContaining('"Alice"'), findsOneWidget);
    expect(find.textContaining('"Lobby"'), findsOneWidget);
    expect(find.byType(CircleAvatar), findsNothing);
    expect(find.byType(RichText), findsWidgets);
  });

  testWidgets('chat page updates while open when backing messages change', (
    tester,
  ) async {
    final messages = <ChatEntry>[
      ChatEntry(
        senderId: BigInt.one,
        senderName: 'Sender',
        message: 'First',
        target: const rust.BridgeMessageTarget.server(),
      ),
    ];
    var currentSnapshot = snapshot(channels: const [], clients: const []);
    final refresh = ValueNotifier<int>(0);

    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: messages,
          snapshot: currentSnapshot,
          messagesSource: () => messages,
          snapshotSource: () => currentSnapshot,
          refreshListenable: refresh,
          initialTarget: const rust.BridgeMessageTarget.server(),
        ),
      ),
    );

    expect(find.text('First'), findsOneWidget);
    expect(find.text('Second'), findsNothing);

    messages.add(
      ChatEntry(
        senderId: BigInt.from(2),
        senderName: 'Other',
        message: 'Second',
        target: const rust.BridgeMessageTarget.server(),
      ),
    );
    refresh.value++;
    await tester.pump();

    expect(find.text('Second'), findsOneWidget);
    refresh.dispose();
  });

  test('blocks channel chat when no voice channel is joined', () {
    expect(
      canSendToChatTarget(const rust.BridgeMessageTarget.channel(), null),
      isFalse,
    );
    expect(
      chatSendBlockedReason(const rust.BridgeMessageTarget.channel(), null),
      'Join a channel to send messages.',
    );
    expect(
      canSendToChatTarget(
        const rust.BridgeMessageTarget.channel(),
        BigInt.from(10),
      ),
      isTrue,
    );
    expect(
      chatSendBlockedReason(
        const rust.BridgeMessageTarget.channel(),
        BigInt.from(10),
      ),
      isNull,
    );
    expect(
      canSendToChatTarget(
        rust.BridgeMessageTarget.client(BigInt.from(2)),
        null,
      ),
      isTrue,
    );
  });

  testWidgets('chat message TeamSpeak links use internal handler', (
    tester,
  ) async {
    Ts3ServerLink? tapped;
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: ChatPage(
          messages: [
            ChatEntry(
              senderId: BigInt.one,
              senderName: 'Sender',
              message:
                  'ts3server://teamspeak.app%3Faddbookmark%3DVigorous%20Pro/',
              target: rust.BridgeMessageTarget.server(),
            ),
          ],
          snapshot: snapshot(channels: const [], clients: const []),
          initialTarget: const rust.BridgeMessageTarget.server(),
          onTs3ServerLink: (link) async => tapped = link,
        ),
      ),
    );

    await tester.tap(
      find.text('ts3server://teamspeak.app%3Faddbookmark%3DVigorous%20Pro/'),
    );
    await tester.pump();

    expect(tapped, isNotNull);
    expect(tapped!.host, 'teamspeak.app');
    expect(tapped!.addBookmark, 'Vigorous Pro');
  });
}
