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

#include "extensions/renderer/bindings/event_emitter.h"

#include <string_view>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ref.h"
#include "base/values.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "extensions/renderer/bindings/api_binding_test.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_event_listeners.h"
#include "extensions/renderer/bindings/exception_handler.h"
#include "extensions/renderer/bindings/listener_tracker.h"
#include "extensions/renderer/bindings/test_js_runner.h"
#include "gin/public/gin_embedders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"

namespace extensions {

namespace {

APIEventListeners::ContextOwnerIdGetter CreateContextOwnerIdGetter() {
  return base::BindRepeating(
      [](v8::Local<v8::Context>) { return std::string("context"); });
}

}  // namespace

class EventEmitterUnittest : public APIBindingTest {
 public:
  EventEmitterUnittest() = default;

  EventEmitterUnittest(const EventEmitterUnittest&) = delete;
  EventEmitterUnittest& operator=(const EventEmitterUnittest&) = delete;

  ~EventEmitterUnittest() override = default;

  // A helper method to dispose of a context and set a flag.
  void DisposeContextWrapper(bool* did_invalidate,
                             v8::Local<v8::Context> context) {
    EXPECT_FALSE(*did_invalidate);
    *did_invalidate = true;
    DisposeContext(context);
  }
};

TEST_F(EventEmitterUnittest, TestDispatchMethod) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  ListenerTracker tracker;
  auto listeners = std::make_unique<UnfilteredEventListeners>(
      base::DoNothing(), "event", CreateContextOwnerIdGetter(),
      binding::kNoListenerMax, true, &tracker);

  auto log_error = [](std::vector<std::string>* errors,
                      v8::Local<v8::Context> context,
                      const std::string& error) { errors->push_back(error); };

  std::vector<std::string> logged_errors;
  ExceptionHandler exception_handler(
      base::BindRepeating(log_error, &logged_errors));

  auto* event_emitter = cppgc::MakeGarbageCollected<EventEmitter>(
      isolate()->GetCppHeap()->GetAllocationHandle(), false,
      std::move(listeners), &exception_handler);

  v8::Local<v8::Value> v8_event =
      event_emitter->GetWrapper(isolate()).ToLocalChecked();

  const char kAddListener[] =
      "(function(event, listener) { event.addListener(listener); })";
  v8::Local<v8::Function> add_listener_function =
      FunctionFromString(context, kAddListener);

  auto add_listener = [context, v8_event,
                       add_listener_function](std::string_view listener) {
    v8::Local<v8::Function> listener_function =
        FunctionFromString(context, listener);
    v8::Local<v8::Value> args[] = {v8_event, listener_function};
    RunFunction(add_listener_function, context, std::size(args), args);
  };

  const char kListener1[] =
      "(function() {\n"
      "  this.eventArgs1 = Array.from(arguments);\n"
      "  return 'listener1';\n"
      "})";
  add_listener(kListener1);
  const char kListener2[] =
      "(function() {\n"
      "  this.eventArgs2 = Array.from(arguments);\n"
      "  return {listener: 'listener2'};\n"
      "})";
  add_listener(kListener2);
  // Listener3 throws, but shouldn't stop the event from reaching other
  // listeners.
  const char kListener3[] =
      "(function() {\n"
      "  this.eventArgs3 = Array.from(arguments);\n"
      "  throw new Error('hahaha');\n"
      "})";
  add_listener(kListener3);
  // Returning undefined should not be added to the array of results from
  // dispatch.
  const char kListener4[] =
      "(function() {\n"
      "  this.eventArgs4 = Array.from(arguments);\n"
      "})";
  add_listener(kListener4);

  const char kDispatch[] =
      "(function(event) {\n"
      "  return event.dispatch('arg1', 2);\n"
      "})";
  v8::Local<v8::Value> dispatch_args[] = {v8_event};
  TestJSRunner::AllowErrors allow_errors;
  v8::Local<v8::Value> dispatch_result =
      RunFunctionOnGlobal(FunctionFromString(context, kDispatch), context,
                          std::size(dispatch_args), dispatch_args);

  const char kExpectedEventArgs[] = "[\"arg1\",2]";
  for (const char* property :
       {"eventArgs1", "eventArgs2", "eventArgs3", "eventArgs4"}) {
    EXPECT_EQ(kExpectedEventArgs, GetStringPropertyFromObject(
                                      context->Global(), context, property));
  }
  EXPECT_EQ("{\"results\":[\"listener1\",{\"listener\":\"listener2\"}]}",
            V8ToString(dispatch_result, context));

  ASSERT_EQ(1u, logged_errors.size());
  EXPECT_THAT(logged_errors[0],
              testing::StartsWith("Error in event handler: Error: hahaha"));
}

// Test dispatching an event when the first listener invalidates the context.
// Nothing should break, and we shouldn't continue to dispatch the event.
TEST_F(EventEmitterUnittest, ListenersDestroyingContext) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  struct ListenerClosureData {
    const raw_ref<EventEmitterUnittest> test;
    bool did_invalidate_context;
  } closure_data = {raw_ref(*this), false};

  // A wrapper that just calls DisposeContextWrapper() on the curried in data.
  auto listener_wrapper = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
    ASSERT_TRUE(info.Data()->IsExternal());
    auto& data = *static_cast<ListenerClosureData*>(
        info.Data().As<v8::External>()->Value(
            gin::kEventEmitterUnittestListenerClosureDataTag));
    data.test->DisposeContextWrapper(&data.did_invalidate_context,
                                     info.GetIsolate()->GetCurrentContext());
  };

  ListenerTracker tracker;
  auto listeners = std::make_unique<UnfilteredEventListeners>(
      base::DoNothing(), "event", CreateContextOwnerIdGetter(),
      binding::kNoListenerMax, true, &tracker);
  ExceptionHandler exception_handler(base::BindRepeating(
      [](v8::Local<v8::Context> context, const std::string& error) {}));
  auto* event_emitter = cppgc::MakeGarbageCollected<EventEmitter>(
      isolate()->GetCppHeap()->GetAllocationHandle(), false,
      std::move(listeners), &exception_handler);

  v8::Local<v8::Value> v8_event =
      event_emitter->GetWrapper(isolate()).ToLocalChecked();

  const char kAddListener[] =
      "(function(event, listener) { event.addListener(listener); })";
  v8::Local<v8::Function> add_listener_function =
      FunctionFromString(context, kAddListener);

  // Queue up three listeners. The first triggered will invalidate the context.
  // The others should never be triggered.
  constexpr size_t kNumListeners = 3;
  for (size_t i = 0; i < kNumListeners; ++i) {
    v8::Local<v8::Function> listener =
        v8::Function::New(
            context, listener_wrapper,
            v8::External::New(isolate(), &closure_data,
                              gin::kEventEmitterUnittestListenerClosureDataTag))
            .ToLocalChecked();
    v8::Local<v8::Value> args[] = {v8_event, listener};
    RunFunction(add_listener_function, context, std::size(args), args);
  }

  EXPECT_EQ(kNumListeners, event_emitter->GetNumListenersForTesting());

  v8::LocalVector<v8::Value> args(isolate());
  event_emitter->Fire(context, &args, /*filter=*/nullptr,
                      /*on_dispatched_callback=*/v8::Local<v8::Function>(),
                      /*listener_error_callback=*/v8::Local<v8::Function>());

  EXPECT_TRUE(closure_data.did_invalidate_context);
}

TEST_F(EventEmitterUnittest, AddListenerWithOptions_WebRequest) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  ListenerTracker tracker;
  auto listeners = std::make_unique<FilteredEventListeners>(
      base::DoNothing(), "webRequest.onBeforeRequest",
      CreateContextOwnerIdGetter(), binding::kNoListenerMax, true, &tracker);
  ExceptionHandler exception_handler(base::DoNothing());
  auto* event_emitter = cppgc::MakeGarbageCollected<EventEmitter>(
      isolate()->GetCppHeap()->GetAllocationHandle(), /*supports_filters=*/true,
      std::move(listeners), &exception_handler);

  v8::Local<v8::Value> v8_event =
      event_emitter->GetWrapper(isolate()).ToLocalChecked();

  const char kAddListener[] =
      "(function(event, listener, filter, options) { "
      "event.addListener(listener, filter, options); })";
  v8::Local<v8::Function> add_listener_function =
      FunctionFromString(context, kAddListener);

  // Providing a valid options object for a webRequest event should succeed.
  {
    v8::Local<v8::Function> listener =
        FunctionFromString(context, "(function() {})");
    v8::Local<v8::Object> filter =
        V8ValueFromScriptSource(context, "({})").As<v8::Object>();
    v8::Local<v8::Value> options =
        V8ValueFromScriptSource(context, "({extraInfo: ['blocking']})");
    v8::Local<v8::Value> args[] = {v8_event, listener, filter, options};
    RunFunction(add_listener_function, context, std::size(args), args);
    EXPECT_EQ(1u, event_emitter->GetNumListenersForTesting());
  }

  // Providing an invalid options type (e.g., a string) should fail.
  {
    v8::Local<v8::Function> listener =
        FunctionFromString(context, "(function() {})");
    v8::Local<v8::Object> filter =
        V8ValueFromScriptSource(context, "({})").As<v8::Object>();
    v8::Local<v8::Value> options =
        V8ValueFromScriptSource(context, "'not-an-object'");
    v8::Local<v8::Value> args[] = {v8_event, listener, filter, options};
    RunFunctionAndExpectError(add_listener_function, context, std::size(args),
                              args, "Uncaught TypeError: Invalid invocation");
    EXPECT_EQ(1u, event_emitter->GetNumListenersForTesting());
  }
}

TEST_F(EventEmitterUnittest, AddListenerWithOptions_FailsForNonWebRequest) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  ListenerTracker tracker;
  auto listeners = std::make_unique<FilteredEventListeners>(
      base::DoNothing(), "other.event", CreateContextOwnerIdGetter(),
      binding::kNoListenerMax, true, &tracker);
  ExceptionHandler exception_handler(base::DoNothing());
  auto* event_emitter = cppgc::MakeGarbageCollected<EventEmitter>(
      isolate()->GetCppHeap()->GetAllocationHandle(), /*supports_filters=*/true,
      std::move(listeners), &exception_handler);

  v8::Local<v8::Value> v8_event =
      event_emitter->GetWrapper(isolate()).ToLocalChecked();

  const char kAddListener[] =
      "(function(event, listener, filter, options) { "
      "event.addListener(listener, filter, options); })";
  v8::Local<v8::Function> add_listener_function =
      FunctionFromString(context, kAddListener);

  // Providing an options argument for a non-webRequest event is not allowed and
  // should fail.
  v8::Local<v8::Function> listener =
      FunctionFromString(context, "(function() {})");
  v8::Local<v8::Object> filter =
      V8ValueFromScriptSource(context, "({})").As<v8::Object>();
  v8::Local<v8::Value> options = V8ValueFromScriptSource(context, "{}");
  v8::Local<v8::Value> args[] = {v8_event, listener, filter, options};

  RunFunctionAndExpectError(
      add_listener_function, context, std::size(args), args,
      "Uncaught TypeError: This event does not support options");
  EXPECT_EQ(0u, event_emitter->GetNumListenersForTesting());
}

}  // namespace extensions
