// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/memory/raw_ptr.h"

#import "content/browser/renderer_host/text_input_client_mac.h"

#include <stddef.h>
#include <stdint.h>

#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/common/features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/fake_local_frame.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_renderer_host.h"
#include "ipc/ipc_test_sink.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace content {

namespace {
const int64_t kTaskDelayMs = 200;

// Stub out local frame mojo binding. Intercepts calls for text input
// and marks the message as received. This class attaches to the first
// RenderFrameHostImpl created.
class TextInputClientLocalFrame : public content::FakeLocalFrame,
                                  public WebContentsObserver {
 public:
  explicit TextInputClientLocalFrame(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

  void RenderFrameCreated(RenderFrameHost* render_frame_host) override {
    if (!initialized_) {
      initialized_ = true;
      Init(render_frame_host->GetRemoteAssociatedInterfaces());
    }
  }

  void GetCharacterIndexAtPoint(const gfx::Point& point) override {
    if (completion_callback_)
      std::move(completion_callback_).Run();
  }

  void GetFirstRectForRange(const gfx::Range& range) override {
    if (completion_callback_)
      std::move(completion_callback_).Run();
  }

  void SetCallback(base::OnceClosure callback) {
    completion_callback_ = std::move(callback);
  }

 private:
  bool initialized_ = false;
  base::OnceClosure completion_callback_;
};

// This test does not test the WebKit side of the dictionary system (which
// performs the actual data fetching), but rather this just tests that the
// service's signaling system works.
class TextInputClientMacTest : public content::RenderViewHostTestHarness {
 public:
  TextInputClientMacTest() : thread_("TextInputClientMacTestThread") {}

  void SetUp() override {
    content::RenderViewHostTestHarness::SetUp();
    local_frame_ = std::make_unique<TextInputClientLocalFrame>(web_contents());
    RenderViewHostTester::For(rvh())->CreateTestRenderView();
    widget_ = rvh()->GetWidget();
    FocusWebContentsOnMainFrame();
  }

  void TearDown() override {
    base::RunLoop().RunUntilIdle();
    RenderViewHostTestHarness::TearDown();
  }

  // Accessor for the TextInputClientMac instance.
  TextInputClientMac* service() {
    return TextInputClientMac::GetInstance();
  }

  // Helper method to post a task on the testing thread's MessageLoop after
  // a short delay.
  void PostTask(base::Location from_here, base::OnceClosure task) {
    PostTask(std::move(from_here), std::move(task),
             base::Milliseconds(kTaskDelayMs));
  }

  void PostTask(base::Location from_here,
                base::OnceClosure task,
                const base::TimeDelta delay) {
    thread_.task_runner()->PostDelayedTask(std::move(from_here),
                                           std::move(task), delay);
  }

  RenderWidgetHost* widget() { return widget_; }
  TextInputClientLocalFrame* local_frame() { return local_frame_.get(); }

  IPC::TestSink& ipc_sink() {
    return static_cast<MockRenderProcessHost*>(widget()->GetProcess())->sink();
  }

 private:
  friend class ScopedTestingThread;

  raw_ptr<RenderWidgetHost, DanglingUntriaged> widget_;
  std::unique_ptr<TextInputClientLocalFrame> local_frame_;

  base::Thread thread_;
};

////////////////////////////////////////////////////////////////////////////////

// Helper class that Start()s and Stop()s a thread according to the scope of the
// object.
class ScopedTestingThread {
 public:
  ScopedTestingThread(TextInputClientMacTest* test) : thread_(test->thread_) {
    thread_.Start();
  }
  ~ScopedTestingThread() {
    thread_.Stop();
  }

 private:
  base::Thread& thread_;
};

}  // namespace

// Test Cases //////////////////////////////////////////////////////////////////

TEST_F(TextInputClientMacTest, GetCharacterIndex) {
  ScopedTestingThread thread(this);
  const NSUInteger kSuccessValue = 42;

  PostTask(FROM_HERE,
           base::BindOnce(&TextInputClientMac::SetCharacterIndexAndSignal,
                          base::Unretained(service()), kSuccessValue));
  base::RunLoop run_loop;
  local_frame()->SetCallback(run_loop.QuitClosure());
  NSUInteger index = service()->GetCharacterIndexAtPoint(
      widget(), gfx::Point(2, 2));

  EXPECT_EQ(kSuccessValue, index);
  run_loop.Run();
}

TEST_F(TextInputClientMacTest, TimeoutCharacterIndex) {
  base::RunLoop run_loop;
  local_frame()->SetCallback(run_loop.QuitClosure());
  uint32_t index =
      service()->GetCharacterIndexAtPoint(widget(), gfx::Point(2, 2));

  EXPECT_EQ(UINT32_MAX, index);
  run_loop.Run();
}

TEST_F(TextInputClientMacTest, NotFoundCharacterIndex) {
  ScopedTestingThread thread(this);
  const NSUInteger kPreviousValue = 42;

  // Set an arbitrary value to ensure the index is not |NSNotFound|.
  PostTask(FROM_HERE,
           base::BindOnce(&TextInputClientMac::SetCharacterIndexAndSignal,
                          base::Unretained(service()), kPreviousValue));

  // Set UINT32_MAX to the index |kTaskDelayMs| after the previous setting.
  PostTask(FROM_HERE,
           base::BindOnce(&TextInputClientMac::SetCharacterIndexAndSignal,
                          base::Unretained(service()), UINT32_MAX),
           base::Milliseconds(kTaskDelayMs) * 2);

  base::RunLoop run_loop1;
  local_frame()->SetCallback(run_loop1.QuitClosure());
  uint32_t index =
      service()->GetCharacterIndexAtPoint(widget(), gfx::Point(2, 2));
  run_loop1.Run();
  EXPECT_EQ(kPreviousValue, index);

  base::RunLoop run_loop2;
  local_frame()->SetCallback(run_loop2.QuitClosure());
  index = service()->GetCharacterIndexAtPoint(widget(), gfx::Point(2, 2));
  run_loop2.Run();
  EXPECT_EQ(UINT32_MAX, index);
}

TEST_F(TextInputClientMacTest, GetRectForRange) {
  ScopedTestingThread thread(this);
  const gfx::Rect kSuccessValue(42, 43, 44, 45);

  PostTask(FROM_HERE,
           base::BindOnce(&TextInputClientMac::SetFirstRectAndSignal,
                          base::Unretained(service()), kSuccessValue));
  base::RunLoop run_loop;
  local_frame()->SetCallback(run_loop.QuitClosure());
  gfx::Rect rect =
      service()->GetFirstRectForRange(widget(), gfx::Range(NSMakeRange(0, 32)));
  run_loop.Run();
  EXPECT_EQ(kSuccessValue, rect);
}

TEST_F(TextInputClientMacTest, TimeoutRectForRange) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(features::kTextInputClient,
                                                  {{"ipc_timeout", "300ms"}});

  base::RunLoop run_loop;
  local_frame()->SetCallback(run_loop.QuitClosure());

  gfx::Rect rect =
      service()->GetFirstRectForRange(widget(), gfx::Range(NSMakeRange(0, 32)));
  run_loop.Run();

  EXPECT_EQ(gfx::Rect(), rect);
}

}  // namespace content
