/* 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 <string.h>

#include "jsapi.h"

#include "frontend/CompilationStencil.h"
#include "js/CompilationAndEvaluation.h"
#include "js/EnvironmentChain.h"  // JS::EnvironmentChain
#include "js/experimental/CompileScript.h"
#include "js/experimental/JSStencil.h"
#include "js/Modules.h"
#include "js/PropertyAndElement.h"  // JS_GetProperty, JS_HasOwnProperty, JS_SetProperty
#include "js/Transcoding.h"
#include "jsapi-tests/tests.h"
#include "vm/HelperThreads.h"  // js::RunPendingSourceCompressions
#include "vm/Monitor.h"        // js::Monitor, js::AutoLockMonitor

BEGIN_TEST(testStencil_Basic) {
  const char* chars =
      "function f() { return 42; }"
      "f();";
  auto result = basic_test<char, mozilla::Utf8Unit>(chars);
  CHECK(result);

  const char16_t* chars16 =
      u"function f() { return 42; }"
      u"f();";
  auto result16 = basic_test<char16_t, char16_t>(chars16);
  CHECK(result16);

  return true;
}

template <typename CharT, typename SourceT>
bool basic_test(const CharT* chars) {
  size_t length = std::char_traits<CharT>::length(chars);

  JS::SourceText<SourceT> srcBuf;
  CHECK(srcBuf.init(cx, chars, length, JS::SourceOwnership::Borrowed));

  JS::CompileOptions options(cx);
  RefPtr<JS::Stencil> stencil =
      JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
  CHECK(stencil);

  JS::InstantiateOptions instantiateOptions(options);
  JS::RootedScript script(
      cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
  CHECK(script);

  JS::RootedValue rval(cx);
  CHECK(JS_ExecuteScript(cx, script, &rval));
  CHECK(rval.isNumber() && rval.toNumber() == 42);

  return true;
}
END_TEST(testStencil_Basic)

BEGIN_TEST(testStencil_Module) {
  const char* chars =
      "export function f() { return 42; }"
      "globalThis.x = f();";
  auto result = basic_test<char, mozilla::Utf8Unit>(chars);
  CHECK(result);

  const char16_t* chars16 =
      u"export function f() { return 42; }"
      u"globalThis.x = f();";
  auto result16 = basic_test<char16_t, char16_t>(chars16);
  CHECK(result16);

  return true;
}

template <typename CharT, typename SourceT>
bool basic_test(const CharT* chars) {
  size_t length = std::char_traits<CharT>::length(chars);

  JS::SourceText<SourceT> srcBuf;
  CHECK(srcBuf.init(cx, chars, length, JS::SourceOwnership::Borrowed));

  JS::CompileOptions options(cx);
  options.setFile("testStencil_Module");

  RefPtr<JS::Stencil> stencil =
      JS::CompileModuleScriptToStencil(cx, options, srcBuf);
  CHECK(stencil);

  JS::InstantiateOptions instantiateOptions(options);
  JS::RootedObject moduleObject(
      cx, JS::InstantiateModuleStencil(cx, instantiateOptions, stencil));
  CHECK(moduleObject);

  CHECK(JS::LoadRequestedModules(cx, moduleObject, JS::UndefinedHandleValue,
                                 OnResolve, OnReject));

  // Link and evaluate the module graph. The link step used to be call
  // "instantiate" but is unrelated to the concept in Stencil with same name.
  JS::RootedValue rval(cx);
  CHECK(JS::ModuleLink(cx, moduleObject));
  CHECK(JS::ModuleEvaluate(cx, moduleObject, &rval));
  CHECK(!rval.isUndefined());

  js::RunJobs(cx);
  CHECK(JS_GetProperty(cx, global, "x", &rval));
  CHECK(rval.isNumber() && rval.toNumber() == 42);

  return true;
}

static bool OnResolve(JSContext* cx, JS::HandleValue hostDefined) {
  return true;
}
static bool OnReject(JSContext* cx, JS::HandleValue hostDefined,
                     JS::HandleValue error) {
  return true;
}

END_TEST(testStencil_Module)

BEGIN_TEST(testStencil_NonSyntactic) {
  const char* chars =
      "function f() { return x; }"
      "f();";

  JS::SourceText<mozilla::Utf8Unit> srcBuf;
  CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed));

  JS::CompileOptions options(cx);
  options.setNonSyntacticScope(true);

  RefPtr<JS::Stencil> stencil =
      JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
  CHECK(stencil);

  JS::InstantiateOptions instantiateOptions(options);
  JS::RootedScript script(
      cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
  CHECK(script);

  JS::RootedObject obj(cx, JS_NewPlainObject(cx));
  JS::RootedValue val(cx, JS::Int32Value(42));
  CHECK(obj);
  CHECK(JS_SetProperty(cx, obj, "x", val));

  JS::EnvironmentChain envChain(cx, JS::SupportUnscopables::No);
  CHECK(envChain.append(obj));

  JS::RootedValue rval(cx);
  CHECK(JS_ExecuteScript(cx, envChain, script, &rval));
  CHECK(rval.isNumber() && rval.toNumber() == 42);

  return true;
}
END_TEST(testStencil_NonSyntactic)

BEGIN_TEST(testStencil_MultiGlobal) {
  const char* chars =
      "/**************************************/"
      "/**************************************/"
      "/**************************************/"
      "/**************************************/"
      "/**************************************/"
      "/**************************************/"
      "function f() { return 42; }"
      "f();";

  JS::SourceText<mozilla::Utf8Unit> srcBuf;
  CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed));

  JS::CompileOptions options(cx);
  RefPtr<JS::Stencil> stencil =
      JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
  CHECK(stencil);

  CHECK(RunInNewGlobal(cx, stencil));
  CHECK(RunInNewGlobal(cx, stencil));
  CHECK(RunInNewGlobal(cx, stencil));

  // Start any pending SourceCompressionTasks now to confirm nothing fell apart
  // when using a JS::Stencil multiple times.
  CHECK(strlen(chars) > js::ScriptSource::MinimumCompressibleLength);
  js::RunPendingSourceCompressions(cx->runtime());

  return true;
}
bool RunInNewGlobal(JSContext* cx, RefPtr<JS::Stencil> stencil) {
  JS::RootedObject otherGlobal(cx, createGlobal());
  CHECK(otherGlobal);

  JSAutoRealm ar(cx, otherGlobal);

  JS::InstantiateOptions instantiateOptions;
  JS::RootedScript script(
      cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
  CHECK(script);

  JS::RootedValue rval(cx);
  CHECK(JS_ExecuteScript(cx, script, &rval));
  CHECK(rval.isNumber() && rval.toNumber() == 42);

  return true;
}
END_TEST(testStencil_MultiGlobal)

BEGIN_TEST(testStencil_Transcode) {
  JS::SetProcessBuildIdOp(TestGetBuildId);

  JS::TranscodeBuffer buffer;

  {
    const char* chars =
        "function f() { return 42; }"
        "f();";

    JS::SourceText<mozilla::Utf8Unit> srcBuf;
    CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed));

    JS::CompileOptions options(cx);
    RefPtr<JS::Stencil> stencil =
        JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
    CHECK(stencil);

    // Encode Stencil to XDR
    JS::TranscodeResult res = JS::EncodeStencil(cx, stencil, buffer);
    CHECK(res == JS::TranscodeResult::Ok);
    CHECK(!buffer.empty());

    // Instantiate and Run
    JS::InstantiateOptions instantiateOptions(options);
    JS::RootedScript script(
        cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
    JS::RootedValue rval(cx);
    CHECK(script);
    CHECK(JS_ExecuteScript(cx, script, &rval));
    CHECK(rval.isNumber() && rval.toNumber() == 42);
  }

  // Create a new global
  CHECK(createGlobal());
  JSAutoRealm ar(cx, global);

  // Confirm it doesn't have the old code
  bool found = false;
  CHECK(JS_HasOwnProperty(cx, global, "f", &found));
  CHECK(!found);

  {
    // Decode the stencil into new range
    RefPtr<JS::Stencil> stencil;

    {
      JS::DecodeOptions decodeOptions;
      JS::TranscodeRange range(buffer.begin(), buffer.length());
      JS::TranscodeResult res =
          JS::DecodeStencil(cx, decodeOptions, range, getter_AddRefs(stencil));
      CHECK(res == JS::TranscodeResult::Ok);
    }

    {
      JS::FrontendContext* fc = JS::NewFrontendContext();
      JS::DecodeOptions decodeOptions;
      JS::TranscodeRange range(buffer.begin(), buffer.length());
      JS::TranscodeResult res =
          JS::DecodeStencil(fc, decodeOptions, range, getter_AddRefs(stencil));
      CHECK(res == JS::TranscodeResult::Ok);
      JS::DestroyFrontendContext(fc);
    }

    // Delete the buffer to verify that the decoded stencil has no dependency
    // to the buffer.
    memset(buffer.begin(), 0, buffer.length());
    buffer.clear();

    // Instantiate and Run
    JS::InstantiateOptions instantiateOptions;
    JS::RootedScript script(
        cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
    stencil = nullptr;
    JS::RootedValue rval(cx);
    CHECK(script);
    CHECK(JS_ExecuteScript(cx, script, &rval));
    CHECK(rval.isNumber() && rval.toNumber() == 42);
  }

  return true;
}
static bool TestGetBuildId(JS::BuildIdCharVector* buildId) {
  const char buildid[] = "testXDR";
  return buildId->append(buildid, sizeof(buildid));
}
END_TEST(testStencil_Transcode)

BEGIN_TEST(testStencil_TranscodeBorrowing) {
  JS::SetProcessBuildIdOp(TestGetBuildId);

  JS::TranscodeBuffer buffer;

  {
    const char* chars =
        "function f() { return 42; }"
        "f();";

    JS::SourceText<mozilla::Utf8Unit> srcBuf;
    CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed));

    JS::CompileOptions options(cx);
    RefPtr<JS::Stencil> stencil =
        JS::CompileGlobalScriptToStencil(cx, options, srcBuf);
    CHECK(stencil);

    // Encode Stencil to XDR
    JS::TranscodeResult res = JS::EncodeStencil(cx, stencil, buffer);
    CHECK(res == JS::TranscodeResult::Ok);
    CHECK(!buffer.empty());
  }

  JS::RootedScript script(cx);
  {
    JS::TranscodeRange range(buffer.begin(), buffer.length());
    JS::DecodeOptions decodeOptions;
    decodeOptions.borrowBuffer = true;
    RefPtr<JS::Stencil> stencil;
    JS::TranscodeResult res =
        JS::DecodeStencil(cx, decodeOptions, range, getter_AddRefs(stencil));
    CHECK(res == JS::TranscodeResult::Ok);

    JS::InstantiateOptions instantiateOptions;
    script = JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil);
    CHECK(script);
  }

  // Delete the buffer to verify that the instantiated script has no dependency
  // to the buffer.
  memset(buffer.begin(), 0, buffer.length());
  buffer.clear();

  JS::RootedValue rval(cx);
  CHECK(JS_ExecuteScript(cx, script, &rval));
  CHECK(rval.isNumber() && rval.toNumber() == 42);

  return true;
}
static bool TestGetBuildId(JS::BuildIdCharVector* buildId) {
  const char buildid[] = "testXDR";
  return buildId->append(buildid, sizeof(buildid));
}
END_TEST(testStencil_TranscodeBorrowing)
