// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/renderer/bindings/argument_spec.h"

#include <string_view>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "extensions/grit/extensions_renderer_generated_resources.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_invocation_errors.h"
#include "extensions/renderer/bindings/api_type_reference_map.h"
#include "extensions/renderer/bindings/argument_spec_builder.h"
#include "extensions/renderer/module_system_test.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
#include "gin/public/isolate_holder.h"
#include "gin/test/v8_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-object.h"
#include "v8/include/v8-primitive.h"
#include "v8/include/v8.h"

namespace extensions {

using api_errors::IndexError;
using api_errors::InvalidType;
using api_errors::kTypeBoolean;
using api_errors::kTypeDouble;
using api_errors::kTypeFunction;
using api_errors::kTypeInteger;
using api_errors::kTypeList;
using api_errors::kTypeNull;
using api_errors::kTypeObject;
using api_errors::kTypeString;
using api_errors::kTypeUndefined;
using api_errors::MissingRequiredProperty;
using api_errors::NumberIsNaNOrInfinity;

using V8Validator = base::OnceCallback<void(v8::Local<v8::Value>)>;

class ArgumentSpecUnitTest : public gin::V8Test {
 public:
  ArgumentSpecUnitTest(const ArgumentSpecUnitTest&) = delete;
  ArgumentSpecUnitTest& operator=(const ArgumentSpecUnitTest&) = delete;

 protected:
  ArgumentSpecUnitTest()
      : type_refs_(APITypeReferenceMap::InitializeTypeCallback()) {}
  ~ArgumentSpecUnitTest() override {}

  enum class TestResult {
    PASS,
    FAIL,
    THROW,
  };

  struct RunTestParams {
    RunTestParams(const ArgumentSpec& spec,
                  std::string_view script_source,
                  TestResult result)
        : spec(spec), script_source(script_source), expected_result(result) {}

    const raw_ref<const ArgumentSpec> spec;
    std::string_view script_source;
    TestResult expected_result;
    std::string_view expected_json;
    std::string_view expected_error;
    std::string_view expected_thrown_message;
    raw_ptr<const base::Value> expected_value = nullptr;
    bool should_convert_to_base = true;
    bool should_convert_to_v8 = false;
    V8Validator validate_v8;
  };

  void ExpectSuccess(const ArgumentSpec& spec,
                     const std::string& script_source,
                     const std::string& expected_json_single_quotes) {
    RunTestParams params(spec, script_source, TestResult::PASS);
    std::string expected_json =
        ReplaceSingleQuotes(expected_json_single_quotes);
    params.expected_json = expected_json;
    RunTest(params);
  }

  void ExpectSuccess(const ArgumentSpec& spec,
                     const std::string& script_source,
                     const base::Value& expected_value) {
    RunTestParams params(spec, script_source, TestResult::PASS);
    params.expected_value = &expected_value;
    RunTest(params);
  }

  void ExpectSuccess(const ArgumentSpec& spec,
                     const std::string& script_source,
                     V8Validator validate_v8) {
    RunTestParams params(spec, script_source, TestResult::PASS);
    params.should_convert_to_base = false;
    params.should_convert_to_v8 = true;
    params.validate_v8 = std::move(validate_v8);
    RunTest(params);
  }

  void ExpectSuccessWithNoConversion(const ArgumentSpec& spec,
                                     std::string_view script_source) {
    RunTestParams params(spec, script_source, TestResult::PASS);
    params.should_convert_to_base = false;
    RunTest(params);
  }

  void ExpectFailure(const ArgumentSpec& spec,
                     std::string_view script_source,
                     std::string_view expected_error) {
    RunTestParams params(spec, script_source, TestResult::FAIL);
    params.expected_error = expected_error;
    RunTest(params);
  }

  void ExpectFailureWithNoConversion(const ArgumentSpec& spec,
                                     std::string_view script_source,
                                     std::string_view expected_error) {
    RunTestParams params(spec, script_source, TestResult::FAIL);
    params.should_convert_to_base = false;
    params.expected_error = expected_error;
    RunTest(params);
  }

  void ExpectThrow(const ArgumentSpec& spec,
                   std::string_view script_source,
                   std::string_view expected_thrown_message) {
    RunTestParams params(spec, script_source, TestResult::THROW);
    params.expected_thrown_message = expected_thrown_message;
    RunTest(params);
  }

  void AddTypeRef(const std::string& id, std::unique_ptr<ArgumentSpec> spec) {
    type_refs_.AddSpec(id, std::move(spec));
  }

  const APITypeReferenceMap& type_refs() const { return type_refs_; }

 private:
  void RunTest(RunTestParams& params);

  APITypeReferenceMap type_refs_;
};

void ArgumentSpecUnitTest::RunTest(RunTestParams& params) {
  v8::Isolate* isolate = instance_->isolate();
  v8::HandleScope handle_scope(instance_->isolate());

  v8::Local<v8::Context> context =
      v8::Local<v8::Context>::New(instance_->isolate(), context_);
  v8::TryCatch try_catch(isolate);
  v8::Local<v8::Value> val =
      V8ValueFromScriptSource(context, params.script_source);
  ASSERT_FALSE(val.IsEmpty()) << params.script_source;

  std::string error;
  std::unique_ptr<base::Value> out_value;
  v8::Local<v8::Value> v8_out_value;
  bool did_succeed = params.spec->ParseArgument(
      context, val, type_refs_,
      params.should_convert_to_base ? &out_value : nullptr,
      params.should_convert_to_v8 ? &v8_out_value : nullptr, &error);
  bool should_succeed = params.expected_result == TestResult::PASS;
  ASSERT_EQ(should_succeed, did_succeed)
      << params.script_source << ", " << error;
  ASSERT_EQ(did_succeed && params.should_convert_to_base, !!out_value);
  ASSERT_EQ(did_succeed && params.should_convert_to_v8,
            !v8_out_value.IsEmpty());
  bool should_throw = params.expected_result == TestResult::THROW;
  ASSERT_EQ(should_throw, try_catch.HasCaught()) << params.script_source;

  if (!params.expected_error.empty())
    EXPECT_EQ(params.expected_error, error) << params.script_source;

  if (should_succeed) {
    if (params.should_convert_to_base) {
      ASSERT_TRUE(out_value);
      if (params.expected_value) {
        EXPECT_EQ(*params.expected_value, *out_value) << params.script_source;
      } else {
        EXPECT_EQ(params.expected_json, ValueToString(*out_value));
      }
    }
    if (params.should_convert_to_v8) {
      ASSERT_FALSE(v8_out_value.IsEmpty()) << params.script_source;
      if (!params.validate_v8.is_null())
        std::move(params.validate_v8).Run(v8_out_value);
    }
  } else if (should_throw) {
    EXPECT_EQ(params.expected_thrown_message,
              gin::V8ToString(isolate, try_catch.Message()->Get()));
  }
}

TEST_F(ArgumentSpecUnitTest, Test) {
  {
    ArgumentSpec spec(DictValueFromString("{'type': 'integer'}"));
    ExpectSuccess(spec, "1", "1");
    ExpectSuccess(spec, "-1", "-1");
    ExpectSuccess(spec, "0", "0");
    ExpectSuccess(spec, "0.0", "0");
    ExpectSuccess(spec, "-0.0", "0");
    ExpectSuccess(spec, "-0.", "0");
    ExpectSuccess(spec, "-.0", "0");
    ExpectSuccess(spec, "-0", "0");
    ExpectFailure(spec, "undefined", InvalidType(kTypeInteger, kTypeUndefined));
    ExpectFailure(spec, "null", InvalidType(kTypeInteger, kTypeNull));
    ExpectFailure(spec, "1.1", InvalidType(kTypeInteger, kTypeDouble));
    ExpectFailure(spec, "'foo'", InvalidType(kTypeInteger, kTypeString));
    ExpectFailure(spec, "'1'", InvalidType(kTypeInteger, kTypeString));
    ExpectFailure(spec, "({})", InvalidType(kTypeInteger, kTypeObject));
    ExpectFailure(spec, "[1]", InvalidType(kTypeInteger, kTypeList));
    ExpectFailure(spec, "NaN", InvalidType(kTypeInteger, kTypeDouble));
    ExpectFailure(spec, "Infinity", InvalidType(kTypeInteger, kTypeDouble));
  }

  {
    ArgumentSpec spec(DictValueFromString("{'type': 'number'}"));
    ExpectSuccess(spec, "1", "1.0");
    ExpectSuccess(spec, "-1", "-1.0");
    ExpectSuccess(spec, "0", "0.0");
    ExpectSuccess(spec, "1.1", "1.1");
    ExpectSuccess(spec, "1.", "1.0");
    ExpectSuccess(spec, ".1", "0.1");
    ExpectFailure(spec, "undefined", InvalidType(kTypeDouble, kTypeUndefined));
    ExpectFailure(spec, "null", InvalidType(kTypeDouble, kTypeNull));
    ExpectFailure(spec, "'foo'", InvalidType(kTypeDouble, kTypeString));
    ExpectFailure(spec, "'1.1'", InvalidType(kTypeDouble, kTypeString));
    ExpectFailure(spec, "({})", InvalidType(kTypeDouble, kTypeObject));
    ExpectFailure(spec, "[1.1]", InvalidType(kTypeDouble, kTypeList));
    ExpectFailure(spec, "NaN", NumberIsNaNOrInfinity());
    ExpectFailure(spec, "Infinity", NumberIsNaNOrInfinity());
  }

  {
    ArgumentSpec spec(DictValueFromString("{'type': 'integer', 'minimum': 1}"));
    ExpectSuccess(spec, "2", "2");
    ExpectSuccess(spec, "1", "1");
    ExpectFailure(spec, "0", api_errors::NumberTooSmall(1));
    ExpectFailure(spec, "-1", api_errors::NumberTooSmall(1));
  }

  {
    ArgumentSpec spec(
        DictValueFromString("{'type': 'integer', 'maximum': 10}"));
    ExpectSuccess(spec, "10", "10");
    ExpectSuccess(spec, "1", "1");
    ExpectFailure(spec, "11", api_errors::NumberTooLarge(10));
  }

  {
    ArgumentSpec spec(DictValueFromString("{'type': 'string'}"));
    ExpectSuccess(spec, "'foo'", "'foo'");
    ExpectSuccess(spec, "''", "''");
    ExpectFailure(spec, "1", InvalidType(kTypeString, kTypeInteger));
    ExpectFailure(spec, "({})", InvalidType(kTypeString, kTypeObject));
    ExpectFailure(spec, "['foo']", InvalidType(kTypeString, kTypeList));
  }

  {
    ArgumentSpec spec(
        DictValueFromString("{'type': 'string', 'enum': ['foo', 'bar']}"));
    std::set<std::string> valid_enums = {"foo", "bar"};
    ExpectSuccess(spec, "'foo'", "'foo'");
    ExpectSuccess(spec, "'bar'", "'bar'");
    ExpectFailure(spec, "['foo']", InvalidType(kTypeString, kTypeList));
    ExpectFailure(spec, "'fo'", api_errors::InvalidEnumValue(valid_enums));
    ExpectFailure(spec, "'foobar'", api_errors::InvalidEnumValue(valid_enums));
    ExpectFailure(spec, "'baz'", api_errors::InvalidEnumValue(valid_enums));
    ExpectFailure(spec, "''", api_errors::InvalidEnumValue(valid_enums));
  }

  {
    ArgumentSpec spec(DictValueFromString(
        "{'type': 'string', 'enum': [{'name': 'foo'}, {'name': 'bar'}]}"));
    std::set<std::string> valid_enums = {"foo", "bar"};
    ExpectSuccess(spec, "'foo'", "'foo'");
    ExpectSuccess(spec, "'bar'", "'bar'");
    ExpectFailure(spec, "['foo']", InvalidType(kTypeString, kTypeList));
    ExpectFailure(spec, "'fo'", api_errors::InvalidEnumValue(valid_enums));
    ExpectFailure(spec, "'foobar'", api_errors::InvalidEnumValue(valid_enums));
    ExpectFailure(spec, "'baz'", api_errors::InvalidEnumValue(valid_enums));
    ExpectFailure(spec, "''", api_errors::InvalidEnumValue(valid_enums));
  }

  {
    ArgumentSpec spec(DictValueFromString("{'type': 'boolean'}"));
    ExpectSuccess(spec, "true", "true");
    ExpectSuccess(spec, "false", "false");
    ExpectFailure(spec, "1", InvalidType(kTypeBoolean, kTypeInteger));
    ExpectFailure(spec, "'true'", InvalidType(kTypeBoolean, kTypeString));
    ExpectFailure(spec, "null", InvalidType(kTypeBoolean, kTypeNull));
  }

  {
    ArgumentSpec spec(
        DictValueFromString("{'type': 'array', 'items': {'type': 'string'}}"));
    ExpectSuccess(spec, "[]", "[]");
    ExpectSuccess(spec, "['foo']", "['foo']");
    ExpectSuccess(spec, "['foo', 'bar']", "['foo','bar']");
    ExpectSuccess(spec, "var x = new Array(); x[0] = 'foo'; x;", "['foo']");
    ExpectFailure(spec, "'foo'", InvalidType(kTypeList, kTypeString));
    ExpectFailure(spec, "[1, 2]",
                  IndexError(0u, InvalidType(kTypeString, kTypeInteger)));
    ExpectFailure(spec, "['foo', 1]",
                  IndexError(1u, InvalidType(kTypeString, kTypeInteger)));
    ExpectFailure(spec,
                  "var x = ['a', 'b', 'c'];"
                  "x[4] = 'd';"  // x[3] is undefined, violating the spec.
                  "x;",
                  IndexError(3u, InvalidType(kTypeString, kTypeUndefined)));
    ExpectThrow(
        spec,
        "var x = [];"
        "Object.defineProperty("
        "    x, 0,"
        "    { get: () => { throw new Error('Badness'); } });"
        "x;",
        "Uncaught Error: Badness");
  }

  {
    const char kObjectSpec[] =
        "{"
        "  'type': 'object',"
        "  'properties': {"
        "    'prop1': {'type': 'string'},"
        "    'prop2': {'type': 'integer', 'optional': true}"
        "  }"
        "}";
    ArgumentSpec spec(DictValueFromString(kObjectSpec));
    ExpectSuccess(spec, "({prop1: 'foo', prop2: 2})",
                  "{'prop1':'foo','prop2':2}");
    ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
    ExpectSuccess(spec, "({prop1: 'foo', prop2: null})", "{'prop1':'foo'}");
    ExpectSuccess(spec, "x = {}; x.prop1 = 'foo'; x;", "{'prop1':'foo'}");
    ExpectFailure(spec, "({prop1: 'foo', prop2: 'bar'})",
                  api_errors::PropertyError(
                      "prop2", InvalidType(kTypeInteger, kTypeString)));
    ExpectFailure(spec, "({prop2: 2})", MissingRequiredProperty("prop1"));
    // Unknown properties are not allowed.
    ExpectFailure(spec, "({prop1: 'foo', prop2: 2, prop3: 'blah'})",
                  api_errors::UnexpectedProperty("prop3"));
    // We only consider properties on the object itself, not its prototype
    // chain.
    ExpectFailure(spec,
                  "function X() {}\n"
                  "X.prototype = { prop1: 'foo' };\n"
                  "var x = new X();\n"
                  "x;",
                  MissingRequiredProperty("prop1"));
    ExpectFailure(spec,
                  "function X() {}\n"
                  "X.prototype = { prop1: 'foo' };\n"
                  "function Y() { this.__proto__ = X.prototype; }\n"
                  "var z = new Y();\n"
                  "z;",
                  MissingRequiredProperty("prop1"));
    // Self-referential fun. Currently we don't have to worry about these much
    // because the spec won't match at some point (and V8ValueConverter has
    // cycle detection and will fail).
    ExpectFailure(spec, "x = {}; x.prop1 = x; x;",
                  api_errors::PropertyError(
                      "prop1", InvalidType(kTypeString, kTypeObject)));
    ExpectThrow(
        spec,
        "({ get prop1() { throw new Error('Badness'); }});",
        "Uncaught Error: Badness");
    ExpectThrow(spec,
                "x = {prop1: 'foo'};\n"
                "Object.defineProperty(\n"
                "    x, 'prop2',\n"
                "    {\n"
                "      get: () => { throw new Error('Badness'); },\n"
                "      enumerable: true,\n"
                "});\n"
                "x;",
                "Uncaught Error: Badness");
    // By default, properties from Object.defineProperty() aren't enumerable,
    // so they will be ignored in our matching.
    ExpectSuccess(spec,
                  "x = {prop1: 'foo'};\n"
                  "Object.defineProperty(\n"
                  "    x, 'prop2',\n"
                  "    { get: () => { throw new Error('Badness'); } });\n"
                  "x;",
                  "{'prop1':'foo'}");
  }

  {
    const char kFunctionSpec[] = "{ 'type': 'function' }";
    ArgumentSpec spec(DictValueFromString(kFunctionSpec));
    // Functions are serialized as empty dictionaries.
    ExpectSuccess(spec, "(function() {})", "{}");
    ExpectSuccessWithNoConversion(spec, "(function() {})");
    ExpectSuccessWithNoConversion(spec, "(function(a, b) { a(); b(); })");
    ExpectSuccessWithNoConversion(spec, "(function(a, b) { a(); b(); })");
    ExpectFailureWithNoConversion(spec, "({a: function() {}})",
                                  InvalidType(kTypeFunction, kTypeObject));
    ExpectFailureWithNoConversion(spec, "([function() {}])",
                                  InvalidType(kTypeFunction, kTypeList));
    ExpectFailureWithNoConversion(spec, "1",
                                  InvalidType(kTypeFunction, kTypeInteger));
  }

  {
    const char kBinarySpec[] = "{ 'type': 'binary' }";
    ArgumentSpec spec(DictValueFromString(kBinarySpec));
    // Simple case: empty ArrayBuffer -> empty BinaryValue.
    ExpectSuccess(spec, "(new ArrayBuffer())",
                  base::Value(base::Value::Type::BINARY));
    {
      // A non-empty (but zero-filled) ArrayBufferView.
      ExpectSuccessWithNoConversion(spec, "(new Int32Array(2))");
    }
    {
      // Actual data.
      const uint8_t kBuffer[] = {'p', 'i', 'n', 'g'};
      base::Value expected_value{base::span(kBuffer)};
      ExpectSuccess(spec,
                    "var b = new ArrayBuffer(4);\n"
                    "var v = new Uint8Array(b);\n"
                    "var s = 'ping';\n"
                    "for (var i = 0; i < s.length; ++i)\n"
                    "  v[i] = s.charCodeAt(i);\n"
                    "b;",
                    expected_value);
    }
    ExpectFailure(spec, "1",
                  InvalidType(api_errors::kTypeBinary, kTypeInteger));
  }
  {
    const char kAnySpec[] = "{ 'type': 'any' }";
    ArgumentSpec spec(DictValueFromString(kAnySpec));
    ExpectSuccess(spec, "42", "42");
    ExpectSuccess(spec, "'foo'", "'foo'");
    ExpectSuccess(spec, "({prop1:'bar'})", "{'prop1':'bar'}");
    ExpectSuccess(spec, "[1, 2, 3]", "[1,2,3]");
    ExpectSuccess(spec, "[1, 'a']", "[1,'a']");
    ExpectSuccess(spec, "null", base::Value());
    ExpectSuccess(spec, "({prop1: 'alpha', prop2: null})", "{'prop1':'alpha'}");
    ExpectSuccess(spec,
                  "x = {alpha: 'alpha'};\n"
                  "y = {beta: 'beta', x: x};\n"
                  "y;",
                  "{'beta':'beta','x':{'alpha':'alpha'}}");
    // We don't serialize undefined.
    // TODO(devlin): This matches current behavior, but should it? Part of the
    // problem is that base::Values don't differentiate between undefined and
    // null, which is a potentially important distinction. However, this means
    // that in serialization of an object {a: 1, foo:undefined}, we lose the
    // 'foo' property.
    ExpectFailure(spec, "undefined", api_errors::UnserializableValue());

    ExpectSuccess(spec, "({prop1: 1, prop2: undefined})", "{'prop1':1}");
  }
}

TEST_F(ArgumentSpecUnitTest, TypeRefsTest) {
  const char kObjectType[] =
      "{"
      "  'id': 'refObj',"
      "  'type': 'object',"
      "  'properties': {"
      "    'prop1': {'type': 'string'},"
      "    'prop2': {'type': 'integer', 'optional': true}"
      "  }"
      "}";
  const char kEnumType[] =
      "{'id': 'refEnum', 'type': 'string', 'enum': ['alpha', 'beta']}";
  AddTypeRef("refObj",
             std::make_unique<ArgumentSpec>(DictValueFromString(kObjectType)));
  AddTypeRef("refEnum",
             std::make_unique<ArgumentSpec>(DictValueFromString(kEnumType)));
  std::set<std::string> valid_enums = {"alpha", "beta"};

  {
    const char kObjectWithRefEnumSpec[] =
        "{"
        "  'name': 'objWithRefEnum',"
        "  'type': 'object',"
        "  'properties': {"
        "    'e': {'$ref': 'refEnum'},"
        "    'sub': {'type': 'integer'}"
        "  }"
        "}";
    ArgumentSpec spec(DictValueFromString(kObjectWithRefEnumSpec));
    ExpectSuccess(spec, "({e: 'alpha', sub: 1})", "{'e':'alpha','sub':1}");
    ExpectSuccess(spec, "({e: 'beta', sub: 1})", "{'e':'beta','sub':1}");
    ExpectFailure(spec, "({e: 'gamma', sub: 1})",
                  api_errors::PropertyError(
                      "e", api_errors::InvalidEnumValue(valid_enums)));
    ExpectFailure(spec, "({e: 'alpha'})", MissingRequiredProperty("sub"));
  }

  {
    const char kObjectWithRefObjectSpec[] =
        "{"
        "  'name': 'objWithRefObject',"
        "  'type': 'object',"
        "  'properties': {"
        "    'o': {'$ref': 'refObj'}"
        "  }"
        "}";
    ArgumentSpec spec(DictValueFromString(kObjectWithRefObjectSpec));
    ExpectSuccess(spec, "({o: {prop1: 'foo'}})", "{'o':{'prop1':'foo'}}");
    ExpectSuccess(spec, "({o: {prop1: 'foo', prop2: 2}})",
                  "{'o':{'prop1':'foo','prop2':2}}");
    ExpectFailure(
        spec, "({o: {prop1: 1}})",
        api_errors::PropertyError(
            "o", api_errors::PropertyError(
                     "prop1", InvalidType(kTypeString, kTypeInteger))));
  }

  {
    const char kRefEnumListSpec[] =
        "{'type': 'array', 'items': {'$ref': 'refEnum'}}";
    ArgumentSpec spec(DictValueFromString(kRefEnumListSpec));
    ExpectSuccess(spec, "['alpha']", "['alpha']");
    ExpectSuccess(spec, "['alpha', 'alpha']", "['alpha','alpha']");
    ExpectSuccess(spec, "['alpha', 'beta']", "['alpha','beta']");
    ExpectFailure(spec, "['alpha', 'beta', 'gamma']",
                  IndexError(2u, api_errors::InvalidEnumValue(valid_enums)));
  }
}

TEST_F(ArgumentSpecUnitTest, TypeChoicesTest) {
  {
    const char kSimpleChoices[] =
        "{'choices': [{'type': 'string'}, {'type': 'integer'}]}";
    ArgumentSpec spec(DictValueFromString(kSimpleChoices));
    ExpectSuccess(spec, "'alpha'", "'alpha'");
    ExpectSuccess(spec, "42", "42");
    const char kChoicesType[] = "[string|integer]";
    ExpectFailure(spec, "true", InvalidType(kChoicesType, kTypeBoolean));
  }

  {
    const char kComplexChoices[] =
        "{"
        "  'choices': ["
        "    {'type': 'array', 'items': {'type': 'string'}},"
        "    {'type': 'object', 'properties': {'prop1': {'type': 'string'}}}"
        "  ]"
        "}";
    ArgumentSpec spec(DictValueFromString(kComplexChoices));
    ExpectSuccess(spec, "['alpha']", "['alpha']");
    ExpectSuccess(spec, "['alpha', 'beta']", "['alpha','beta']");
    ExpectSuccess(spec, "({prop1: 'alpha'})", "{'prop1':'alpha'}");

    const char kChoicesType[] = "[array|object]";
    ExpectFailure(spec, "({prop1: 1})", api_errors::InvalidChoice());
    ExpectFailure(spec, "'alpha'", InvalidType(kChoicesType, kTypeString));
    ExpectFailure(spec, "42", InvalidType(kChoicesType, kTypeInteger));
  }
}

TEST_F(ArgumentSpecUnitTest, AdditionalPropertiesTest) {
  {
    const char kOnlyAnyAdditionalProperties[] =
        "{"
        "  'type': 'object',"
        "  'additionalProperties': {'type': 'any'}"
        "}";
    ArgumentSpec spec(DictValueFromString(kOnlyAnyAdditionalProperties));
    ExpectSuccess(spec, "({prop1: 'alpha', prop2: 42, prop3: {foo: 'bar'}})",
                  "{'prop1':'alpha','prop2':42,'prop3':{'foo':'bar'}}");
    ExpectSuccess(spec, "({})", "{}");
    // Test some crazy keys.
    ExpectSuccess(spec,
                  "var x = {};\n"
                  "var y = {prop1: 'alpha'};\n"
                  "y[42] = 'beta';\n"
                  "y[x] = 'gamma';\n"
                  "y[undefined] = 'delta';\n"
                  "y;",
                  "{'42':'beta','[object Object]':'gamma','prop1':'alpha',"
                  "'undefined':'delta'}");
    // We (typically*, see "Fun case" below) don't serialize properties on an
    // object prototype.
    ExpectSuccess(spec,
                  "({\n"
                  "  __proto__: {protoProp: 'proto'},\n"
                  "  instanceProp: 'instance'\n"
                  "})",
                  "{'instanceProp':'instance'}");
    // Fun case: Remove a property as a result of getting another. Currently,
    // we don't check each property with HasOwnProperty() during iteration, so
    // Fun case: Remove a property as a result of getting another. Currently,
    // we don't check each property with HasOwnProperty() during iteration, so
    // we still try to serialize it. But we don't serialize undefined, so in the
    // case of the property not being defined on the prototype, this works as
    // expected.
    ExpectSuccess(spec,
                  "var x = {};\n"
                  "Object.defineProperty(\n"
                  "    x, 'alpha',\n"
                  "    {\n"
                  "      enumerable: true,\n"
                  "      get: () => { delete x.omega; return 'alpha'; }\n"
                  "    });\n"
                  "x.omega = 'omega';\n"
                  "x;",
                  "{'alpha':'alpha'}");
    // Fun case continued: If an object removes the property, and the property
    // *is* present on the prototype, then we serialize the value from the
    // prototype. This is inconsistent, but only manifests scripts are doing
    // crazy things (and is still safe).
    // TODO(devlin): We *could* add a HasOwnProperty() check, in which case
    // the result of this call should be {'alpha':'alpha'}.
    ExpectSuccess(spec,
                  "var x = {\n"
                  "  __proto__: { omega: 'different omega' }\n"
                  "};\n"
                  "Object.defineProperty(\n"
                  "    x, 'alpha',\n"
                  "    {\n"
                  "      enumerable: true,\n"
                  "      get: () => { delete x.omega; return 'alpha'; }\n"
                  "    });\n"
                  "x.omega = 'omega';\n"
                  "x;",
                  "{'alpha':'alpha','omega':'different omega'}");
  }
  {
    const char kPropertiesAndAnyAdditionalProperties[] =
        "{"
        "  'type': 'object',"
        "  'properties': {"
        "    'prop1': {'type': 'string'}"
        "  },"
        "  'additionalProperties': {'type': 'any'}"
        "}";
    ArgumentSpec spec(
        DictValueFromString(kPropertiesAndAnyAdditionalProperties));
    ExpectSuccess(spec, "({prop1: 'alpha', prop2: 42, prop3: {foo: 'bar'}})",
                  "{'prop1':'alpha','prop2':42,'prop3':{'foo':'bar'}}");
    // Additional properties are optional.
    ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
    ExpectFailure(spec, "({prop2: 42, prop3: {foo: 'bar'}})",
                  MissingRequiredProperty("prop1"));
    ExpectFailure(spec, "({prop1: 42})",
                  api_errors::PropertyError(
                      "prop1", InvalidType(kTypeString, kTypeInteger)));
  }
  {
    const char kTypedAdditionalProperties[] =
        "{"
        "  'type': 'object',"
        "  'additionalProperties': {'type': 'string'}"
        "}";
    ArgumentSpec spec(DictValueFromString(kTypedAdditionalProperties));
    ExpectSuccess(spec, "({prop1: 'alpha', prop2: 'beta', prop3: 'gamma'})",
                  "{'prop1':'alpha','prop2':'beta','prop3':'gamma'}");
    ExpectFailure(spec, "({prop1: 'alpha', prop2: 42})",
                  api_errors::PropertyError(
                      "prop2", InvalidType(kTypeString, kTypeInteger)));
  }
}

TEST_F(ArgumentSpecUnitTest, IgnoreAdditionalPropertiesTest) {
  {
    constexpr char kPropertiesWithIgnoreAdditionalProperties[] =
        R"({
             'type': 'object',
             'properties': {
               'prop1': {'type': 'string'}
             },
             'ignoreAdditionalProperties': true
           })";
    ArgumentSpec spec(
        DictValueFromString(kPropertiesWithIgnoreAdditionalProperties));
    ExpectSuccess(spec, "({prop1: 'alpha', prop2: 42, prop3: {foo: 'bar'}})",
                  "{'prop1':'alpha'}");
    ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
    ExpectFailure(spec, "({prop2: 42, prop3: {foo: 'bar'}})",
                  MissingRequiredProperty("prop1"));
    ExpectFailure(spec, "({prop1: 42})",
                  api_errors::PropertyError(
                      "prop1", InvalidType(kTypeString, kTypeInteger)));
  }
  {
    constexpr char kEmptyPropertiesWithIgnoreAdditionalProperties[] =
        R"({
             'type': 'object',
             'properties': {},
             'ignoreAdditionalProperties': true
           })";
    ArgumentSpec spec(
        DictValueFromString(kEmptyPropertiesWithIgnoreAdditionalProperties));
    ExpectSuccess(spec, "({prop1: 'alpha', prop2: {foo: 'bar'}})", "{}");
    ExpectSuccess(spec, "({})", "{}");
  }
}

TEST_F(ArgumentSpecUnitTest, InstanceOfTest) {
  {
    const char kInstanceOfRegExp[] =
        "{"
        "  'type': 'object',"
        "  'isInstanceOf': 'RegExp'"
        "}";
    ArgumentSpec spec(DictValueFromString(kInstanceOfRegExp));
    ExpectSuccess(spec, "(new RegExp())", "{}");
    ExpectSuccess(spec, "({ __proto__: RegExp.prototype })", "{}");
    ExpectSuccess(spec,
                  "(function() {\n"
                  "  function subRegExp() {}\n"
                  "  subRegExp.prototype = { __proto__: RegExp.prototype };\n"
                  "  return new subRegExp();\n"
                  "})()",
                  "{}");
    ExpectSuccess(spec,
                  "(function() {\n"
                  "  function RegExp() {}\n"
                  "  return new RegExp();\n"
                  "})()",
                  "{}");
    ExpectFailure(spec, "({})", api_errors::NotAnInstance("RegExp"));
    ExpectFailure(spec, "('')", InvalidType("RegExp", kTypeString));
    ExpectFailure(spec, "('.*')", InvalidType("RegExp", kTypeString));
    ExpectFailure(spec, "({ __proto__: Date.prototype })",
                  api_errors::NotAnInstance("RegExp"));
  }

  {
    const char kInstanceOfCustomClass[] =
        "{"
        "  'type': 'object',"
        "  'isInstanceOf': 'customClass'"
        "}";
    ArgumentSpec spec(DictValueFromString(kInstanceOfCustomClass));
    ExpectSuccess(spec,
                  "(function() {\n"
                  "  function customClass() {}\n"
                  "  return new customClass();\n"
                  "})()",
                  "{}");
    ExpectSuccess(spec,
                  "(function() {\n"
                  "  function customClass() {}\n"
                  "  function otherClass() {}\n"
                  "  otherClass.prototype = \n"
                  "      { __proto__: customClass.prototype };\n"
                  "  return new otherClass();\n"
                  "})()",
                  "{}");
    ExpectFailure(spec, "({})", api_errors::NotAnInstance("customClass"));
    ExpectFailure(spec,
                  "(function() {\n"
                  "  function otherClass() {}\n"
                  "  return new otherClass();\n"
                  "})()",
                  api_errors::NotAnInstance("customClass"));
  }
}

TEST_F(ArgumentSpecUnitTest, MinAndMaxLengths) {
  {
    const char kMinLengthString[] = "{'type': 'string', 'minLength': 3}";
    ArgumentSpec spec(DictValueFromString(kMinLengthString));
    ExpectSuccess(spec, "'aaa'", "'aaa'");
    ExpectSuccess(spec, "'aaaa'", "'aaaa'");
    ExpectFailure(spec, "'aa'", api_errors::TooFewStringChars(3, 2));
    ExpectFailure(spec, "''", api_errors::TooFewStringChars(3, 0));
  }

  {
    const char kMaxLengthString[] = "{'type': 'string', 'maxLength': 3}";
    ArgumentSpec spec(DictValueFromString(kMaxLengthString));
    ExpectSuccess(spec, "'aaa'", "'aaa'");
    ExpectSuccess(spec, "'aa'", "'aa'");
    ExpectSuccess(spec, "''", "''");
    ExpectFailure(spec, "'aaaa'", api_errors::TooManyStringChars(3, 4));
  }

  {
    const char kMinLengthArray[] =
        "{'type': 'array', 'items': {'type': 'integer'}, 'minItems': 3}";
    ArgumentSpec spec(DictValueFromString(kMinLengthArray));
    ExpectSuccess(spec, "[1, 2, 3]", "[1,2,3]");
    ExpectSuccess(spec, "[1, 2, 3, 4]", "[1,2,3,4]");
    ExpectFailure(spec, "[1, 2]", api_errors::TooFewArrayItems(3, 2));
    ExpectFailure(spec, "[]", api_errors::TooFewArrayItems(3, 0));
  }

  {
    const char kMaxLengthArray[] =
        "{'type': 'array', 'items': {'type': 'integer'}, 'maxItems': 3}";
    ArgumentSpec spec(DictValueFromString(kMaxLengthArray));
    ExpectSuccess(spec, "[1, 2, 3]", "[1,2,3]");
    ExpectSuccess(spec, "[1, 2]", "[1,2]");
    ExpectSuccess(spec, "[]", "[]");
    ExpectFailure(spec, "[1, 2, 3, 4]", api_errors::TooManyArrayItems(3, 4));
  }
}

TEST_F(ArgumentSpecUnitTest, PreserveNull) {
  {
    const char kObjectSpec[] =
        "{"
        "  'type': 'object',"
        "  'additionalProperties': {'type': 'any'},"
        "  'preserveNull': true"
        "}";
    ArgumentSpec spec(DictValueFromString(kObjectSpec));
    ExpectSuccess(spec, "({foo: 1, bar: null})", "{'bar':null,'foo':1}");
    // Subproperties shouldn't preserve null (if not specified).
    ExpectSuccess(spec, "({prop: {subprop1: 'foo', subprop2: null}})",
                  "{'prop':{'subprop1':'foo'}}");
  }

  {
    const char kObjectSpec[] =
        "{"
        "  'type': 'object',"
        "  'additionalProperties': {'type': 'any', 'preserveNull': true},"
        "  'preserveNull': true"
        "}";
    ArgumentSpec spec(DictValueFromString(kObjectSpec));
    ExpectSuccess(spec, "({foo: 1, bar: null})", "{'bar':null,'foo':1}");
    // Here, subproperties should preserve null.
    ExpectSuccess(spec, "({prop: {subprop1: 'foo', subprop2: null}})",
                  "{'prop':{'subprop1':'foo','subprop2':null}}");
  }

  {
    const char kObjectSpec[] =
        "{"
        "  'type': 'object',"
        "  'properties': {'prop1': {'type': 'string', 'optional': true}},"
        "  'preserveNull': true"
        "}";
    ArgumentSpec spec(DictValueFromString(kObjectSpec));
    ExpectSuccess(spec, "({})", "{}");
    ExpectSuccess(spec, "({prop1: null})", "{'prop1':null}");
    ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
    // Undefined should not be preserved.
    ExpectSuccess(spec, "({prop1: undefined})", "{}");
    // preserveNull shouldn't affect normal parsing restrictions.
    ExpectFailure(spec, "({prop1: 1})",
                  api_errors::PropertyError(
                      "prop1", InvalidType(kTypeString, kTypeInteger)));
  }
}

TEST_F(ArgumentSpecUnitTest, NaNFun) {
  {
    const char kAnySpec[] = "{'type': 'any'}";
    ArgumentSpec spec(DictValueFromString(kAnySpec));
    ExpectFailure(spec, "NaN", api_errors::UnserializableValue());
  }

  {
    const char kObjectWithAnyPropertiesSpec[] =
        "{'type': 'object', 'additionalProperties': {'type': 'any'}}";
    ArgumentSpec spec(DictValueFromString(kObjectWithAnyPropertiesSpec));
    ExpectSuccess(spec, "({foo: NaN, bar: 'baz'})", "{'bar':'baz'}");
  }
}

TEST_F(ArgumentSpecUnitTest, GetTypeName) {
  struct {
    ArgumentType type;
    const char* expected_type_name;
  } simple_cases[] = {
      {ArgumentType::BOOLEAN, kTypeBoolean},
      {ArgumentType::INTEGER, kTypeInteger},
      {ArgumentType::OBJECT, kTypeObject},
      {ArgumentType::LIST, kTypeList},
      {ArgumentType::BINARY, api_errors::kTypeBinary},
      {ArgumentType::FUNCTION, kTypeFunction},
      {ArgumentType::ANY, api_errors::kTypeAny},
  };

  for (const auto& test_case : simple_cases) {
    ArgumentSpec spec(test_case.type);
    EXPECT_EQ(test_case.expected_type_name, spec.GetTypeName());
  }

  {
    const char kRefName[] = "someRef";
    ArgumentSpec ref_spec(ArgumentType::REF);
    ref_spec.set_ref(kRefName);
    EXPECT_EQ(kRefName, ref_spec.GetTypeName());
  }

  {
    std::vector<std::unique_ptr<ArgumentSpec>> choices;
    choices.push_back(std::make_unique<ArgumentSpec>(ArgumentType::INTEGER));
    choices.push_back(std::make_unique<ArgumentSpec>(ArgumentType::STRING));
    ArgumentSpec choices_spec(ArgumentType::CHOICES);
    choices_spec.set_choices(std::move(choices));
    EXPECT_EQ("[integer|string]", choices_spec.GetTypeName());
  }
}

TEST_F(ArgumentSpecUnitTest, V8Conversion) {
  {
    // Sanity check: a simple conversion.
    ArgumentSpec spec(ArgumentType::INTEGER);
    ExpectSuccess(spec, "1", base::BindOnce([](v8::Local<v8::Value> value) {
                    ASSERT_TRUE(value->IsInt32());
                    EXPECT_EQ(1, value.As<v8::Int32>()->Value());
                  }));
    // The conversion should handle the -0 value (which is considered an
    // integer but stored in v8 has a number) by converting it to a 0 integer.
    ExpectSuccess(spec, "-0", base::BindOnce([](v8::Local<v8::Value> value) {
                    ASSERT_TRUE(value->IsInt32());
                    EXPECT_EQ(0, value.As<v8::Int32>()->Value());
                  }));
  }

  {
    std::unique_ptr<ArgumentSpec> prop =
        ArgumentSpecBuilder(ArgumentType::STRING, "str").Build();
    std::unique_ptr<ArgumentSpec> spec =
        ArgumentSpecBuilder(ArgumentType::OBJECT, "obj")
            .AddProperty("str", std::move(prop))
            .Build();
    // The conversion to a v8 value should handle tricky cases like this, where
    // a subtle getter would invalidate expectations if the value was passed
    // directly. Since the conversion creates a new object (free of any sneaky
    // getters), we don't need to repeatedly check for types.
    constexpr char kTrickyInterceptors[] = R"(
        ({
          get str() {
            if (this.checkedOnce)
              return 42;
            this.checkedOnce = true;
            return 'a string';
          }
         }))";
    ExpectSuccess(*spec, kTrickyInterceptors,
                  base::BindOnce([](v8::Local<v8::Value> value) {
                    ASSERT_TRUE(value->IsObject());
                    v8::Local<v8::Object> object = value.As<v8::Object>();
                    gin::Dictionary dict(v8::Isolate::GetCurrent(), object);
                    std::string result;
                    ASSERT_TRUE(dict.Get("str", &result));
                    EXPECT_EQ("a string", result);
                    ASSERT_TRUE(dict.Get("str", &result));
                    // The value should remain constant (even though there's a
                    // subtle getter).
                    EXPECT_EQ("a string", result);
                  }));
  }

  {
    std::unique_ptr<ArgumentSpec> prop =
        ArgumentSpecBuilder(ArgumentType::STRING, "str").MakeOptional().Build();
    std::unique_ptr<ArgumentSpec> spec =
        ArgumentSpecBuilder(ArgumentType::OBJECT, "obj")
            .AddProperty("str", std::move(prop))
            .Build();
    // The conversion to a v8 value should also protect set an undefined value
    // on the result value for any absent optional properties. This protects
    // against cases where an Object.prototype getter would be invoked when a
    // handler tried to check the value of an argument.
    constexpr const char kMessWithObjectPrototype[] =
        R"((function() {
             Object.defineProperty(
                 Object.prototype, 'str',
                 { get() { throw new Error('tricked!'); } });
           }))";
    v8::HandleScope handle_scope(instance_->isolate());
    v8::Local<v8::Context> context =
        v8::Local<v8::Context>::New(instance_->isolate(), context_);
    v8::Local<v8::Function> mess_with_proto =
        FunctionFromString(context, kMessWithObjectPrototype);
    RunFunction(mess_with_proto, context, 0, nullptr);
    ExpectSuccess(*spec, "({})", base::BindOnce([](v8::Local<v8::Value> value) {
      ASSERT_TRUE(value->IsObject());
      v8::Local<v8::Object> object = value.As<v8::Object>();
      // We expect a null prototype to ensure we avoid tricky getters/setters on
      // the Object prototype.
      EXPECT_TRUE(object->GetPrototype()->IsNull());
      gin::Dictionary dict(v8::Isolate::GetCurrent(), object);
      v8::Local<v8::Value> result;
      ASSERT_TRUE(dict.Get("str", &result));
      EXPECT_TRUE(result->IsUndefined());
    }));
  }

  {
    std::unique_ptr<ArgumentSpec> prop =
        ArgumentSpecBuilder(ArgumentType::STRING, "prop")
            .MakeOptional()
            .Build();
    std::unique_ptr<ArgumentSpec> preserve_null_spec =
        ArgumentSpecBuilder(ArgumentType::OBJECT, "spec")
            .AddProperty("prop", std::move(prop))
            .PreserveNull()
            .Build();
    ExpectSuccess(
        *preserve_null_spec, "({prop: null})",
        base::BindOnce([](v8::Local<v8::Value> value) {
          ASSERT_TRUE(value->IsObject());
          v8::Local<v8::Object> object = value.As<v8::Object>();
          v8::Local<v8::Context> context = object->GetCreationContextChecked();
          v8::Local<v8::Value> prop =
              object
                  ->Get(context,
                        gin::StringToV8(v8::Isolate::GetCurrent(), "prop"))
                  .ToLocalChecked();
          EXPECT_TRUE(prop->IsNull());
        }));
  }
}

// Certain argument types (any, binary, and function) will be passed through
// directly to the v8 arguments because we won't be able to parse them properly.
TEST_F(ArgumentSpecUnitTest, TestV8ValuePassedThrough) {
  v8::Isolate* isolate = instance_->isolate();
  v8::HandleScope handle_scope(instance_->isolate());

  v8::Local<v8::Context> context =
      v8::Local<v8::Context>::New(instance_->isolate(), context_);
  v8::TryCatch try_catch(isolate);

  auto test_is_same_value = [this, context](const ArgumentSpec& spec,
                                            const char* value_str) {
    SCOPED_TRACE(value_str);
    v8::Local<v8::Value> value_in = V8ValueFromScriptSource(context, value_str);
    v8::Local<v8::Value> value_out;
    std::string error;
    ASSERT_TRUE(spec.ParseArgument(context, value_in, type_refs(), nullptr,
                                   &value_out, &error));
    ASSERT_FALSE(value_out.IsEmpty());
    EXPECT_EQ(value_in, value_out);
  };

  // Functions are passed directly because we reply directly to the passed-in
  // function.
  test_is_same_value(ArgumentSpec(ArgumentType::FUNCTION), "(function() {})");
  // 'Any' and 'Binary' arguments are passed directly because we can't know if
  // we can copy them safely, accurately, and efficiently.
  test_is_same_value(ArgumentSpec(ArgumentType::ANY), "({foo: 'bar'})");
  test_is_same_value(ArgumentSpec(ArgumentType::BINARY), "(new ArrayBuffer())");
  // See comments in ArgumentSpec::ParseArgumentToObject() for why these are
  // passed directly.
  {
    std::unique_ptr<ArgumentSpec> instance_of_spec =
        ArgumentSpecBuilder(ArgumentType::OBJECT, "instance_of")
            .SetInstanceOf("RegExp")
            .Build();
    test_is_same_value(*instance_of_spec, "(new RegExp('hi'))");
  }

  {
    std::unique_ptr<ArgumentSpec> additional_properties_spec =
        ArgumentSpecBuilder(ArgumentType::OBJECT, "additional props")
            .SetAdditionalProperties(
                std::make_unique<ArgumentSpec>(ArgumentType::ANY))
            .Build();
    test_is_same_value(*additional_properties_spec, "({foo: 'bar'})");
  }
}

// Tests the serialization of functions that are explicitly marked as
// serializable (otherwise, they are represented as empty DictionaryValues).
TEST_F(ArgumentSpecUnitTest, SerializableFunctions) {
  constexpr char kFunctionSpec[] =
      R"({
           "type": "function",
           "serializableFunction": true
         })";
  ArgumentSpec spec(DictValueFromString(kFunctionSpec));

  constexpr char kExpectedSerialization[] = R"("function() { }")";
  ExpectSuccess(spec, "(function() { })", kExpectedSerialization);

  {
    constexpr char kNonTrivialFunction[] =
        R"(function(foo, bar, baz) { let alpha = baz; })";
    ExpectSuccess(spec, base::StringPrintf("(%s)", kNonTrivialFunction),
                  base::StringPrintf(R"("%s")", kNonTrivialFunction));
  }

  // Test a couple tricky values with custom toString() implementations.
  ExpectSuccess(spec,
                R"(var f = function() { };
         f.toString = function() { throw new Error('haha!'); };
         f;)",
                kExpectedSerialization);
  ExpectSuccess(spec,
                R"(var g = function() { };
         g.toString = function() { return 'function() { return 3; }'; };
         g;)",
                kExpectedSerialization);
}

class ArgumentSpecIntegrationTest : public ModuleSystemTest {
 public:
  // See UtilsUnittest::SetUp().
  void SetUp() override {
    ModuleSystemTest::SetUp();

    env()->RegisterModule("utils", IDR_EXTENSIONS_RENDERER_GENERATED_UTILS_JS);
    env()->OverrideNativeHandler("schema_registry",
                                 "exports.$set('GetSchema', function() {});");
    env()->OverrideNativeHandler("logging",
                                 "exports.$set('CHECK', function() {});\n"
                                 "exports.$set('DCHECK', function() {});\n"
                                 "exports.$set('WARNING', function() {});");
    env()->OverrideNativeHandler("v8_context", "");
  }
};

TEST_F(ArgumentSpecIntegrationTest, ExposeIsInstanceOfIntegration) {
  ExpectNoAssertionsMade();
  ModuleSystem::NativesEnabledScope natives_enabled_scope(
      env()->module_system());

  constexpr char kModuleBody[] = R"(
      var utils = require('utils');

      // Base class.
      function MyClassImpl() {}
      function MyClass() {
        privates(MyClass).constructPrivate(this, arguments);
      }
      utils.expose(MyClass, MyClassImpl, {});

      // Subclass.
      function MySubClassImpl() {}
      function MySubClass() {
        privates(MySubClass).constructPrivate(this, arguments);
      }
      utils.expose(MySubClass, MySubClassImpl, {
        superclass: MyClass
      });

      exports.$set('MyClass', MyClass);
      exports.$set('MySubClass', MySubClass);
  )";

  env()->RegisterModule("argument_spec_unittest", kModuleBody);

  ASSERT_FALSE(env()
                   ->module_system()
                   ->Require("argument_spec_unittest")
                   .ToLocalChecked()
                   .IsEmpty());

  // 1. Retrieve constructors from the module exports.
  v8::Isolate* isolate = env()->isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = env()->context()->v8_context();

  v8::Local<v8::Object> exports;
  ASSERT_TRUE(env()
                  ->module_system()
                  ->Require("argument_spec_unittest")
                  .ToLocal(&exports));

  v8::Local<v8::Function> my_class;
  {
    v8::Local<v8::Value> my_class_val;
    ASSERT_TRUE(exports->Get(context, gin::StringToSymbol(isolate, "MyClass"))
                    .ToLocal(&my_class_val));
    ASSERT_TRUE(my_class_val->IsFunction());
    my_class = my_class_val.As<v8::Function>();
  }

  v8::Local<v8::Function> my_sub_class;
  {
    v8::Local<v8::Value> my_sub_class_val;
    ASSERT_TRUE(
        exports->Get(context, gin::StringToSymbol(isolate, "MySubClass"))
            .ToLocal(&my_sub_class_val));
    ASSERT_TRUE(my_sub_class_val->IsFunction());
    my_sub_class = my_sub_class_val.As<v8::Function>();
  }

  // 2. Create class instances.
  v8::Local<v8::Object> my_instance;
  ASSERT_TRUE(my_class->NewInstance(context).ToLocal(&my_instance));

  v8::Local<v8::Object> my_sub_instance;
  ASSERT_TRUE(my_sub_class->NewInstance(context).ToLocal(&my_sub_instance));

  // 3. Create an ArgumentSpec expecting "MyClass".
  APITypeReferenceMap type_refs =
      APITypeReferenceMap(APITypeReferenceMap::InitializeTypeCallback());
  std::unique_ptr<ArgumentSpec> spec =
      ArgumentSpecBuilder(ArgumentType::OBJECT, "instance_of")
          .SetInstanceOf("MyClass")
          .Build();

  // 4. Validate the JS object (base class) using ArgumentSpec.
  v8::Local<v8::Value> value_out;
  std::string error;
  EXPECT_TRUE(spec->ParseArgument(context, my_instance, type_refs, nullptr,
                                  &value_out, &error))
      << error;
  EXPECT_FALSE(value_out.IsEmpty());

  // 5. Validate the JS object (subclass class) using ArgumentSpec.
  v8::Local<v8::Value> value_sub_out;
  std::string error_sub;
  EXPECT_TRUE(spec->ParseArgument(context, my_sub_instance, type_refs, nullptr,
                                  &value_sub_out, &error_sub))
      << error_sub;
  EXPECT_FALSE(value_sub_out.IsEmpty());

  // 6. Validate that an unrelated object (e.g. RegExp) fails this check.
  v8::Local<v8::Value> regexp_instance =
      V8ValueFromScriptSource(context, "(new RegExp('hi'))");
  ASSERT_FALSE(regexp_instance.IsEmpty());
  v8::Local<v8::Value> value_out_fail;
  std::string error_fail;
  EXPECT_FALSE(spec->ParseArgument(context, regexp_instance, type_refs, nullptr,
                                   &value_out_fail, &error_fail));
}

}  // namespace extensions
