import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:plezy/focus/dpad_navigator.dart';
import 'package:plezy/utils/platform_detector.dart';
import 'package:plezy/widgets/tv_virtual_keyboard.dart';

void main() {
  tearDown(() {
    TvDetectionService.debugSetAppleTVOverride(null);
  });

  testWidgets('keyboard enter submits without inserting highlighted key', (tester) async {
    final controller = TextEditingController();
    String? submitted;
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller, onSubmitted: (value) => submitted = value);

    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
    await tester.pumpAndSettle();

    expect(controller.text, isEmpty);
    expect(submitted, isEmpty);
    expect(find.byType(Dialog), findsNothing);
  });

  testWidgets('engine-synthesized select activates highlighted key', (tester) async {
    final controller = TextEditingController(text: 'query');
    String? submitted;
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller, onSubmitted: (value) => submitted = value);

    await tester.sendKeyEvent(LogicalKeyboardKey.select);
    await tester.pumpAndSettle();

    expect(controller.text, 'query1');
    expect(submitted, isNull);
    expect(find.byType(Dialog), findsOneWidget);
  });

  testWidgets('held select activates the highlighted key once', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    await tester.sendKeyDownEvent(LogicalKeyboardKey.select);
    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.select);
    await tester.sendKeyUpEvent(LogicalKeyboardKey.select);
    await tester.pump();

    expect(controller.text, '1');
    expect(find.byType(Dialog), findsOneWidget);
  });

  testWidgets('unowned keyboard keys remain available to ancestor handlers', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    final result = _dispatchKey(
      const KeyDownEvent(
        physicalKey: PhysicalKeyboardKey.f12,
        logicalKey: LogicalKeyboardKey.f12,
        timeStamp: Duration.zero,
      ),
    );

    expect(result, KeyEventResult.ignored);
    expect(controller.text, isEmpty);
    expect(find.byType(Dialog), findsOneWidget);
  });
  testWidgets('directional pad enter activates highlighted key', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    _dispatchKey(
      const KeyDownEvent(
        physicalKey: PhysicalKeyboardKey.enter,
        logicalKey: LogicalKeyboardKey.enter,
        timeStamp: Duration.zero,
        deviceType: ui.KeyEventDeviceType.directionalPad,
      ),
    );
    await tester.pump();

    expect(controller.text, '1');
    expect(find.byType(Dialog), findsOneWidget);
  });

  testWidgets('directional navigation selects the expected key and wraps around spacers', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
    await tester.sendKeyEvent(LogicalKeyboardKey.select);
    await tester.pump();
    expect(controller.text, '2');

    // Re-open at the first key, then wrap left past the leading spacer to 0.
    await tester.sendKeyEvent(LogicalKeyboardKey.escape);
    await tester.pumpAndSettle();
    await _pumpKeyboard(tester, controller: controller);
    await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
    await tester.sendKeyEvent(LogicalKeyboardKey.select);
    await tester.pump();
    expect(controller.text, '20');
  });

  testWidgets('external controller changes rebuild the preview', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);
    controller.text = 'updated externally';
    await tester.pump();

    expect(find.text('updated externally'), findsOneWidget);
  });

  testWidgets('keyboard enter inserts newline for multiline input', (tester) async {
    final controller = TextEditingController(text: 'a');
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller, keyboardType: TextInputType.multiline, maxLines: 2);

    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
    await tester.pumpAndSettle();

    expect(controller.text, 'a\n');
    expect(find.byType(Dialog), findsNothing);
  });

  testWidgets('physical keyboard character inserts text and hides keyboard', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
    await tester.pumpAndSettle();

    expect(controller.text, 'a');
    expect(find.byType(Dialog), findsNothing);
  });

  testWidgets('physical keyboard backspace deletes existing text from end and hides keyboard', (tester) async {
    final controller = TextEditingController(text: 'query');
    controller.selection = const TextSelection.collapsed(offset: 0);
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
    await tester.pumpAndSettle();

    expect(controller.text, 'quer');
    expect(find.byType(Dialog), findsNothing);
  });

  testWidgets('keyboard is compact and bottom aligned on TV', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    final panelRect = tester.getRect(find.byKey(const Key('tv_virtual_keyboard_panel')));
    expect(panelRect.height, lessThanOrEqualTo(330));
    expect(panelRect.width, lessThanOrEqualTo(650));
    expect(panelRect.bottom, greaterThan(680));
  });

  testWidgets('keyboard keeps equals on main page and exposes symbols page', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    await _pumpKeyboard(tester, controller: controller);

    expect(find.text('='), findsOneWidget);
    expect(find.byIcon(Symbols.functions_rounded), findsOneWidget);

    await tester.tap(find.byIcon(Symbols.functions_rounded));
    await tester.pumpAndSettle();

    expect(find.text('ABC'), findsOneWidget);
    expect(find.text('!'), findsOneWidget);
    expect(find.text('='), findsOneWidget);
  });

  testWidgets('back key dismisses on key up without leaking to underlying route', (tester) async {
    TvDetectionService.debugSetAppleTVOverride(true);
    await tester.binding.setSurfaceSize(const Size(1280, 720));
    addTearDown(() => tester.binding.setSurfaceSize(null));
    final controller = TextEditingController();
    final underlyingFocusNode = FocusNode(debugLabel: 'underlying_route');
    var underlyingBackEvents = 0;
    late BuildContext context;
    addTearDown(controller.dispose);
    addTearDown(underlyingFocusNode.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Focus(
            focusNode: underlyingFocusNode,
            autofocus: true,
            onKeyEvent: (_, event) {
              if (event.logicalKey.isBackKey) {
                underlyingBackEvents++;
                return KeyEventResult.handled;
              }
              return KeyEventResult.ignored;
            },
            child: Builder(
              builder: (builderContext) {
                context = builderContext;
                return const SizedBox.shrink();
              },
            ),
          ),
        ),
      ),
    );

    showTvVirtualKeyboard(context: context, controller: controller);
    await tester.pumpAndSettle();
    expect(find.byType(Dialog), findsOneWidget);

    await tester.sendKeyDownEvent(LogicalKeyboardKey.escape);
    await tester.pump();

    expect(find.byType(Dialog), findsOneWidget);
    expect(underlyingBackEvents, 0);

    await tester.sendKeyUpEvent(LogicalKeyboardKey.escape);
    await tester.pumpAndSettle();

    expect(find.byType(Dialog), findsNothing);
    expect(underlyingBackEvents, 0);
  });

  testWidgets('rendered backspace deletes whole graphemes and keeps the keyboard open', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    for (final grapheme in ['😀', 'e\u0301', '🇯🇵', '👨‍👩‍👧‍👦']) {
      controller.value = TextEditingValue(
        text: 'A${grapheme}B',
        selection: TextSelection.collapsed(offset: 1 + grapheme.length),
      );
      await _pumpKeyboard(tester, controller: controller);

      await tester.tap(find.byIcon(Symbols.backspace_rounded));
      await tester.pump();

      expect(controller.text, 'AB');
      expect(controller.selection, const TextSelection.collapsed(offset: 1));
      expect(find.byType(Dialog), findsOneWidget);

      await tester.sendKeyEvent(LogicalKeyboardKey.escape);
      await tester.pumpAndSettle();
    }
  });

  testWidgets('physical delete removes a whole grapheme and dismisses the keyboard', (tester) async {
    final controller = TextEditingController();
    addTearDown(controller.dispose);

    for (final grapheme in ['😀', 'e\u0301', '🇯🇵', '👨‍👩‍👧‍👦']) {
      controller.value = TextEditingValue(text: 'A${grapheme}B', selection: const TextSelection.collapsed(offset: 1));
      await _pumpKeyboard(tester, controller: controller);

      await tester.sendKeyEvent(LogicalKeyboardKey.delete);
      await tester.pumpAndSettle();

      expect(controller.text, 'AB');
      expect(controller.selection, const TextSelection.collapsed(offset: 1));
      expect(find.byType(Dialog), findsNothing);
    }
  });

  testWidgets('grapheme selection deletion preserves formatter and callback ordering', (tester) async {
    const grapheme = '🇯🇵';
    final controller = TextEditingController()
      ..value = TextEditingValue(
        text: 'A${grapheme}B',
        selection: TextSelection(baseOffset: grapheme.length, extentOffset: 1),
      );
    final formatterCandidates = <TextEditingValue>[];
    final changes = <String>[];
    addTearDown(controller.dispose);

    await _pumpKeyboard(
      tester,
      controller: controller,
      maxLength: 8,
      inputFormatters: [
        TextInputFormatter.withFunction((_, nextValue) {
          formatterCandidates.add(nextValue);
          return nextValue;
        }),
      ],
      onChanged: changes.add,
    );

    await tester.tap(find.byIcon(Symbols.backspace_rounded));
    await tester.pump();

    expect(formatterCandidates.single.text, 'AB');
    expect(controller.text, 'AB');
    expect(controller.selection, const TextSelection.collapsed(offset: 1));
    expect(changes, ['AB']);
  });
}

Future<void> _pumpKeyboard(
  WidgetTester tester, {
  required TextEditingController controller,
  TextInputType? keyboardType,
  int? maxLines,
  List<TextInputFormatter>? inputFormatters,
  int? maxLength,
  ValueChanged<String>? onChanged,
  ValueChanged<String>? onSubmitted,
}) async {
  TvDetectionService.debugSetAppleTVOverride(true);
  await tester.binding.setSurfaceSize(const Size(1280, 720));
  addTearDown(() => tester.binding.setSurfaceSize(null));
  late BuildContext context;

  await tester.pumpWidget(
    MaterialApp(
      home: Builder(
        builder: (builderContext) {
          context = builderContext;
          return const SizedBox.shrink();
        },
      ),
    ),
  );

  showTvVirtualKeyboard(
    context: context,
    controller: controller,
    keyboardType: keyboardType,
    maxLines: maxLines,
    inputFormatters: inputFormatters,
    maxLength: maxLength,
    onChanged: onChanged,
    onSubmitted: onSubmitted,
  );
  await tester.pumpAndSettle();
}

KeyEventResult _dispatchKey(KeyEvent event) {
  FocusNode? node = FocusManager.instance.primaryFocus;
  while (node != null) {
    final result = node.onKeyEvent?.call(node, event) ?? KeyEventResult.ignored;
    if (result == KeyEventResult.handled) return result;
    node = node.parent;
  }
  return KeyEventResult.ignored;
}
