/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#include "ReactInstanceIntegrationTest.h"
#include "FollyDynamicMatchers.h"
#include "UniquePtrFactory.h"
#include "prelude.js.h"

#include <folly/json.h>
#include <glog/logging.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
#include <react/runtime/hermes/HermesInstance.h>

using namespace ::testing;

namespace facebook::react::jsinspector_modern {

#pragma region ReactInstanceIntegrationTest

ReactInstanceIntegrationTest::ReactInstanceIntegrationTest()
    : runtime(nullptr),
      instance(nullptr),
      messageQueueThread(std::make_shared<MockMessageQueueThread>()),
      errorHandler(std::make_shared<ErrorUtils>()) {}

void ReactInstanceIntegrationTest::SetUp() {
  auto mockRegistry = std::make_unique<MockTimerRegistry>();
  auto timerManager =
      std::make_shared<react::TimerManager>(std::move(mockRegistry));

  auto onJsError = [](const JsErrorHandler::ParsedError& errorMap) noexcept {
    LOG(INFO) << "[jsErrorHandlingFunc called]";
    LOG(INFO) << "message: " << errorMap.message;
    LOG(INFO) << "exceptionId: " << std::to_string(errorMap.exceptionId);
    LOG(INFO) << "isFatal: "
              << std::to_string(static_cast<int>(errorMap.isFatal));
    auto frames = errorMap.frames;
    for (const auto& mapBuffer : frames) {
      LOG(INFO) << "[Frame]" << std::endl << "\tfile: " << mapBuffer.fileName;
      LOG(INFO) << "\tmethodName: " << mapBuffer.methodName;
      LOG(INFO) << "\tlineNumber: " << std::to_string(mapBuffer.lineNumber);
      LOG(INFO) << "\tcolumn: " << std::to_string(mapBuffer.columnNumber);
    }
  };

  auto jsRuntimeFactory = std::make_unique<react::HermesInstance>();
  std::unique_ptr<react::JSRuntime> runtime_ =
      jsRuntimeFactory->createJSRuntime(
          nullptr, nullptr, messageQueueThread, false);
  jsi::Runtime* jsiRuntime = &runtime_->getRuntime();

  // Error handler:
  jsiRuntime->global().setProperty(
      *jsiRuntime,
      "ErrorUtils",
      jsi::Object::createFromHostObject(*jsiRuntime, errorHandler));

  std::shared_ptr<HostTarget> hostTargetIfModernCDP = nullptr;

  if (InspectorFlags::getInstance().getFuseboxEnabled()) {
    VoidExecutor inspectorExecutor = [this](auto callback) {
      immediateExecutor_.add(callback);
    };
    MockHostTargetDelegate hostTargetDelegate;
    hostTargetIfModernCDP =
        HostTarget::create(hostTargetDelegate, inspectorExecutor);
  }

  instance = std::make_unique<react::ReactInstance>(
      std::move(runtime_),
      messageQueueThread,
      timerManager,
      std::move(onJsError),
      hostTargetIfModernCDP == nullptr ? nullptr : hostTargetIfModernCDP.get());

  timerManager->setRuntimeExecutor(instance->getBufferedRuntimeExecutor());

  // JS Environment:
  initializeRuntime(preludeJsCode);

  // Inspector:
  auto& inspector = getInspectorInstance();

  if (hostTargetIfModernCDP != nullptr) {
    // Under modern CDP, the React host is responsible for adding itself as
    // the root target on startup.
    pageId_ = inspector.addPage(
        "mock-title",
        "mock-vm",
        [hostTargetIfModernCDP](std::unique_ptr<IRemoteConnection> remote)
            -> std::unique_ptr<ILocalConnection> {
          auto localConnection =
              hostTargetIfModernCDP->connect(std::move(remote));
          return localConnection;
        },
        // TODO: Allow customisation of InspectorTargetCapabilities
        {});
  } else {
    // Under legacy CDP, Hermes' DecoratedRuntime adds its page automatically
    // within ConnectionDemux.enableDebugging.
    auto pages = inspector.getPages();
    ASSERT_GT(pages.size(), 0);
    pageId_ = pages.back().id;
  }

  clientToVM_ =
      inspector.connect(pageId_.value(), mockRemoteConnections_.make_unique());

  ASSERT_NE(clientToVM_, nullptr);

  // Default to ignoring console messages originating inside the backend.
  EXPECT_CALL(
      getRemoteConnection(),
      onMessage(JsonParsed(AllOf(
          AtJsonPtr("/method", "Runtime.consoleAPICalled"),
          AtJsonPtr("/params/context", "main#InstanceAgent")))))
      .Times(AnyNumber());
}

void ReactInstanceIntegrationTest::TearDown() {
  clientToVM_->disconnect();
  // Destroy the local connection.
  clientToVM_.reset();

  if (pageId_.has_value() &&
      InspectorFlags::getInstance().getFuseboxEnabled()) {
    // Under modern CDP, clean up the page we added in SetUp and destroy
    // resources owned by HostTarget.
    getInspectorInstance().removePage(pageId_.value());
  }
  pageId_.reset();

  // Expect the remote connection to have been destroyed.
  EXPECT_EQ(mockRemoteConnections_[0], nullptr);
  ReactNativeFeatureFlags::dangerouslyReset();
}

void ReactInstanceIntegrationTest::initializeRuntime(std::string_view script) {
  react::ReactInstance::JSRuntimeFlags flags{
      .isProfiling = false,
  };
  instance->initializeRuntime(flags, [](jsi::Runtime& rt) {
    // NOTE: RN's console polyfill (included in prelude.js.h) depends on the
    // native logging hook being installed, even if it's a noop.
    facebook::react::bindNativeLogger(rt, [](auto, auto) {});
  });

  messageQueueThread->tick();

  std::string init(script);
  // JS calls no longer buffered after calling loadScript
  instance->loadScript(std::make_unique<react::JSBigStdString>(init), "");
}

void ReactInstanceIntegrationTest::send(
    const std::string& method,
    const folly::dynamic& params) {
  folly::dynamic request = folly::dynamic::object();

  request["method"] = method;
  request["id"] = id_++;
  request["params"] = params;

  sendJSONString(folly::toJson(request));
}

void ReactInstanceIntegrationTest::sendJSONString(const std::string& message) {
  // The runtime must be initialized and connected to before messaging
  clientToVM_->sendMessage(message);
}

jsi::Value ReactInstanceIntegrationTest::run(const std::string& script) {
  auto runtimeExecutor = instance->getUnbufferedRuntimeExecutor();
  auto ret = jsi::Value::undefined();

  runtimeExecutor([script, &ret](jsi::Runtime& rt) {
    ret = rt.evaluateJavaScript(
        std::make_unique<jsi::StringBuffer>(script), "<test>");
  });

  messageQueueThread->flush();

  while (verbose_ && errorHandler->size() > 0) {
    LOG(INFO) << "Error: " << errorHandler->getLastError().getMessage();
  }

  return ret;
}

bool ReactInstanceIntegrationTest::verbose(bool isVerbose) {
  const bool previous = verbose_;
  verbose_ = isVerbose;
  return previous;
}

#pragma endregion

TEST_F(ReactInstanceIntegrationTest, RuntimeEvalTest) {
  auto val = run("1 + 2");
  EXPECT_EQ(val.asNumber(), 3);
}

TEST_P(ReactInstanceIntegrationTestWithFlags, ConsoleLog) {
  EXPECT_CALL(
      getRemoteConnection(),
      onMessage(JsonParsed(
          AtJsonPtr("/method", Eq("Runtime.executionContextCreated")))));

  EXPECT_CALL(
      getRemoteConnection(), onMessage(JsonParsed(AtJsonPtr("/id", Eq(1)))));

  InSequence s;

  EXPECT_CALL(
      getRemoteConnection(),
      onMessage(JsonParsed(AllOf(
          AtJsonPtr("/params/args/0/value", Eq("Hello, World!")),
          AtJsonPtr("/method", Eq("Runtime.consoleAPICalled"))))));

  EXPECT_CALL(getRemoteConnection(), onDisconnect());

  send("Runtime.enable");
  run("console.log('Hello, World!');");
}

INSTANTIATE_TEST_SUITE_P(
    ReactInstanceVaryingInspectorFlags,
    ReactInstanceIntegrationTestWithFlags,
    ::testing::Values(
        InspectorFlagOverrides{.fuseboxEnabledDebug = false},
        InspectorFlagOverrides{.fuseboxEnabledDebug = false},
        InspectorFlagOverrides{.fuseboxEnabledDebug = true}));

} // namespace facebook::react::jsinspector_modern
