// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert' show jsonDecode;

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'text_input_utils.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  group('DeltaTextInputClient', () {
    late FakeTextChannel fakeTextChannel;

    setUp(() {
      fakeTextChannel = FakeTextChannel((MethodCall call) async {});
      TextInput.setChannel(fakeTextChannel);
    });

    tearDown(() {
      TextInputConnection.debugResetId();
      TextInput.setChannel(SystemChannels.textInput);
    });

    test(
      'DeltaTextInputClient send the correct configuration to the platform and responds to updateEditingValueWithDeltas method correctly',
      () async {
        // Assemble a TextInputConnection so we can verify its change in state.
        final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(TextEditingValue.empty);
        const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
        TextInput.attach(client, configuration);
        expect(client.configuration.enableDeltaModel, true);

        expect(client.latestMethodCall, isEmpty);

        const String jsonDelta = '{'
            '"oldText": "",'
            ' "deltaText": "let there be text",'
            ' "deltaStart": 0,'
            ' "deltaEnd": 0,'
            ' "selectionBase": 17,'
            ' "selectionExtent": 17,'
            ' "selectionAffinity" : "TextAffinity.downstream" ,'
            ' "selectionIsDirectional": false,'
            ' "composingBase": -1,'
            ' "composingExtent": -1}';

        // Send updateEditingValueWithDeltas message.
        final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
          'args': <dynamic>[
            1,
            jsonDecode('{"deltas": [$jsonDelta]}'),
          ],
          'method': 'TextInputClient.updateEditingStateWithDeltas',
        });
        await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
          'flutter/textinput',
          messageBytes,
              (ByteData? _) {},
        );

        expect(client.latestMethodCall, 'updateEditingValueWithDeltas');
      },
    );

    test('Invalid TextRange fails loudly when being converted to JSON - NonTextUpdate', () async {
      final List<FlutterErrorDetails> record = <FlutterErrorDetails>[];
      FlutterError.onError = (FlutterErrorDetails details) {
        record.add(details);
      };

      final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(const TextEditingValue(text: '1'));
      const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
      TextInput.attach(client, configuration);

      const String jsonDelta = '{'
          '"oldText": "1",'
          ' "deltaText": "",'
          ' "deltaStart": -1,'
          ' "deltaEnd": -1,'
          ' "selectionBase": 3,'
          ' "selectionExtent": 3,'
          ' "selectionAffinity" : "TextAffinity.downstream" ,'
          ' "selectionIsDirectional": false,'
          ' "composingBase": -1,'
          ' "composingExtent": -1}';

      final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
        'method': 'TextInputClient.updateEditingStateWithDeltas',
        'args': <dynamic>[-1, jsonDecode('{"deltas": [$jsonDelta]}')],
      });

      await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
        'flutter/textinput',
        messageBytes,
        (ByteData? _) {},
      );
      expect(record.length, 1);
      // Verify the error message in parts because Web formats the message
      // differently from others.
      expect(record[0].exception.toString(), matches(RegExp(r'\bThe selection range: TextSelection.collapsed\(offset: 3, affinity: TextAffinity.downstream, isDirectional: false\)(?!\w)')));
      expect(record[0].exception.toString(), matches(RegExp(r'\bis not within the bounds of text: 1 of length: 1\b')));
    });

    test('Invalid TextRange fails loudly when being converted to JSON - Faulty deltaStart and deltaEnd', () async {
      final List<FlutterErrorDetails> record = <FlutterErrorDetails>[];
      FlutterError.onError = (FlutterErrorDetails details) {
        record.add(details);
      };

      final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(TextEditingValue.empty);
      const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
      TextInput.attach(client, configuration);

      const String jsonDelta = '{'
          '"oldText": "",'
          ' "deltaText": "hello",'
          ' "deltaStart": 0,'
          ' "deltaEnd": 1,'
          ' "selectionBase": 5,'
          ' "selectionExtent": 5,'
          ' "selectionAffinity" : "TextAffinity.downstream" ,'
          ' "selectionIsDirectional": false,'
          ' "composingBase": -1,'
          ' "composingExtent": -1}';

      final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
        'method': 'TextInputClient.updateEditingStateWithDeltas',
        'args': <dynamic>[-1, jsonDecode('{"deltas": [$jsonDelta]}')],
      });

      await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
        'flutter/textinput',
        messageBytes,
        (ByteData? _) {},
      );
      expect(record.length, 1);
      // Verify the error message in parts because Web formats the message
      // differently from others.
      expect(record[0].exception.toString(), matches(RegExp(r'\bThe delta range: TextRange\(start: 0, end: 5\)(?!\w)')));
      expect(record[0].exception.toString(), matches(RegExp(r'\bis not within the bounds of text:  of length: 0\b')));
    });

    test('Invalid TextRange fails loudly when being converted to JSON - Faulty Selection', () async {
      final List<FlutterErrorDetails> record = <FlutterErrorDetails>[];
      FlutterError.onError = (FlutterErrorDetails details) {
        record.add(details);
      };

      final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(TextEditingValue.empty);
      const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
      TextInput.attach(client, configuration);

      const String jsonDelta = '{'
          '"oldText": "",'
          ' "deltaText": "hello",'
          ' "deltaStart": 0,'
          ' "deltaEnd": 0,'
          ' "selectionBase": 6,'
          ' "selectionExtent": 6,'
          ' "selectionAffinity" : "TextAffinity.downstream" ,'
          ' "selectionIsDirectional": false,'
          ' "composingBase": -1,'
          ' "composingExtent": -1}';

      final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
        'method': 'TextInputClient.updateEditingStateWithDeltas',
        'args': <dynamic>[-1, jsonDecode('{"deltas": [$jsonDelta]}')],
      });

      await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
        'flutter/textinput',
        messageBytes,
        (ByteData? _) {},
      );
      expect(record.length, 1);
      // Verify the error message in parts because Web formats the message
      // differently from others.
      expect(record[0].exception.toString(), matches(RegExp(r'\bThe selection range: TextSelection.collapsed\(offset: 6, affinity: TextAffinity.downstream, isDirectional: false\)(?!\w)')));
      expect(record[0].exception.toString(), matches(RegExp(r'\bis not within the bounds of text: hello of length: 5\b')));
    });

    test('Invalid TextRange fails loudly when being converted to JSON - Faulty Composing Region', () async {
      final List<FlutterErrorDetails> record = <FlutterErrorDetails>[];
      FlutterError.onError = (FlutterErrorDetails details) {
        record.add(details);
      };

      final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(const TextEditingValue(text: 'worl'));
      const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
      TextInput.attach(client, configuration);

      const String jsonDelta = '{'
          '"oldText": "worl",'
          ' "deltaText": "world",'
          ' "deltaStart": 0,'
          ' "deltaEnd": 4,'
          ' "selectionBase": 5,'
          ' "selectionExtent": 5,'
          ' "selectionAffinity" : "TextAffinity.downstream" ,'
          ' "selectionIsDirectional": false,'
          ' "composingBase": 0,'
          ' "composingExtent": 6}';

      final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
        'method': 'TextInputClient.updateEditingStateWithDeltas',
        'args': <dynamic>[-1, jsonDecode('{"deltas": [$jsonDelta]}')],
      });

      await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
        'flutter/textinput',
        messageBytes,
        (ByteData? _) {},
      );
      expect(record.length, 1);
      // Verify the error message in parts because Web formats the message
      // differently from others.
      expect(record[0].exception.toString(), matches(RegExp(r'\bThe composing range: TextRange\(start: 0, end: 6\)(?!\w)')));
      expect(record[0].exception.toString(), matches(RegExp(r'\bis not within the bounds of text: world of length: 5\b')));
    });
  });
}

class FakeDeltaTextInputClient implements DeltaTextInputClient {
  FakeDeltaTextInputClient(this.currentTextEditingValue);

  String latestMethodCall = '';

  @override
  TextEditingValue currentTextEditingValue;

  @override
  AutofillScope? get currentAutofillScope => null;

  @override
  void performAction(TextInputAction action) {
    latestMethodCall = 'performAction';
  }

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {
    latestMethodCall = 'performPrivateCommand';
  }

  @override
  void insertContent(KeyboardInsertedContent content) {
    latestMethodCall = 'commitContent';
  }

  @override
  void updateEditingValue(TextEditingValue value) {
    latestMethodCall = 'updateEditingValue';
  }

  @override
  void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
    latestMethodCall = 'updateEditingValueWithDeltas';
  }

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {
    latestMethodCall = 'updateFloatingCursor';
  }

  @override
  void connectionClosed() {
    latestMethodCall = 'connectionClosed';
  }

  @override
  void showAutocorrectionPromptRect(int start, int end) {
    latestMethodCall = 'showAutocorrectionPromptRect';
  }

  @override
  void insertTextPlaceholder(Size size) {
    latestMethodCall = 'insertTextPlaceholder';
  }

  @override
  void removeTextPlaceholder() {
    latestMethodCall = 'removeTextPlaceholder';
  }

  @override
  void showToolbar() {
    latestMethodCall = 'showToolbar';
  }

  @override
  void performSelector(String selectorName) {
    latestMethodCall = 'performSelector';
  }

  TextInputConfiguration get configuration => const TextInputConfiguration(enableDeltaModel: true);

  @override
  void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
    latestMethodCall = 'didChangeInputControl';
  }
}
