// Copyright 2013 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.

#include <utility>

#include "flutter/shell/platform/linux/fl_method_codec_private.h"
#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_method_codec.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"
#include "flutter/shell/platform/linux/testing/fl_test.h"
#include "flutter/shell/platform/linux/testing/mock_binary_messenger.h"
#include "flutter/shell/platform/linux/testing/mock_binary_messenger_response_handle.h"
#include "flutter/shell/platform/linux/testing/mock_im_context.h"
#include "flutter/shell/platform/linux/testing/mock_text_input_view_delegate.h"
#include "flutter/testing/testing.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"

void printTo(FlMethodResponse* response, ::std::ostream* os) {
  *os << ::testing::PrintToString(
      fl_method_response_get_result(response, nullptr));
}

MATCHER_P(SuccessResponse, result, "") {
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(FlMethodResponse) response =
      fl_method_codec_decode_response(FL_METHOD_CODEC(codec), arg, nullptr);
  if (fl_value_equal(fl_method_response_get_result(response, nullptr),
                     result)) {
    return true;
  }
  *result_listener << ::testing::PrintToString(response);
  return false;
}

MATCHER_P(FlValueEq, value, "equal to " + ::testing::PrintToString(value)) {
  return fl_value_equal(arg, value);
}

class MethodCallMatcher {
 public:
  using is_gtest_matcher = void;

  explicit MethodCallMatcher(::testing::Matcher<std::string> name,
                             ::testing::Matcher<FlValue*> args)
      : name_(std::move(name)), args_(std::move(args)) {}

  bool MatchAndExplain(GBytes* method_call,
                       ::testing::MatchResultListener* result_listener) const {
    g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
    g_autoptr(GError) error = nullptr;
    g_autofree gchar* name = nullptr;
    g_autoptr(FlValue) args = nullptr;
    gboolean result = fl_method_codec_decode_method_call(
        FL_METHOD_CODEC(codec), method_call, &name, &args, &error);
    if (!result) {
      *result_listener << ::testing::PrintToString(error->message);
      return false;
    }
    if (!name_.MatchAndExplain(name, result_listener)) {
      *result_listener << " where the name doesn't match: \"" << name << "\"";
      return false;
    }
    if (!args_.MatchAndExplain(args, result_listener)) {
      *result_listener << " where the args don't match: "
                       << ::testing::PrintToString(args);
      return false;
    }
    return true;
  }

  void DescribeTo(std::ostream* os) const {
    *os << "method name ";
    name_.DescribeTo(os);
    *os << " and args ";
    args_.DescribeTo(os);
  }

  void DescribeNegationTo(std::ostream* os) const {
    *os << "method name ";
    name_.DescribeNegationTo(os);
    *os << " or args ";
    args_.DescribeNegationTo(os);
  }

 private:
  ::testing::Matcher<std::string> name_;
  ::testing::Matcher<FlValue*> args_;
};

::testing::Matcher<GBytes*> MethodCall(const std::string& name,
                                       ::testing::Matcher<FlValue*> args) {
  return MethodCallMatcher(::testing::StrEq(name), std::move(args));
}

static FlValue* build_map(std::map<const gchar*, FlValue*> args) {
  FlValue* value = fl_value_new_map();
  for (auto it = args.begin(); it != args.end(); ++it) {
    fl_value_set_string_take(value, it->first, it->second);
  }
  return value;
}

static FlValue* build_list(std::vector<FlValue*> args) {
  FlValue* value = fl_value_new_list();
  for (auto it = args.begin(); it != args.end(); ++it) {
    fl_value_append_take(value, *it);
  }
  return value;
}

struct InputConfig {
  int64_t client_id = -1;
  const gchar* input_type = "TextInputType.text";
  const gchar* input_action = "TextInputAction.none";
  gboolean enable_delta_model = false;
};

static FlValue* build_input_config(InputConfig config) {
  return build_list({
      fl_value_new_int(config.client_id),
      build_map({
          {"inputAction", fl_value_new_string(config.input_action)},
          {"inputType", build_map({
                            {"name", fl_value_new_string(config.input_type)},
                        })},
          {"enableDeltaModel", fl_value_new_bool(config.enable_delta_model)},
      }),
  });
}

struct EditingState {
  const gchar* text = "";
  int selection_base = -1;
  int selection_extent = -1;
  int composing_base = -1;
  int composing_extent = -1;
};

static FlValue* build_editing_state(EditingState state) {
  return build_map({
      {"text", fl_value_new_string(state.text)},
      {"selectionBase", fl_value_new_int(state.selection_base)},
      {"selectionExtent", fl_value_new_int(state.selection_extent)},
      {"selectionAffinity", fl_value_new_string("TextAffinity.downstream")},
      {"selectionIsDirectional", fl_value_new_bool(false)},
      {"composingBase", fl_value_new_int(state.composing_base)},
      {"composingExtent", fl_value_new_int(state.composing_extent)},
  });
}

struct EditingDelta {
  const gchar* old_text = "";
  const gchar* delta_text = "";
  int delta_start = -1;
  int delta_end = -1;
  int selection_base = -1;
  int selection_extent = -1;
  int composing_base = -1;
  int composing_extent = -1;
};

static FlValue* build_editing_delta(EditingDelta delta) {
  return build_map({
      {"oldText", fl_value_new_string(delta.old_text)},
      {"deltaText", fl_value_new_string(delta.delta_text)},
      {"deltaStart", fl_value_new_int(delta.delta_start)},
      {"deltaEnd", fl_value_new_int(delta.delta_end)},
      {"selectionBase", fl_value_new_int(delta.selection_base)},
      {"selectionExtent", fl_value_new_int(delta.selection_extent)},
      {"selectionAffinity", fl_value_new_string("TextAffinity.downstream")},
      {"selectionIsDirectional", fl_value_new_bool(false)},
      {"composingBase", fl_value_new_int(delta.composing_base)},
      {"composingExtent", fl_value_new_int(delta.composing_extent)},
  });
}

static void send_key_event(FlTextInputPlugin* plugin,
                           gint keyval,
                           gint state = 0) {
  GdkEvent* gdk_event = gdk_event_new(GDK_KEY_PRESS);
  gdk_event->key.keyval = keyval;
  gdk_event->key.state = state;
  FlKeyEvent* key_event = fl_key_event_new_from_gdk_event(gdk_event);
  fl_text_input_plugin_filter_keypress(plugin, key_event);
  fl_key_event_dispose(key_event);
}

TEST(FlTextInputPluginTest, MessageHandler) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  EXPECT_TRUE(messenger.HasMessageHandler("flutter/textinput"));
}

TEST(FlTextInputPluginTest, SetClient) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  g_autoptr(FlValue) args = build_input_config({.client_id = 1});
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", message);
}

TEST(FlTextInputPluginTest, Show) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  EXPECT_CALL(context,
              gtk_im_context_focus_in(::testing::Eq<GtkIMContext*>(context)));

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.show", nullptr, nullptr);

  messenger.ReceiveMessage("flutter/textinput", message);
}

TEST(FlTextInputPluginTest, Hide) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  EXPECT_CALL(context,
              gtk_im_context_focus_out(::testing::Eq<GtkIMContext*>(context)));

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.hide", nullptr, nullptr);

  messenger.ReceiveMessage("flutter/textinput", message);
}

TEST(FlTextInputPluginTest, ClearClient) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.clearClient", nullptr, nullptr);

  messenger.ReceiveMessage("flutter/textinput", message);
}

TEST(FlTextInputPluginTest, PerformAction) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set input config
  g_autoptr(FlValue) config = build_input_config({
      .client_id = 1,
      .input_type = "TextInputType.multiline",
      .input_action = "TextInputAction.newline",
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // set editing state
  g_autoptr(FlValue) state = build_editing_state({
      .text = "Flutter",
      .selection_base = 7,
      .selection_extent = 7,
  });
  g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_state);

  // update editing state
  g_autoptr(FlValue) new_state = build_list({
      fl_value_new_int(1),  // client_id
      build_editing_state({
          .text = "Flutter\n",
          .selection_base = 8,
          .selection_extent = 8,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(new_state)),
                             ::testing::_, ::testing::_, ::testing::_));

  // perform action
  g_autoptr(FlValue) action = build_list({
      fl_value_new_int(1),  // client_id
      fl_value_new_string("TextInputAction.newline"),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.performAction",
                                        FlValueEq(action)),
                             ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_Return);
}

// Regression test for https://github.com/flutter/flutter/issues/125879.
TEST(FlTextInputPluginTest, MultilineWithSendAction) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // Set input config.
  g_autoptr(FlValue) config = build_input_config({
      .client_id = 1,
      .input_type = "TextInputType.multiline",
      .input_action = "TextInputAction.send",
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // Set editing state.
  g_autoptr(FlValue) state = build_editing_state({
      .text = "Flutter",
      .selection_base = 7,
      .selection_extent = 7,
  });
  g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_state);

  // Perform action.
  g_autoptr(FlValue) action = build_list({
      fl_value_new_int(1),  // client_id
      fl_value_new_string("TextInputAction.send"),
  });

  // Because the input action is not set to TextInputAction.newline, the next
  // expected call is "TextInputClient.performAction". If the input action was
  // set to TextInputAction.newline the next call would be
  // "TextInputClient.updateEditingState" (this case is tested in the test named
  // 'PerformAction').
  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.performAction",
                                        FlValueEq(action)),
                             ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_Return);
}

TEST(FlTextInputPluginTest, MoveCursor) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set input config
  g_autoptr(FlValue) config = build_input_config({.client_id = 1});
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // set editing state
  g_autoptr(FlValue) state = build_editing_state({
      .text = "Flutter",
      .selection_base = 4,
      .selection_extent = 4,
  });
  g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_state);

  // move cursor to beginning
  g_autoptr(FlValue) beginning = build_list({
      fl_value_new_int(1),  // client_id
      build_editing_state({
          .text = "Flutter",
          .selection_base = 0,
          .selection_extent = 0,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(beginning)),
                             ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_Home);

  // move cursor to end
  g_autoptr(FlValue) end = build_list({
      fl_value_new_int(1),  // client_id
      build_editing_state({
          .text = "Flutter",
          .selection_base = 7,
          .selection_extent = 7,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(end)),
                             ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_End);
}

TEST(FlTextInputPluginTest, Select) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set input config
  g_autoptr(FlValue) config = build_input_config({.client_id = 1});
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // set editing state
  g_autoptr(FlValue) state = build_editing_state({
      .text = "Flutter",
      .selection_base = 4,
      .selection_extent = 4,
  });
  g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_state);

  // select to end
  g_autoptr(FlValue) select_to_end = build_list({
      fl_value_new_int(1),  // client_id
      build_editing_state({
          .text = "Flutter",
          .selection_base = 4,
          .selection_extent = 7,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(select_to_end)),
                             ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_End, GDK_SHIFT_MASK);

  // select to beginning
  g_autoptr(FlValue) select_to_beginning = build_list({
      fl_value_new_int(1),  // client_id
      build_editing_state({
          .text = "Flutter",
          .selection_base = 4,
          .selection_extent = 0,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(select_to_beginning)),
                             ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_Home, GDK_SHIFT_MASK);
}

TEST(FlTextInputPluginTest, Composing) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  g_signal_emit_by_name(context, "preedit-start", nullptr);

  // update
  EXPECT_CALL(context,
              gtk_im_context_get_preedit_string(
                  ::testing::Eq<GtkIMContext*>(context),
                  ::testing::A<gchar**>(), ::testing::_, ::testing::A<gint*>()))
      .WillOnce(
          ::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter")),
                           ::testing::SetArgPointee<3>(0)));

  g_autoptr(FlValue) state = build_list({
      fl_value_new_int(-1),  // client_id
      build_editing_state({
          .text = "Flutter",
          .selection_base = 0,
          .selection_extent = 0,
          .composing_base = 0,
          .composing_extent = 7,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(state)),
                             ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "preedit-changed", nullptr);

  // commit
  g_autoptr(FlValue) commit = build_list({
      fl_value_new_int(-1),  // client_id
      build_editing_state({
          .text = "engine",
          .selection_base = 6,
          .selection_extent = 6,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(commit)),
                             ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "engine", nullptr);

  // end
  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        ::testing::_),
                             ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "preedit-end", nullptr);
}

TEST(FlTextInputPluginTest, SurroundingText) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set input config
  g_autoptr(FlValue) config = build_input_config({.client_id = 1});
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // set editing state
  g_autoptr(FlValue) state = build_editing_state({
      .text = "Flutter",
      .selection_base = 3,
      .selection_extent = 3,
  });
  g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_state);

  // retrieve
  EXPECT_CALL(context, gtk_im_context_set_surrounding(
                           ::testing::Eq<GtkIMContext*>(context),
                           ::testing::StrEq("Flutter"), 7, 3));

  gboolean retrieved = false;
  g_signal_emit_by_name(context, "retrieve-surrounding", &retrieved, nullptr);
  EXPECT_TRUE(retrieved);

  // delete
  g_autoptr(FlValue) update = build_list({
      fl_value_new_int(1),  // client_id
      build_editing_state({
          .text = "Flutr",
          .selection_base = 3,
          .selection_extent = 3,
      }),
  });

  EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::StrEq("flutter/textinput"),
                             MethodCall("TextInputClient.updateEditingState",
                                        FlValueEq(update)),
                             ::testing::_, ::testing::_, ::testing::_));

  gboolean deleted = false;
  g_signal_emit_by_name(context, "delete-surrounding", 1, 2, &deleted, nullptr);
  EXPECT_TRUE(deleted);
}

TEST(FlTextInputPluginTest, SetMarkedTextRect) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  g_signal_emit_by_name(context, "preedit-start", nullptr);

  // set editable size and transform
  g_autoptr(FlValue) size_and_transform = build_map({
      {
          "transform",
          build_list({
              fl_value_new_float(1),
              fl_value_new_float(2),
              fl_value_new_float(3),
              fl_value_new_float(4),
              fl_value_new_float(5),
              fl_value_new_float(6),
              fl_value_new_float(7),
              fl_value_new_float(8),
              fl_value_new_float(9),
              fl_value_new_float(10),
              fl_value_new_float(11),
              fl_value_new_float(12),
              fl_value_new_float(13),
              fl_value_new_float(14),
              fl_value_new_float(15),
              fl_value_new_float(16),
          }),
      },
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_editable_size_and_transform =
      fl_method_codec_encode_method_call(
          FL_METHOD_CODEC(codec), "TextInput.setEditableSizeAndTransform",
          size_and_transform, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput",
                           set_editable_size_and_transform);

  // set marked text rect
  g_autoptr(FlValue) rect = build_map({
      {"x", fl_value_new_float(1)},
      {"y", fl_value_new_float(2)},
      {"width", fl_value_new_float(3)},
      {"height", fl_value_new_float(4)},
  });
  g_autoptr(GBytes) set_marked_text_rect = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setMarkedTextRect", rect, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  EXPECT_CALL(delegate, fl_text_input_view_delegate_translate_coordinates(
                            ::testing::Eq<FlTextInputViewDelegate*>(delegate),
                            ::testing::Eq(27), ::testing::Eq(32), ::testing::_,
                            ::testing::_))
      .WillOnce(::testing::DoAll(::testing::SetArgPointee<3>(123),
                                 ::testing::SetArgPointee<4>(456)));

  EXPECT_CALL(context, gtk_im_context_set_cursor_location(
                           ::testing::Eq<GtkIMContext*>(context),
                           ::testing::Pointee(::testing::AllOf(
                               ::testing::Field(&GdkRectangle::x, 123),
                               ::testing::Field(&GdkRectangle::y, 456),
                               ::testing::Field(&GdkRectangle::width, 0),
                               ::testing::Field(&GdkRectangle::height, 0)))));

  messenger.ReceiveMessage("flutter/textinput", set_marked_text_rect);
}

TEST(FlTextInputPluginTest, TextInputTypeNone) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  g_autoptr(FlValue) args = build_input_config({
      .client_id = 1,
      .input_type = "TextInputType.none",
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::A<FlBinaryMessengerResponseHandle*>(),
                             SuccessResponse(null), ::testing::A<GError**>()))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  EXPECT_CALL(context,
              gtk_im_context_focus_in(::testing::Eq<GtkIMContext*>(context)))
      .Times(0);
  EXPECT_CALL(context,
              gtk_im_context_focus_out(::testing::Eq<GtkIMContext*>(context)));

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  g_autoptr(GBytes) show = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.show", nullptr, nullptr);

  messenger.ReceiveMessage("flutter/textinput", show);
}

TEST(FlTextInputPluginTest, TextEditingDelta) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set config
  g_autoptr(FlValue) args = build_input_config({
      .client_id = 1,
      .enable_delta_model = true,
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::A<FlBinaryMessengerResponseHandle*>(),
                             SuccessResponse(null), ::testing::A<GError**>()))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // set editing state
  g_autoptr(FlValue) state = build_editing_state({
      .text = "Flutter",
      .selection_base = 7,
      .selection_extent = 7,
  });
  g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr);

  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::_, SuccessResponse(null), ::testing::_))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_state);

  // update editing state with deltas
  g_autoptr(FlValue) deltas = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flutter",
                  .delta_text = "Flutter",
                  .delta_start = 7,
                  .delta_end = 7,
                  .selection_base = 0,
                  .selection_extent = 0,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(deltas)),
                  ::testing::_, ::testing::_, ::testing::_));

  send_key_event(plugin, GDK_KEY_Home);
}

TEST(FlTextInputPluginTest, ComposingDelta) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set config
  g_autoptr(FlValue) args = build_input_config({
      .client_id = 1,
      .enable_delta_model = true,
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::A<FlBinaryMessengerResponseHandle*>(),
                             SuccessResponse(null), ::testing::A<GError**>()))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  g_signal_emit_by_name(context, "preedit-start", nullptr);

  // update
  EXPECT_CALL(context,
              gtk_im_context_get_preedit_string(
                  ::testing::Eq<GtkIMContext*>(context),
                  ::testing::A<gchar**>(), ::testing::_, ::testing::A<gint*>()))
      .WillOnce(
          ::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter ")),
                           ::testing::SetArgPointee<3>(8)));

  g_autoptr(FlValue) update = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "",
                  .delta_text = "Flutter ",
                  .delta_start = 0,
                  .delta_end = 0,
                  .selection_base = 8,
                  .selection_extent = 8,
                  .composing_base = 0,
                  .composing_extent = 8,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(update)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "preedit-changed", nullptr);

  // commit
  g_autoptr(FlValue) commit = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flutter ",
                  .delta_text = "Flutter engine",
                  .delta_start = 0,
                  .delta_end = 8,
                  .selection_base = 14,
                  .selection_extent = 14,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commit)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "Flutter engine", nullptr);

  // end
  g_autoptr(FlValue) end = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flutter engine",
                  .selection_base = 14,
                  .selection_extent = 14,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(end)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "preedit-end", nullptr);
}

TEST(FlTextInputPluginTest, NonComposingDelta) {
  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
  ::testing::NiceMock<flutter::testing::MockIMContext> context;
  ::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;

  g_autoptr(FlTextInputPlugin) plugin =
      fl_text_input_plugin_new(messenger, context, delegate);
  EXPECT_NE(plugin, nullptr);

  // set config
  g_autoptr(FlValue) args = build_input_config({
      .client_id = 1,
      .enable_delta_model = true,
  });
  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
  g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
      FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);

  g_autoptr(FlValue) null = fl_value_new_null();
  EXPECT_CALL(messenger, fl_binary_messenger_send_response(
                             ::testing::Eq<FlBinaryMessenger*>(messenger),
                             ::testing::A<FlBinaryMessengerResponseHandle*>(),
                             SuccessResponse(null), ::testing::A<GError**>()))
      .WillOnce(::testing::Return(true));

  messenger.ReceiveMessage("flutter/textinput", set_client);

  // commit F
  g_autoptr(FlValue) commit = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "",
                  .delta_text = "F",
                  .delta_start = 0,
                  .delta_end = 0,
                  .selection_base = 1,
                  .selection_extent = 1,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commit)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "F", nullptr);

  // commit l
  g_autoptr(FlValue) commitL = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "F",
                  .delta_text = "l",
                  .delta_start = 1,
                  .delta_end = 1,
                  .selection_base = 2,
                  .selection_extent = 2,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commitL)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "l", nullptr);

  // commit u
  g_autoptr(FlValue) commitU = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Fl",
                  .delta_text = "u",
                  .delta_start = 2,
                  .delta_end = 2,
                  .selection_base = 3,
                  .selection_extent = 3,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commitU)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "u", nullptr);

  // commit t
  g_autoptr(FlValue) commitTa = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flu",
                  .delta_text = "t",
                  .delta_start = 3,
                  .delta_end = 3,
                  .selection_base = 4,
                  .selection_extent = 4,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commitTa)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "t", nullptr);

  // commit t again
  g_autoptr(FlValue) commitTb = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flut",
                  .delta_text = "t",
                  .delta_start = 4,
                  .delta_end = 4,
                  .selection_base = 5,
                  .selection_extent = 5,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commitTb)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "t", nullptr);

  // commit e
  g_autoptr(FlValue) commitE = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flutt",
                  .delta_text = "e",
                  .delta_start = 5,
                  .delta_end = 5,
                  .selection_base = 6,
                  .selection_extent = 6,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commitE)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "e", nullptr);

  // commit r
  g_autoptr(FlValue) commitR = build_list({
      fl_value_new_int(1),  // client_id
      build_map({{
          "deltas",
          build_list({
              build_editing_delta({
                  .old_text = "Flutte",
                  .delta_text = "r",
                  .delta_start = 6,
                  .delta_end = 6,
                  .selection_base = 7,
                  .selection_extent = 7,
                  .composing_base = -1,
                  .composing_extent = -1,
              }),
          }),
      }}),
  });

  EXPECT_CALL(messenger,
              fl_binary_messenger_send_on_channel(
                  ::testing::Eq<FlBinaryMessenger*>(messenger),
                  ::testing::StrEq("flutter/textinput"),
                  MethodCall("TextInputClient.updateEditingStateWithDeltas",
                             FlValueEq(commitR)),
                  ::testing::_, ::testing::_, ::testing::_));

  g_signal_emit_by_name(context, "commit", "r", nullptr);
}
