/* 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/Array.h"               // JS::GetArrayLength, JS::IsArrayObject
#include "js/CallAndConstruct.h"    // JS::Construct
#include "js/Object.h"              // JS::GetClass
#include "js/PropertyAndElement.h"  // JS_GetElement, JS_SetElement
#include "jsapi-tests/tests.h"
#include "vm/PlainObject.h"  // js::PlainObject::class_

#include "vm/NativeObject-inl.h"

using namespace js;

static bool constructHook(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = CallArgsFromVp(argc, vp);

  // Check that arguments were passed properly from JS_New.

  JS::RootedObject obj(cx, JS_NewPlainObject(cx));
  if (!obj) {
    JS_ReportErrorASCII(cx, "test failed, could not construct object");
    return false;
  }
  if (strcmp(JS::GetClass(obj)->name, "Object") != 0) {
    JS_ReportErrorASCII(cx, "test failed, wrong class for 'this'");
    return false;
  }
  if (args.length() != 3) {
    JS_ReportErrorASCII(cx, "test failed, argc == %d", args.length());
    return false;
  }
  if (!args[0].isInt32() || args[2].toInt32() != 2) {
    JS_ReportErrorASCII(cx, "test failed, wrong value in args[2]");
    return false;
  }
  if (!args.isConstructing()) {
    JS_ReportErrorASCII(cx, "test failed, not constructing");
    return false;
  }

  // Perform a side-effect to indicate that this hook was actually called.
  JS::RootedValue value(cx, args[0]);
  JS::RootedObject callee(cx, &args.callee());
  if (!JS_SetElement(cx, callee, 0, value)) {
    return false;
  }

  args.rval().setObject(*obj);

  // trash the argv, perversely
  args[0].setUndefined();
  args[1].setUndefined();
  args[2].setUndefined();

  return true;
}

BEGIN_TEST(testNewObject_1) {
  static const size_t N = 1000;
  JS::RootedValueVector argv(cx);
  CHECK(argv.resize(N));

  JS::RootedValue Array(cx);
  EVAL("Array", &Array);

  bool isArray;

  // With no arguments.
  JS::RootedObject obj(cx);
  CHECK(JS::Construct(cx, Array, JS::HandleValueArray::empty(), &obj));
  CHECK(JS::IsArrayObject(cx, obj, &isArray));
  CHECK(isArray);
  uint32_t len;
  CHECK(JS::GetArrayLength(cx, obj, &len));
  CHECK_EQUAL(len, 0u);

  // With one argument.
  argv[0].setInt32(4);
  CHECK(JS::Construct(cx, Array, JS::HandleValueArray::subarray(argv, 0, 1),
                      &obj));
  CHECK(JS::IsArrayObject(cx, obj, &isArray));
  CHECK(isArray);
  CHECK(JS::GetArrayLength(cx, obj, &len));
  CHECK_EQUAL(len, 4u);

  // With N arguments.
  for (size_t i = 0; i < N; i++) {
    argv[i].setInt32(i);
  }
  CHECK(JS::Construct(cx, Array, JS::HandleValueArray::subarray(argv, 0, N),
                      &obj));
  CHECK(JS::IsArrayObject(cx, obj, &isArray));
  CHECK(isArray);
  CHECK(JS::GetArrayLength(cx, obj, &len));
  CHECK_EQUAL(len, N);
  JS::RootedValue v(cx);
  CHECK(JS_GetElement(cx, obj, N - 1, &v));
  CHECK(v.isInt32(N - 1));

  // With JSClass.construct.
  static const JSClassOps clsOps = {
      .construct = constructHook,
  };
  static const JSClass cls = {
      "testNewObject_1",
      0,
      &clsOps,
  };
  JS::RootedObject ctor(cx, JS_NewObject(cx, &cls));
  CHECK(ctor);
  JS::RootedValue ctorVal(cx, JS::ObjectValue(*ctor));
  CHECK(JS::Construct(cx, ctorVal, JS::HandleValueArray::subarray(argv, 0, 3),
                      &obj));
  CHECK(JS_GetElement(cx, ctor, 0, &v));
  CHECK(v.isInt32(0));

  return true;
}
END_TEST(testNewObject_1)

BEGIN_TEST(testNewObject_IsMapObject) {
  // Test IsMapObject and IsSetObject

  JS::RootedValue vMap(cx);
  EVAL("Map", &vMap);

  bool isMap = false;
  bool isSet = false;
  JS::RootedObject mapObj(cx);
  CHECK(JS::Construct(cx, vMap, JS::HandleValueArray::empty(), &mapObj));
  CHECK(JS::IsMapObject(cx, mapObj, &isMap));
  CHECK(isMap);
  CHECK(JS::IsSetObject(cx, mapObj, &isSet));
  CHECK(!isSet);

  JS::RootedValue vSet(cx);
  EVAL("Set", &vSet);

  JS::RootedObject setObj(cx);
  CHECK(JS::Construct(cx, vSet, JS::HandleValueArray::empty(), &setObj));
  CHECK(JS::IsMapObject(cx, setObj, &isMap));
  CHECK(!isMap);
  CHECK(JS::IsSetObject(cx, setObj, &isSet));
  CHECK(isSet);

  return true;
}
END_TEST(testNewObject_IsMapObject)

static const JSClass Base_class = {
    "Base",
    JSCLASS_HAS_RESERVED_SLOTS(8),  // flags
};

BEGIN_TEST(testNewObject_Subclassing) {
  JSObject* proto =
      JS_InitClass(cx, global, nullptr, nullptr, "Base", Base_constructor, 0,
                   nullptr, nullptr, nullptr, nullptr);
  if (!proto) {
    return false;
  }

  CHECK_EQUAL(JS::GetClass(proto), &PlainObject::class_);

  // Calling Base without `new` should fail with a TypeError.
  JS::RootedValue expectedError(cx);
  EVAL("TypeError", &expectedError);
  JS::RootedValue actualError(cx);
  EVAL(
      "try {\n"
      "  Base();\n"
      "} catch (e) {\n"
      "  e.constructor;\n"
      "}\n",
      &actualError);
  CHECK_SAME(actualError, expectedError);

  // Check prototype chains when a JS class extends a base class that's
  // implemented in C++ using JS_NewObjectForConstructor.
  EXEC(
      "class MyClass extends Base {\n"
      "  ok() { return true; }\n"
      "}\n"
      "let myObj = new MyClass();\n");

  JS::RootedValue result(cx);
  EVAL("myObj.ok()", &result);
  CHECK_SAME(result, JS::TrueValue());

  EVAL("myObj.__proto__ === MyClass.prototype", &result);
  CHECK_SAME(result, JS::TrueValue());
  EVAL("myObj.__proto__.__proto__ === Base.prototype", &result);
  CHECK_SAME(result, JS::TrueValue());

  EVAL("myObj", &result);
  CHECK_EQUAL(JS::GetClass(&result.toObject()), &Base_class);

  // All reserved slots are initialized to undefined.
  for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(&Base_class); i++) {
    CHECK_SAME(JS::GetReservedSlot(&result.toObject(), i),
               JS::UndefinedValue());
  }

  return true;
}

static bool Base_constructor(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = CallArgsFromVp(argc, vp);
  JS::RootedObject obj(cx, JS_NewObjectForConstructor(cx, &Base_class, args));
  if (!obj) {
    return false;
  }
  args.rval().setObject(*obj);
  return true;
}

END_TEST(testNewObject_Subclassing)

static const JSClass TestClass = {
    "TestObject",
    JSCLASS_HAS_RESERVED_SLOTS(0),
};

BEGIN_TEST(testNewObject_elements) {
  Rooted<NativeObject*> obj(cx, NewBuiltinClassInstance(cx, &TestClass));
  CHECK(obj);
  CHECK(!obj->isTenured());
  CHECK(obj->hasEmptyElements());
  CHECK(!obj->hasFixedElements());
  CHECK(!obj->hasDynamicElements());

  CHECK(obj->ensureElements(cx, 1));
  CHECK(!obj->hasEmptyElements());
  CHECK(!obj->hasFixedElements());
  CHECK(obj->hasDynamicElements());

  RootedObject array(cx, NewArrayObject(cx, 1));
  CHECK(array);
  obj = &array->as<NativeObject>();
  CHECK(!obj->isTenured());
  CHECK(!obj->hasEmptyElements());
  CHECK(obj->hasFixedElements());
  CHECK(!obj->hasDynamicElements());

  return true;
}
END_TEST(testNewObject_elements)
