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

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

void main() {
  rust.BridgeChannel channel({
    required int id,
    int parent = 0,
    required String name,
    int order = 0,
    bool hasPassword = false,
    int neededTalkPower = 0,
  }) {
    return rust.BridgeChannel(
      id: BigInt.from(id),
      parent: BigInt.from(parent),
      name: name,
      order: order,
      hasPassword: hasPassword,
      neededTalkPower: neededTalkPower,
    );
  }

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

  Widget snapshotHarness({
    required List<rust.BridgeChannel> channels,
    required List<rust.BridgeClient> clients,
    String welcomeMessage = '',
    BigInt? ownClientId,
    BigInt? currentVoiceChannelId,
    rust.BridgeAudioStats? audioStats,
    bool enableClientLongPressMenu = false,
    ValueChanged<rust.BridgeClient>? onOpenClientInfo,
    ValueChanged<rust.BridgeClient>? onOpenClientChat,
    ValueChanged<rust.BridgeClient>? onOpenClientPoke,
  }) {
    return MaterialApp(
      localizationsDelegates: AppL10n.localizationsDelegates,
      supportedLocales: AppL10n.supportedLocales,
      home: Scaffold(
        body: SnapshotView(
          snapshot: rust.BridgeSnapshot(
            serverName: 'Server',
            welcomeMessage: welcomeMessage,
            platform: '',
            version: '',
            channels: channels,
            clients: clients,
            ownClientId: ownClientId ?? BigInt.from(100),
          ),
          audioStats: audioStats,
          currentVoiceChannelId: currentVoiceChannelId,
          pendingVoiceChannelId: null,
          localInputMuted: false,
          localOutputMuted: false,
          hasJoinPending: false,
          canJoinVoiceChannel: true,
          onJoinChannel: (_) {},
          onJoinChannelWithPassword: (_) {},
          enableClientLongPressMenu: enableClientLongPressMenu,
          onOpenClientInfo: onOpenClientInfo,
          onOpenClientChat: onOpenClientChat,
          onOpenClientPoke: onOpenClientPoke,
        ),
      ),
    );
  }

  testWidgets('renders channel tree with users and subchannels expanded', (
    tester,
  ) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [
          channel(id: 1, name: 'Default Channel'),
          channel(id: 2, parent: 1, name: 'Default Sub Channel'),
          channel(id: 3, name: 'Quiet Zone'),
        ],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 2, name: 'Bob'),
          client(id: 102, channelId: 3, name: 'Carol'),
        ],
        ownClientId: BigInt.from(100),
        currentVoiceChannelId: BigInt.from(1),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('Default Channel'), findsOneWidget);
    expect(find.text('Alice'), findsOneWidget);
    expect(find.text('Default Sub Channel'), findsOneWidget);
    expect(find.text('Bob'), findsOneWidget);
    expect(find.text('Quiet Zone'), findsOneWidget);
    expect(find.text('Carol'), findsOneWidget);

    expect(
      tester.getTopLeft(find.text('Alice')).dy,
      greaterThan(tester.getTopLeft(find.text('Default Channel')).dy),
    );
    expect(
      tester.getTopLeft(find.text('Default Sub Channel')).dy,
      greaterThan(tester.getTopLeft(find.text('Alice')).dy),
    );
    expect(
      tester.getTopLeft(find.text('Bob')).dy,
      greaterThan(tester.getTopLeft(find.text('Default Sub Channel')).dy),
    );
  });

  testWidgets('collapsing a channel hides users and child channels', (
    tester,
  ) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [
          channel(id: 1, name: 'Default Channel'),
          channel(id: 2, parent: 1, name: 'Default Sub Channel'),
          channel(id: 3, name: 'Quiet Zone'),
        ],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 2, name: 'Bob'),
          client(id: 102, channelId: 3, name: 'Carol'),
        ],
      ),
    );

    await tester.pumpAndSettle();
    await tester.tap(find.byIcon(Icons.expand_more).first);
    await tester.pumpAndSettle();

    expect(find.text('Default Channel'), findsOneWidget);
    expect(find.text('Alice'), findsNothing);
    expect(find.text('Default Sub Channel'), findsNothing);
    expect(find.text('Bob'), findsNothing);
    expect(find.text('Quiet Zone'), findsOneWidget);
    expect(find.text('Carol'), findsOneWidget);

    await tester.tap(find.byIcon(Icons.chevron_right).first);
    await tester.pumpAndSettle();
    expect(find.text('Alice'), findsOneWidget);
    expect(find.text('Default Sub Channel'), findsOneWidget);
    expect(find.text('Bob'), findsOneWidget);
  });

  testWidgets('channel tree uses compact fixed columns', (tester) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [
          channel(id: 1, name: 'Default Channel'),
          channel(id: 2, parent: 1, name: 'Default Sub Channel'),
          channel(id: 3, name: 'Empty Channel'),
        ],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 2, name: 'Bob'),
        ],
      ),
    );

    await tester.pumpAndSettle();

    final parentX = tester.getTopLeft(find.text('Default Channel')).dx;
    final parentUserX = tester.getTopLeft(find.text('Alice')).dx;
    final childX = tester.getTopLeft(find.text('Default Sub Channel')).dx;
    final childUserX = tester.getTopLeft(find.text('Bob')).dx;

    expect(find.text('Channels'), findsNothing);
    expect(find.byIcon(Icons.tag), findsNWidgets(3));
    expect(parentX, inInclusiveRange(58, 66));
    expect(parentUserX - parentX, inInclusiveRange(38, 44));
    expect(childX - parentX, inInclusiveRange(10, 14));
    expect(childUserX - childX, inInclusiveRange(38, 44));
  });

  testWidgets('server welcome starts collapsed above channel tree', (
    tester,
  ) async {
    await tester.pumpWidget(
      snapshotHarness(
        welcomeMessage:
            'This is a long server welcome that should not push channels '
            'below the first connected screen.',
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [client(id: 100, channelId: 1, name: 'Alice')],
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('Server welcome message'), findsOneWidget);
    expect(find.textContaining('long server welcome'), findsNothing);
    expect(find.text('Default Channel'), findsOneWidget);
    expect(find.text('Alice'), findsOneWidget);
  });

  testWidgets('password channel shows lock at row end', (tester) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Locked Channel', hasPassword: true)],
        clients: const [],
      ),
    );

    await tester.pumpAndSettle();

    final rowRight = tester.getTopRight(find.text('Locked Channel')).dx;
    final lockLeft = tester.getTopLeft(find.byIcon(Icons.lock_outline)).dx;

    expect(find.byIcon(Icons.tag), findsOneWidget);
    expect(lockLeft, greaterThan(rowRight));
  });

  testWidgets('current user row does not show speaking background while idle', (
    tester,
  ) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [client(id: 100, channelId: 1, name: 'Alice')],
        ownClientId: BigInt.from(100),
        currentVoiceChannelId: BigInt.from(1),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('Alice'), findsOneWidget);
    expect(find.byIcon(Icons.tag), findsOneWidget);
    expect(find.byIcon(Icons.mic_none), findsNothing);
    expect(find.byType(ListTile), findsOneWidget);

    final userHighlight = tester.widget<AnimatedContainer>(
      find.ancestor(
        of: find.text('Alice'),
        matching: find.byType(AnimatedContainer),
      ),
    );
    expect(userHighlight.decoration, isNull);
  });

  testWidgets('long press opens client menu for other users', (tester) async {
    rust.BridgeClient? chatClient;
    rust.BridgeClient? pokeClient;

    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        enableClientLongPressMenu: true,
        onOpenClientChat: (client) => chatClient = client,
        onOpenClientPoke: (client) => pokeClient = client,
      ),
    );

    await tester.pumpAndSettle();
    await tester.longPress(find.text('Bob'));
    await tester.pumpAndSettle();

    expect(find.text('Private message'), findsOneWidget);
    expect(find.text('Poke'), findsOneWidget);

    await tester.tap(find.text('Private message'));
    await tester.pumpAndSettle();

    expect(chatClient?.id, BigInt.from(101));
    expect(chatClient?.name, 'Bob');
    expect(pokeClient, isNull);
  });

  testWidgets('long press is ignored when mobile client menus are disabled', (
    tester,
  ) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        enableClientLongPressMenu: false,
        onOpenClientChat: (_) {},
        onOpenClientPoke: (_) {},
      ),
    );

    await tester.pumpAndSettle();
    await tester.longPress(find.text('Bob'));
    await tester.pumpAndSettle();

    expect(find.text('Private message'), findsNothing);
    expect(find.text('Poke'), findsNothing);
  });

  testWidgets('secondary click opens client menu for other users', (
    tester,
  ) async {
    rust.BridgeClient? chatClient;

    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        onOpenClientChat: (client) => chatClient = client,
      ),
    );

    await tester.pumpAndSettle();
    await tester.tap(
      find.text('Bob'),
      buttons: kSecondaryMouseButton,
      kind: PointerDeviceKind.mouse,
    );
    await tester.pumpAndSettle();

    expect(find.text('Private message'), findsOneWidget);

    await tester.tap(find.text('Private message'));
    await tester.pumpAndSettle();

    expect(chatClient?.id, BigInt.from(101));
  });

  testWidgets('client menu poke action targets selected other user', (
    tester,
  ) async {
    rust.BridgeClient? pokeClient;

    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        enableClientLongPressMenu: true,
        onOpenClientPoke: (client) => pokeClient = client,
      ),
    );

    await tester.pumpAndSettle();
    await tester.longPress(find.text('Bob'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Poke'));
    await tester.pumpAndSettle();

    expect(pokeClient?.id, BigInt.from(101));
    expect(pokeClient?.name, 'Bob');
    expect(find.text('Poke'), findsNothing);
  });

  testWidgets('client menu info action targets selected other user', (
    tester,
  ) async {
    rust.BridgeClient? infoClient;

    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        enableClientLongPressMenu: true,
        onOpenClientInfo: (client) => infoClient = client,
      ),
    );

    await tester.pumpAndSettle();
    await tester.longPress(find.text('Bob'));
    await tester.pumpAndSettle();

    expect(find.text('Info'), findsOneWidget);

    await tester.tap(find.text('Info'));
    await tester.pumpAndSettle();

    expect(infoClient?.id, BigInt.from(101));
    expect(infoClient?.name, 'Bob');
  });

  testWidgets('client menu info action is available for self', (tester) async {
    rust.BridgeClient? infoClient;
    rust.BridgeClient? chatClient;
    rust.BridgeClient? pokeClient;

    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        enableClientLongPressMenu: true,
        onOpenClientInfo: (client) => infoClient = client,
        onOpenClientChat: (client) => chatClient = client,
        onOpenClientPoke: (client) => pokeClient = client,
      ),
    );

    await tester.pumpAndSettle();
    await tester.longPress(find.text('Alice'));
    await tester.pumpAndSettle();

    expect(find.text('Info'), findsOneWidget);
    expect(find.text('Private message'), findsNothing);
    expect(find.text('Poke'), findsNothing);

    await tester.tap(find.text('Info'));
    await tester.pumpAndSettle();

    expect(infoClient?.id, BigInt.from(100));
    expect(infoClient?.name, 'Alice');
    expect(chatClient, isNull);
    expect(pokeClient, isNull);
  });

  testWidgets('self menu is hidden when only peer actions are available', (
    tester,
  ) async {
    await tester.pumpWidget(
      snapshotHarness(
        channels: [channel(id: 1, name: 'Default Channel')],
        clients: [
          client(id: 100, channelId: 1, name: 'Alice'),
          client(id: 101, channelId: 1, name: 'Bob'),
        ],
        ownClientId: BigInt.from(100),
        enableClientLongPressMenu: true,
        onOpenClientChat: (_) {},
        onOpenClientPoke: (_) {},
      ),
    );

    await tester.pumpAndSettle();
    await tester.longPress(find.text('Alice'));
    await tester.pumpAndSettle();

    expect(find.text('Private message'), findsNothing);
    expect(find.text('Poke'), findsNothing);
  });

  testWidgets(
    'local voice activity does not light speaking state when blocked',
    (tester) async {
      await tester.pumpWidget(
        snapshotHarness(
          channels: [
            channel(id: 1, name: 'Default Channel', neededTalkPower: 10),
          ],
          clients: [client(id: 100, channelId: 1, name: 'Alice', talkPower: 0)],
          ownClientId: BigInt.from(100),
          currentVoiceChannelId: BigInt.from(1),
          audioStats: const rust.BridgeAudioStats(
            framesSent: 1,
            framesReceived: 0,
            pttActive: true,
            inputLevel: -30.0,
          ),
        ),
      );

      await tester.pumpAndSettle();

      final userText = tester.widget<Text>(find.text('Alice'));
      expect(find.byIcon(Icons.volume_off), findsOneWidget);
      expect(find.byIcon(Icons.mic), findsNothing);
      expect(userText.style?.fontWeight, isNot(FontWeight.w600));
    },
  );

  testWidgets('spacer channels render as layout rows and keep channel taps', (
    tester,
  ) async {
    final spacerChannel = rust.BridgeChannel(
      id: BigInt.from(42),
      parent: BigInt.from(7),
      name: '[cSpacerabc]Spacer Heading',
      order: 99,
      hasPassword: false,
      neededTalkPower: 12,
    );
    final normalChannel = rust.BridgeChannel(
      id: BigInt.from(43),
      parent: BigInt.zero,
      name: 'Normal Room',
      order: 100,
      hasPassword: false,
      neededTalkPower: 0,
    );
    rust.BridgeChannel? tapped;

    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: Scaffold(
          body: SnapshotView(
            snapshot: rust.BridgeSnapshot(
              serverName: 'Server',
              welcomeMessage: '',
              platform: '',
              version: '',
              channels: [spacerChannel, normalChannel],
              clients: const [],
              ownClientId: BigInt.one,
            ),
            audioStats: null,
            currentVoiceChannelId: null,
            pendingVoiceChannelId: null,
            localInputMuted: false,
            localOutputMuted: false,
            hasJoinPending: false,
            canJoinVoiceChannel: true,
            onJoinChannel: (channel) => tapped = channel,
            onJoinChannelWithPassword: (_) {},
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();
    expect(find.text('Spacer Heading'), findsOneWidget);
    expect(find.text('[cSpacerabc]Spacer Heading'), findsNothing);
    expect(find.byIcon(Icons.tag), findsOneWidget);

    final spacerText = tester.widget<Text>(find.text('Spacer Heading'));
    expect(spacerText.textAlign, TextAlign.center);

    await tester.tap(find.text('Spacer Heading'));
    await tester.pump();

    expect(tapped, isNotNull);
    expect(tapped!.id, BigInt.from(42));
    expect(tapped!.parent, BigInt.from(7));
    expect(tapped!.name, '[cSpacerabc]Spacer Heading');
    expect(tapped!.order, 99);
    expect(tapped!.hasPassword, isFalse);
    expect(tapped!.neededTalkPower, 12);
  });

  testWidgets('separator spacers render as line painters without raw text', (
    tester,
  ) async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: Scaffold(
          body: SnapshotView(
            snapshot: rust.BridgeSnapshot(
              serverName: 'Server',
              welcomeMessage: '',
              platform: '',
              version: '',
              channels: [
                rust.BridgeChannel(
                  id: BigInt.from(1),
                  parent: BigInt.zero,
                  name: '[spacer]---',
                  order: 1,
                  hasPassword: false,
                  neededTalkPower: 0,
                ),
              ],
              clients: const [],
              ownClientId: BigInt.one,
            ),
            audioStats: null,
            currentVoiceChannelId: null,
            pendingVoiceChannelId: null,
            localInputMuted: false,
            localOutputMuted: false,
            hasJoinPending: false,
            canJoinVoiceChannel: true,
            onJoinChannel: (_) {},
            onJoinChannelWithPassword: (_) {},
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();
    expect(find.text('[spacer]---'), findsNothing);
    expect(find.text('---'), findsNothing);
    expect(find.byType(CustomPaint), findsWidgets);
    expect(find.byIcon(Icons.tag), findsNothing);
  });

  testWidgets('repeating and blank spacers hide raw tags', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: AppL10n.localizationsDelegates,
        supportedLocales: AppL10n.supportedLocales,
        home: Scaffold(
          body: SnapshotView(
            snapshot: rust.BridgeSnapshot(
              serverName: 'Server',
              welcomeMessage: '',
              platform: '',
              version: '',
              channels: [
                rust.BridgeChannel(
                  id: BigInt.from(1),
                  parent: BigInt.zero,
                  name: '[*spacer0]#==',
                  order: 1,
                  hasPassword: false,
                  neededTalkPower: 0,
                ),
                rust.BridgeChannel(
                  id: BigInt.from(2),
                  parent: BigInt.zero,
                  name: '[rSpacer0].',
                  order: 2,
                  hasPassword: false,
                  neededTalkPower: 0,
                ),
              ],
              clients: const [],
              ownClientId: BigInt.one,
            ),
            audioStats: null,
            currentVoiceChannelId: null,
            pendingVoiceChannelId: null,
            localInputMuted: false,
            localOutputMuted: false,
            hasJoinPending: false,
            canJoinVoiceChannel: true,
            onJoinChannel: (_) {},
            onJoinChannelWithPassword: (_) {},
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();
    expect(find.text('[*spacer0]#=='), findsNothing);
    expect(find.text('[rSpacer0].'), findsNothing);
    expect(find.text('.'), findsNothing);
    expect(find.textContaining('#==#=='), findsOneWidget);
    expect(find.byIcon(Icons.tag), findsNothing);
  });
}
