// Copyright 2004-present Facebook. All Rights Reserved.

#include "AsyncHermesRuntime.h"
#include "SyncConnection.h"

#include <chrono>
#include <initializer_list>
#include <iostream>
#include <limits>

#include <folly/Conv.h>
#include <folly/Unit.h>
#include <folly/dynamic.h>
#include <folly/futures/Future.h>
#include <folly/json.h>
#include <gtest/gtest.h>
#include <hermes/DebuggerAPI.h>
#include <hermes/hermes.h>
#include <hermes/inspector/chrome/MessageTypes.h>

namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {

namespace m = ::facebook::hermes::inspector::chrome::message;

using namespace std::chrono_literals;

namespace {

// This class mostly exists to call AsyncHermesRuntime::wait() before we
// destruct either AsyncHermesRuntime or SyncConnection.
//
// The reason for this is that we need to make sure the runtime is finished
// executing scripts before we destruct the debugger connection. Otherwise, if
// we destruct the connection while scripts are still executing, the script
// could perform an action (like hitting a breakpoint) that sends a message to
// the already-deallocated connection.
class TestContext {
 public:
  TestContext() : conn_(runtime_.runtime()) {}
  ~TestContext() {
    runtime_.wait();
  }

  AsyncHermesRuntime &runtime() {
    return runtime_;
  }

  SyncConnection &conn() {
    return conn_;
  }

 private:
  AsyncHermesRuntime runtime_;
  SyncConnection conn_;
};

template <typename ResponseType>
ResponseType expectResponse(SyncConnection &conn, int id) {
  ResponseType resp;

  conn.waitForResponse([id, &resp](const std::string &str) {
    resp = ResponseType(folly::parseJson(str));
    EXPECT_EQ(resp.id, id);
  });

  return resp;
}

template <typename NotificationType>
NotificationType expectNotification(SyncConnection &conn) {
  NotificationType note;

  conn.waitForNotification([&note](const std::string &str) {
    std::string parseError;

    try {
      note = NotificationType(folly::parseJson(str));
    } catch (const std::exception &e) {
      parseError = e.what();
    }

    EXPECT_EQ(parseError, "");
  });

  return note;
}

class UnexpectedNotificationException : public std::runtime_error {
 public:
  UnexpectedNotificationException()
      : std::runtime_error("unexpected notification") {}
};

void expectNothing(SyncConnection &conn) {
  auto promise = std::make_shared<folly::Promise<folly::Unit>>();
  auto future = promise->getFuture();
  try {
    conn.waitForNotification([promise](const std::string &str) {
      // if we receive a value fail the promise
      promise->setException(UnexpectedNotificationException());
    });
  } catch (...) {
    // if no values are received it times out with an exception
    // so we can say that we've succeeded at seeing nothing
    promise->setValue();
  }
  // timeout is 2500 milliseconds in SyncConnection::waitForNotification
  // so this value is mostly a redundant safety measure
  std::move(future).get(3000ms);
}

struct FrameInfo {
  FrameInfo(const std::string &functionName, int lineNumber, int scopeCount)
      : functionName(functionName),
        lineNumberMin(lineNumber),
        lineNumberMax(lineNumber),
        scopeCount(scopeCount),
        columnNumber(debugger::kInvalidLocation) {}

  FrameInfo &setLineNumberMax(int lineNumberMaxParam) {
    lineNumberMax = lineNumberMaxParam;
    return *this;
  }

  FrameInfo &setScriptId(const std::string &scriptIdParam) {
    scriptId = scriptIdParam;
    return *this;
  }

  FrameInfo &setColumnNumber(int columnNumberParam) {
    columnNumber = columnNumberParam;
    return *this;
  }

  std::string functionName;
  int lineNumberMin;
  int lineNumberMax;
  int scopeCount;
  int columnNumber;
  std::string scriptId;
};

void expectCallFrames(
    const std::vector<m::debugger::CallFrame> &frames,
    const std::vector<FrameInfo> &infos) {
  EXPECT_EQ(frames.size(), infos.size());

  int i = 0;
  for (const FrameInfo &info : infos) {
    m::debugger::CallFrame frame = frames[i];

    EXPECT_EQ(frame.callFrameId, folly::to<std::string>(i));
    EXPECT_EQ(frame.functionName, info.functionName);
    EXPECT_GE(frame.location.lineNumber, info.lineNumberMin);
    EXPECT_LE(frame.location.lineNumber, info.lineNumberMax);

    if (info.columnNumber != debugger::kInvalidLocation) {
      EXPECT_EQ(frame.location.columnNumber, info.columnNumber);
    }

    if (info.scriptId.size() > 0) {
      EXPECT_EQ(frame.location.scriptId, info.scriptId);
    }

    // TODO: make expectation more specific once Hermes gives us something other
    // than kInvalidBreakpoint for the file id
    EXPECT_FALSE(frame.location.scriptId.empty());

    if (info.scopeCount > 0) {
      EXPECT_EQ(frame.scopeChain.size(), info.scopeCount);

      for (int j = 0; j < info.scopeCount; j++) {
        EXPECT_TRUE(frame.scopeChain[j].object.objectId.hasValue());
      }
    }

    i++;
  }
}

// Helper to send a request wait for an empty response containing the req id.
template <typename RequestType>
void send(SyncConnection &conn, int id) {
  RequestType req;
  req.id = id;
  conn.send(req.toJson());

  expectResponse<m::OkResponse>(conn, id);
}

void sendRuntimeEvalRequest(
    SyncConnection &conn,
    int id,
    const std::string &expression) {
  m::runtime::EvaluateRequest req;
  req.id = id;
  req.expression = expression;
  conn.send(req.toJson());
}

void sendEvalRequest(
    SyncConnection &conn,
    int id,
    int callFrameId,
    const std::string &expression) {
  m::debugger::EvaluateOnCallFrameRequest req;
  req.id = id;
  req.callFrameId = folly::to<std::string>(callFrameId);
  req.expression = expression;
  conn.send(req.toJson());
}

m::runtime::ExecutionContextCreatedNotification expectExecutionContextCreated(
    SyncConnection &conn) {
  auto note =
      expectNotification<m::runtime::ExecutionContextCreatedNotification>(conn);

  EXPECT_EQ(note.context.id, 1);
  EXPECT_EQ(note.context.origin, "");
  EXPECT_EQ(note.context.name, "hermes");
  EXPECT_EQ(note.context.isDefault, true);
  EXPECT_EQ(note.context.isPageContext, true);

  return note;
}

m::debugger::ScriptParsedNotification expectScriptParsed(
    SyncConnection &conn,
    const std::string &url,
    const std::string &sourceMapURL) {
  auto note = expectNotification<m::debugger::ScriptParsedNotification>(conn);

  EXPECT_EQ(note.url, url);
  EXPECT_GT(note.scriptId.size(), 0);

  if (sourceMapURL.empty()) {
    EXPECT_FALSE(note.sourceMapURL.hasValue());
  } else {
    EXPECT_EQ(note.sourceMapURL.value(), sourceMapURL);
  }

  return note;
}

m::debugger::PausedNotification expectPaused(
    SyncConnection &conn,
    const std::string &reason,
    const std::vector<FrameInfo> &infos) {
  auto note = expectNotification<m::debugger::PausedNotification>(conn);

  EXPECT_EQ(note.reason, reason);
  expectCallFrames(note.callFrames, infos);
  // TODO: check breakpoint location for pause once hermes gives that to us

  return note;
}

m::debugger::BreakpointId expectBreakpointResponse(
    SyncConnection &conn,
    int id,
    int line,
    int resolvedLine) {
  auto resp = expectResponse<m::debugger::SetBreakpointByUrlResponse>(conn, id);

  EXPECT_EQ(resp.id, id);
  EXPECT_FALSE(resp.breakpointId.empty());
  EXPECT_NE(
      resp.breakpointId,
      folly::to<std::string>(facebook::hermes::debugger::kInvalidBreakpoint));

  if (line == -1) {
    EXPECT_EQ(resp.locations.size(), 0);
  } else {
    EXPECT_EQ(resp.locations.size(), 1);
    EXPECT_EQ(resp.locations[0].lineNumber, resolvedLine);
  }

  return resp.breakpointId;
}

void expectEvalResponse(
    SyncConnection &conn,
    int id,
    const char *expectedValue) {
  auto resp =
      expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);

  EXPECT_EQ(resp.id, id);
  EXPECT_EQ(resp.result.type, "string");
  EXPECT_EQ(resp.result.value, expectedValue);
  EXPECT_FALSE(resp.exceptionDetails.hasValue());
}

void expectEvalResponse(SyncConnection &conn, int id, bool expectedValue) {
  auto resp =
      expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);

  EXPECT_EQ(resp.id, id);
  EXPECT_EQ(resp.result.type, "boolean");
  EXPECT_EQ(resp.result.value, expectedValue);
  EXPECT_FALSE(resp.exceptionDetails.hasValue());
}

void expectEvalResponse(SyncConnection &conn, int id, int expectedValue) {
  auto resp =
      expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);

  EXPECT_EQ(resp.id, id);
  EXPECT_EQ(resp.result.type, "number");
  EXPECT_EQ(resp.result.value, expectedValue);
  EXPECT_FALSE(resp.exceptionDetails.hasValue());
}

void expectEvalException(
    SyncConnection &conn,
    int id,
    const std::string &exceptionText,
    const std::vector<FrameInfo> infos) {
  auto resp =
      expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);

  EXPECT_EQ(resp.id, id);
  EXPECT_TRUE(resp.exceptionDetails.hasValue());

  m::runtime::ExceptionDetails &details = resp.exceptionDetails.value();
  EXPECT_EQ(details.text, exceptionText);

  // TODO: Hermes doesn't seem to populate the line number for the exception?
  EXPECT_EQ(details.lineNumber, 0);

  EXPECT_TRUE(details.stackTrace.hasValue());

  m::runtime::StackTrace &stackTrace = details.stackTrace.value();
  EXPECT_EQ(stackTrace.callFrames.size(), infos.size());

  int i = 0;
  for (const FrameInfo &info : infos) {
    const m::runtime::CallFrame &callFrame = stackTrace.callFrames[i];

    EXPECT_GE(callFrame.lineNumber, info.lineNumberMin);
    EXPECT_LE(callFrame.lineNumber, info.lineNumberMax);
    EXPECT_EQ(callFrame.functionName, info.functionName);

    i++;
  }
}

struct PropInfo {
  PropInfo(const std::string &type) : type(type) {}

  PropInfo &setSubtype(const std::string &subtypeParam) {
    subtype = subtypeParam;
    return *this;
  }

  PropInfo &setValue(const folly::dynamic &valueParam) {
    value = valueParam;
    return *this;
  }

  PropInfo &setUnserializableValue(
      const std::string &unserializableValueParam) {
    unserializableValue = unserializableValueParam;
    return *this;
  }

  std::string type;
  folly::Optional<std::string> subtype;
  folly::Optional<folly::dynamic> value;
  folly::Optional<std::string> unserializableValue;
};

std::unordered_map<std::string, std::string> expectProps(
    SyncConnection &conn,
    int msgId,
    const std::string &objectId,
    const std::unordered_map<std::string, PropInfo> &infos,
    bool ownProperties = true) {
  m::runtime::GetPropertiesRequest req;
  req.id = msgId;
  req.objectId = objectId;
  req.ownProperties = ownProperties;
  conn.send(req.toJson());

  std::unordered_map<std::string, std::string> objectIds;
  auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, msgId);

  EXPECT_EQ(resp.result.size(), infos.size());

  for (int i = 0; i < resp.result.size(); i++) {
    m::runtime::PropertyDescriptor &desc = resp.result[i];

    auto infoIt = infos.find(desc.name);
    EXPECT_FALSE(infoIt == infos.end());

    if (infoIt != infos.end()) {
      const PropInfo &info = infoIt->second;

      EXPECT_TRUE(desc.value.hasValue());

      m::runtime::RemoteObject &remoteObj = desc.value.value();
      EXPECT_EQ(remoteObj.type, info.type);

      if (info.subtype.hasValue()) {
        EXPECT_TRUE(remoteObj.subtype.hasValue());
        EXPECT_EQ(remoteObj.subtype.value(), info.subtype.value());
      }

      if (info.value.hasValue()) {
        EXPECT_TRUE(remoteObj.value.hasValue());
        EXPECT_EQ(remoteObj.value.value(), info.value.value());
      }

      if (info.unserializableValue.hasValue()) {
        EXPECT_TRUE(remoteObj.unserializableValue.hasValue());
        EXPECT_EQ(
            remoteObj.unserializableValue.value(),
            info.unserializableValue.value());
      }

      if ((info.type == "object" && info.subtype != "null") ||
          info.type == "function") {
        EXPECT_TRUE(remoteObj.objectId.hasValue());
        objectIds[desc.name] = remoteObj.objectId.value();
      }
    }
  }

  return objectIds;
}

void expectEvalResponse(
    SyncConnection &conn,
    int id,
    const std::unordered_map<std::string, PropInfo> &infos) {
  auto resp =
      expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);

  EXPECT_EQ(resp.id, id);
  EXPECT_EQ(resp.result.type, "object");
  EXPECT_FALSE(resp.exceptionDetails.hasValue());

  EXPECT_TRUE(resp.result.objectId.hasValue());
  expectProps(conn, id + 1, resp.result.objectId.value(), infos);
}

} // namespace

TEST(ConnectionTests, testRespondsOkToUnknownRequests) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();

  asyncRuntime.executeScriptAsync(R"(
    var a = 1 + 2;
    var b = a / 2;
  )");

  send<m::debugger::EnableRequest>(conn, 1);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  conn.send(R"({"id": 2, "method": "Debugger.foo"})");
  conn.send(R"({"id": 3, "method": "Debugger.bar", "params": {"a": "b"}})");

  expectResponse<m::OkResponse>(conn, 2);
  expectResponse<m::OkResponse>(conn, 3);
}

TEST(ConnectionTests, testDebuggerStatement) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var a = 1 + 2;
    debugger;       // [1] (line 2) hit debugger statement, resume
    var b = a / 2;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement, resume
  expectPaused(conn, "other", {{"global", 2, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testDebuggerStatementFromPausedWaitEnable) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var a = 1 + 2;
    debugger;       // [1] (line 2) hit debugger statement, resume
    var b = a / 2;
  )");

  // TODO: hack that gives JS the chance to run so that we end up in the
  // PausedWaitEnable state. Will move the entire test to InspectorTests once
  // I get around to refactoring InspectorTests.
  std::this_thread::sleep_for(250ms);

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement, resume
  expectPaused(conn, "other", {{"global", 2, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testIsDebuggerAttached) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var a = 1 + 2;
    debugger;       // [1] (line 2) hit debugger statement
                    // [2] evaluate DebuggerInternal.isDebuggerAttached to true
    var b = a / 2;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement
  expectPaused(conn, "other", {{"global", 2, 1}});

  // [2] evaluate DebuggerInternal.IsDebuggerAttached to true
  sendEvalRequest(conn, 0, 0, R"("-> " + DebuggerInternal.isDebuggerAttached)");
  expectEvalResponse(conn, 0, "-> true");

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testStepOver) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var a = 1 + 2;
    debugger;       // [1] (line 2) hit debugger statement, step over
    var b = a / 2;  // [2] (line 3) step over
    var c = a + b;  // [3] (line 4) resume
    var d = b - c;
    var e = c * d;
    var f = 10;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2): hit debugger statement, step over
  expectPaused(conn, "other", {{"global", 2, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 3): step over
  expectPaused(conn, "other", {{"global", 3, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] (line 4): resume
  expectPaused(conn, "other", {{"global", 4, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testStepIn) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    function addOne(val) {
      return val + 1;   // [3]: resume
    }

    var a = 1 + 2;
    debugger;           // [1] (line 6) hit debugger statement, step over
    var b = addOne(a);  // [2] (line 7) step in
    var c = a + b;
    var d = b - c;
    var e = c * d;
    var f = 10;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 6): hit debugger statement, step over
  expectPaused(conn, "other", {{"global", 6, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 7): step in
  expectPaused(conn, "other", {{"global", 7, 1}});
  send<m::debugger::StepIntoRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] (line 2): resume
  expectPaused(conn, "other", {{"addOne", 2, 2}, {"global", 7, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testStepOut) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    function addSquares(a, b) {
      var a2 = a * a;
      debugger;        // [1] (line 3) hit debugger statement, step over
      var b2 = b * b;  // [2] (line 4) step out
      return a2 + b2;
    }

    var c = addSquares(1, 2); // [3] (line 8) resume
    var d = c * c;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 3) hit debugger statement, step over
  expectPaused(conn, "other", {{"addSquares", 3, 2}, {"global", 8, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 4) step out
  expectPaused(conn, "other", {{"addSquares", 4, 2}, {"global", 8, 1}});
  send<m::debugger::StepOutRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] (line 8): resume
  expectPaused(conn, "other", {{"global", 8, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testSetBreakpoint) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var a = 1 + 2;
    debugger;      // [1] (line 2) hit debugger statement,
                   //     set breakpoint on line 5
    var b = a / 2;
    var c = a + b; // [2] (line 5) hit breakpoint, step over
    var d = b - c; // [3] (line 6) resume
    var e = c * d;
    var f = 10;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement, set breakpoint on line 6
  expectPaused(conn, "other", {{"global", 2, 1}});

  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 5;
  req.columnNumber = 0;
  conn.send(req.toJson());

  expectBreakpointResponse(conn, req.id, 5, 5);

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 5) hit breakpoint, step over
  expectPaused(conn, "other", {{"global", 5, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] (line 6) resume
  expectPaused(conn, "other", {{"global", 6, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testSetLazyBreakpoint) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  facebook::hermes::HermesRuntime::DebugFlags flags{};
  flags.lazy = true;

  asyncRuntime.executeScriptAsync(
      R"(
    var a = 1 + 2;
    debugger;      // [1] (line 2) hit debugger statement,
                   //     set breakpoint on line 5

    function foo() {
      var b = a / 2;
      var c = a + b; // [2] (line 7) hit breakpoint, step over
      var d = b - c; // [3] (line 8) resume
      var e = c * d;
      var f = 10;
    }

    foo();
  )",
      "url",
      flags);

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement, set breakpoint on line 6
  expectPaused(conn, "other", {{"global", 2, 1}});

  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 7;
  req.columnNumber = 0;
  conn.send(req.toJson());

  auto breakpointId = expectBreakpointResponse(conn, req.id, 7, 7);

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 7) hit breakpoint, step over
  expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 13, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] (line 8) resume
  expectPaused(conn, "other", {{"foo", 8, 2}, {"global", 13, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testSetBreakpointWhileRunning) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    while (!shouldStop()) {
      var a = 1;
      var b = 2;
      var c = a + b; // [1] (line 4) first time: step over
                     // [3]          second time: set stop flag, resume
      var d = 10;    // [2] (line 6) resume
    }
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // set breakpoint on line 4: "var c = ..."
  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 4;
  req.columnNumber = 0;
  conn.send(req.toJson());

  expectBreakpointResponse(conn, req.id, 4, 4);

  // [1] (line 4) hit breakpoint, step over
  expectPaused(conn, "other", {{"global", 4, 1}});
  send<m::debugger::StepOverRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 6) resume
  expectPaused(conn, "other", {{"global", 6, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] (line 4) hit breakpoint again, set stop flag, resume
  expectPaused(conn, "other", {{"global", 4, 1}});
  asyncRuntime.stop();
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testSetBreakpointConditional) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var a = 3;
    debugger;      // [1] (line 2) hit debugger statement,
                   //  set conditional breakpoint on lines 4, 5 and 6
    var b = a + 5; // [2] (line 4) skip breakpoint, condition throws
    var c = b - a; // [3] (line 5) skip breakpoint, condition false
    var d = b - c; // [4] (line 6) hit breakpoint, condition true
    var e = c * d;
    var f = 10;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement,
  // set conditional breakpoint on lines 4, 5 and 6
  expectPaused(conn, "other", {{"global", 2, 1}});

  m::debugger::SetBreakpointByUrlRequest req0;
  req0.id = msgId++;
  req0.lineNumber = 4;
  req0.condition = folly::Optional<std::string>("throw Error('Boom!')");
  conn.send(req0.toJson());

  expectBreakpointResponse(conn, req0.id, 4, 4);

  m::debugger::SetBreakpointByUrlRequest req1;
  req1.id = msgId++;
  req1.lineNumber = 5;
  req1.condition = folly::Optional<std::string>("b === a");
  conn.send(req1.toJson());

  expectBreakpointResponse(conn, req1.id, 5, 5);

  m::debugger::SetBreakpointByUrlRequest req2;
  req2.id = msgId++;
  req2.lineNumber = 6;
  req2.condition = folly::Optional<std::string>("c === 5");
  conn.send(req2.toJson());

  expectBreakpointResponse(conn, req2.id, 6, 6);

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 4) skip breakpoint, condition throws

  // [3] (line 5) skip breakpoint, condition false

  // [4] (line 6) hit breakpoint, condition true
  expectPaused(conn, "other", {{"global", 6, 1}});

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testRemoveBreakpoint) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    // [1] (line 2) hit debugger statement, set breakpoint on line 7
    debugger;
    var a = 1;

    for (var i = 1; i <= 2; i++) {
      // [1] (line 7) hit breakpoint and then remove it
      a += i;
    }

    storeValue(a);
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 2) hit debugger statement, set breakpoint on line 7
  expectPaused(conn, "other", {{"global", 2, 1}});

  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 7;
  conn.send(req.toJson());

  auto breakpointId = expectBreakpointResponse(conn, req.id, 7, 7);

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [1] (line 7) hit breakpoint and then remove it
  expectPaused(conn, "other", {{"global", 7, 1}});

  m::debugger::RemoveBreakpointRequest removeReq;
  removeReq.id = msgId++;
  removeReq.breakpointId = breakpointId;
  conn.send(removeReq.toJson());
  expectResponse<m::OkResponse>(conn, removeReq.id);

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // check final value
  jsi::Value finalValue = asyncRuntime.awaitStoredValue();
  EXPECT_EQ(finalValue.asNumber(), 4);
}

TEST(ConnectionTests, testAsyncPauseWhileRunning) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var accum = 10;

    while (!shouldStop()) {
      var a = 1;
      var b = 2;
      var c = a + b;

      accum += c;
    }                        // (line 9)

    var d = -accum;
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // send some number of async pauses, make sure that we always stop before
  // the end of the loop on line 9
  for (int i = 0; i < 10; i++) {
    send<m::debugger::PauseRequest>(conn, msgId++);
    expectPaused(
        conn, "other", {FrameInfo("global", 0, 1).setLineNumberMax(9)});

    send<m::debugger::ResumeRequest>(conn, msgId++);
    expectNotification<m::debugger::ResumedNotification>(conn);
  }

  // break out of loop
  asyncRuntime.stop();
}

TEST(ConnectionTests, testEvalOnCallFrame) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var globalVar = "omega";
    var booleanVar = true;
    var numberVar = 42;
    var objectVar = {number: 1, bool: false, str: "string"};

    function func1(closure, f1param) { // frame 4
        var f1v1 = "alpha";
        var f1v2 = "beta";
        function func1b() {            // frame 3
            var f1bv1 = "gamma";
            function func1c() {        // frame 2
                var f1cv1 = 19;
                closure();
            }
            func1c();
        }
        func1b();
    }

    function func2() {                 // frame 1
        var f2v1 = "baker";
        var f2v2 = "charlie";
        function func2b() {            // frame 0
            var f2bv1 = "dog";
            debugger;                  // [1] (line 25) hit debugger statement
                                       // [2]           run evals
                                       // [3]           resume
            print(globalVar);
            print(f2bv1);
        }
        func2b();
    }

    func1(func2, "tau");
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 25) hit debugger statement
  expectPaused(
      conn,
      "other",
      {{"func2b", 25, 3},
       {"func2", 31, 2},
       {"func1c", 13, 4},
       {"func1b", 15, 3},
       {"func1", 17, 2},
       {"global", 34, 1}});

  // [2] run eval statements
  int frame = 0;
  sendEvalRequest(conn, msgId + 0, frame, R"("0: " + globalVar)");
  sendEvalRequest(conn, msgId + 1, frame, R"("1: " + f2bv1)");
  sendEvalRequest(conn, msgId + 2, frame, R"("2: " + f2v2)");
  sendEvalRequest(conn, msgId + 3, frame, R"("3: " + f2bv1 + " && " + f2v2)");
  expectEvalResponse(conn, msgId + 0, "0: omega");
  expectEvalResponse(conn, msgId + 1, "1: dog");
  expectEvalResponse(conn, msgId + 2, "2: charlie");
  expectEvalResponse(conn, msgId + 3, "3: dog && charlie");
  msgId += 4;

  frame = 1;
  sendEvalRequest(conn, msgId + 0, frame, R"("4: " + f2v1)");
  sendEvalRequest(conn, msgId + 1, frame, R"("5: " + f2v2)");
  sendEvalRequest(conn, msgId + 2, frame, R"(globalVar = "mod by debugger")");
  expectEvalResponse(conn, msgId + 0, "4: baker");
  expectEvalResponse(conn, msgId + 1, "5: charlie");
  expectEvalResponse(conn, msgId + 2, "mod by debugger");
  msgId += 3;

  frame = 2;
  sendEvalRequest(conn, msgId + 0, frame, R"("6: " + f1cv1 + f1bv1 + f1v1)");
  sendEvalRequest(conn, msgId + 1, frame, R"("7: " + globalVar)");
  expectEvalResponse(conn, msgId + 0, "6: 19gammaalpha");
  expectEvalResponse(conn, msgId + 1, "7: mod by debugger");
  msgId += 2;

  // [2.1] run eval statements that return non-string primitive values
  frame = 0;
  sendEvalRequest(conn, msgId + 0, frame, "booleanVar");
  sendEvalRequest(conn, msgId + 1, frame, "numberVar");
  expectEvalResponse(conn, msgId + 0, true);
  expectEvalResponse(conn, msgId + 1, 42);
  msgId += 2;

  // [2.2] run eval statement that returns object
  frame = 0;
  sendEvalRequest(conn, msgId + 0, frame, "objectVar");
  expectEvalResponse(
      conn,
      msgId + 0,
      {{"number", PropInfo("number").setValue(1)},
       {"bool", PropInfo("boolean").setValue(false)},
       {"str", PropInfo("string").setValue("string")},
       {"__proto__", PropInfo("object")}});

  // msgId is increased by 2 because expectEvalResponse will make additional
  // request with expectProps.
  msgId += 2;

  // [3] resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testRuntimeEvaluate) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var globalVar = "omega";
    var booleanVar = true;
    var numberVar = 42;
    var objectVar = {number: 1, bool: false, str: "string"};

    while(!shouldStop()) {  // [1] (line 6) hit infinite loop
      var a = 1;            // [2] run evals
      a++;                  // [3] exit run loop
    }
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 6) hit infinite loop

  // [2] run eval statements
  sendRuntimeEvalRequest(conn, msgId + 0, R"("0: " + globalVar)");
  expectEvalResponse(conn, msgId + 0, "0: omega");

  // [2.1] run eval statements that return non-string primitive values
  sendRuntimeEvalRequest(conn, msgId + 1, "booleanVar");
  sendRuntimeEvalRequest(conn, msgId + 2, "numberVar");
  expectEvalResponse(conn, msgId + 1, true);
  expectEvalResponse(conn, msgId + 2, 42);

  // [2.2] run eval statement that returns object
  sendRuntimeEvalRequest(conn, msgId + 3, "objectVar");
  expectEvalResponse(
      conn,
      msgId + 3,
      {{"number", PropInfo("number").setValue(1)},
       {"bool", PropInfo("boolean").setValue(false)},
       {"str", PropInfo("string").setValue("string")},
       {"__proto__", PropInfo("object")}});

  // [3] exit run loop
  asyncRuntime.stop();
}

TEST(ConnectionTests, testEvalOnCallFrameException) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var count = 0;

    function eventuallyThrows(x) {
      if (x <= 0)
        throw new Error("I frew up.");
      count++;
      eventuallyThrows(x-1);
    }

    function callme() {
      print("Hello");
      debugger;          // [1] (line 12) hit debugger statement
                         // [2]           run evals
                         // [3]           resume
      print("Goodbye");
    }

    callme();
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 12) hit debugger statement
  expectPaused(conn, "other", {{"callme", 12, 2}, {"global", 18, 1}});

  // [2] run evals
  int frame = 0;
  sendEvalRequest(conn, msgId + 0, frame, "this is not valid javascript");
  sendEvalRequest(conn, msgId + 1, frame, "eventuallyThrows(5)");
  sendEvalRequest(conn, msgId + 2, frame, "count");

  expectEvalException(conn, msgId + 0, "SyntaxError: 1:6:';' expected", {});
  expectEvalException(
      conn,
      msgId + 1,
      "Error: I frew up.",
      {{"eventuallyThrows", 5, 0},
       {"eventuallyThrows", 7, 0},
       {"eventuallyThrows", 7, 0},
       {"eventuallyThrows", 7, 0},
       {"eventuallyThrows", 7, 0},
       {"eventuallyThrows", 7, 0},

       // TODO: unsure why these frames are here, but they're in hdb tests
       // too. Ask Hermes about if they really should be there.
       FrameInfo("eval", 0, 0).setLineNumberMax(19),
       FrameInfo("(native)", 0, 0),
       FrameInfo("global", 0, 0).setLineNumberMax(19)});
  expectEvalResponse(conn, msgId + 2, 5);
  msgId += 3;

  // [3] resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testLoadMultipleScripts) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(
      R"(
    function foo(x) {
      debugger;
      print(x);
    }

    var a = 1 + 1;

    //# sourceMappingURL=/foo/bar/url1.js.map
  )",
      "url1");

  send<m::debugger::EnableRequest>(conn, msgId++);

  expectExecutionContextCreated(conn);

  m::debugger::ScriptParsedNotification script1 =
      expectScriptParsed(conn, "url1", "/foo/bar/url1.js.map");

  asyncRuntime.executeScriptAsync(
      R"(
    var b = a + 2;
    var c = b - 1;
    foo(c);

    //# sourceMappingURL=/foo/bar/url2.js.map
  )",
      "url2");

  m::debugger::ScriptParsedNotification script2 =
      expectScriptParsed(conn, "url2", "/foo/bar/url2.js.map");

  // [1] (line 2) hit debugger statement, resume
  expectPaused(
      conn,
      "other",
      {FrameInfo("foo", 2, 2).setScriptId(script1.scriptId),
       FrameInfo("global", 3, 1).setScriptId(script2.scriptId)});

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testGetProperties) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;
  std::vector<std::string> objIds;

  asyncRuntime.executeScriptAsync(R"(
    function foo() {
      var num = 123;
      var obj = {
        "depth": 0,
        "value": {
          "a": -1/0,
          "b": 1/0,
          "c": Math.sqrt(-2),
          "d": -0,
          "e": "e_string"
        }
      };
      var arr = [1, 2, 3];
      function bar() {
        var num = 456;
        var obj = {"depth": 1, "value": {"c": 5, "d": "d_string"}};
        debugger;
      };
      bar();
      debugger;
    }

    foo();
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  auto pausedNote = expectPaused(
      conn, "other", {{"bar", 17, 3}, {"foo", 19, 2}, {"global", 23, 1}});

  auto &scopeObj = pausedNote.callFrames.at(1).scopeChain.at(0).object;
  EXPECT_TRUE(scopeObj.objectId.hasValue());
  std::string scopeObjId = scopeObj.objectId.value();
  objIds.push_back(scopeObjId);

  auto scopeChildren = expectProps(
      conn,
      msgId++,
      scopeObjId,
      {{"num", PropInfo("number").setValue(123)},
       {"obj", PropInfo("object")},
       {"arr", PropInfo("object").setSubtype("array")},
       {"bar", PropInfo("function")}});
  EXPECT_EQ(scopeChildren.size(), 3);

  EXPECT_EQ(scopeChildren.count("obj"), 1);
  std::string objId = scopeChildren.at("obj");
  objIds.push_back(objId);

  auto objChildren = expectProps(
      conn,
      msgId++,
      objId,
      {{"depth", PropInfo("number").setValue(0)},
       {"value", PropInfo("object")},
       {"__proto__", PropInfo("object")}});
  EXPECT_EQ(objChildren.size(), 2);

  EXPECT_EQ(objChildren.count("value"), 1);
  std::string valueId = objChildren.at("value");
  objIds.push_back(valueId);

  auto valueChildren = expectProps(
      conn,
      msgId++,
      valueId,
      {{"a", PropInfo("number").setUnserializableValue("-Infinity")},
       {"b", PropInfo("number").setUnserializableValue("Infinity")},
       {"c", PropInfo("number").setUnserializableValue("NaN")},
       {"d", PropInfo("number").setUnserializableValue("-0")},
       {"e", PropInfo("string").setValue("e_string")},
       {"__proto__", PropInfo("object")}});
  EXPECT_EQ(valueChildren.size(), 1);

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  expectPaused(conn, "other", {{"foo", 20, 2}, {"global", 23, 1}});

  // all old object ids should be invalid after resuming
  for (std::string oldObjId : objIds) {
    expectProps(
        conn, msgId++, oldObjId, std::unordered_map<std::string, PropInfo>{});
  }

  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testGetPropertiesOnlyOwnProperties) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    function foo() {
      var protoObject = {
        "protoNum": 77
      };
      var obj = Object.create(protoObject);
      obj.num = 42;
      debugger;
    }
    foo();
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // wait for a pause on debugger statement and get object ID from the local
  // scope.
  auto pausedNote =
      expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 9, 1}});
  auto scopeObject = pausedNote.callFrames.at(0).scopeChain.at(0).object;
  auto scopeChildren = expectProps(
      conn,
      msgId++,
      scopeObject.objectId.value(),
      {{"obj", PropInfo("object")}, {"protoObject", PropInfo("object")}});
  EXPECT_EQ(scopeChildren.count("obj"), 1);
  std::string objId = scopeChildren.at("obj");

  // Check that GetProperties request for obj object only have own properties
  // when onlyOwnProperties = true.
  expectProps(
      conn,
      msgId++,
      objId,
      {{"num", PropInfo("number").setValue(42)},
       {"__proto__", PropInfo("object")}},
      true);

  // Check that GetProperties request for obj object only have all properties
  // when onlyOwnProperties = false.
  // __proto__ is not returned here because all properties from proto chain
  // are already included in the result.
  expectProps(
      conn,
      msgId++,
      objId,
      {{"num", PropInfo("number").setValue(42)},
       {"protoNum", PropInfo("number").setValue(77)}},
      false);

  // resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testDisable) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    while (!shouldStop()) {
      var a = 1;
      var b = 2;
      var c = a + b; // [1] (line 4) disable to remove breakpoints and resume
                     // [2] (line 4) the breakpoint should not hit anymore
      var d = 10;
    }
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // set breakpoint on line 4: "var c = ..."
  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 4;
  conn.send(req.toJson());

  expectBreakpointResponse(conn, req.id, 4, 4);

  // [1] (line 4) disable to remove breakpoints and resume
  expectPaused(conn, "other", {{"global", 4, 1}});

  send<m::debugger::DisableRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] (line 4) the breakpoint should not hit anymore
  expectNothing(conn);
  asyncRuntime.stop();
}

TEST(ConnectionTests, testDisableWhileRunning) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] initial pause to set the breakpoint on line 6
    while (!shouldStop()) { // [2] loop running until we receive a detach request
      var a = 1;
    }
    while (shouldStop()) {
      var c = a + 1; // [3] (line 6) the breakpoint should not hit after detach
    }
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] initial pause to set the breakpoint on line 6
  expectPaused(conn, "other", {{"global", 1, 1}});

  // set breakpoint on line 6: "var c = ..."
  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 6;
  conn.send(req.toJson());

  expectBreakpointResponse(conn, req.id, 6, 6);

  // [2] loop running until we receive a detach request
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  send<m::debugger::DisableRequest>(conn, msgId++);
  asyncRuntime.stop();

  // [3] (line 6) the breakpoint should not hit after detach
  expectNothing(conn);
  asyncRuntime.start();
}

TEST(ConnectionTests, testSetPauseOnExceptionsAll) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] (line 1) initial pause, set throw on exceptions to 'All'

    try {
      var a = 123;
      throw new Error('Caught error'); // [2] line 5, pause on exception
    } catch (err) {
      // Do nothing.
    }

    throw new Error('Uncaught exception'); // [3] line 10, pause on exception
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 1) initial pause, set throw on exceptions to 'All'
  expectPaused(conn, "other", {{"global", 1, 1}});
  m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
  allExceptionsReq.id = msgId++;
  allExceptionsReq.state = "all";
  conn.send(allExceptionsReq.toJson());
  expectResponse<m::OkResponse>(conn, allExceptionsReq.id);

  // Resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [2] line 5, pause on exception
  expectPaused(conn, "exception", {{"global", 5, 1}});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] line 10, pause on exception
  expectPaused(conn, "exception", {{"global", 10, 1}});

  // Send resume event and check that Hermes has thrown an exception.
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
  asyncRuntime.wait();
  EXPECT_EQ(asyncRuntime.getNumberOfExceptions(), 1);
  EXPECT_EQ(asyncRuntime.getLastThrownExceptionMessage(), "Uncaught exception");
}

TEST(ConnectionTests, testSetPauseOnExceptionsNone) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] (line 1) initial pause, set throw on exceptions to 'None'

    try {
      var a = 123;
      throw new Error('Caught error');
    } catch (err) {
      // Do nothing.
    }

    throw new Error('Uncaught exception');
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 1) initial pause, set throw on exceptions to 'None'
  expectPaused(conn, "other", {{"global", 1, 1}});
  m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
  allExceptionsReq.id = msgId++;
  allExceptionsReq.state = "none";
  conn.send(allExceptionsReq.toJson());
  expectResponse<m::OkResponse>(conn, allExceptionsReq.id);

  // Resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // Check that Hermes has thrown an exception (without reporting it).
  expectNothing(conn);
  asyncRuntime.wait();
  EXPECT_EQ(asyncRuntime.getNumberOfExceptions(), 1);
  EXPECT_EQ(asyncRuntime.getLastThrownExceptionMessage(), "Uncaught exception");
}

TEST(ConnectionTests, testSetPauseOnExceptionsUncaught) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] (line 1) initial pause, set throw on exceptions to 'Uncaught'

    try {
      var a = 123;
      throw new Error('Caught error');
    } catch (err) {
      // Do nothing.
    }

    throw new Error('Uncaught exception'); // [3] line 10, pause on exception
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 1) initial pause, set throw on exceptions to 'Uncaught'
  expectPaused(conn, "other", {{"global", 1, 1}});
  m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
  allExceptionsReq.id = msgId++;
  allExceptionsReq.state = "uncaught";
  conn.send(allExceptionsReq.toJson());
  expectResponse<m::OkResponse>(conn, allExceptionsReq.id);

  // Resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // [3] line 10, pause on exception
  expectPaused(conn, "exception", {{"global", 10, 1}});

  // Send resume event and check that Hermes has thrown an exception.
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
  asyncRuntime.wait();
  EXPECT_EQ(asyncRuntime.getNumberOfExceptions(), 1);
  EXPECT_EQ(asyncRuntime.getLastThrownExceptionMessage(), "Uncaught exception");
}

TEST(ConnectionTests, testShouldPauseOnThrow) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] (line 1) initial pause, check shouldPauseOnThrow is false
              // [2] set throw to 'All', check shouldPauseOnThrow is true
              // [3] set throw to 'None', check shouldPauseOnThrow is false
              // [4] set throw to 'Uncaught', check shouldPauseOnThrow is true
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  auto shouldPauseOnThrowEvalMsg =
      R"("-> " + DebuggerInternal.shouldPauseOnThrow)";
  auto responseTrue = "-> true";
  auto responseFalse = "-> false";

  // [1] (line 1) initial pause, check shouldPauseOnThrow is false
  expectPaused(conn, "other", {{"global", 1, 1}});
  sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
  expectEvalResponse(conn, msgId + 1, responseFalse);

  // [2] set throw to 'All', check shouldPauseOnThrow is true
  m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
  allExceptionsReq.id = msgId++;
  allExceptionsReq.state = "all";
  conn.send(allExceptionsReq.toJson());
  expectResponse<m::OkResponse>(conn, allExceptionsReq.id);

  sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
  expectEvalResponse(conn, msgId + 1, responseTrue);

  // [3] set throw to 'None', check shouldPauseOnThrow is false
  m::debugger::SetPauseOnExceptionsRequest noExceptionsReq;
  noExceptionsReq.id = msgId++;
  noExceptionsReq.state = "none";
  conn.send(noExceptionsReq.toJson());
  expectResponse<m::OkResponse>(conn, noExceptionsReq.id);

  sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
  expectEvalResponse(conn, msgId + 1, responseFalse);

  // [4] set throw to 'Uncaught', check shouldPauseOnThrow is true
  m::debugger::SetPauseOnExceptionsRequest uncaughtExceptionsReq;
  uncaughtExceptionsReq.id = msgId++;
  uncaughtExceptionsReq.state = "uncaught";
  conn.send(uncaughtExceptionsReq.toJson());
  expectResponse<m::OkResponse>(conn, uncaughtExceptionsReq.id);

  sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
  expectEvalResponse(conn, msgId + 1, responseTrue);

  // Resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testScopeVariables) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var globalString = "global-string";
    var globalObject = {number: 1, bool: false};

    function func() {
      var localString = "local-string";
      var localObject = {number: 2, bool: true};
      debugger; // [1] (line 7) hit debugger statement
                // two local vars - localString and localObject
                // two global vars - globalString and globalObject
    }

    func(); // line 12
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 7) hit debugger statement
  auto pausedNote =
      expectPaused(conn, "other", {{"func", 7, 2}, {"global", 12, 1}});
  auto scopeChain = pausedNote.callFrames.at(0).scopeChain;
  EXPECT_EQ(scopeChain.size(), 2);

  // [2] inspect local scope
  EXPECT_EQ(scopeChain.at(0).name, "Scope 0");
  EXPECT_EQ(scopeChain.at(0).type, "local");
  auto localScopeObject = scopeChain.at(0).object;
  auto localScopeObjectChildren = expectProps(
      conn,
      msgId++,
      localScopeObject.objectId.value(),
      {{"localString", PropInfo("string").setValue("local-string")},
       {"localObject", PropInfo("object")}});
  auto localObjectId = localScopeObjectChildren.at("localObject");
  expectProps(
      conn,
      msgId++,
      localObjectId,
      {{"number", PropInfo("number").setValue(2)},
       {"bool", PropInfo("boolean").setValue(true)},
       {"__proto__", PropInfo("object")}});

  // [3] inspect global scope
  // Global scope can contain more properties than we have defined
  // in our test code and we can't use expectProps() method here.
  // As a workaround we create a Map of properties and check that
  // those global properties that we have defined are in the map.
  EXPECT_EQ(scopeChain.at(1).name, "Global Scope");
  EXPECT_EQ(scopeChain.at(1).type, "global");
  auto globalScopeObject = scopeChain.at(1).object;
  m::runtime::GetPropertiesRequest req;
  req.id = msgId++;
  req.objectId = globalScopeObject.objectId.value();
  conn.send(req.toJson());
  auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, req.id);
  std::unordered_map<std::string, folly::Optional<m::runtime::RemoteObject>>
      globalProperties;
  for (auto propertyDescriptor : resp.result) {
    globalProperties[propertyDescriptor.name] = propertyDescriptor.value;
  }
  EXPECT_GE(globalProperties.size(), 3);

  // globalString should be of type "string" and have value "global-string".
  EXPECT_EQ(globalProperties.count("globalString"), 1);
  EXPECT_TRUE(globalProperties["globalString"].hasValue());
  EXPECT_EQ(globalProperties["globalString"].value().type, "string");
  EXPECT_EQ(
      globalProperties["globalString"].value().value.value(), "global-string");

  // func should be of type "function".
  EXPECT_EQ(globalProperties.count("func"), 1);
  EXPECT_TRUE(globalProperties["func"].hasValue());
  EXPECT_EQ(globalProperties["func"].value().type, "function");

  // globalObject should be of type "object" with "number" and "bool"
  // properties.
  EXPECT_EQ(globalProperties.count("globalObject"), 1);
  EXPECT_TRUE(globalProperties["globalObject"].hasValue());
  EXPECT_EQ(globalProperties["globalObject"].value().type, "object");
  expectProps(
      conn,
      msgId++,
      globalProperties["globalObject"].value().objectId.value(),
      {{"number", PropInfo("number").setValue(1)},
       {"bool", PropInfo("boolean").setValue(false)},
       {"__proto__", PropInfo("object")}});

  // [4] resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testConsoleLog) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var object1 = {number1: 1, bool1: false};
    var object2 = {number2: 2, bool2: true};
    console.warn('string value', object1, object2);

    debugger; // Hit debugger statement so that we receive console
              // api notification before VM gets destroyed.
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // Two notifications (hitting debugger and console API call) can appear
  // in any order. We wait for two notifications here and later check
  // that both of them were hit.
  bool receivedConsoleNotification = false;
  bool receivedPausedNotification = false;
  for (size_t i = 0; i < 2; ++i) {
    conn.waitForNotification([&receivedConsoleNotification,
                              &receivedPausedNotification,
                              &conn,
                              &msgId](const std::string &str) {
      auto parsedNote = folly::parseJson(str);
      auto method = parsedNote.at("method").asString();
      if (method == "Runtime.consoleAPICalled") {
        receivedConsoleNotification = true;
        auto note = m::runtime::ConsoleAPICalledNotification(parsedNote);
        EXPECT_EQ(note.type, "warning");
        EXPECT_EQ(note.args.size(), 3);

        EXPECT_EQ(note.args[0].type, "string");
        EXPECT_EQ(note.args[0].value, "string value");

        EXPECT_EQ(note.args[1].type, "object");
        expectProps(
            conn,
            msgId++,
            note.args[1].objectId.value(),
            {{"number1", PropInfo("number").setValue(1)},
             {"bool1", PropInfo("boolean").setValue(false)},
             {"__proto__", PropInfo("object")}});

        EXPECT_EQ(note.args[2].type, "object");
        expectProps(
            conn,
            msgId++,
            note.args[2].objectId.value(),
            {{"number2", PropInfo("number").setValue(2)},
             {"bool2", PropInfo("boolean").setValue(true)},
             {"__proto__", PropInfo("object")}});
      } else if (method == "Debugger.paused") {
        receivedPausedNotification = true;
        auto note = m::debugger::PausedNotification(parsedNote);
        EXPECT_EQ(note.reason, "other");
        EXPECT_EQ(note.callFrames.size(), 1);
        EXPECT_EQ(note.callFrames[0].functionName, "global");
        EXPECT_EQ(note.callFrames[0].location.lineNumber, 5);
      } else {
        throw UnexpectedNotificationException();
      }
    });
  }

  EXPECT_TRUE(receivedConsoleNotification);
  EXPECT_TRUE(receivedPausedNotification);

  // Resume and expect no further notifications
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
  expectNothing(conn);
}

TEST(ConnectionTests, testThisObject) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(R"(
    var globalString = "global-string";

    var object = {
      someVar: "object var",
      foo: function() {
        var localString = "local-string";
        debugger; // [1] (line 7) hit debugger statement.
      }
    }

    object.foo(); // (line 11)
  )");

  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] (line 1) hit debugger statement
  auto pausedNote =
      expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 11, 1}});

  // [2] inspect first call frame (foo)
  auto localThisObj = pausedNote.callFrames.at(0).thisObj;
  expectProps(
      conn,
      msgId++,
      localThisObj.objectId.value(),
      {{"someVar", PropInfo("string").setValue("object var")},
       {"foo", PropInfo("function")},
       {"__proto__", PropInfo("object")}});

  // [3] inspect second call frame (global)
  // Global scope can contain more properties than we have defined
  // in our test code and we can't use expectProps() method here.
  // As a workaround we create a Map of properties and check that
  // those global properties that we have defined are in the map.
  auto globalThisObj = pausedNote.callFrames.at(1).thisObj;
  m::runtime::GetPropertiesRequest req;
  req.id = msgId++;
  req.objectId = globalThisObj.objectId.value();
  conn.send(req.toJson());
  auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, req.id);
  std::unordered_map<std::string, folly::Optional<m::runtime::RemoteObject>>
      properties;
  for (auto propertyDescriptor : resp.result) {
    properties[propertyDescriptor.name] = propertyDescriptor.value;
  }

  // globalString should be of type "string" and have value "global-string".
  EXPECT_EQ(properties.count("globalString"), 1);
  EXPECT_TRUE(properties["globalString"].hasValue());
  EXPECT_EQ(properties["globalString"].value().type, "string");
  EXPECT_EQ(properties["globalString"].value().value.value(), "global-string");

  // [4] resume
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testSetBreakpointsMultipleScripts) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  std::string url1 = "first-url";
  asyncRuntime.executeScriptAsync(
      R"(
    function foo1() {
      var somevar1 = 111; // (line 2) hit breakpoint
      var somevar2 = 222;
    }
  )",
      url1);
  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  auto scriptParsed1 =
      expectNotification<m::debugger::ScriptParsedNotification>(conn);

  std::string url2 = "second-url";
  asyncRuntime.executeScriptAsync(
      R"(
    function foo2() {
      var somevar3 = 333;
      var somevar4 = 444; // (line 3) hit breakpoint
    }
  )",
      url2);
  auto scriptParsed2 =
      expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // In the third script we will set breakpoint (on debugger statement)
  // and call both functions (script url doesn't matter).
  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] (line 1) set breakpoints in both files
    foo1();
    foo2();
  )");
  auto scriptParsed3 =
      expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] hit debugger statement
  expectPaused(conn, "other", {{"global", 1, 1}});

  // Set breakpoint on line 2 in the first script and line 3 in the second
  // script.
  m::debugger::SetBreakpointByUrlRequest req1;
  req1.id = msgId++;
  req1.lineNumber = 2;
  req1.url = url1;
  conn.send(req1.toJson());

  expectBreakpointResponse(conn, req1.id, 2, 2);

  m::debugger::SetBreakpointByUrlRequest req2;
  req2.id = msgId++;
  req2.lineNumber = 3;
  req2.url = url2;
  conn.send(req2.toJson());

  expectBreakpointResponse(conn, req2.id, 3, 3);

  // Resume and check that we hit correct breakpoints.
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // First we should stop on line 2 of the first script.
  expectPaused(
      conn,
      "other",
      {FrameInfo("foo1", 2, 2).setScriptId(scriptParsed1.scriptId),
       FrameInfo("global", 2, 1).setScriptId(scriptParsed3.scriptId)});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // Next we should stop on line 3 of the second script.
  expectPaused(
      conn,
      "other",
      {FrameInfo("foo2", 3, 2).setScriptId(scriptParsed2.scriptId),
       FrameInfo("global", 3, 1).setScriptId(scriptParsed3.scriptId)});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testSetBreakpointByUrlRegex) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  std::string url1 = "https://www.example.com/123456";
  asyncRuntime.executeScriptAsync(
      R"(
    function foo1() {
      var somevar1 = 111; // (line 2) hit breakpoint
    }
  )",
      url1);
  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  auto scriptParsed1 =
      expectNotification<m::debugger::ScriptParsedNotification>(conn);

  std::string url2 = "https://www.example.com/abcdefg";
  asyncRuntime.executeScriptAsync(
      R"(
    function foo2() {
      var aaa = 'bbb';
      var somevar2 = 222; // (line 3) hit breakpoint
    }
  )",
      url2);
  auto scriptParsed2 =
      expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // In the third script we will set breakpoint (on debugger statement)
  // and call both functions (script url doesn't matter).
  asyncRuntime.executeScriptAsync(R"(
    debugger; // [1] (line 1) set breakpoints in both files
    foo1();
    foo2();
  )");
  auto scriptParsed3 =
      expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // [1] hit debugger statement
  expectPaused(conn, "other", {{"global", 1, 1}});

  // Set breakpoint on line 2 of URL matching "www\.example\.com\/[\d]+"
  // (should match url1).
  m::debugger::SetBreakpointByUrlRequest req1;
  req1.id = msgId++;
  req1.lineNumber = 2;
  req1.urlRegex = R"(https://www\.example\.com\/[\d]+)";
  conn.send(req1.toJson());

  expectBreakpointResponse(conn, req1.id, 2, 2);

  // Set breakpoint on line 3 of URL matching "www\.example\.com\/[a-zA-z]+"
  // (should match url2).
  m::debugger::SetBreakpointByUrlRequest req2;
  req2.id = msgId++;
  req2.lineNumber = 3;
  req2.urlRegex = R"(https://www\.example\.com\/[a-zA-z]+)";
  conn.send(req2.toJson());

  expectBreakpointResponse(conn, req2.id, 3, 3);

  // Resume and check that we hit correct breakpoints.
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // First we should stop on line 2 of the first script.
  expectPaused(
      conn,
      "other",
      {FrameInfo("foo1", 2, 2).setScriptId(scriptParsed1.scriptId),
       FrameInfo("global", 2, 1).setScriptId(scriptParsed3.scriptId)});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);

  // Next we should stop on line 3 of the second script.
  expectPaused(
      conn,
      "other",
      {FrameInfo("foo2", 3, 2).setScriptId(scriptParsed2.scriptId),
       FrameInfo("global", 3, 1).setScriptId(scriptParsed3.scriptId)});
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

TEST(ConnectionTests, testColumnBreakpoint) {
  TestContext context;
  AsyncHermesRuntime &asyncRuntime = context.runtime();
  SyncConnection &conn = context.conn();
  int msgId = 1;

  asyncRuntime.executeScriptAsync(
      R"(
function foo(){x=1}debugger;foo();
)",
      "url");
  send<m::debugger::EnableRequest>(conn, msgId++);
  expectExecutionContextCreated(conn);
  expectNotification<m::debugger::ScriptParsedNotification>(conn);

  // Hit debugger statement.
  expectPaused(conn, "other", {{"global", 1, 1}});

  // Set breakpoint on position 1:16 (x=1).
  m::debugger::SetBreakpointByUrlRequest req;
  req.id = msgId++;
  req.lineNumber = 1;
  req.columnNumber = 16;
  req.url = "url";
  conn.send(req.toJson());

  expectBreakpointResponse(conn, req.id, 1, 1);

  // Resume and except to pause on a breakpoint.
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
  expectPaused(
      conn,
      "other",
      {FrameInfo("foo", 1, 2).setColumnNumber(16), FrameInfo("global", 1, 1)});

  // Resume execution
  send<m::debugger::ResumeRequest>(conn, msgId++);
  expectNotification<m::debugger::ResumedNotification>(conn);
}

} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook
