/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "js/Object.h"              // JS::GetReservedSlot, JS::SetReservedSlot
#include "js/PropertyAndElement.h"  // JS_DefineProperty, JS_DefinePropertyById
#include "jsapi-tests/tests.h"

/*
 * Test that resolve hook recursion for the same object and property is
 * prevented.
 */
BEGIN_TEST(testResolveRecursion) {
  static const JSClassOps my_resolve_classOps = {
      .resolve = my_resolve,
  };

  static const JSClass my_resolve_class = {
      "MyResolve",
      JSCLASS_HAS_RESERVED_SLOTS(SlotCount),
      &my_resolve_classOps,
  };

  obj1.init(cx, JS_NewObject(cx, &my_resolve_class));
  CHECK(obj1);
  obj2.init(cx, JS_NewObject(cx, &my_resolve_class));
  CHECK(obj2);
  JS::SetReservedSlot(obj1, TestSlot, JS::PrivateValue(this));
  JS::SetReservedSlot(obj2, TestSlot, JS::PrivateValue(this));

  JS::RootedValue obj1Val(cx, JS::ObjectValue(*obj1));
  JS::RootedValue obj2Val(cx, JS::ObjectValue(*obj2));
  CHECK(JS_DefineProperty(cx, global, "obj1", obj1Val, 0));
  CHECK(JS_DefineProperty(cx, global, "obj2", obj2Val, 0));

  resolveEntryCount = 0;
  resolveExitCount = 0;

  /* Start the essence of the test via invoking the first resolve hook. */
  JS::RootedValue v(cx);
  EVAL("obj1.x", &v);
  CHECK(v.isFalse());
  CHECK_EQUAL(resolveEntryCount, 4);
  CHECK_EQUAL(resolveExitCount, 4);

  obj1 = nullptr;
  obj2 = nullptr;
  return true;
}

enum Slots { TestSlot, SlotCount };

JS::PersistentRootedObject obj1;
JS::PersistentRootedObject obj2;
int resolveEntryCount;
int resolveExitCount;

struct AutoIncrCounters {
  explicit AutoIncrCounters(cls_testResolveRecursion* t) : t(t) {
    t->resolveEntryCount++;
  }

  ~AutoIncrCounters() { t->resolveExitCount++; }

  cls_testResolveRecursion* t;
};

bool doResolve(JS::HandleObject obj, JS::HandleId id, bool* resolvedp) {
  CHECK_EQUAL(resolveExitCount, 0);
  AutoIncrCounters incr(this);
  CHECK(obj == obj1 || obj == obj2);

  CHECK(id.isString());

  JSLinearString* str = JS_EnsureLinearString(cx, id.toString());
  CHECK(str);
  JS::RootedValue v(cx);
  if (JS_LinearStringEqualsLiteral(str, "x")) {
    if (obj == obj1) {
      /* First resolve hook invocation. */
      CHECK_EQUAL(resolveEntryCount, 1);
      EVAL("obj2.y = true", &v);
      CHECK(v.isTrue());
      CHECK(JS_DefinePropertyById(cx, obj, id, JS::FalseHandleValue,
                                  JSPROP_RESOLVING));
      *resolvedp = true;
      return true;
    }
    if (obj == obj2) {
      CHECK_EQUAL(resolveEntryCount, 4);
      *resolvedp = false;
      return true;
    }
  } else if (JS_LinearStringEqualsLiteral(str, "y")) {
    if (obj == obj2) {
      CHECK_EQUAL(resolveEntryCount, 2);
      CHECK(JS_DefinePropertyById(cx, obj, id, JS::NullHandleValue,
                                  JSPROP_RESOLVING));
      EVAL("obj1.x", &v);
      CHECK(v.isUndefined());
      EVAL("obj1.y", &v);
      CHECK(v.isInt32(0));
      *resolvedp = true;
      return true;
    }
    if (obj == obj1) {
      CHECK_EQUAL(resolveEntryCount, 3);
      EVAL("obj1.x", &v);
      CHECK(v.isUndefined());
      EVAL("obj1.y", &v);
      CHECK(v.isUndefined());
      EVAL("obj2.y", &v);
      CHECK(v.isNull());
      EVAL("obj2.x", &v);
      CHECK(v.isUndefined());
      EVAL("obj1.y = 0", &v);
      CHECK(v.isInt32(0));
      *resolvedp = true;
      return true;
    }
  }
  CHECK(false);
  return false;
}

static bool my_resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
                       bool* resolvedp) {
  void* p = JS::GetReservedSlot(obj, TestSlot).toPrivate();
  return static_cast<cls_testResolveRecursion*>(p)->doResolve(obj, id,
                                                              resolvedp);
}
END_TEST(testResolveRecursion)

/*
 * Test that JS_InitStandardClasses does not cause resolve hooks to be called.
 *
 * (XPConnect apparently does have global classes, such as the one created by
 * nsMessageManagerScriptExecutor::InitChildGlobalInternal(), that have resolve
 * hooks which can call back into JS, and on which JS_InitStandardClasses is
 * called. Calling back into JS in the middle of resolving `undefined` is bad.)
 */
BEGIN_TEST(testResolveRecursion_InitStandardClasses) {
  CHECK(JS::InitRealmStandardClasses(cx));
  return true;
}

const JSClass* getGlobalClass() override {
  static const JSClassOps myGlobalClassOps = {
      .resolve = my_resolve,
      .trace = JS_GlobalObjectTraceHook,
  };

  static const JSClass myGlobalClass = {
      "testResolveRecursion_InitStandardClasses_myGlobalClass",
      JSCLASS_GLOBAL_FLAGS,
      &myGlobalClassOps,
  };

  return &myGlobalClass;
}

static bool my_resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
                       bool* resolvedp) {
  MOZ_ASSERT_UNREACHABLE(
      "resolve hook should not be called from InitStandardClasses");
  JS_ReportErrorASCII(cx, "FAIL");
  return false;
}
END_TEST(testResolveRecursion_InitStandardClasses)
