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

#include "components/policy/core/common/schema.h"

#include <stddef.h>

#include <array>
#include <memory>
#include <utility>

#include "base/strings/stringprintf.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "components/policy/core/common/schema.h"
#include "components/policy/core/common/schema_internal.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace policy {

namespace {

#define TestSchemaValidation(a, b, c, d) \
    TestSchemaValidationHelper(          \
        base::StringPrintf("%s:%i", __FILE__, __LINE__), a, b, c, d)

const char kTestSchema[] = R"({
  "type": "object",
  "properties": {
    "Boolean": { "type": "boolean" },
    "Integer": { "type": "integer" },
    "Number": { "type": "number" },
    "String": { "type": "string" },
    "Array": {
      "type": "array",
      "items": { "type": "string" }
    },
    "ArrayOfObjects": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "one": { "type": "string" },
          "two": { "type": "integer" }
        }
      }
    },
    "ArrayOfArray": {
      "type": "array",
      "items": {
        "type": "array",
        "items": { "type": "string" }
      }
    },
    "Object": {
      "type": "object",
      "properties": {
        "one": { "type": "boolean" },
        "two": { "type": "integer" }
      },
      "additionalProperties": { "type": "string" }
    },
    "ObjectOfObject": {
      "type": "object",
      "properties": {
        "Object": {
          "type": "object",
          "properties": {
            "one": { "type": "string" },
            "two": { "type": "integer" }
          }
        }
      }
    },
    "IntegerWithEnums": {
      "type": "integer",
      "enum": [1, 2, 3]
    },
    "IntegerWithEnumsGaps": {
      "type": "integer",
      "enum": [10, 20, 30]
    },
    "StringWithEnums": {
      "type": "string",
      "enum": ["one", "two", "three"]
    },
    "IntegerWithRange": {
      "type": "integer",
      "minimum": 1,
      "maximum": 3
    },
    "ObjectOfArray": {
      "type": "object",
      "properties": {
        "List": {
          "type": "array",
          "items": { "type": "integer" }
        }
      }
    },
    "ArrayOfObjectOfArray": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "List": {
            "type": "array",
            "items": { "type": "string" }
          }
        }
      }
    },
    "StringWithPattern": {
      "type": "string",
      "pattern": "^foo+$"
    },
    "ObjectWithPatternProperties": {
      "type": "object",
      "patternProperties": {
        "^foo+$": { "type": "integer" },
        "^bar+$": {
          "type": "string",
          "enum": ["one", "two"]
        }
      },
      "properties": {
        "bar": {
          "type": "string",
          "enum": ["one", "three"]
        }
      }
    },
    "ObjectWithRequiredProperties": {
      "type": "object",
      "properties": {
        "Integer": {
          "type": "integer",
          "enum": [1, 2]
        },
        "String": { "type": "string" },
        "Number": { "type": "number" }
      },
      "patternProperties": {
        "^Integer": {
          "type": "integer",
          "enum": [1, 3]
        }
      },
      "required": [ "Integer", "String" ]
    }
  }
})";

bool ParseFails(const std::string& content) {
  ASSIGN_OR_RETURN(const auto schema, Schema::Parse(content),
                   [](const auto& e) { return true; });
  return false;
}

void TestSchemaValidationHelper(const std::string& source,
                                const Schema& schema,
                                const base::Value& value,
                                SchemaOnErrorStrategy strategy,
                                bool expected_return_value) {
  std::string error;
  static const char kNoErrorReturned[] = "No error returned.";

  // Test that Schema::Validate() works as expected.
  error = kNoErrorReturned;
  bool returned = schema.Validate(value, strategy, nullptr, &error);
  ASSERT_EQ(expected_return_value, returned) << source << ": " << error;

  // Test that Schema::Normalize() will return the same value as
  // Schema::Validate().
  error = kNoErrorReturned;
  base::Value cloned_value(value.Clone());
  bool touched = false;
  returned =
      schema.Normalize(&cloned_value, strategy, nullptr, &error, &touched);
  EXPECT_EQ(expected_return_value, returned) << source << ": " << error;

  if (strategy == SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING)
    return;

  bool strictly_valid = schema.Validate(value, SCHEMA_STRICT, nullptr, &error);
  EXPECT_EQ(touched, !strictly_valid && returned) << source;

  // Test that Schema::Normalize() have actually dropped invalid and unknown
  // properties.
  if (expected_return_value) {
    EXPECT_TRUE(schema.Validate(cloned_value, SCHEMA_STRICT, nullptr, &error))
        << source;
    EXPECT_TRUE(schema.Normalize(&cloned_value, SCHEMA_STRICT, nullptr, &error,
                                 nullptr))
        << source;
  }
}

void TestSchemaValidationWithPath(const Schema& schema,
                                  const base::Value& value,
                                  PolicyErrorPath expected_failure_path) {
  PolicyErrorPath error_path;
  std::string error;

  bool returned = schema.Validate(value, SCHEMA_STRICT, &error_path, &error);
  ASSERT_FALSE(returned) << error_path.size();
  EXPECT_EQ(error_path, expected_failure_path);
}

std::string SchemaObjectWrapper(const std::string& subschema) {
  return "{"
         "  \"type\": \"object\","
         "  \"properties\": {"
         "    \"SomePropertyName\":" + subschema +
         "  }"
         "}";
}

}  // namespace

TEST(SchemaTest, MinimalSchema) {
  EXPECT_FALSE(ParseFails(R"({ "type": "object" })"));
}

TEST(SchemaTest, InvalidSchemas) {
  EXPECT_TRUE(ParseFails(""));
  EXPECT_TRUE(ParseFails("omg"));
  EXPECT_TRUE(ParseFails("\"omg\""));
  EXPECT_TRUE(ParseFails("123"));
  EXPECT_TRUE(ParseFails("[]"));
  EXPECT_TRUE(ParseFails("null"));
  EXPECT_TRUE(ParseFails("{}"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "additionalProperties": { "type":"object" }
  })"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "patternProperties": { "a+b*": { "type": "object" } }
  })"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": { "Policy": { "type": "bogus" } }
  })"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": { "Policy": { "type": ["string", "number"] } }
  })"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": { "Policy": { "type": "any" } }
  })"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": { "Policy": 123 }
  })"));

  EXPECT_FALSE(ParseFails(R"({
    "type": "object",
    "unknown attribute": "is ignored"
  })"));
}

TEST(SchemaTest, Ownership) {
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "sub": {
        "type": "object",
        "properties": {
          "subsub": { "type": "string" }
        }
      }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());

  Schema sub = schema->GetKnownProperty("sub");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::DICT, sub.type());

  {
    Schema::Iterator it = sub.GetPropertiesIterator();
    ASSERT_FALSE(it.IsAtEnd());
    EXPECT_STREQ("subsub", it.key());

    sub = it.schema();
    it.Advance();
    EXPECT_TRUE(it.IsAtEnd());
  }

  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::STRING, sub.type());

  // This test shouldn't leak nor use invalid memory.
}

TEST(SchemaTest, ValidSchema) {
  const auto schema = Schema::Parse(kTestSchema);
  ASSERT_TRUE(schema.has_value()) << schema.error();

  ASSERT_EQ(base::Value::Type::DICT, schema->type());
  EXPECT_FALSE(schema->GetProperty("invalid").valid());

  Schema sub = schema->GetProperty("Boolean");
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, sub.type());

  sub = schema->GetProperty("Integer");
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::INTEGER, sub.type());

  sub = schema->GetProperty("Number");
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::DOUBLE, sub.type());

  sub = schema->GetProperty("String");
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::STRING, sub.type());

  sub = schema->GetProperty("Array");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::LIST, sub.type());
  sub = sub.GetItems();
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::STRING, sub.type());

  sub = schema->GetProperty("ArrayOfObjects");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::LIST, sub.type());
  sub = sub.GetItems();
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::DICT, sub.type());
  Schema subsub = sub.GetProperty("one");
  ASSERT_TRUE(subsub.valid());
  EXPECT_EQ(base::Value::Type::STRING, subsub.type());
  subsub = sub.GetProperty("two");
  ASSERT_TRUE(subsub.valid());
  EXPECT_EQ(base::Value::Type::INTEGER, subsub.type());
  subsub = sub.GetProperty("invalid");
  EXPECT_FALSE(subsub.valid());

  sub = schema->GetProperty("ArrayOfArray");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::LIST, sub.type());
  sub = sub.GetItems();
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::LIST, sub.type());
  sub = sub.GetItems();
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::STRING, sub.type());

  sub = schema->GetProperty("Object");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::DICT, sub.type());
  subsub = sub.GetProperty("one");
  ASSERT_TRUE(subsub.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, subsub.type());
  subsub = sub.GetProperty("two");
  ASSERT_TRUE(subsub.valid());
  EXPECT_EQ(base::Value::Type::INTEGER, subsub.type());
  subsub = sub.GetProperty("undeclared");
  ASSERT_TRUE(subsub.valid());
  EXPECT_EQ(base::Value::Type::STRING, subsub.type());

  sub = schema->GetProperty("IntegerWithEnums");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::INTEGER, sub.type());

  sub = schema->GetProperty("IntegerWithEnumsGaps");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::INTEGER, sub.type());

  sub = schema->GetProperty("StringWithEnums");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::STRING, sub.type());

  sub = schema->GetProperty("IntegerWithRange");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::INTEGER, sub.type());

  sub = schema->GetProperty("StringWithPattern");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::STRING, sub.type());

  sub = schema->GetProperty("ObjectWithPatternProperties");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::DICT, sub.type());

  sub = schema->GetProperty("ObjectWithRequiredProperties");
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::DICT, sub.type());

  struct {
    const char* expected_key;
    base::Value::Type expected_type;
  } kExpectedProperties[] = {
      {"Array", base::Value::Type::LIST},
      {"ArrayOfArray", base::Value::Type::LIST},
      {"ArrayOfObjectOfArray", base::Value::Type::LIST},
      {"ArrayOfObjects", base::Value::Type::LIST},
      {"Boolean", base::Value::Type::BOOLEAN},
      {"Integer", base::Value::Type::INTEGER},
      {"IntegerWithEnums", base::Value::Type::INTEGER},
      {"IntegerWithEnumsGaps", base::Value::Type::INTEGER},
      {"IntegerWithRange", base::Value::Type::INTEGER},
      {"Number", base::Value::Type::DOUBLE},
      {"Object", base::Value::Type::DICT},
      {"ObjectOfArray", base::Value::Type::DICT},
      {"ObjectOfObject", base::Value::Type::DICT},
      {"ObjectWithPatternProperties", base::Value::Type::DICT},
      {"ObjectWithRequiredProperties", base::Value::Type::DICT},
      {"String", base::Value::Type::STRING},
      {"StringWithEnums", base::Value::Type::STRING},
      {"StringWithPattern", base::Value::Type::STRING},
  };
  Schema::Iterator it = schema->GetPropertiesIterator();
  for (const auto& props : kExpectedProperties) {
    ASSERT_FALSE(it.IsAtEnd());
    EXPECT_STREQ(props.expected_key, it.key());
    ASSERT_TRUE(it.schema().valid());
    EXPECT_EQ(props.expected_type, it.schema().type());
    it.Advance();
  }
  EXPECT_TRUE(it.IsAtEnd());
}

TEST(SchemaTest, Lookups) {
  {
    const auto schema = Schema::Parse(R"({ "type": "object" })");
    ASSERT_TRUE(schema.has_value()) << schema.error();
    ASSERT_EQ(base::Value::Type::DICT, schema->type());

    // This empty schema should never find named properties.
    EXPECT_FALSE(schema->GetKnownProperty("").valid());
    EXPECT_FALSE(schema->GetKnownProperty("xyz").valid());
    EXPECT_TRUE(schema->GetRequiredProperties().empty());
    EXPECT_TRUE(schema->GetPatternProperties("").empty());
    EXPECT_FALSE(schema->GetAdditionalProperties().valid());
    EXPECT_TRUE(schema->GetPropertiesIterator().IsAtEnd());
  }

  {
    const auto schema = Schema::Parse(R"({
      "type": "object",
      "properties": {
        "Boolean": { "type": "boolean" }
      }
    })");
    ASSERT_TRUE(schema.has_value()) << schema.error();
    ASSERT_EQ(base::Value::Type::DICT, schema->type());

    EXPECT_FALSE(schema->GetKnownProperty("").valid());
    EXPECT_FALSE(schema->GetKnownProperty("xyz").valid());
    EXPECT_TRUE(schema->GetKnownProperty("Boolean").valid());
  }

  {
    const auto schema = Schema::Parse(R"({
      "type": "object",
      "properties": {
        "aa" : { "type": "boolean" },
        "abab" : { "type": "string" },
        "ab" : { "type": "number" },
        "aba" : { "type": "integer" }
      }
    })");
    ASSERT_TRUE(schema.has_value()) << schema.error();
    ASSERT_EQ(base::Value::Type::DICT, schema->type());

    EXPECT_FALSE(schema->GetKnownProperty("").valid());
    EXPECT_FALSE(schema->GetKnownProperty("xyz").valid());

    struct {
      const char* expected_key;
      base::Value::Type expected_type;
    } kExpectedKeys[] = {
        {"aa", base::Value::Type::BOOLEAN},
        {"ab", base::Value::Type::DOUBLE},
        {"aba", base::Value::Type::INTEGER},
        {"abab", base::Value::Type::STRING},
    };
    for (const auto& key : kExpectedKeys) {
      Schema sub = schema->GetKnownProperty(key.expected_key);
      ASSERT_TRUE(sub.valid());
      EXPECT_EQ(key.expected_type, sub.type());
    }
  }

  {
    const auto schema = Schema::Parse(R"(
    {
      "type": "object",
      "properties": {
        "String": { "type": "string" },
        "Object": {
          "type": "object",
          "properties": {"Integer": {"type": "integer"}},
          "required": [ "Integer" ]
        },
        "Number": { "type": "number" }
      },
      "required": [ "String", "Object"]
    })");
    ASSERT_TRUE(schema.has_value()) << schema.error();
    ASSERT_EQ(base::Value::Type::DICT, schema->type());

    EXPECT_EQ(std::vector<std::string>({"String", "Object"}),
              schema->GetRequiredProperties());

    Schema object = schema->GetKnownProperty("Object");
    ASSERT_TRUE(object.valid());
    ASSERT_EQ(base::Value::Type::DICT, object.type());

    EXPECT_EQ(std::vector<std::string>({"Integer"}),
              object.GetRequiredProperties());
  }
}

TEST(SchemaTest, GetKnownPropertyKeyCaseInsensitive) {
  {
    const auto schema = Schema::Parse(R"({ "type": "object" })");
    ASSERT_TRUE(schema.has_value()) << schema.error();
    EXPECT_EQ(std::nullopt,
              schema->GetKnownPropertyKeyCaseInsensitive("Boolean"));
  }

  {
    const auto schema = Schema::Parse(R"({
      "type": "object",
      "properties": {
        "Boolean": { "type": "boolean" },
        "Integer": { "type": "integer" },
        "URL": { "type": "string" }
      }
    })");
    ASSERT_TRUE(schema.has_value()) << schema.error();

    // Exact match
    EXPECT_EQ("Boolean", schema->GetKnownPropertyKeyCaseInsensitive("Boolean"));
    EXPECT_EQ("Integer", schema->GetKnownPropertyKeyCaseInsensitive("Integer"));
    EXPECT_EQ("URL", schema->GetKnownPropertyKeyCaseInsensitive("URL"));

    // Case-insensitive match (remapping)
    EXPECT_EQ("Boolean", schema->GetKnownPropertyKeyCaseInsensitive("boolean"));
    EXPECT_EQ("Boolean", schema->GetKnownPropertyKeyCaseInsensitive("BOOLEAN"));
    EXPECT_EQ("Boolean", schema->GetKnownPropertyKeyCaseInsensitive("bOoLeAn"));
    EXPECT_EQ("URL", schema->GetKnownPropertyKeyCaseInsensitive("url"));
    EXPECT_EQ("URL", schema->GetKnownPropertyKeyCaseInsensitive("Url"));

    // Non-existent properties
    EXPECT_EQ(std::nullopt, schema->GetKnownPropertyKeyCaseInsensitive("xyz"));
    EXPECT_EQ(std::nullopt, schema->GetKnownPropertyKeyCaseInsensitive(""));
    EXPECT_EQ(std::nullopt,
              schema->GetKnownPropertyKeyCaseInsensitive("1Boolean"));
    EXPECT_EQ(std::nullopt,
              schema->GetKnownPropertyKeyCaseInsensitive("_Boolean"));
  }
}

TEST(SchemaTest, Wrap) {
  const internal::SchemaNode kSchemas[] = {
      {base::Value::Type::DICT, 0},      //  0: root node
      {base::Value::Type::BOOLEAN, -1},  //  1
      {base::Value::Type::INTEGER, -1},  //  2
      {base::Value::Type::DOUBLE, -1},   //  3
      {base::Value::Type::STRING, -1},   //  4
      {base::Value::Type::LIST, 4},      //  5: list of strings.
      {base::Value::Type::LIST, 5},      //  6: list of lists of strings.
      {base::Value::Type::INTEGER, 0},   //  7: integer enumerations.
      {base::Value::Type::INTEGER, 1},   //  8: ranged integers.
      {base::Value::Type::STRING, 2},    //  9: string enumerations.
      {base::Value::Type::STRING, 3},    // 10: string with pattern.
      {base::Value::Type::DICT, 1},      // 11: dictionary with required
                                         //     properties
  };

  const internal::PropertyNode kPropertyNodes[] = {
    { "Boolean",       1},  //  0
    { "DictRequired", 11},  //  1
    { "Integer",       2},  //  2
    { "List",          5},  //  3
    { "Number",        3},  //  4
    { "String",        4},  //  5
    { "IntEnum",       7},  //  6
    { "RangedInt",     8},  //  7
    { "StrEnum",       9},  //  8
    { "StrPat",       10},  //  9
    { "bar+$",         4},  // 10
    { "String",        4},  // 11
    { "Number",        3},  // 12
  };

  const internal::PropertiesNode kProperties[] = {
      // 0 to 10 (exclusive) are the known properties in kPropertyNodes, 10 is
      // patternProperties and 6 is the additionalProperties node.
      {0, 10, 11, 0, 0, 6, 0, 10},
      // 11 to 13 (exclusive) are the known properties in kPropertyNodes. 0 to
      // 1 (exclusive) are the required properties in kRequired. -1 indicates
      // no additionalProperties.
      {11, 13, 13, 0, 1, -1, 10, 12},
  };

  const int16_t kCaseInsensitiveLookup[] = {
      0, 1, 6, 2, 3, 4, 7, 8, 5, 9, 12, 11,
  };

  const internal::RestrictionNode kRestriction[] = {
    {{0, 3}},  // 0: [1, 2, 3]
    {{5, 1}},  // 1: minimum = 1, maximum = 5
    {{0, 3}},  // 2: ["one", "two", "three"]
    {{3, 3}},  // 3: pattern "foo+"
  };

  const char* kRequired[] = {"String"};

  const int kIntEnums[] = {1, 2, 3};

  const char* kStringEnums[] = {
    "one",    // 0
    "two",    // 1
    "three",  // 2
    "foo+",   // 3
  };

  const internal::SchemaData kData = {
      kSchemas,  kPropertyNodes, kProperties,  kRestriction,
      kRequired, kIntEnums,      kStringEnums, kCaseInsensitiveLookup,
      -1  // validation_schema_root_index
  };

  Schema schema = Schema::Wrap(&kData);
  ASSERT_TRUE(schema.valid());
  EXPECT_EQ(base::Value::Type::DICT, schema.type());

  // Wrapped schemas have no sensitive values.
  EXPECT_FALSE(schema.IsSensitiveValue());

  struct ExpectedProperties {
    const char* key;
    base::Value::Type type;
  };
  auto kExpectedProperties = std::to_array<ExpectedProperties>({
      {"Boolean", base::Value::Type::BOOLEAN},
      {"DictRequired", base::Value::Type::DICT},
      {"Integer", base::Value::Type::INTEGER},
      {"List", base::Value::Type::LIST},
      {"Number", base::Value::Type::DOUBLE},
      {"String", base::Value::Type::STRING},
      {"IntEnum", base::Value::Type::INTEGER},
      {"RangedInt", base::Value::Type::INTEGER},
      {"StrEnum", base::Value::Type::STRING},
      {"StrPat", base::Value::Type::STRING},
  });

  Schema::Iterator it = schema.GetPropertiesIterator();
  for (size_t i = 0; i < std::size(kExpectedProperties); ++i) {
    ASSERT_FALSE(it.IsAtEnd());
    EXPECT_STREQ(kExpectedProperties[i].key, it.key());
    Schema sub = it.schema();
    ASSERT_TRUE(sub.valid());
    EXPECT_EQ(kExpectedProperties[i].type, sub.type());

    if (sub.type() == base::Value::Type::LIST) {
      Schema items = sub.GetItems();
      ASSERT_TRUE(items.valid());
      EXPECT_EQ(base::Value::Type::STRING, items.type());
    }

    it.Advance();
  }
  EXPECT_TRUE(it.IsAtEnd());

  Schema sub = schema.GetAdditionalProperties();
  ASSERT_TRUE(sub.valid());
  ASSERT_EQ(base::Value::Type::LIST, sub.type());
  Schema subsub = sub.GetItems();
  ASSERT_TRUE(subsub.valid());
  ASSERT_EQ(base::Value::Type::LIST, subsub.type());
  Schema subsubsub = subsub.GetItems();
  ASSERT_TRUE(subsubsub.valid());
  ASSERT_EQ(base::Value::Type::STRING, subsubsub.type());

  SchemaList schema_list = schema.GetPatternProperties("barr");
  ASSERT_EQ(1u, schema_list.size());
  sub = schema_list[0];
  ASSERT_TRUE(sub.valid());
  EXPECT_EQ(base::Value::Type::STRING, sub.type());

  EXPECT_TRUE(schema.GetPatternProperties("ba").empty());
  EXPECT_TRUE(schema.GetPatternProperties("bar+$").empty());

  Schema dict = schema.GetKnownProperty("DictRequired");
  ASSERT_TRUE(dict.valid());
  ASSERT_EQ(base::Value::Type::DICT, dict.type());

  EXPECT_EQ(std::vector<std::string>({"String"}), dict.GetRequiredProperties());
}

TEST(SchemaTest, Validate) {
  const auto schema = Schema::Parse(kTestSchema);
  ASSERT_TRUE(schema.has_value()) << schema.error();

  base::Value bundle((base::DictValue()));
  base::DictValue& dict = bundle.GetDict();
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, true);

  // Wrong type, expected integer.
  dict.Set("Integer", true);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);

  // Wrong type, expected list of strings.
  {
    dict.clear();
    base::ListValue list;
    list.Append(1);
    dict.Set("Array", std::move(list));
    TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  }

  // Wrong type in a sub-object.
  {
    dict.clear();
    base::DictValue subdict;
    subdict.Set("one", "one");
    dict.Set("Object", std::move(subdict));
    TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  }

  // Unknown name.
  dict.clear();
  dict.Set("Unknown", true);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);

  // All of these will be valid.
  dict.clear();
  dict.Set("Boolean", true);
  dict.Set("Integer", 123);
  dict.Set("Number", 3.14);
  dict.Set("String", "omg");

  {
    base::ListValue list;
    list.Append("a string");
    list.Append("another string");
    dict.Set("Array", std::move(list));
  }

  {
    base::DictValue subdict;
    subdict.Set("one", "string");
    subdict.Set("two", 2);
    base::ListValue list;
    list.Append(subdict.Clone());
    list.Append(std::move(subdict));
    dict.Set("ArrayOfObjects", std::move(list));
  }

  {
    base::ListValue list;
    list.Append("a string");
    list.Append("another string");
    base::ListValue listlist;
    listlist.Append(list.Clone());
    listlist.Append(std::move(list));
    dict.Set("ArrayOfArray", std::move(listlist));
  }

  {
    base::DictValue subdict;
    subdict.Set("one", true);
    subdict.Set("two", 2);
    subdict.Set("additionally", "a string");
    subdict.Set("and also", "another string");
    dict.Set("Object", std::move(subdict));
  }

  {
    base::DictValue subdict;
    subdict.Set("Integer", 1);
    subdict.Set("String", "a string");
    subdict.Set("Number", 3.14);
    dict.Set("ObjectWithRequiredProperties", std::move(subdict));
  }

  dict.Set("IntegerWithEnums", 1);
  dict.Set("IntegerWithEnumsGaps", 20);
  dict.Set("StringWithEnums", "two");
  dict.Set("IntegerWithRange", 3);

  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, true);

  dict.Set("IntegerWithEnums", 0);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnums", 1);

  dict.Set("IntegerWithEnumsGaps", 0);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 9);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 10);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, true);
  dict.Set("IntegerWithEnumsGaps", 11);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 19);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 21);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 29);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 30);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, true);
  dict.Set("IntegerWithEnumsGaps", 31);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 100);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithEnumsGaps", 20);

  dict.Set("StringWithEnums", "FOUR");
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("StringWithEnums", "two");

  dict.Set("IntegerWithRange", 4);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  dict.Set("IntegerWithRange", 3);

  // Unknown top level property.
  dict.Set("boom", "bang");
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  TestSchemaValidation(*schema, bundle, SCHEMA_ALLOW_UNKNOWN, true);
  TestSchemaValidation(*schema, bundle,
                       SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
  TestSchemaValidation(*schema, bundle, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                       true);
  TestSchemaValidationWithPath(*schema, bundle, {});
  dict.Remove("boom");

  // Invalid top level property.
  dict.Set("Boolean", 12345);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, false);
  TestSchemaValidation(*schema, bundle, SCHEMA_ALLOW_UNKNOWN, false);
  TestSchemaValidation(*schema, bundle,
                       SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, false);
  TestSchemaValidation(*schema, bundle, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                       false);
  TestSchemaValidationWithPath(*schema, bundle, {"Boolean"});
  dict.Set("Boolean", true);

  // Tests on ObjectOfObject.
  {
    Schema subschema = schema->GetProperty("ObjectOfObject");
    ASSERT_TRUE(subschema.valid());
    base::Value root((base::DictValue()));
    base::DictValue& root_dict = root.GetDict();

    // Unknown property.
    root_dict.SetByDottedPath("Object.three", false);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, true);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         true);
    TestSchemaValidationWithPath(subschema, root, {"Object"});
    root_dict.RemoveByDottedPath("Object.three");

    // Invalid property.
    root_dict.SetByDottedPath("Object.one", 12345);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);
    TestSchemaValidationWithPath(subschema, root, {"Object", "one"});
    root_dict.RemoveByDottedPath("Object.one");
  }

  // Tests on ArrayOfObjects.
  {
    Schema subschema = schema->GetProperty("ArrayOfObjects");
    ASSERT_TRUE(subschema.valid());
    base::Value root(base::Value::Type::LIST);
    base::ListValue& root_list = root.GetList();

    // Unknown property.
    base::DictValue dict1;
    dict1.Set("three", true);
    root_list.Append(std::move(dict1));
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, true);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         true);
    TestSchemaValidationWithPath(subschema, root, {0});
    root_list.erase(root_list.end() - 1);

    // Invalid property.
    base::DictValue dict2;
    dict2.Set("two", true);
    root_list.Append(std::move(dict2));
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);
    TestSchemaValidationWithPath(subschema, root, {0, "two"});
  }

  // Tests on ObjectOfArray.
  {
    Schema subschema = schema->GetProperty("ObjectOfArray");
    ASSERT_TRUE(subschema.valid());
    base::Value root((base::DictValue()));

    base::ListValue& list_value =
        root.GetDict().Set("List", base::ListValue())->GetList();

    // Test that there are not errors here.
    list_value.Append(12345);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, true);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         true);

    // Invalid list item.
    list_value.Append("blabla");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);
    TestSchemaValidationWithPath(subschema, root, {"List", 1});
  }

  // Tests on ArrayOfObjectOfArray.
  {
    Schema subschema = schema->GetProperty("ArrayOfObjectOfArray");
    ASSERT_TRUE(subschema.valid());
    base::Value root{base::ListValue()};

    base::DictValue dict_value;
    base::Value* list_value = dict_value.Set("List", base::ListValue());
    root.GetList().Append(std::move(dict_value));

    // Test that there are not errors here.
    list_value->GetList().Append("blabla");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, true);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         true);

    // Invalid list item.
    list_value->GetList().Append(12345);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);
    TestSchemaValidationWithPath(subschema, root, {0, "List", 1});
  }

  // Tests on StringWithPattern.
  {
    Schema subschema = schema->GetProperty("StringWithPattern");
    ASSERT_TRUE(subschema.valid());

    TestSchemaValidation(subschema, base::Value("foobar"), SCHEMA_STRICT,
                         false);
    TestSchemaValidation(subschema, base::Value("foo"), SCHEMA_STRICT, true);
    TestSchemaValidation(subschema, base::Value("fo"), SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, base::Value("fooo"), SCHEMA_STRICT, true);
    TestSchemaValidation(subschema, base::Value("^foo+$"), SCHEMA_STRICT,
                         false);
  }

  // Tests on ObjectWithPatternProperties.
  {
    Schema subschema = schema->GetProperty("ObjectWithPatternProperties");
    ASSERT_TRUE(subschema.valid());
    base::Value root((base::DictValue()));
    base::DictValue& root_dict = root.GetDict();

    ASSERT_EQ(1u, subschema.GetPatternProperties("fooo").size());
    ASSERT_EQ(1u, subschema.GetPatternProperties("foo").size());
    ASSERT_EQ(1u, subschema.GetPatternProperties("barr").size());
    ASSERT_EQ(1u, subschema.GetPatternProperties("bar").size());
    ASSERT_EQ(1u, subschema.GetMatchingProperties("fooo").size());
    ASSERT_EQ(1u, subschema.GetMatchingProperties("foo").size());
    ASSERT_EQ(1u, subschema.GetMatchingProperties("barr").size());
    ASSERT_EQ(2u, subschema.GetMatchingProperties("bar").size());
    ASSERT_TRUE(subschema.GetPatternProperties("foobar").empty());

    root_dict.Set("fooo", 123);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, true);
    root_dict.Set("fooo", false);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    root_dict.Remove("fooo");

    root_dict.Set("foo", 123);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, true);
    root_dict.Set("foo", false);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    root_dict.Remove("foo");

    root_dict.Set("barr", "one");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, true);
    root_dict.Set("barr", "three");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    root_dict.Set("barr", false);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    root_dict.Remove("barr");

    root_dict.Set("bar", "one");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, true);
    root_dict.Set("bar", "two");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    root_dict.Set("bar", "three");
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    root_dict.Remove("bar");

    root_dict.Set("foobar", 123);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, true);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, true);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         true);
    root_dict.Remove("foobar");
  }

  // Tests on ObjectWithRequiredProperties
  {
    Schema subschema = schema->GetProperty("ObjectWithRequiredProperties");
    ASSERT_TRUE(subschema.valid());
    base::Value root((base::DictValue()));
    base::DictValue& root_dict = root.GetDict();

    // Required property missing.
    root_dict.Set("Integer", 1);
    root_dict.Set("Number", 3.14);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);

    // Invalid required property.
    root_dict.Set("String", 123);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);
    root_dict.Set("String", "a string");

    // Invalid subschema of required property with multiple subschemas.
    //
    // The "Integer" property has two subschemas, one in "properties" and one
    // in "patternProperties". The first test generates a valid schema for the
    // first subschema and the second test generates a valid schema for the
    // second subschema. In both cases validation should fail because one of the
    // required properties is invalid.
    root_dict.Set("Integer", 2);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);

    root_dict.Set("Integer", 3);
    TestSchemaValidation(subschema, root, SCHEMA_STRICT, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN, false);
    TestSchemaValidation(subschema, root,
                         SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY, false);
    TestSchemaValidation(subschema, root, SCHEMA_ALLOW_UNKNOWN_WITHOUT_WARNING,
                         false);
  }

  // Test that integer to double promotion is allowed.
  dict.Set("Number", 31415);
  TestSchemaValidation(*schema, bundle, SCHEMA_STRICT, true);
}

TEST(SchemaTest, InvalidReferences) {
  // References to undeclared schemas fail.
  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": {
      "name": { "$ref": "undeclared" }
    }
  })"));

  // Can't refer to self.
  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": {
      "name": {
        "id": "self",
        "$ref": "self"
      }
    }
  })"));

  // Duplicated IDs are invalid.
  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "properties": {
      "name": {
        "id": "x",
        "type": "string"
      },
      "another": {
        "id": "x",
        "type": "string"
      }
    }
  })"));

  // Main object can't be a reference.
  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "id": "main",
    "$ref": "main"
  })"));

  EXPECT_TRUE(ParseFails(R"({
    "type": "object",
    "$ref": "main"
  })"));
}

TEST(SchemaTest, RecursiveReferences) {
  // Verifies that references can go to a parent schema, to define a
  // recursive type.
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "bookmarks": {
        "type": "array",
        "id": "ListOfBookmarks",
        "items": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "url": { "type": "string" },
            "children": { "$ref": "ListOfBookmarks" }
          }
        }
      }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());

  Schema parent = schema->GetKnownProperty("bookmarks");
  ASSERT_TRUE(parent.valid());
  ASSERT_EQ(base::Value::Type::LIST, parent.type());

  // Check the recursive type a number of times.
  for (int i = 0; i < 10; ++i) {
    Schema items = parent.GetItems();
    ASSERT_TRUE(items.valid());
    ASSERT_EQ(base::Value::Type::DICT, items.type());

    Schema prop = items.GetKnownProperty("name");
    ASSERT_TRUE(prop.valid());
    ASSERT_EQ(base::Value::Type::STRING, prop.type());

    prop = items.GetKnownProperty("url");
    ASSERT_TRUE(prop.valid());
    ASSERT_EQ(base::Value::Type::STRING, prop.type());

    prop = items.GetKnownProperty("children");
    ASSERT_TRUE(prop.valid());
    ASSERT_EQ(base::Value::Type::LIST, prop.type());

    parent = prop;
  }
}

TEST(SchemaTest, UnorderedReferences) {
  // Verifies that references and IDs can come in any order.
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "a": { "$ref": "shared" },
      "b": { "$ref": "shared" },
      "c": { "$ref": "shared" },
      "d": { "$ref": "shared" },
      "e": {
        "type": "boolean",
        "id": "shared"
      },
      "f": { "$ref": "shared" },
      "g": { "$ref": "shared" },
      "h": { "$ref": "shared" },
      "i": { "$ref": "shared" }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());

  for (char c = 'a'; c <= 'i'; ++c) {
    Schema sub = schema->GetKnownProperty(std::string(1, c));
    ASSERT_TRUE(sub.valid()) << c;
    ASSERT_EQ(base::Value::Type::BOOLEAN, sub.type()) << c;
  }
}

TEST(SchemaTest, AdditionalPropertiesReference) {
  // Verifies that "additionalProperties" can be a reference.
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "policy": {
        "type": "object",
        "properties": {
          "foo": {
            "type": "boolean",
            "id": "FooId"
          }
        },
        "additionalProperties": { "$ref": "FooId" }
      }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());

  Schema policy = schema->GetKnownProperty("policy");
  ASSERT_TRUE(policy.valid());
  ASSERT_EQ(base::Value::Type::DICT, policy.type());

  Schema foo = policy.GetKnownProperty("foo");
  ASSERT_TRUE(foo.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, foo.type());

  Schema additional = policy.GetAdditionalProperties();
  ASSERT_TRUE(additional.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, additional.type());

  Schema x = policy.GetProperty("x");
  ASSERT_TRUE(x.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, x.type());
}

TEST(SchemaTest, ItemsReference) {
  // Verifies that "items" can be a reference.
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "foo": {
        "type": "boolean",
        "id": "FooId"
      },
      "list": {
        "type": "array",
        "items": { "$ref": "FooId" }
      }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());

  Schema foo = schema->GetKnownProperty("foo");
  ASSERT_TRUE(foo.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, foo.type());

  Schema list = schema->GetKnownProperty("list");
  ASSERT_TRUE(list.valid());
  ASSERT_EQ(base::Value::Type::LIST, list.type());

  Schema items = list.GetItems();
  ASSERT_TRUE(items.valid());
  ASSERT_EQ(base::Value::Type::BOOLEAN, items.type());
}

TEST(SchemaTest, SchemaNodeSensitiveValues) {
  const std::string kNormalBooleanSchema = "normal_boolean";
  const std::string kSensitiveBooleanSchema = "sensitive_boolean";
  const std::string kSensitiveStringSchema = "sensitive_string";
  const std::string kSensitiveObjectSchema = "sensitive_object";
  const std::string kSensitiveArraySchema = "sensitive_array";
  const std::string kSensitiveIntegerSchema = "sensitive_integer";
  const std::string kSensitiveNumberSchema = "sensitive_number";
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "normal_boolean": {
        "type": "boolean"
      },
      "sensitive_boolean": {
        "type": "boolean",
        "sensitiveValue": true
      },
      "sensitive_string": {
        "type": "string",
        "sensitiveValue": true
      },
      "sensitive_object": {
        "type": "object",
        "additionalProperties": {
          "type": "boolean"
        },
        "sensitiveValue": true
      },
      "sensitive_array": {
        "type": "array",
        "items": {
          "type": "boolean"
        },
        "sensitiveValue": true
      },
      "sensitive_integer": {
        "type": "integer",
        "sensitiveValue": true
      },
      "sensitive_number": {
        "type": "number",
        "sensitiveValue": true
      }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());
  EXPECT_FALSE(schema->IsSensitiveValue());
  EXPECT_TRUE(schema->HasSensitiveChildren());

  Schema normal_boolean = schema->GetKnownProperty(kNormalBooleanSchema);
  ASSERT_TRUE(normal_boolean.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, normal_boolean.type());
  EXPECT_FALSE(normal_boolean.IsSensitiveValue());
  EXPECT_FALSE(normal_boolean.HasSensitiveChildren());

  Schema sensitive_boolean = schema->GetKnownProperty(kSensitiveBooleanSchema);
  ASSERT_TRUE(sensitive_boolean.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, sensitive_boolean.type());
  EXPECT_TRUE(sensitive_boolean.IsSensitiveValue());
  EXPECT_FALSE(sensitive_boolean.HasSensitiveChildren());

  Schema sensitive_string = schema->GetKnownProperty(kSensitiveStringSchema);
  ASSERT_TRUE(sensitive_string.valid());
  EXPECT_EQ(base::Value::Type::STRING, sensitive_string.type());
  EXPECT_TRUE(sensitive_string.IsSensitiveValue());
  EXPECT_FALSE(sensitive_string.HasSensitiveChildren());

  Schema sensitive_object = schema->GetKnownProperty(kSensitiveObjectSchema);
  ASSERT_TRUE(sensitive_object.valid());
  EXPECT_EQ(base::Value::Type::DICT, sensitive_object.type());
  EXPECT_TRUE(sensitive_object.IsSensitiveValue());
  EXPECT_FALSE(sensitive_object.HasSensitiveChildren());

  Schema sensitive_array = schema->GetKnownProperty(kSensitiveArraySchema);
  ASSERT_TRUE(sensitive_array.valid());
  EXPECT_EQ(base::Value::Type::LIST, sensitive_array.type());
  EXPECT_TRUE(sensitive_array.IsSensitiveValue());
  EXPECT_FALSE(sensitive_array.HasSensitiveChildren());

  Schema sensitive_integer = schema->GetKnownProperty(kSensitiveIntegerSchema);
  ASSERT_TRUE(sensitive_integer.valid());
  EXPECT_EQ(base::Value::Type::INTEGER, sensitive_integer.type());
  EXPECT_TRUE(sensitive_integer.IsSensitiveValue());
  EXPECT_FALSE(sensitive_integer.HasSensitiveChildren());

  Schema sensitive_number = schema->GetKnownProperty(kSensitiveNumberSchema);
  ASSERT_TRUE(sensitive_number.valid());
  EXPECT_EQ(base::Value::Type::DOUBLE, sensitive_number.type());
  EXPECT_TRUE(sensitive_number.IsSensitiveValue());
  EXPECT_FALSE(sensitive_number.HasSensitiveChildren());

  // Run |MaskSensitiveValues| on the top-level schema
  base::DictValue object;
  object.Set("objectProperty", true);
  base::ListValue array;
  array.Append(true);

  base::Value value((base::DictValue()));
  base::DictValue& value_dict = value.GetDict();
  value_dict.Set(kNormalBooleanSchema, true);
  value_dict.Set(kSensitiveBooleanSchema, true);
  value_dict.Set(kSensitiveStringSchema, "testvalue");
  value_dict.Set(kSensitiveObjectSchema, std::move(object));
  value_dict.Set(kSensitiveArraySchema, std::move(array));
  value_dict.Set(kSensitiveIntegerSchema, 42);
  value_dict.Set(kSensitiveNumberSchema, 3.141);
  schema->MaskSensitiveValues(&value);

  base::Value value_masked("********");
  base::DictValue value_expected;
  value_expected.Set(kNormalBooleanSchema, true);
  value_expected.Set(kSensitiveBooleanSchema, value_masked.Clone());
  value_expected.Set(kSensitiveStringSchema, value_masked.Clone());
  value_expected.Set(kSensitiveObjectSchema, value_masked.Clone());
  value_expected.Set(kSensitiveArraySchema, value_masked.Clone());
  value_expected.Set(kSensitiveIntegerSchema, value_masked.Clone());
  value_expected.Set(kSensitiveNumberSchema, value_masked.Clone());
  EXPECT_EQ(value_expected, value_dict);

  // Run |MaskSensitiveValues| on a sub-schema
  base::Value string_value("testvalue");
  sensitive_string.MaskSensitiveValues(&string_value);
  EXPECT_EQ(value_masked.Clone(), string_value);
}

TEST(SchemaTest, SchemaNodeNoSensitiveValues) {
  const auto schema = Schema::Parse(R"({
    "type": "object",
    "properties": {
      "foo": {
        "type": "boolean"
      }
    }
  })");
  ASSERT_TRUE(schema.has_value()) << schema.error();
  ASSERT_EQ(base::Value::Type::DICT, schema->type());
  EXPECT_FALSE(schema->IsSensitiveValue());

  Schema foo = schema->GetKnownProperty("foo");
  ASSERT_TRUE(foo.valid());
  EXPECT_EQ(base::Value::Type::BOOLEAN, foo.type());
  EXPECT_FALSE(foo.IsSensitiveValue());

  base::Value value(base::Value::Type::DICT);
  value.GetDict().Set("foo", true);

  base::Value expected_value = value.Clone();
  schema->MaskSensitiveValues(&value);
  EXPECT_EQ(expected_value, value);
}

TEST(SchemaTest, EnumerationRestriction) {
  // Enum attribute is a list.
  EXPECT_TRUE(ParseFails(SchemaObjectWrapper(R"({
    "type": "string",
    "enum": 12
  })")));

  // Empty enum attributes is not allowed.
  EXPECT_TRUE(ParseFails(SchemaObjectWrapper(R"({
    "type": "integer",
    "enum": []
  })")));

  // Enum elements type should be same as stated.
  EXPECT_TRUE(ParseFails(SchemaObjectWrapper(R"({
    "type": "string",
    "enum": [1, 2, 3]
  })")));

  EXPECT_FALSE(ParseFails(SchemaObjectWrapper(R"({
    "type": "integer",
    "enum": [1, 2, 3]
  })")));

  EXPECT_FALSE(ParseFails(SchemaObjectWrapper(R"({
    "type": "string",
    "enum": ["1", "2", "3"]
  })")));
}

TEST(SchemaTest, RangedRestriction) {
  EXPECT_TRUE(ParseFails(SchemaObjectWrapper(R"({
    "type": "integer",
    "minimum": 10,
    "maximum": 5
  })")));

  EXPECT_FALSE(ParseFails(SchemaObjectWrapper(R"({
    "type": "integer",
    "minimum": 10,
    "maximum": 20
  })")));
}

TEST(SchemaTest, ParseToDictAndValidate) {
  EXPECT_FALSE(
      Schema::ParseToDictAndValidate("", kSchemaOptionsNone).has_value());
  EXPECT_FALSE(
      Schema::ParseToDictAndValidate("\0", kSchemaOptionsNone).has_value());
  EXPECT_FALSE(
      Schema::ParseToDictAndValidate("string", kSchemaOptionsNone).has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(R"("string")", kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(
      Schema::ParseToDictAndValidate("[]", kSchemaOptionsNone).has_value());
  EXPECT_FALSE(
      Schema::ParseToDictAndValidate("{}", kSchemaOptionsNone).has_value());
  EXPECT_FALSE(
      Schema::ParseToDictAndValidate(R"({ "type": 123 })", kSchemaOptionsNone)
          .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(R"({ "type": "invalid" })",
                                              kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "object",
        "properties": []
      })",  // Invalid properties type.
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "string",
        "enum": [ {} ]
      })",  // "enum" dict values must contain "name".
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "string",
        "enum": [ { "name": {} } ]
      })",  // "enum" name must be a simple value.
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "array",
        "items": [ 123 ],
      })",  // "items" must contain a schema or schemas.
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_TRUE(Schema::ParseToDictAndValidate(R"({ "type": "object" })",
                                             kSchemaOptionsNone)
                  .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({ "type": ["object", "array"] })", kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "array",
        "items": [
          { "type": "string" },
          { "type": "integer" }
        ]
      })",
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_TRUE(Schema::ParseToDictAndValidate(
                  R"({
        "type": "object",
          "properties": {
            "string-property": {
              "type": "string",
              "title": "The String Policy",
              "description": "This policy controls the String widget."
            },
            "integer-property": {
              "type": "number"
            },
            "enum-property": {
              "type": "integer",
              "enum": [0, 1, 10, 100]
            },
            "items-property": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
        },
        "additionalProperties": {
          "type": "boolean"
        }
      })",
                  kSchemaOptionsNone)
                  .has_value());
  EXPECT_TRUE(Schema::ParseToDictAndValidate(
                  R"#({
        "type": "object",
        "patternProperties": {
          ".": { "type": "boolean" },
          "foo": { "type": "boolean" },
          "^foo$": { "type": "boolean" },
          "foo+": { "type": "boolean" },
          "foo?": { "type": "boolean" },
          "fo{2,4}": { "type": "boolean" },
          "(left)|(right)": { "type": "boolean" }
        }
      })#",
                  kSchemaOptionsNone)
                  .has_value());
  EXPECT_TRUE(Schema::ParseToDictAndValidate(
                  R"({
        "type": "object",
        "unknown attribute": "that should just be ignored"
      })",
                  kSchemaOptionsIgnoreUnknownAttributes)
                  .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "object",
        "unknown attribute": "that will cause a failure"
      })",
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "object",
        "properties": {"foo": {"type": "number"}},
        "required": 123
      })",
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "object",
        "properties": {"foo": {"type": "number"}},
        "required": [ 123 ]
      })",
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "object",
        "properties": {"foo": {"type": "number"}},
        "required": ["bar"]
      })",
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_FALSE(Schema::ParseToDictAndValidate(
                   R"({
        "type": "object",
        "required": ["bar"]
      })",
                   kSchemaOptionsNone)
                   .has_value());
  EXPECT_TRUE(Schema::ParseToDictAndValidate(
                  R"({
        "type": "object",
        "properties": {"foo": {"type": "number"}},
        "required": ["foo"]
      })",
                  kSchemaOptionsNone)
                  .has_value());
}

TEST(SchemaTest, ErrorPathToString) {
  EXPECT_EQ(ErrorPathToString("Policy", {}), "");
  EXPECT_EQ(ErrorPathToString("Policy", {"key"}), "Policy.key");
  EXPECT_EQ(ErrorPathToString("Policy", {0}), "Policy[0]");
  EXPECT_EQ(ErrorPathToString("", {"key"}), ".key");
  EXPECT_EQ(ErrorPathToString("", {0}), "[0]");
  EXPECT_EQ(ErrorPathToString("Policy", {"key", "key", "key"}),
            "Policy.key.key.key");
  EXPECT_EQ(ErrorPathToString("Policy", {"a", "b", "c"}), "Policy.a.b.c");
  EXPECT_EQ(ErrorPathToString("Policy", {0, 0, 0}), "Policy[0][0][0]");
  EXPECT_EQ(ErrorPathToString("Policy", {1, 2, 3}), "Policy[1][2][3]");
  EXPECT_EQ(ErrorPathToString("Policy", {0, "key", 5, 7, "example"}),
            "Policy[0].key[5][7].example");
}

}  // namespace policy
