/**
 * 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
 *
 *     http://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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
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.Utf8;
import org.codehaus.jackson.JsonNode;
import org.junit.Test;

public class TestSchema {

  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
  public void testNull() throws Exception {
    assertEquals(Schema.create(Type.NULL), Schema.parse("\"null\""));
    assertEquals(Schema.create(Type.NULL), Schema.parse("{\"type\":\"null\"}"));
    check("\"null\"", "null", null);
  }

  @Test
  public void testBoolean() throws Exception {
    assertEquals(Schema.create(Type.BOOLEAN), Schema.parse("\"boolean\""));
    assertEquals(Schema.create(Type.BOOLEAN),
                 Schema.parse("{\"type\":\"boolean\"}"));
    check("\"boolean\"", "true", Boolean.TRUE);
  }

  @Test
  public void testString() throws Exception {
    assertEquals(Schema.create(Type.STRING), Schema.parse("\"string\""));
    assertEquals(Schema.create(Type.STRING),
                 Schema.parse("{\"type\":\"string\"}"));
    check("\"string\"", "\"foo\"", new Utf8("foo"));
  }

  @Test
  public void testBytes() throws Exception {
    assertEquals(Schema.create(Type.BYTES), Schema.parse("\"bytes\""));
    assertEquals(Schema.create(Type.BYTES),
                 Schema.parse("{\"type\":\"bytes\"}"));
    check("\"bytes\"", "\"\\u0000ABC\\u00FF\"",
          ByteBuffer.wrap(new byte[]{0,65,66,67,-1}));
  }

  @Test
  public void testInt() throws Exception {
    assertEquals(Schema.create(Type.INT), Schema.parse("\"int\""));
    assertEquals(Schema.create(Type.INT), Schema.parse("{\"type\":\"int\"}"));
    check("\"int\"", "9", new Integer(9));
  }

  @Test
  public void testLong() throws Exception {
    assertEquals(Schema.create(Type.LONG), Schema.parse("\"long\""));
    assertEquals(Schema.create(Type.LONG), Schema.parse("{\"type\":\"long\"}"));
    check("\"long\"", "11", new Long(11));
  }

  @Test
  public void testFloat() throws Exception {
    assertEquals(Schema.create(Type.FLOAT), Schema.parse("\"float\""));
    assertEquals(Schema.create(Type.FLOAT),
                 Schema.parse("{\"type\":\"float\"}"));
    check("\"float\"", "1.1", new Float(1.1));
    checkDefault("\"float\"", "\"NaN\"", Float.NaN);
    checkDefault("\"float\"", "\"Infinity\"", Float.POSITIVE_INFINITY);
    checkDefault("\"float\"", "\"-Infinity\"", Float.NEGATIVE_INFINITY);
  }

  @Test
  public void testDouble() throws Exception {
    assertEquals(Schema.create(Type.DOUBLE), Schema.parse("\"double\""));
    assertEquals(Schema.create(Type.DOUBLE),
                 Schema.parse("{\"type\":\"double\"}"));
    check("\"double\"", "1.2", new Double(1.2));
    checkDefault("\"double\"", "\"NaN\"", Double.NaN);
    checkDefault("\"double\"", "\"Infinity\"", Double.POSITIVE_INFINITY);
    checkDefault("\"double\"", "\"-Infinity\"", Double.NEGATIVE_INFINITY);
  }

  @Test
  public void testArray() throws Exception {
    String json = "{\"type\":\"array\", \"items\": \"long\"}";
    Schema schema = Schema.parse(json);
    Collection<Long> array = new GenericData.Array<Long>(1, schema);
    array.add(1L);
    check(json, "[1]", array);
    array = new ArrayList<Long>(1);
    array.add(1L);
    check(json, "[1]", array);
    checkParseError("{\"type\":\"array\"}");      // items required
  }

  @Test
  public void testMap() throws Exception {
    HashMap<Utf8,Long> map = new HashMap<Utf8,Long>();
    map.put(new Utf8("a"), 1L);
    check("{\"type\":\"map\", \"values\":\"long\"}", "{\"a\":1}", map);
    checkParseError("{\"type\":\"map\"}");        // values required
  }

  @Test
  public void testUnionMap() throws Exception {
    String unionMapSchema = "{\"name\":\"foo\", \"type\":\"record\"," +
        " \"fields\":[ {\"name\":\"mymap\", \"type\":" +
        "   [{\"type\":\"map\", \"values\":" +
        "      [\"int\",\"long\",\"float\",\"string\"]}," +
        "    \"null\"]" +
        "   }]" +
        " }";
    check(unionMapSchema, true);
  }

  @Test
  public void testRecord() throws Exception {
    String recordJson = "{\"type\":\"record\", \"name\":\"Test\", \"fields\":"
      +"[{\"name\":\"f\", \"type\":\"long\", \"foo\":\"bar\"}]}";
    Schema schema = Schema.parse(recordJson);

    GenericData.Record record = new GenericData.Record(schema);
    record.put("f", 11L);
    check(recordJson, "{\"f\":11}", record, false);

    // test field props
    assertEquals("bar", schema.getField("f").getProp("foo"));
    assertEquals("bar", Schema.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 public void testInvalidNameTolerance() {
    Schema.parse("{\"type\":\"record\",\"name\":\"1X\",\"fields\":[]}", false);
    Schema.parse("{\"type\":\"record\",\"name\":\"X-\",\"fields\":[]}", false);
    Schema.parse("{\"type\":\"record\",\"name\":\"X$\",\"fields\":[]}", false);
  }

  @Test
  public void testMapInRecord() throws Exception {
    String json = "{\"type\":\"record\", \"name\":\"Test\", \"fields\":"
      +"[{\"name\":\"f\", \"type\": {\"type\":\"map\", \"values\":\"long\"}}]}";
    Schema schema = Schema.parse(json);

    HashMap<Utf8,Long> map = new HashMap<Utf8,Long>();
    map.put(new Utf8("a"), 1L);
    GenericData.Record record = new GenericData.Record(schema);
    record.put("f", map);
    check(json, "{\"f\":{\"a\":1}}", record, false);
  }


  @Test
  public void testEnum() throws Exception {
    check(BASIC_ENUM_SCHEMA, "\"B\"",
          new GenericData.EnumSymbol(Schema.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
  public void testFixed() throws Exception {
    String json = "{\"type\": \"fixed\", \"name\":\"Test\", \"size\": 1}";
    Schema schema = Schema.parse(json);
    check(json, "\"a\"",
          new GenericData.Fixed(schema, new byte[]{(byte)'a'}), false);
    checkParseError("{\"type\":\"fixed\"}");        // size required
  }

  @Test
  public void testRecursive() throws Exception {
    check("{\"type\": \"record\", \"name\": \"Node\", \"fields\": ["
          +"{\"name\":\"label\", \"type\":\"string\"},"
          +"{\"name\":\"children\", \"type\":"
          +"{\"type\": \"array\", \"items\": \"Node\" }}]}",
          false);
  }

  @Test
  public void testRecursiveEquals() throws Exception {
    String jsonSchema = "{\"type\":\"record\", \"name\":\"List\", \"fields\": ["
      +"{\"name\":\"next\", \"type\":\"List\"}]}";
    Schema s1 = Schema.parse(jsonSchema);
    Schema s2 = Schema.parse(jsonSchema);
    assertEquals(s1, s2);
    s1.hashCode();                                // test no stackoverflow
  }

  @Test
  /** Test that equals() and hashCode() don't require exponential time on
   *  certain pathological schemas. */
  public void testSchemaExplosion() 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<Schema>();
      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<Field>();
        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 = Schema.parse(s1.toString());
        assertEquals(s1.hashCode(), s2.hashCode());
        assertEquals(s1, s2);
      }
    }
  }

  @Test
  public void testLisp() throws Exception {
    check(LISP_SCHEMA, false);
  }

  @Test
  public void testUnion() throws Exception {
    check("[\"string\", \"long\"]", false);
    checkDefault("[\"double\", \"long\"]", "1.1", new Double(1.1));

    // 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
      boolean error = false;
      try {
        checkDefault("[\""+type+"\", \"null\"]", "null", 0); // read time
      } catch (AvroTypeException e) {
        error = true;
      }
      assertTrue(error);
      checkValidateDefaults("[\"null\", \""+type+"\"]", "0");  // schema parse time
      error = false;
      try {
        checkDefault("[\"null\", \""+type+"\"]", "0", null); // read time
      } catch (AvroTypeException e) {
        error = true;
      }
      assertTrue(error);
    }

    // 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 = Schema.parse("[\"null\",\"string\","
                                +record+","+ enu+","+fixed+"]");
    checkJson(union, null, "null");
    checkJson(union, new Utf8("foo"), "{\"string\":\"foo\"}");
    checkJson(union,
              new GenericData.Record(Schema.parse(record)),
              "{\"Foo\":{}}");
    checkJson(union,
              new GenericData.Fixed(Schema.parse(fixed), new byte[]{(byte)'a'}),
              "{\"Bar\":\"a\"}");
    checkJson(union,
              new GenericData.EnumSymbol(Schema.parse(enu), "X"),
              "{\"Baz\":\"X\"}");
  }

  @Test
  public void testComplexUnions() 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(partial + namedTypes + "]", false);
    check(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<String>();
    symbols.add("NOTHING");

    // succeed with two branches of the same named type, if different names
    Schema u;
    u = buildUnion(new Schema[] {
        Schema.parse("{\"type\":\"record\",\"name\":\"x.A\",\"fields\":[]}"),
        Schema.parse("{\"type\":\"record\",\"name\":\"y.A\",\"fields\":[]}")});
    check(u.toString(), false);

    u = buildUnion(new Schema[] {
        Schema.parse
        ("{\"type\":\"enum\",\"name\":\"x.A\",\"symbols\":[\"X\"]}"),
        Schema.parse
        ("{\"type\":\"enum\",\"name\":\"y.A\",\"symbols\":[\"Y\"]}")});
    check(u.toString(), false);

    u = buildUnion(new Schema[] {
        Schema.parse("{\"type\":\"fixed\",\"name\":\"x.A\",\"size\":4}"),
        Schema.parse("{\"type\":\"fixed\",\"name\":\"y.A\",\"size\":8}")});
    check(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
  public void testComplexProp() throws Exception {
    String json = "{\"type\":\"null\", \"foo\": [0]}";
    Schema s = Schema.parse(json);
    assertEquals(null, s.getProp("foo"));
  }

  @Test public void testPropOrdering() throws Exception {
    String json = "{\"type\":\"int\",\"z\":\"c\",\"yy\":\"b\",\"x\":\"a\"}";
    Schema s = Schema.parse(json);
    assertEquals(json, s.toString());
  }

  @Test
  public void testParseInputStream() throws IOException {
    Schema s = Schema.parse(
        new ByteArrayInputStream("\"boolean\"".getBytes("UTF-8")));
    assertEquals(Schema.parse("\"boolean\""), s);
  }

  @Test
  public void testNamespaceScope() throws Exception {
    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 = Schema.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
  public void testNamespaceNesting() throws Exception {
    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 = Schema.parse(x);
    assertEquals(xs, Schema.parse(xs.toString()));
  }

  @Test
  public void testNestedNullNamespace() throws Exception {
    Schema inner =
      Schema.parse("{\"type\":\"record\",\"name\":\"Inner\",\"fields\":[]}");
    Schema outer = Schema.createRecord("Outer", null, "space", false);
    outer.setFields(Arrays.asList(new Field("f", inner, null, null)));
    assertEquals(outer, Schema.parse(outer.toString()));
  }

  @Test
  public void testNestedNullNamespaceReferencing() {
    Schema inner =
        Schema.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, Schema.parse(outer.toString()));
  }

  @Test
  public void testNestedNullNamespaceReferencingWithUnion() {
    Schema inner =
        Schema.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, Schema.parse(outer.toString()));
  }

  @Test
  public void testNestedNonNullNamespace1() throws Exception {
    Schema inner1 = Schema.createEnum("InnerEnum", null, "space", Arrays.asList("x"));
    Schema inner2 = Schema.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, Schema.parse(nullOuter.toString()));
  }

  @Test
  public void testNestedNonNullNamespace2() throws Exception {
    Schema inner1 = Schema.createFixed("InnerFixed", null, "space", 1);
    Schema inner2 = Schema.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, Schema.parse(nullOuter.toString()));
  }

  @Test
  public void testNullNamespaceAlias() throws Exception {
    Schema s =
      Schema.parse("{\"type\":\"record\",\"name\":\"Z\",\"fields\":[]}");
    Schema t =
      Schema.parse("{\"type\":\"record\",\"name\":\"x.Y\",\"aliases\":[\".Z\"],"
                   +"\"fields\":[]}");
    Schema u = Schema.applyAliases(s, t);
    assertEquals("x.Y", u.getFullName());
  }

  @Test
  public void testNullPointer() throws Exception {
    String recordJson = "{\"type\":\"record\", \"name\":\"Test\", \"fields\":"
      +"[{\"name\":\"x\", \"type\":\"string\"}]}";
    Schema schema = Schema.parse(recordJson);
    GenericData.Record record = new GenericData.Record(schema);
    try {
      checkBinary(schema, record,
                  new GenericDatumWriter<Object>(),
                  new GenericDatumReader<Object>());
    } catch (NullPointerException e) {
      assertEquals("null of string in field x of Test", e.getMessage());
    }
  }

  private static void checkParseError(String json) {
    try {
      Schema.parse(json);
    } catch (SchemaParseException 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 are) {
      return;
    }
  }

  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
  public void testDocs() {
    Schema schema = Schema.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
  public void testFieldDocs() {
    String schemaStr = "{\"name\": \"Rec\",\"type\": \"record\",\"fields\" : ["+
      "{\"name\": \"f\", \"type\": \"int\", \"doc\": \"test\"}]}";

    // check field doc is parsed correctly
    Schema schema = Schema.parse(schemaStr);
    assertEquals("test", schema.getField("f").doc());

    // check print/read cycle preserves field doc
    schema = Schema.parse(schema.toString());
    assertEquals("test", schema.getField("f").doc());
  }

  @Test
  public void testAliases() throws Exception {
    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 = Schema.parse(t1);
    Schema s2 = Schema.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);
    assertFalse(s2 == s3);
    assertEquals(s2, s3);

    t1 = "{\"type\":\"enum\",\"name\":\"a.b\","
      +"\"symbols\":[\"x\"]}";
    t2 = "{\"type\":\"enum\",\"name\":\"a.c\",\"aliases\":[\"b\"],"
      +"\"symbols\":[\"x\"]}";
    s1 = Schema.parse(t1);
    s2 = Schema.parse(t2);
    s3 = Schema.applyAliases(s1,s2);
    assertFalse(s2 == s3);
    assertEquals(s2, s3);

    t1 = "{\"type\":\"fixed\",\"name\":\"a\","
      +"\"size\": 5}";
    t2 = "{\"type\":\"fixed\",\"name\":\"b\",\"aliases\":[\"a\"],"
      +"\"size\": 5}";
    s1 = Schema.parse(t1);
    s2 = Schema.parse(t2);
    s3 = Schema.applyAliases(s1,s2);
    assertFalse(s2 == s3);
    assertEquals(s2, s3);
  }

  private static void check(String schemaJson, String defaultJson,
                            Object defaultValue) throws Exception {
    check(schemaJson, defaultJson, defaultValue, true);
  }
  private static void check(String schemaJson, String defaultJson,
                            Object defaultValue, boolean induce)
    throws Exception {
    check(schemaJson, induce);
    checkDefault(schemaJson, defaultJson, defaultValue);
  }

  private static void check(String jsonSchema, boolean induce)
    throws Exception {
    Schema schema = Schema.parse(jsonSchema);
    checkProp(schema);
    Object reuse = null;
    for (Object datum : new RandomData(schema, COUNT)) {

      if (induce) {
        Schema induced = GenericData.get().induce(datum);
        assertEquals("Induced schema does not match.", schema, induced);
      }

      assertTrue("Datum does not validate against schema "+datum,
                 GenericData.get().validate(schema, datum));

      checkBinary(schema, datum,
                  new GenericDatumWriter<Object>(),
                  new GenericDatumReader<Object>(), null);
      reuse = checkBinary(schema, datum,
          new GenericDatumWriter<Object>(),
          new GenericDatumReader<Object>(), reuse);
      checkDirectBinary(schema, datum,
                  new GenericDatumWriter<Object>(),
                  new GenericDatumReader<Object>());
      checkBlockingBinary(schema, datum,
                  new GenericDatumWriter<Object>(),
                  new GenericDatumReader<Object>());
      checkJson(schema, datum,
                  new GenericDatumWriter<Object>(),
                  new GenericDatumReader<Object>());

      // Check that we can generate the code for every schema we see.
      TestSpecificCompiler.assertCompiles(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
    assertEquals(null, s0.getProp("foo"));
    Schema s1 = Schema.parse(s0.toString());
    s1.addProp("foo", "bar");
    assertEquals("bar", s1.getProp("foo"));
    assertFalse(s0.equals(s1));
    Schema s2 = Schema.parse(s1.toString());
    assertEquals("bar", s2.getProp("foo"));
    assertEquals(s1, s2);
    assertFalse(s0.equals(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("Decoded data does not match.", datum, decoded);
    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("Decoded data does not match.", datum, decoded);
  }

  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("Decoded data does not match.", datum, decoded);
  }

  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("Decoded data does not match.", datum, decoded);

    decoded = reader.read(decoded, decoder);
    assertEquals("Decoded data does not match.", datum, decoded);
  }

  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<Object>();
    writer.setSchema(schema);
    writer.write(datum, encoder);
    encoder.flush();
    byte[] data = out.toByteArray();

    String encoded = new String(data, "UTF-8");
    assertEquals("Encoded data does not match.", json, encoded);

    DatumReader<Object> reader = new GenericDatumReader<Object>();
    reader.setSchema(schema);
    Object decoded = reader.read(null, DecoderFactory.get()
        .jsonDecoder(schema, new ByteArrayInputStream(data)));

    assertEquals("Decoded data does not match.", datum, decoded);
  }

  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("Decoded json does not match.", Json.toString(node), Json.toString(decoded));
  }

  private static final Schema ACTUAL =            // an empty record schema
    Schema.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 = Schema.parse(recordJson);
    DatumReader<Object> in = new GenericDatumReader<Object>(ACTUAL, expected);
    GenericData.Record record = (GenericData.Record)
      in.read(null, DecoderFactory.get().binaryDecoder(
          new byte[0], null));
    assertEquals("Wrong default.", defaultValue, record.get("f"));
    assertEquals("Wrong toString", expected, Schema.parse(expected.toString()));
  }

  private static void checkValidateDefaults(String schemaJson, String defaultJson) {
    try {
      Schema.Parser parser = new Schema.Parser();
      parser.setValidateDefaults(true);
      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 e) {
      return;
    }
  }

  @Test(expected=AvroTypeException.class)
  public void testNoDefaultField() throws Exception {
    Schema expected =
      Schema.parse("{\"type\":\"record\", \"name\":\"Foo\", \"fields\":"+
                   "[{\"name\":\"f\", \"type\": \"string\"}]}");
    DatumReader<Object> in = new GenericDatumReader<Object>(ACTUAL, expected);
    in.read(null, DecoderFactory.get().binaryDecoder(
        new ByteArrayInputStream(new byte[0]), null));
  }

  @Test
  public void testEnumMismatch() throws Exception {
    Schema actual = Schema.parse
      ("{\"type\":\"enum\",\"name\":\"E\",\"symbols\":[\"X\",\"Y\"]}");
    Schema expected = Schema.parse
      ("{\"type\":\"enum\",\"name\":\"E\",\"symbols\":[\"Y\",\"Z\"]}");
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    DatumWriter<Object> writer = new GenericDatumWriter<Object>(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<String>(actual, expected);
    assertEquals("Wrong value", new GenericData.EnumSymbol(expected, "Y"),
                 in.read(null, decoder));
    try {
      in.read(null, decoder);
      fail("Should have thrown exception.");
    } catch (AvroTypeException e) {
      // expected
    }
  }

  @Test(expected=AvroTypeException.class)
  public void testRecordWithPrimitiveName() {
    Schema.parse("{\"type\":\"record\", \"name\":\"string\", \"fields\": []}");
  }

  @Test(expected=AvroTypeException.class)
  public void testEnumWithPrimitiveName() {
    Schema.parse("{\"type\":\"enum\", \"name\":\"null\", \"symbols\": [\"A\"]}");
  }

  private static Schema enumSchema() {
    return Schema.parse("{ \"type\": \"enum\", \"name\": \"e\", "
        + "\"symbols\": [\"a\", \"b\"]}");
  }

  @Test(expected=AvroRuntimeException.class)
  public void testImmutability1() {
    Schema s = enumSchema();
    s.addProp("p1", "1");
    s.addProp("p1", "2");
  }

  @Test(expected=AvroRuntimeException.class)
  public void testImmutability2() {
    Schema s = enumSchema();
    s.addProp("p1", (String)null);
  }

  private static List<String> lockedArrayList() {
    return new Schema.LockableArrayList<String>(Arrays.asList(new String[] {
        "a", "b", "c" })).lock();
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList1() {
    lockedArrayList().add("p");
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList2() {
    lockedArrayList().remove("a");
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList3() {
    lockedArrayList().addAll(Arrays.asList(new String[] { "p" }));
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList4() {
    lockedArrayList().addAll(0,
        Arrays.asList(new String[] { "p" }));
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList5() {
    lockedArrayList().
      removeAll(Arrays.asList(new String[] { "a" }));
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList6() {
    lockedArrayList().
      retainAll(Arrays.asList(new String[] { "a" }));
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList7() {
    lockedArrayList().clear();
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList8() {
    lockedArrayList().iterator().remove();
  }


  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList9() {
    Iterator<String> it = lockedArrayList().iterator();
    it.next();
    it.remove();
  }

  @Test(expected=IllegalStateException.class)
  public void testLockedArrayList10() {
    lockedArrayList().remove(1);
  }

  @Test
  public void testNames_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
  public void testNames_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
  public void testNames_GetNotFound() {
    Schema.Names names = new Schema.Names("space");
    names.put(new Schema.Name("Name", "otherspace"), Schema.create(Type.STRING));

    assertNull(names.get("Name"));
  }
}
