import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart' show AsyncData;
import 'package:flutter_test/flutter_test.dart';
import 'package:whitenoise/providers/auth_provider.dart';
import 'package:whitenoise/routes.dart';
import 'package:whitenoise/screens/chat_info_screen.dart';
import 'package:whitenoise/src/rust/api/account_groups.dart';
import 'package:whitenoise/src/rust/api/chat_summary.dart';
import 'package:whitenoise/src/rust/api/groups.dart' show GroupType;
import 'package:whitenoise/src/rust/frb_generated.dart';
import 'package:whitenoise/widgets/wn_avatar.dart';
import 'package:whitenoise/widgets/wn_button.dart';
import 'package:whitenoise/widgets/wn_copy_card.dart';
import 'package:whitenoise/widgets/wn_overlay.dart';
import 'package:whitenoise/widgets/wn_slate.dart';
import 'package:whitenoise/widgets/wn_slate_navigation_header.dart';
import 'package:whitenoise/widgets/wn_system_notice.dart';

import '../mocks/mock_clipboard.dart' show clearClipboardMock, mockClipboard, mockClipboardFailing;
import '../mocks/mock_wn_api.dart';
import '../test_helpers.dart';

const _testPubkey = testPubkeyA;
const _otherPubkey = testPubkeyB;

ChatSummary _dmSummary({
  String? dmPeerPubkey = _otherPubkey,
  String? name,
}) => ChatSummary(
  mlsGroupId: testGroupId,
  groupType: GroupType.directMessage,
  createdAt: DateTime(2024),
  pendingConfirmation: false,
  selfRemoved: false,
  unreadCount: BigInt.zero,
  dmPeerPubkey: dmPeerPubkey,
  name: name,
);

ChatSummary _groupSummary({
  String? name = 'Test Group',
}) => ChatSummary(
  mlsGroupId: testGroupId,
  groupType: GroupType.group,
  createdAt: DateTime(2024),
  pendingConfirmation: false,
  selfRemoved: false,
  unreadCount: BigInt.zero,
  name: name,
);

class _MockApi extends MockWnApi {
  String? dmDisplayName;
  String? dmPeerPubkey = _otherPubkey;
  bool returnGroupSummary = false;
  String? groupDisplayName = 'Test Group';
  bool archivedAtResult = false;
  Completer<ChatSummary>? chatSummaryCompleter;
  Completer<void>? followCompleter;
  Completer<void>? unfollowCompleter;
  Completer<bool>? isBlockedCompleter;
  Completer<void>? blockCompleter;
  Completer<void>? unblockCompleter;
  Exception? followError;
  Exception? unfollowError;
  Exception? blockError;
  Exception? unblockError;
  Exception? chatSummaryError;
  final followCalls = <({String account, String target})>[];
  final unfollowCalls = <({String account, String target})>[];
  final blockCalls = <({String account, String target})>[];
  final unblockCalls = <({String account, String target})>[];
  final Set<String> followingPubkeys = {};

  @override
  Future<ChatSummary> crateApiChatSummaryGetChatSummary({
    required String accountPubkey,
    required String mlsGroupId,
  }) async {
    if (chatSummaryCompleter != null) return chatSummaryCompleter!.future;
    if (chatSummaryError != null) throw chatSummaryError!;
    if (returnGroupSummary) return _groupSummary(name: groupDisplayName);
    return _dmSummary(dmPeerPubkey: dmPeerPubkey, name: dmDisplayName);
  }

  @override
  Future<AccountGroup> crateApiAccountGroupsGetAccountGroup({
    required String accountPubkey,
    required String mlsGroupId,
  }) async {
    return AccountGroup(
      accountPubkey: accountPubkey,
      mlsGroupId: mlsGroupId,
      archivedAt: archivedAtResult ? DateTime.now().millisecondsSinceEpoch : null,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      updatedAt: DateTime.now().millisecondsSinceEpoch,
    );
  }

  @override
  Future<void> crateApiAccountsFollowUser({
    required String accountPubkey,
    required String userToFollowPubkey,
  }) async {
    followCalls.add((account: accountPubkey, target: userToFollowPubkey));
    if (followCompleter != null) await followCompleter!.future;
    if (followError != null) throw followError!;
  }

  @override
  Future<void> crateApiAccountsUnfollowUser({
    required String accountPubkey,
    required String userToUnfollowPubkey,
  }) async {
    unfollowCalls.add((account: accountPubkey, target: userToUnfollowPubkey));
    if (unfollowCompleter != null) await unfollowCompleter!.future;
    if (unfollowError != null) throw unfollowError!;
  }

  @override
  Future<bool> crateApiAccountsIsFollowingUser({
    required String accountPubkey,
    required String userPubkey,
  }) async {
    return followingPubkeys.contains(userPubkey);
  }

  @override
  Future<bool> crateApiMuteListIsUserBlocked({
    required String accountPubkey,
    required String targetPubkey,
  }) async {
    if (isBlockedCompleter != null) return isBlockedCompleter!.future;
    return blockedPubkeys.contains(targetPubkey);
  }

  @override
  Future<void> crateApiMuteListBlockUser({
    required String accountPubkey,
    required String targetPubkey,
  }) async {
    blockCalls.add((account: accountPubkey, target: targetPubkey));
    if (blockCompleter != null) await blockCompleter!.future;
    if (blockError != null) throw blockError!;
    blockedPubkeys.add(targetPubkey);
  }

  @override
  Future<void> crateApiMuteListUnblockUser({
    required String accountPubkey,
    required String targetPubkey,
  }) async {
    unblockCalls.add((account: accountPubkey, target: targetPubkey));
    if (unblockCompleter != null) await unblockCompleter!.future;
    if (unblockError != null) throw unblockError!;
    blockedPubkeys.remove(targetPubkey);
  }

  @override
  void reset() {
    super.reset();
    dmDisplayName = null;
    dmPeerPubkey = _otherPubkey;
    returnGroupSummary = false;
    groupDisplayName = 'Test Group';
    chatSummaryCompleter = null;
    followCompleter = null;
    unfollowCompleter = null;
    isBlockedCompleter = null;
    blockCompleter = null;
    unblockCompleter = null;
    followError = null;
    unfollowError = null;
    blockError = null;
    unblockError = null;
    chatSummaryError = null;
    followCalls.clear();
    unfollowCalls.clear();
    blockCalls.clear();
    unblockCalls.clear();
    followingPubkeys.clear();
    blockedPubkeys.clear();
    archivedAtResult = false;
  }
}

class _MockAuthNotifier extends AuthNotifier {
  @override
  Future<String?> build() async {
    state = const AsyncData(_testPubkey);
    return _testPubkey;
  }
}

final _api = _MockApi();

void main() {
  setUpAll(() => RustLib.initMock(api: _api));
  setUp(() => _api.reset());

  Future<void> pumpChatInfoScreen(
    WidgetTester tester, {
    String mlsGroupId = testGroupId,
    bool settle = true,
    bool showSearch = true,
  }) async {
    await mountTestApp(
      tester,
      overrides: [authProvider.overrideWith(() => _MockAuthNotifier())],
    );
    await tester.pumpAndSettle();
    final context = tester.element(find.byType(Scaffold));
    if (showSearch) {
      unawaited(Routes.pushToChatInfo(context, mlsGroupId));
    } else {
      unawaited(Routes.pushToInviteInfo(context, mlsGroupId));
    }
    if (settle) {
      await tester.pumpAndSettle();
    } else {
      await tester.pump();
    }
  }

  group('ChatInfoScreen', () {
    Finder chatInfoSlateFinder() {
      return find.ancestor(
        of: find.text('Chat Information'),
        matching: find.byType(WnSlate),
      );
    }

    testWidgets('displays slate container and chat info header', (tester) async {
      await pumpChatInfoScreen(tester);

      expect(chatInfoSlateFinder(), findsOneWidget);
      expect(find.byType(WnSlateNavigationHeader), findsOneWidget);
      expect(find.text('Chat Information'), findsOneWidget);
      expect(find.byKey(const Key('slate_back_button')), findsOneWidget);
    });

    testWidgets('uses light overlay variant', (tester) async {
      await pumpChatInfoScreen(tester);

      final overlay = tester.widget<WnOverlay>(find.byType(WnOverlay));
      expect(overlay.variant, WnOverlayVariant.light);
    });

    testWidgets('slate height matches content and is not full screen', (tester) async {
      await pumpChatInfoScreen(tester);

      final slateHeight = tester.getSize(chatInfoSlateFinder()).height;
      final screenHeight = tester.view.physicalSize.height / tester.view.devicePixelRatio;
      expect(slateHeight, lessThan(screenHeight));
    });

    testWidgets('renders profile card and copy card', (tester) async {
      _api.dmDisplayName = 'Alice';
      await pumpChatInfoScreen(tester);

      expect(find.byType(WnAvatar), findsOneWidget);
      expect(find.text('Alice'), findsOneWidget);
      expect(find.byType(WnCopyCard), findsOneWidget);
    });

    testWidgets('does not render nip05 and about in chat info card', (tester) async {
      _api.dmDisplayName = 'Alice';
      await pumpChatInfoScreen(tester);

      expect(find.text('alice@example.com'), findsNothing);
      expect(find.text('I love Nostr!'), findsNothing);
    });

    testWidgets('shows Follow for non-followed user', (tester) async {
      await pumpChatInfoScreen(tester);
      expect(find.text('Follow'), findsOneWidget);
    });

    testWidgets('shows Unfollow for followed user', (tester) async {
      _api.followingPubkeys.add(_otherPubkey);
      await pumpChatInfoScreen(tester);
      expect(find.text('Unfollow'), findsOneWidget);
    });

    testWidgets('calls follow API when follow is tapped', (tester) async {
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('contact_button')));
      await tester.pumpAndSettle();

      expect(_api.followCalls.length, 1);
      expect(_api.followCalls[0].account, _testPubkey);
      expect(_api.followCalls[0].target, _otherPubkey);
    });

    testWidgets('calls unfollow API when unfollow is tapped', (tester) async {
      _api.followingPubkeys.add(_otherPubkey);
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('contact_button')));
      await tester.pumpAndSettle();

      expect(_api.unfollowCalls.length, 1);
      expect(_api.unfollowCalls[0].account, _testPubkey);
      expect(_api.unfollowCalls[0].target, _otherPubkey);
    });

    testWidgets('shows contact button loading state during follow action', (tester) async {
      _api.followCompleter = Completer();
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('contact_button')));
      await tester.pump();

      final button = tester.widget<WnButton>(find.byKey(const Key('contact_button')));
      expect(button.loading, isTrue);
    });

    testWidgets('shows error notice when follow action fails', (tester) async {
      _api.followError = Exception('Network error');
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('contact_button')));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsOneWidget);
      expect(
        find.text('Failed to update follow status. Please try again.'),
        findsOneWidget,
      );
    });

    testWidgets('shows error notice when unfollow action fails', (tester) async {
      _api.followingPubkeys.add(_otherPubkey);
      _api.unfollowError = Exception('Network error');
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('contact_button')));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsOneWidget);
      expect(
        find.text('Failed to update follow status. Please try again.'),
        findsOneWidget,
      );
    });

    testWidgets('hides peer actions when dm peer pubkey is unresolved', (tester) async {
      _api.dmPeerPubkey = null;
      await pumpChatInfoScreen(tester);

      expect(find.byKey(const Key('contact_button')), findsNothing);
      expect(find.byKey(const Key('search_button')), findsNothing);
      expect(find.byKey(const Key('add_to_group_button')), findsNothing);
      expect(find.byKey(const Key('block_button')), findsNothing);
      expect(find.byKey(const Key('archive_button')), findsNothing);
      expect(_api.followCalls, isEmpty);
      expect(_api.unfollowCalls, isEmpty);
    });

    testWidgets('does not treat loading chat summary as unresolved dm', (tester) async {
      _api.chatSummaryCompleter = Completer<ChatSummary>();
      await mountWidget(
        const ChatInfoScreen(mlsGroupId: testGroupId),
        tester,
        overrides: [authProvider.overrideWith(() => _MockAuthNotifier())],
      );
      await tester.pump();

      expect(find.text('Chat Information'), findsOneWidget);
      expect(find.byKey(const Key('search_button')), findsOneWidget);
    });

    testWidgets('renders group summary without peer actions', (tester) async {
      _api.returnGroupSummary = true;
      _api.groupDisplayName = 'Family Group';
      await pumpChatInfoScreen(tester);

      expect(find.text('Family Group'), findsOneWidget);
      expect(find.byType(WnAvatar), findsOneWidget);
      expect(find.byKey(const Key('search_button')), findsOneWidget);
      expect(find.byKey(const Key('archive_button')), findsOneWidget);
      expect(find.byKey(const Key('contact_button')), findsNothing);
      expect(find.byKey(const Key('add_to_group_button')), findsNothing);
      expect(find.byKey(const Key('block_button')), findsNothing);
    });

    testWidgets('hides block button while block status is loading', (tester) async {
      _api.isBlockedCompleter = Completer();
      await pumpChatInfoScreen(tester, settle: false);

      expect(find.byKey(const Key('block_button')), findsNothing);
    });

    testWidgets('shows block user button for non-blocked user', (tester) async {
      await pumpChatInfoScreen(tester);
      expect(find.text('Block user'), findsOneWidget);
    });

    testWidgets('shows unblock user button for blocked user', (tester) async {
      _api.blockedPubkeys.add(_otherPubkey);
      await pumpChatInfoScreen(tester);
      expect(find.text('Unblock user'), findsOneWidget);
    });

    testWidgets('calls block API when block is tapped', (tester) async {
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('block_button')));
      await tester.pumpAndSettle();

      expect(_api.blockCalls.length, 1);
      expect(_api.blockCalls[0].account, _testPubkey);
      expect(_api.blockCalls[0].target, _otherPubkey);
    });

    testWidgets('calls unblock API when unblock is tapped', (tester) async {
      _api.blockedPubkeys.add(_otherPubkey);
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('block_button')));
      await tester.pumpAndSettle();

      expect(_api.unblockCalls.length, 1);
      expect(_api.unblockCalls[0].account, _testPubkey);
      expect(_api.unblockCalls[0].target, _otherPubkey);
    });

    testWidgets('shows block button loading state during block action', (tester) async {
      _api.blockCompleter = Completer();
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('block_button')));
      await tester.pump();

      final button = tester.widget<WnButton>(find.byKey(const Key('block_button')));
      expect(button.loading, isTrue);
    });

    testWidgets('shows error notice when block action fails', (tester) async {
      _api.blockError = Exception('Network error');
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('block_button')));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsOneWidget);
      expect(find.text('Failed to block user. Please try again.'), findsOneWidget);
    });

    testWidgets('shows error notice when unblock action fails', (tester) async {
      _api.blockedPubkeys.add(_otherPubkey);
      _api.unblockError = Exception('Network error');
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('block_button')));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsOneWidget);
      expect(find.text('Failed to unblock user. Please try again.'), findsOneWidget);
    });

    testWidgets('shows notice when public key is copied', (tester) async {
      mockClipboard();
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('copy_button')));
      await tester.pump();

      expect(find.text('Public key copied to clipboard'), findsOneWidget);
    });

    testWidgets('shows error notice when public key copy fails', (tester) async {
      mockClipboardFailing();
      addTearDown(clearClipboardMock);
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('copy_button')));
      await tester.pumpAndSettle();

      expect(find.text('Failed to copy public key. Please try again.'), findsOneWidget);
    });

    testWidgets('dismisses notice after auto-hide duration', (tester) async {
      mockClipboard();
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('copy_button')));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 300));
      expect(find.byType(WnSystemNotice), findsOneWidget);

      await tester.pump(const Duration(seconds: 3));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsNothing);
    });

    testWidgets('pops screen when search is pressed', (tester) async {
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('search_button')));
      await tester.pumpAndSettle();

      expect(find.text('Chat Information'), findsNothing);
    });

    testWidgets('does not show mute action', (tester) async {
      await pumpChatInfoScreen(tester);

      expect(find.byKey(const Key('mute_button')), findsNothing);
      expect(find.text('Mute'), findsNothing);
    });

    testWidgets('navigates to add to group screen when add to group is pressed', (tester) async {
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('add_to_group_button')));
      await tester.pumpAndSettle();

      expect(find.text('Add to group'), findsWidgets);
    });

    testWidgets('navigates back when back button is pressed', (tester) async {
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('slate_back_button')));
      await tester.pumpAndSettle();

      expect(find.text('Chat Information'), findsNothing);
    });

    testWidgets('does not show archive or delete actions', (tester) async {
      await pumpChatInfoScreen(tester);

      expect(find.text('Delete chat'), findsNothing);
    });

    testWidgets('shows archive button when chat is not archived', (tester) async {
      _api.archivedAtResult = false;
      await pumpChatInfoScreen(tester);

      expect(find.widgetWithText(WnButton, 'Archive'), findsOneWidget);
    });

    testWidgets('shows unarchive button when chat is archived', (tester) async {
      _api.archivedAtResult = true;
      await pumpChatInfoScreen(tester);

      expect(find.widgetWithText(WnButton, 'Unarchive'), findsOneWidget);
    });

    testWidgets('calls archive API when archive is tapped', (tester) async {
      _api.archivedAtResult = false;
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('archive_button')));
      await tester.pumpAndSettle();

      expect(_api.archiveChatCallCount, 1);
    });

    testWidgets('calls unarchive API when unarchive is tapped', (tester) async {
      _api.archivedAtResult = true;
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('archive_button')));
      await tester.pumpAndSettle();

      expect(_api.unarchiveChatCallCount, 1);
    });

    testWidgets('shows archive failure notice when archive action fails', (tester) async {
      _api.archivedAtResult = false;
      _api.shouldFailArchiveChat = true;
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('archive_button')));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsOneWidget);
      expect(find.text('Failed to archive chat. Please try again.'), findsOneWidget);
    });

    testWidgets('shows unarchive failure notice when unarchive action fails', (tester) async {
      _api.archivedAtResult = true;
      _api.shouldFailUnarchiveChat = true;
      await pumpChatInfoScreen(tester);

      await tester.tap(find.byKey(const Key('archive_button')));
      await tester.pumpAndSettle();

      expect(find.byType(WnSystemNotice), findsOneWidget);
      expect(find.text('Failed to unarchive chat. Please try again.'), findsOneWidget);
    });

    testWidgets('hides search button when opened from invite', (tester) async {
      await pumpChatInfoScreen(tester, showSearch: false);

      expect(find.byKey(const Key('search_button')), findsNothing);
      expect(find.byKey(const Key('contact_button')), findsOneWidget);
      expect(find.byKey(const Key('add_to_group_button')), findsOneWidget);
    });

    testWidgets('shows profile load error when chat profile fails', (tester) async {
      _api.chatSummaryError = Exception('chat summary load failed');
      await pumpChatInfoScreen(tester);

      expect(find.text('Unable to load profile. Please try again.'), findsOneWidget);
    });
  });
}
