content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *     https://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.avro;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  
  import org.apache.avro.Schema.Field;
  import org.apache.avro.Schema.Type;
  import org.apache.avro.compiler.specific.TestSpecificCompiler;
  import org.apache.avro.data.Json;
  import org.apache.avro.generic.GenericData;
  import org.apache.avro.generic.GenericDatumReader;
  import org.apache.avro.generic.GenericDatumWriter;
  import org.apache.avro.io.DatumReader;
  import org.apache.avro.io.DatumWriter;
  import org.apache.avro.io.Decoder;
  import org.apache.avro.io.DecoderFactory;
  import org.apache.avro.io.Encoder;
  import org.apache.avro.io.EncoderFactory;
  import org.apache.avro.util.RandomData;
  import org.apache.avro.util.Utf8;
  import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.TestInfo;
  import org.junit.jupiter.api.io.TempDir;
  
  import static org.junit.jupiter.api.Assertions.*;
  
  public class TestSchema {
  
    @TempDir
    public File DIR;
  
    public static final String LISP_SCHEMA = "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": ["
        + "{\"name\":\"value\", \"type\":[\"null\", \"string\","
        + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": [" + "{\"name\":\"car\", \"type\":\"Lisp\"},"
        + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}";
  
    public static final String BASIC_ENUM_SCHEMA = "{\"type\":\"enum\", \"name\":\"Test\","
        + "\"symbols\": [\"A\", \"B\"]}";
  
    public static final String SCHEMA_WITH_DOC_TAGS = "{\n" + "  \"type\": \"record\",\n"
        + "  \"name\": \"outer_record\",\n" + "  \"doc\": \"This is not a world record.\",\n" + "  \"fields\": [\n"
        + "    { \"type\": { \"type\": \"fixed\", \"doc\": \"Very Inner Fixed\", "
        + "                  \"name\": \"very_inner_fixed\", \"size\": 1 },\n"
        + "      \"doc\": \"Inner Fixed\", \"name\": \"inner_fixed\" },\n" + "    { \"type\": \"string\",\n"
        + "      \"name\": \"inner_string\",\n" + "      \"doc\": \"Inner String\" },\n"
        + "    { \"type\": { \"type\": \"enum\", \"doc\": \"Very Inner Enum\", \n"
        + "                  \"name\": \"very_inner_enum\", \n"
        + "                  \"symbols\": [ \"A\", \"B\", \"C\" ] },\n"
        + "      \"doc\": \"Inner Enum\", \"name\": \"inner_enum\" },\n"
        + "    { \"type\": [\"string\", \"int\"], \"doc\": \"Inner Union\", \n" + "      \"name\": \"inner_union\" }\n"
        + "  ]\n" + "}\n";
  
    private static final int COUNT = Integer.parseInt(System.getProperty("test.count", "30"));
  
    @Test
    void testNull(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.NULL), new Schema.Parser().parse("\"null\""));
      assertEquals(Schema.create(Type.NULL), new Schema.Parser().parse("{\"type\":\"null\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"null\"", "null", null);
    }
  
    @Test
    void testBoolean(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.BOOLEAN), new Schema.Parser().parse("\"boolean\""));
      assertEquals(Schema.create(Type.BOOLEAN), new Schema.Parser().parse("{\"type\":\"boolean\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"boolean\"", "true", Boolean.TRUE);
    }
  
    @Test
    void string(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.STRING), new Schema.Parser().parse("\"string\""));
      assertEquals(Schema.create(Type.STRING), new Schema.Parser().parse("{\"type\":\"string\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"string\"", "\"foo\"", new Utf8("foo"));
    }
  
    @Test
    void bytes(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.BYTES), new Schema.Parser().parse("\"bytes\""));
      assertEquals(Schema.create(Type.BYTES), new Schema.Parser().parse("{\"type\":\"bytes\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"bytes\"", "\"\\u0000ABC\\u00FF\"",
          ByteBuffer.wrap(new byte[] { 0, 65, 66, 67, -1 }));
    }
  
    @Test
    void testInt(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.INT), new Schema.Parser().parse("\"int\""));
      assertEquals(Schema.create(Type.INT), new Schema.Parser().parse("{\"type\":\"int\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"int\"", "9", 9);
    }
  
    @Test
    void testLong(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.LONG), new Schema.Parser().parse("\"long\""));
      assertEquals(Schema.create(Type.LONG), new Schema.Parser().parse("{\"type\":\"long\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"long\"", "11", 11L);
    }
  
    @Test
    void testFloat(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.FLOAT), new Schema.Parser().parse("\"float\""));
      assertEquals(Schema.create(Type.FLOAT), new Schema.Parser().parse("{\"type\":\"float\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"float\"", "1.1", 1.1f);
      checkDefault("\"float\"", "\"NaN\"", Float.NaN);
      checkDefault("\"float\"", "\"Infinity\"", Float.POSITIVE_INFINITY);
      checkDefault("\"float\"", "\"-Infinity\"", Float.NEGATIVE_INFINITY);
    }
  
    @Test
    void testDouble(TestInfo testInfo) throws Exception {
      assertEquals(Schema.create(Type.DOUBLE), new Schema.Parser().parse("\"double\""));
      assertEquals(Schema.create(Type.DOUBLE), new Schema.Parser().parse("{\"type\":\"double\"}"));
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "\"double\"", "1.2", 1.2);
      checkDefault("\"double\"", "\"NaN\"", Double.NaN);
      checkDefault("\"double\"", "\"Infinity\"", Double.POSITIVE_INFINITY);
      checkDefault("\"double\"", "\"-Infinity\"", Double.NEGATIVE_INFINITY);
    }
  
    @Test
    void array(TestInfo testInfo) throws Exception {
      String json = "{\"type\":\"array\", \"items\": \"long\"}";
      Schema schema = new Schema.Parser().parse(json);
      Collection<Long> array = new GenericData.Array<>(1, schema);
      array.add(1L);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), json, "[1]", array);
      array = new ArrayList<>(1);
      array.add(1L);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), json, "[1]", array);
      checkParseError("{\"type\":\"array\"}"); // items required
    }
  
    @Test
    void map(TestInfo testInfo) throws Exception {
      HashMap<Utf8, Long> map = new HashMap<>();
      map.put(new Utf8("a"), 1L);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "{\"type\":\"map\", \"values\":\"long\"}",
          "{\"a\":1}", map);
      checkParseError("{\"type\":\"map\"}"); // values required
    }
  
    @Test
    void unionMap(TestInfo testInfo) throws Exception {
      String unionMapSchema = "{\"name\":\"foo\", \"type\":\"record\"," + " \"fields\":[ {\"name\":\"mymap\", \"type\":"
          + "   [{\"type\":\"map\", \"values\":" + "      [\"int\",\"long\",\"float\",\"string\"]}," + "    \"null\"]"
          + "   }]" + " }";
      check(new File(DIR, testInfo.getTestMethod().get().getName()), unionMapSchema, true);
    }
  
    @Test
    void record(TestInfo testInfo) throws Exception {
      String recordJson = "{\"type\":\"record\", \"name\":\"Test\", \"fields\":"
          + "[{\"name\":\"f\", \"type\":\"long\", \"foo\":\"bar\"}]}";
      Schema schema = new Schema.Parser().parse(recordJson);
  
      GenericData.Record record = new GenericData.Record(schema);
      record.put("f", 11L);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), recordJson, "{\"f\":11}", record, false);
  
      // test field props
      assertEquals("bar", schema.getField("f").getProp("foo"));
      assertEquals("bar", new Schema.Parser().parse(schema.toString()).getField("f").getProp("foo"));
      schema.getField("f").addProp("baz", "boo");
      assertEquals("boo", schema.getField("f").getProp("baz"));
  
      checkParseError("{\"type\":\"record\"}");
      checkParseError("{\"type\":\"record\",\"name\":\"X\"}");
      checkParseError("{\"type\":\"record\",\"name\":\"X\",\"fields\":\"Y\"}");
      checkParseError("{\"type\":\"record\",\"name\":\"X\",\"fields\":" + "[{\"name\":\"f\"}]}"); // no type
      checkParseError("{\"type\":\"record\",\"name\":\"X\",\"fields\":" + "[{\"type\":\"long\"}]}"); // no name
      // check invalid record names
      checkParseError("{\"type\":\"record\",\"name\":\"1X\",\"fields\":[]}");
      checkParseError("{\"type\":\"record\",\"name\":\"X$\",\"fields\":[]}");
      // check invalid field names
      checkParseError("{\"type\":\"record\",\"name\":\"X\",\"fields\":[" + "{\"name\":\"1f\",\"type\":\"int\"}]}");
      checkParseError("{\"type\":\"record\",\"name\":\"X\",\"fields\":[" + "{\"name\":\"f$\",\"type\":\"int\"}]}");
      checkParseError("{\"type\":\"record\",\"name\":\"X\",\"fields\":[" + "{\"name\":\"f.g\",\"type\":\"int\"}]}");
    }
  
    @Test
    void invalidNameTolerance() {
      new Schema.Parser(NameValidator.NO_VALIDATION).parse("{\"type\":\"record\",\"name\":\"1X\",\"fields\":[]}");
      new Schema.Parser(NameValidator.NO_VALIDATION).parse("{\"type\":\"record\",\"name\":\"X-\",\"fields\":[]}");
      new Schema.Parser(NameValidator.NO_VALIDATION).parse("{\"type\":\"record\",\"name\":\"X$\",\"fields\":[]}");
    }
  
    @Test
    void mapInRecord(TestInfo testInfo) throws Exception {
      String json = "{\"type\":\"record\", \"name\":\"Test\", \"fields\":"
          + "[{\"name\":\"f\", \"type\": {\"type\":\"map\", \"values\":\"long\"}}]}";
      Schema schema = new Schema.Parser().parse(json);
  
      HashMap<Utf8, Long> map = new HashMap<>();
      map.put(new Utf8("a"), 1L);
      GenericData.Record record = new GenericData.Record(schema);
      record.put("f", map);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), json, "{\"f\":{\"a\":1}}", record, false);
    }
  
    @Test
    void testEnum(TestInfo testInfo) throws Exception {
      check(new File(DIR, testInfo.getTestMethod().get().getName()), BASIC_ENUM_SCHEMA, "\"B\"",
          new GenericData.EnumSymbol(new Schema.Parser().parse(BASIC_ENUM_SCHEMA), "B"), false);
      checkParseError("{\"type\":\"enum\"}"); // symbols required
      checkParseError("{\"type\":\"enum\",\"symbols\": [\"X\"]}"); // name reqd
      // check no duplicate symbols
      checkParseError("{\"type\":\"enum\",\"name\":\"X\",\"symbols\":[\"X\",\"X\"]}");
      // check no invalid symbols
      checkParseError("{\"type\":\"enum\",\"name\":\"X\",\"symbols\":[\"1X\"]}");
      checkParseError("{\"type\":\"enum\",\"name\":\"X\",\"symbols\":[\"X$\"]}");
      checkParseError("{\"type\":\"enum\",\"name\":\"X\",\"symbols\":[\"X.Y\"]}");
    }
  
    @Test
    void fixed(TestInfo testInfo) throws Exception {
      String json = "{\"type\": \"fixed\", \"name\":\"Test\", \"size\": 1}";
      Schema schema = new Schema.Parser().parse(json);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), json, "\"a\"",
          new GenericData.Fixed(schema, new byte[] { (byte) 'a' }), false);
      checkParseError("{\"type\":\"fixed\"}"); // size required
    }
  
    @Test
    void recursive(TestInfo testInfo) throws Exception {
      check(new File(DIR, testInfo.getTestMethod().get().getName()),
          "{\"type\": \"record\", \"name\": \"Node\", \"fields\": [" + "{\"name\":\"label\", \"type\":\"string\"},"
              + "{\"name\":\"children\", \"type\":" + "{\"type\": \"array\", \"items\": \"Node\" }}]}",
          false);
    }
  
    @Test
    void recursiveEquals() throws Exception {
      String jsonSchema = "{\"type\":\"record\", \"name\":\"List\", \"fields\": ["
          + "{\"name\":\"next\", \"type\":\"List\"}]}";
      Schema s1 = new Schema.Parser().parse(jsonSchema);
      Schema s2 = new Schema.Parser().parse(jsonSchema);
      assertEquals(s1, s2);
      s1.hashCode(); // test no stackoverflow
    }
  
    /**
     * Test that equals() and hashCode() don't require exponential time on certain
     * pathological schemas.
     */
    @Test
    void schemaExplosion() throws Exception {
      for (int i = 1; i < 15; i++) { // 15 is big enough to trigger
        // create a list of records, each with a single field whose type is a
        // union of all of the records.
        List<Schema> recs = new ArrayList<>();
        for (int j = 0; j < i; j++)
          recs.add(Schema.createRecord("" + (char) ('A' + j), null, null, false));
        for (Schema s : recs) {
          Schema union = Schema.createUnion(recs);
          Field f = new Field("x", union, null, null);
          List<Field> fields = new ArrayList<>();
          fields.add(f);
          s.setFields(fields);
        }
        // check that equals and hashcode are correct and complete in a
        // reasonable amount of time
        for (Schema s1 : recs) {
          Schema s2 = new Schema.Parser().parse(s1.toString());
          assertEquals(s1.hashCode(), s2.hashCode());
          assertEquals(s1, s2);
        }
      }
    }
  
    @Test
    void lisp(TestInfo testInfo) throws Exception {
      check(new File(DIR, testInfo.getTestMethod().get().getName()), LISP_SCHEMA, false);
    }
  
    @Test
    void union(TestInfo testInfo) throws Exception {
      check(new File(DIR, testInfo.getTestMethod().get().getName()), "[\"string\", \"long\"]", false);
      checkDefault("[\"double\", \"long\"]", "1.1", 1.1);
      checkDefault("[\"double\", \"string\"]", "\"TheString\"", new Utf8("TheString"));
  
      // test that erroneous default values cause errors
      for (String type : new String[] { "int", "long", "float", "double", "string", "bytes", "boolean" }) {
  //      checkValidateDefaults("[\"" + type + "\", \"null\"]", "null"); // schema parse time
        checkDefault("[\"" + type + "\", \"null\"]", "null", null); // read time
      }
      checkDefault("[\"null\", \"int\"]", "0", 0);
      checkDefault("[\"null\", \"long\"]", "0", 0l);
      checkDefault("[\"null\", \"float\"]", "0.0", 0.0f);
      checkDefault("[\"null\", \"double\"]", "0.0", 0.0d);
      checkDefault("[\"null\", \"string\"]", "\"Hi\"", new Utf8("Hi"));
      checkDefault("[\"null\", \"bytes\"]", "\"01\"", ByteBuffer.wrap("01".getBytes(StandardCharsets.UTF_8)));
      checkDefault("[\"null\", \"boolean\"]", "true", true);
  
      // check union json
      String record = "{\"type\":\"record\",\"name\":\"Foo\",\"fields\":[]}";
      String fixed = "{\"type\":\"fixed\",\"name\":\"Bar\",\"size\": 1}";
      String enu = "{\"type\":\"enum\",\"name\":\"Baz\",\"symbols\": [\"X\"]}";
      Schema union = new Schema.Parser().parse("[\"null\",\"string\"," + record + "," + enu + "," + fixed + "]");
      checkJson(union, null, "null");
      checkJson(union, new Utf8("foo"), "{\"string\":\"foo\"}");
      checkJson(union, new GenericData.Record(new Schema.Parser().parse(record)), "{\"Foo\":{}}");
      checkJson(union, new GenericData.Fixed(new Schema.Parser().parse(fixed), new byte[] { (byte) 'a' }),
          "{\"Bar\":\"a\"}");
      checkJson(union, new GenericData.EnumSymbol(new Schema.Parser().parse(enu), "X"), "{\"Baz\":\"X\"}");
    }
  
    @Test
    void complexUnions(TestInfo testInfo) throws Exception {
      // one of each unnamed type and two of named types
      String partial = "[\"int\", \"long\", \"float\", \"double\", \"boolean\", \"bytes\","
          + " \"string\", {\"type\":\"array\", \"items\": \"long\"}," + " {\"type\":\"map\", \"values\":\"long\"}";
      String namedTypes = ", {\"type\":\"record\",\"name\":\"Foo\",\"fields\":[]},"
          + " {\"type\":\"fixed\",\"name\":\"Bar\",\"size\": 1},"
          + " {\"type\":\"enum\",\"name\":\"Baz\",\"symbols\": [\"X\"]}";
  
      String namedTypes2 = ", {\"type\":\"record\",\"name\":\"Foo2\",\"fields\":[]},"
          + " {\"type\":\"fixed\",\"name\":\"Bar2\",\"size\": 1},"
          + " {\"type\":\"enum\",\"name\":\"Baz2\",\"symbols\": [\"X\"]}";
  
      check(new File(DIR, testInfo.getTestMethod().get().getName()), partial + namedTypes + "]", false);
      check(new File(DIR, testInfo.getTestMethod().get().getName()), partial + namedTypes + namedTypes2 + "]", false);
      checkParseError(partial + namedTypes + namedTypes + "]");
  
      // fail with two branches of the same unnamed type
      checkUnionError(new Schema[] { Schema.create(Type.INT), Schema.create(Type.INT) });
      checkUnionError(new Schema[] { Schema.create(Type.LONG), Schema.create(Type.LONG) });
      checkUnionError(new Schema[] { Schema.create(Type.FLOAT), Schema.create(Type.FLOAT) });
      checkUnionError(new Schema[] { Schema.create(Type.DOUBLE), Schema.create(Type.DOUBLE) });
      checkUnionError(new Schema[] { Schema.create(Type.BOOLEAN), Schema.create(Type.BOOLEAN) });
      checkUnionError(new Schema[] { Schema.create(Type.BYTES), Schema.create(Type.BYTES) });
      checkUnionError(new Schema[] { Schema.create(Type.STRING), Schema.create(Type.STRING) });
      checkUnionError(
          new Schema[] { Schema.createArray(Schema.create(Type.INT)), Schema.createArray(Schema.create(Type.INT)) });
      checkUnionError(
          new Schema[] { Schema.createMap(Schema.create(Type.INT)), Schema.createMap(Schema.create(Type.INT)) });
  
      List<String> symbols = new ArrayList<>();
      symbols.add("NOTHING");
  
      // succeed with two branches of the same named type, if different names
      Schema u;
      u = buildUnion(new Schema[] { new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"x.A\",\"fields\":[]}"),
          new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"y.A\",\"fields\":[]}") });
      check(new File(DIR, testInfo.getTestMethod().get().getName()), u.toString(), false);
  
      u = buildUnion(new Schema[] { new Schema.Parser().parse("{\"type\":\"enum\",\"name\":\"x.A\",\"symbols\":[\"X\"]}"),
          new Schema.Parser().parse("{\"type\":\"enum\",\"name\":\"y.A\",\"symbols\":[\"Y\"]}") });
      check(new File(DIR, testInfo.getTestMethod().get().getName()), u.toString(), false);
  
      u = buildUnion(new Schema[] { new Schema.Parser().parse("{\"type\":\"fixed\",\"name\":\"x.A\",\"size\":4}"),
          new Schema.Parser().parse("{\"type\":\"fixed\",\"name\":\"y.A\",\"size\":8}") });
      check(new File(DIR, testInfo.getTestMethod().get().getName()), u.toString(), false);
  
      // fail with two branches of the same named type, but same names
      checkUnionError(new Schema[] { Schema.createRecord("Foo", null, "org.test", false),
          Schema.createRecord("Foo", null, "org.test", false) });
      checkUnionError(new Schema[] { Schema.createEnum("Bar", null, "org.test", symbols),
          Schema.createEnum("Bar", null, "org.test", symbols) });
      checkUnionError(new Schema[] { Schema.createFixed("Baz", null, "org.test", 2),
          Schema.createFixed("Baz", null, "org.test", 1) });
  
      Schema union = buildUnion(new Schema[] { Schema.create(Type.INT) });
      // fail if creating a union of a union
      checkUnionError(new Schema[] { union });
    }
  
    @Test
    void complexProp() {
      String json = "{\"type\":\"null\", \"foo\": [0]}";
      Schema s = new Schema.Parser().parse(json);
      assertNull(s.getProp("foo"));
    }
  
    @Test
    void propOrdering() {
      String json = "{\"type\":\"int\",\"z\":\"c\",\"yy\":\"b\",\"x\":\"a\"}";
      Schema s = new Schema.Parser().parse(json);
      assertEquals(json, s.toString());
    }
  
    @Test
    void parseInputStream() throws IOException {
      Schema s = new Schema.Parser().parse(new ByteArrayInputStream("\"boolean\"".getBytes(StandardCharsets.UTF_8)));
      assertEquals(new Schema.Parser().parse("\"boolean\""), s);
    }
  
    @Test
    void namespaceScope() {
      String z = "{\"type\":\"record\",\"name\":\"Z\",\"fields\":[]}";
      String y = "{\"type\":\"record\",\"name\":\"q.Y\",\"fields\":[" + "{\"name\":\"f\",\"type\":" + z + "}]}";
      String x = "{\"type\":\"record\",\"name\":\"p.X\",\"fields\":[" + "{\"name\":\"f\",\"type\":" + y + "},"
          + "{\"name\":\"g\",\"type\":" + z + "}" + "]}";
      Schema xs = new Schema.Parser().parse(x);
      Schema ys = xs.getField("f").schema();
      assertEquals("p.Z", xs.getField("g").schema().getFullName());
      assertEquals("q.Z", ys.getField("f").schema().getFullName());
    }
  
    @Test
    void namespaceNesting() {
      String y = "{\"type\":\"record\",\"name\":\"y.Y\",\"fields\":[" + "{\"name\":\"f\",\"type\":\"x.X\"}]}";
      String x = "{\"type\":\"record\",\"name\":\"x.X\",\"fields\":[" + "{\"name\":\"f\",\"type\":" + y + "}" + "]}";
      Schema xs = new Schema.Parser().parse(x);
      assertEquals(xs, new Schema.Parser().parse(xs.toString()));
    }
  
    @Test
    void nestedNullNamespace() {
      Schema inner = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Inner\",\"fields\":[]}");
      Schema outer = Schema.createRecord("Outer", null, "space", false);
      outer.setFields(Collections.singletonList(new Field("f", inner, null, null)));
      assertEquals(outer, new Schema.Parser().parse(outer.toString()));
    }
  
    @Test
    void deeplyNestedNullNamespace() {
      Schema inner = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Inner\",\"fields\":["
          + "{\"name\":\"x\",\"type\":{\"type\":\"record\",\"name\":\"Deeper\",\"fields\":["
          + "{\"name\":\"y\",\"type\":\"int\"}]}}]}");
      Schema outer = Schema.createRecord("Outer", null, "space", false);
      outer.setFields(Collections.singletonList(new Field("f", inner, null, null)));
      assertEquals(outer, new Schema.Parser().parse(outer.toString()));
    }
  
    @Test
    void nestedNullNamespaceReferencing() {
      Schema inner = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Inner\",\"fields\":[]}");
      Schema outer = Schema.createRecord("Outer", null, "space", false);
      outer.setFields(Arrays.asList(new Field("f1", inner, null, null), new Field("f2", inner, null, null)));
      assertEquals(outer, new Schema.Parser().parse(outer.toString()));
    }
  
    @Test
    void nestedNullNamespaceReferencingWithUnion() {
      Schema inner = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Inner\",\"fields\":[]}");
      Schema innerUnion = Schema.createUnion(Arrays.asList(inner, Schema.create(Type.NULL)));
      Schema outer = Schema.createRecord("Outer", null, "space", false);
      outer.setFields(Arrays.asList(new Field("f1", innerUnion, null, null), new Field("f2", innerUnion, null, null)));
      assertEquals(outer, new Schema.Parser().parse(outer.toString()));
    }
  
    @Test
    void nestedNonNullNamespace1() {
      Schema inner1 = Schema.createEnum("InnerEnum", null, "space", Collections.singletonList("x"));
      Schema inner2 = new Schema.Parser()
          .parse("{\"type\":\"record\",\"namespace\":\"space\",\"name\":" + "\"InnerRecord\",\"fields\":[]}");
      Schema nullOuter = Schema.createRecord("Outer", null, null, false);
      nullOuter.setFields(Arrays.asList(new Field("f1", inner1, null, null), new Field("f2", inner2, null, null)));
      assertEquals(nullOuter, new Schema.Parser().parse(nullOuter.toString()));
    }
  
    @Test
    void nestedNonNullNamespace2() {
      Schema inner1 = Schema.createFixed("InnerFixed", null, "space", 1);
      Schema inner2 = new Schema.Parser()
          .parse("{\"type\":\"record\",\"namespace\":\"space\",\"name\":" + "\"InnerRecord\",\"fields\":[]}");
      Schema nullOuter = Schema.createRecord("Outer", null, null, false);
      nullOuter.setFields(Arrays.asList(new Field("f1", inner1, null, null), new Field("f2", inner2, null, null)));
      assertEquals(nullOuter, new Schema.Parser().parse(nullOuter.toString()));
    }
  
    @Test
    void nullNamespaceAlias() {
      Schema s = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Z\",\"fields\":[]}");
      Schema t = new Schema.Parser()
          .parse("{\"type\":\"record\",\"name\":\"x.Y\",\"aliases\":[\".Z\"]," + "\"fields\":[]}");
      Schema u = Schema.applyAliases(s, t);
      assertEquals("x.Y", u.getFullName());
    }
  
    @Test
    void nullPointer() throws Exception {
      String recordJson = "{\"type\":\"record\", \"name\":\"Test\", \"fields\":"
          + "[{\"name\":\"x\", \"type\":\"string\"}]}";
      Schema schema = new Schema.Parser().parse(recordJson);
      GenericData.Record record = new GenericData.Record(schema);
      assertThrows(NullPointerException.class,
          () -> checkBinary(schema, record, new GenericDatumWriter<>(), new GenericDatumReader<>()));
    }
  
    private static void checkParseError(String json) {
      try {
        new Schema.Parser().parse(json);
      } catch (AvroRuntimeException e) {
        return;
      }
      fail("Should not have parsed: " + json);
    }
  
    private static void checkUnionError(Schema[] branches) {
      List<Schema> branchList = Arrays.asList(branches);
      try {
        Schema.createUnion(branchList);
        fail("Union should not have constructed from: " + branchList);
      } catch (AvroRuntimeException ignored) {
      }
    }
  
    private static Schema buildUnion(Schema[] branches) {
      List<Schema> branchList = Arrays.asList(branches);
      return Schema.createUnion(branchList);
    }
  
    /**
     * Makes sure that "doc" tags are transcribed in the schemas. Note that there
     * are docs both for fields and for the records themselves.
     */
    @Test
    void docs() {
      Schema schema = new Schema.Parser().parse(SCHEMA_WITH_DOC_TAGS);
      assertEquals("This is not a world record.", schema.getDoc());
      assertEquals("Inner Fixed", schema.getField("inner_fixed").doc());
      assertEquals("Very Inner Fixed", schema.getField("inner_fixed").schema().getDoc());
      assertEquals("Inner String", schema.getField("inner_string").doc());
      assertEquals("Inner Enum", schema.getField("inner_enum").doc());
      assertEquals("Very Inner Enum", schema.getField("inner_enum").schema().getDoc());
      assertEquals("Inner Union", schema.getField("inner_union").doc());
    }
  
    @Test
    void fieldDocs() {
      String schemaStr = "{\"name\": \"Rec\",\"type\": \"record\",\"fields\" : ["
          + "{\"name\": \"f\", \"type\": \"int\", \"doc\": \"test\"}]}";
  
      // check field doc is parsed correctly
      Schema schema = new Schema.Parser().parse(schemaStr);
      assertEquals("test", schema.getField("f").doc());
  
      // check print/read cycle preserves field doc
      schema = new Schema.Parser().parse(schema.toString());
      assertEquals("test", schema.getField("f").doc());
    }
  
    @Test
    void aliases() {
      String t1 = "{\"type\":\"record\",\"name\":\"a.b\",\"fields\":[" + "{\"name\":\"f\",\"type\":\"long\"},"
          + "{\"name\":\"h\",\"type\":\"int\"}]}";
      String t2 = "{\"type\":\"record\",\"name\":\"x.y\",\"aliases\":[\"a.b\"],"
          + "\"fields\":[{\"name\":\"g\",\"type\":\"long\",\"aliases\":[\"f\"]}," + "{\"name\":\"h\",\"type\":\"int\"}]}";
      Schema s1 = new Schema.Parser().parse(t1);
      Schema s2 = new Schema.Parser().parse(t2);
  
      assertEquals(s1.getAliases(), Collections.emptySet());
      assertEquals(s1.getField("f").aliases(), Collections.emptySet());
      assertEquals(s2.getAliases(), Collections.singleton("a.b"));
      assertEquals(s2.getField("g").aliases(), Collections.singleton("f"));
  
      Schema s3 = Schema.applyAliases(s1, s2);
      assertNotSame(s2, s3);
      assertEquals(s2, s3);
  
      t1 = "{\"type\":\"enum\",\"name\":\"a.b\"," + "\"symbols\":[\"x\"]}";
      t2 = "{\"type\":\"enum\",\"name\":\"a.c\",\"aliases\":[\"b\"]," + "\"symbols\":[\"x\"]}";
      s1 = new Schema.Parser().parse(t1);
      s2 = new Schema.Parser().parse(t2);
      s3 = Schema.applyAliases(s1, s2);
      assertNotSame(s2, s3);
      assertEquals(s2, s3);
  
      t1 = "{\"type\":\"fixed\",\"name\":\"a\"," + "\"size\": 5}";
      t2 = "{\"type\":\"fixed\",\"name\":\"b\",\"aliases\":[\"a\"]," + "\"size\": 5}";
      s1 = new Schema.Parser().parse(t1);
      s2 = new Schema.Parser().parse(t2);
      s3 = Schema.applyAliases(s1, s2);
      assertNotSame(s2, s3);
      assertEquals(s2, s3);
    }
  
    @Test
    void aliasesSelfReferential() {
      String t1 = "{\"type\":\"record\",\"name\":\"a\",\"fields\":[{\"name\":\"f\",\"type\":{\"type\":\"record\",\"name\":\"C\",\"fields\":[{\"name\":\"c\",\"type\":{\"type\":\"array\",\"items\":[\"null\",\"C\"]}}]}}]}";
      String t2 = "{\"type\":\"record\",\"name\":\"x\",\"fields\":[{\"name\":\"f\",\"type\":{\"type\":\"record\",\"name\":\"C\",\"fields\":[{\"name\":\"d\",\"type\":{\"type\":\"array\",\"items\":[\"null\",\"C\"]},\"aliases\":[\"c\"]}]}}],\"aliases\":[\"a\"]}";
      Schema s1 = new Schema.Parser().parse(t1);
      Schema s2 = new Schema.Parser().parse(t2);
  
      assertEquals(s1.getAliases(), Collections.emptySet());
      assertEquals(s2.getAliases(), Collections.singleton("a"));
  
      Schema s3 = Schema.applyAliases(s1, s2);
      assertNotSame(s2, s3);
      assertEquals(s2, s3);
    }
  
    private static void check(File dst, String schemaJson, String defaultJson, Object defaultValue) throws Exception {
      check(dst, schemaJson, defaultJson, defaultValue, true);
    }
  
    private static void check(File dst, String schemaJson, String defaultJson, Object defaultValue, boolean induce)
        throws Exception {
      check(dst, schemaJson, induce);
      checkDefault(schemaJson, defaultJson, defaultValue);
    }
  
    private static void check(File dst, String jsonSchema, boolean induce) throws Exception {
      Schema schema = new Schema.Parser().parse(jsonSchema);
      checkProp(schema);
      Object reuse = null;
      for (Object datum : new RandomData(schema, COUNT, true)) {
  
        if (induce) {
          Schema induced = GenericData.get().induce(datum);
          assertEquals(schema, induced, "Induced schema does not match.");
        }
  
        assertTrue(GenericData.get().validate(schema, datum), "Datum does not validate against schema " + datum);
  
        checkBinary(schema, datum, new GenericDatumWriter<>(), new GenericDatumReader<>(), null);
        reuse = checkBinary(schema, datum, new GenericDatumWriter<>(), new GenericDatumReader<>(), reuse);
        checkDirectBinary(schema, datum, new GenericDatumWriter<>(), new GenericDatumReader<>());
        checkBlockingBinary(schema, datum, new GenericDatumWriter<>(), new GenericDatumReader<>());
        checkJson(schema, datum, new GenericDatumWriter<>(), new GenericDatumReader<>());
  
        // Check that we can generate the code for every schema we see.
        TestSpecificCompiler.assertCompiles(dst, schema, false);
  
        // Check that we can read/write the json of every schema we see.
        checkBinaryJson(jsonSchema);
      }
    }
  
    private static void checkProp(Schema s0) throws Exception {
      if (s0.getType().equals(Schema.Type.UNION))
        return; // unions have no props
      assertNull(s0.getProp("foo"));
      Schema s1 = new Schema.Parser().parse(s0.toString());
      s1.addProp("foo", "bar");
      assertEquals("bar", s1.getProp("foo"));
      assertNotEquals(s0, s1);
      Schema s2 = new Schema.Parser().parse(s1.toString());
      assertEquals("bar", s2.getProp("foo"));
      assertEquals(s1, s2);
      assertNotEquals(s0, s2);
    }
  
    public static void checkBinary(Schema schema, Object datum, DatumWriter<Object> writer, DatumReader<Object> reader)
        throws IOException {
      checkBinary(schema, datum, writer, reader, null);
    }
  
    public static Object checkBinary(Schema schema, Object datum, DatumWriter<Object> writer, DatumReader<Object> reader,
        Object reuse) throws IOException {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      writer.setSchema(schema);
      Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
      writer.write(datum, encoder);
      encoder.flush();
      byte[] data = out.toByteArray();
  
      reader.setSchema(schema);
  
      Object decoded = reader.read(reuse, DecoderFactory.get().binaryDecoder(data, null));
  
      assertEquals(datum, decoded, "Decoded data does not match.");
      return decoded;
    }
  
    public static void checkDirectBinary(Schema schema, Object datum, DatumWriter<Object> writer,
        DatumReader<Object> reader) throws IOException {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      writer.setSchema(schema);
      Encoder encoder = EncoderFactory.get().directBinaryEncoder(out, null);
      writer.write(datum, encoder);
      // no flush for direct
      byte[] data = out.toByteArray();
  
      reader.setSchema(schema);
  
      Object decoded = reader.read(null, DecoderFactory.get().binaryDecoder(data, null));
  
      assertEquals(datum, decoded, "Decoded data does not match.");
    }
  
    public static void checkBlockingBinary(Schema schema, Object datum, DatumWriter<Object> writer,
        DatumReader<Object> reader) throws IOException {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      writer.setSchema(schema);
      Encoder encoder = EncoderFactory.get().blockingBinaryEncoder(out, null);
      writer.write(datum, encoder);
      encoder.flush();
      byte[] data = out.toByteArray();
  
      reader.setSchema(schema);
  
      Object decoded = reader.read(null, DecoderFactory.get().binaryDecoder(data, null));
  
      assertEquals(datum, decoded, "Decoded data does not match.");
    }
  
    private static void checkJson(Schema schema, Object datum, DatumWriter<Object> writer, DatumReader<Object> reader)
        throws IOException {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      Encoder encoder = EncoderFactory.get().jsonEncoder(schema, out);
      writer.setSchema(schema);
      writer.write(datum, encoder);
      writer.write(datum, encoder);
      encoder.flush();
      byte[] data = out.toByteArray();
  
      reader.setSchema(schema);
      Decoder decoder = DecoderFactory.get().jsonDecoder(schema, new ByteArrayInputStream(data));
      Object decoded = reader.read(null, decoder);
      assertEquals(datum, decoded, "Decoded data does not match.");
  
      decoded = reader.read(decoded, decoder);
      assertEquals(datum, decoded, "Decoded data does not match.");
    }
  
    private static void checkJson(Schema schema, Object datum, String json) throws Exception {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      Encoder encoder = EncoderFactory.get().jsonEncoder(schema, out);
      DatumWriter<Object> writer = new GenericDatumWriter<>();
      writer.setSchema(schema);
      writer.write(datum, encoder);
      encoder.flush();
      byte[] data = out.toByteArray();
  
      String encoded = new String(data, StandardCharsets.UTF_8);
      assertEquals(json, encoded, "Encoded data does not match.");
  
      DatumReader<Object> reader = new GenericDatumReader<>();
      reader.setSchema(schema);
      Object decoded = reader.read(null, DecoderFactory.get().jsonDecoder(schema, new ByteArrayInputStream(data)));
  
      assertEquals(datum, decoded, "Decoded data does not match.");
    }
  
    public static void checkBinaryJson(String json) throws Exception {
      Object node = Json.parseJson(json);
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      DatumWriter<Object> writer = new Json.ObjectWriter();
      Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
      encoder = EncoderFactory.get().validatingEncoder(Json.SCHEMA, encoder);
      writer.write(node, encoder);
      encoder.flush();
      byte[] bytes = out.toByteArray();
  
      DatumReader<Object> reader = new Json.ObjectReader();
      Decoder decoder = DecoderFactory.get().binaryDecoder(bytes, null);
      decoder = DecoderFactory.get().validatingDecoder(Json.SCHEMA, decoder);
      Object decoded = reader.read(null, decoder);
  
      assertEquals(Json.toString(node), Json.toString(decoded), "Decoded json does not match.");
    }
  
    private static final Schema ACTUAL = // an empty record schema
        new Schema.Parser().parse("{\"type\":\"record\", \"name\":\"Foo\", \"fields\":[]}");
  
    private static void checkDefault(String schemaJson, String defaultJson, Object defaultValue) throws Exception {
      String recordJson = "{\"type\":\"record\", \"name\":\"Foo\", \"fields\":[{\"name\":\"f\", " + "\"type\":"
          + schemaJson + ", " + "\"default\":" + defaultJson + "}]}";
      Schema expected = new Schema.Parser().parse(recordJson);
      DatumReader<Object> in = new GenericDatumReader<>(ACTUAL, expected);
      GenericData.Record record = (GenericData.Record) in.read(null,
          DecoderFactory.get().binaryDecoder(new byte[0], null));
      assertEquals(defaultValue, record.get("f"), "Wrong default.");
      assertEquals(expected, new Schema.Parser().parse(expected.toString()), "Wrong toString");
    }
  
    private static void checkValidateDefaults(String schemaJson, String defaultJson) {
      try {
        Schema.Parser parser = new Schema.Parser();
        String recordJson = "{\"type\":\"record\", \"name\":\"Foo\", \"fields\":[{\"name\":\"f\", " + "\"type\":"
            + schemaJson + ", " + "\"default\":" + defaultJson + "}]}";
        parser.parse(recordJson);
        fail("Schema of type " + schemaJson + " should not have default " + defaultJson);
      } catch (AvroTypeException ignored) {
      }
    }
  
    @Test
    void noDefaultField() throws Exception {
      assertThrows(AvroTypeException.class, () -> {
        Schema expected = new Schema.Parser()
            .parse("{\"type\":\"record\", \"name\":\"Foo\", \"fields\":" + "[{\"name\":\"f\", \"type\": \"string\"}]}");
        DatumReader<Object> in = new GenericDatumReader<>(ACTUAL, expected);
        in.read(null, DecoderFactory.get().binaryDecoder(new ByteArrayInputStream(new byte[0]), null));
      });
    }
  
    @Test
    void enumMismatch() throws Exception {
      Schema actual = new Schema.Parser().parse("{\"type\":\"enum\",\"name\":\"E\",\"symbols\":[\"X\",\"Y\"]}");
      Schema expected = new Schema.Parser().parse("{\"type\":\"enum\",\"name\":\"E\",\"symbols\":[\"Y\",\"Z\"]}");
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      DatumWriter<Object> writer = new GenericDatumWriter<>(actual);
      Encoder encoder = EncoderFactory.get().directBinaryEncoder(out, null);
      writer.write(new GenericData.EnumSymbol(actual, "Y"), encoder);
      writer.write(new GenericData.EnumSymbol(actual, "X"), encoder);
      encoder.flush();
      byte[] data = out.toByteArray();
      Decoder decoder = DecoderFactory.get().binaryDecoder(data, null);
      DatumReader<String> in = new GenericDatumReader<>(actual, expected);
      assertEquals(new GenericData.EnumSymbol(expected, "Y"), in.read(null, decoder), "Wrong value");
      try {
        in.read(null, decoder);
        fail("Should have thrown exception.");
      } catch (AvroTypeException e) {
        // expected
      }
    }
  
    @Test
    void recordWithPrimitiveName() {
      assertThrows(AvroTypeException.class, () -> {
        new Schema.Parser().parse("{\"type\":\"record\", \"name\":\"string\", \"fields\": []}");
      });
    }
  
    @Test
    void enumWithPrimitiveName() {
      assertThrows(AvroTypeException.class, () -> {
        new Schema.Parser().parse("{\"type\":\"enum\", \"name\":\"null\", \"symbols\": [\"A\"]}");
      });
    }
  
    private static Schema enumSchema() {
      return new Schema.Parser().parse("{ \"type\": \"enum\", \"name\": \"e\", " + "\"symbols\": [\"a\", \"b\"]}");
    }
  
    @Test
    void immutability1() {
      assertThrows(AvroRuntimeException.class, () -> {
        Schema s = enumSchema();
        s.addProp("p1", "1");
        s.addProp("p1", "2");
      });
    }
  
    @Test
    void immutability2() {
      assertThrows(AvroRuntimeException.class, () -> {
        Schema s = enumSchema();
        s.addProp("p1", null);
      });
    }
  
    private static List<String> lockedArrayList() {
      return new Schema.LockableArrayList<>(Arrays.asList("a", "b", "c")).lock();
    }
  
    @Test
    void lockedArrayList1() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().add("p");
      });
    }
  
    @Test
    void lockedArrayList2() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().remove("a");
      });
    }
  
    @Test
    void lockedArrayList3() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().addAll(Collections.singletonList("p"));
      });
    }
  
    @Test
    void lockedArrayList4() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().addAll(0, Collections.singletonList("p"));
      });
    }
  
    @Test
    void lockedArrayList5() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().removeAll(Collections.singletonList("a"));
      });
    }
  
    @Test
    void lockedArrayList6() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().retainAll(Collections.singletonList("a"));
      });
    }
  
    @Test
    void lockedArrayList7() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().clear();
      });
    }
  
    @Test
    void lockedArrayList8() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().iterator().remove();
      });
    }
  
    @Test
    void lockedArrayList9() {
      assertThrows(IllegalStateException.class, () -> {
        Iterator<String> it = lockedArrayList().iterator();
        it.next();
        it.remove();
      });
    }
  
    @Test
    void lockedArrayList10() {
      assertThrows(IllegalStateException.class, () -> {
        lockedArrayList().remove(1);
      });
    }
  
    @Test
    void names_GetWithInheritedNamespace() {
      Schema schema = Schema.create(Type.STRING);
      Schema.Names names = new Schema.Names("space");
      names.put(new Schema.Name("Name", "space"), schema);
  
      assertEquals(schema, names.get(new Schema.Name("Name", "space")));
      assertEquals(schema, names.get("Name"));
    }
  
    @Test
    void names_GetWithNullNamespace() {
      Schema schema = Schema.create(Type.STRING);
      Schema.Names names = new Schema.Names("space");
      names.put(new Schema.Name("Name", ""), schema);
  
      assertEquals(schema, names.get(new Schema.Name("Name", "")));
      assertEquals(schema, names.get("Name"));
    }
  
    @Test
    void names_GetNotFound() {
      Schema.Names names = new Schema.Names("space");
      names.put(new Schema.Name("Name", "otherspace"), Schema.create(Type.STRING));
  
      assertNull(names.get("Name"));
    }
  }
  