/*
 * 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 static org.junit.jupiter.api.Assertions.*;

import com.fasterxml.jackson.databind.node.NullNode;
import java.io.File;
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 java.util.Map;
import org.apache.avro.Schema.Field.Order;
import org.apache.avro.file.DataFileReader;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecordBuilder;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class TestSchemaBuilder {

  @TempDir
  public File DIR;

  @Test
  void record() {
    Schema schema = SchemaBuilder.record("myrecord").namespace("org.example").aliases("oldrecord").fields().name("f0")
        .aliases("f0alias").type().stringType().noDefault().name("f1").doc("This is f1").type().longType().noDefault()
        .name("f2").type().nullable().booleanType().booleanDefault(true).name("f3").type().unionOf().nullType().and()
        .booleanType().endUnion().nullDefault().endRecord();

    assertEquals("myrecord", schema.getName());
    assertEquals("org.example", schema.getNamespace());
    assertEquals("org.example.oldrecord", schema.getAliases().iterator().next());
    assertFalse(schema.isError());
    List<Schema.Field> fields = schema.getFields();
    assertEquals(4, fields.size());
    assertEquals(new Schema.Field("f0", Schema.create(Schema.Type.STRING)), fields.get(0));
    assertTrue(fields.get(0).aliases().contains("f0alias"));
    assertEquals(new Schema.Field("f1", Schema.create(Schema.Type.LONG), "This is f1"), fields.get(1));

    List<Schema> types = new ArrayList<>();
    types.add(Schema.create(Schema.Type.BOOLEAN));
    types.add(Schema.create(Schema.Type.NULL));
    Schema optional = Schema.createUnion(types);
    assertEquals(new Schema.Field("f2", optional, null, true), fields.get(2));

    List<Schema> types2 = new ArrayList<>();
    types2.add(Schema.create(Schema.Type.NULL));
    types2.add(Schema.create(Schema.Type.BOOLEAN));
    Schema optional2 = Schema.createUnion(types2);

    assertNotEquals(new Schema.Field("f3", optional2, null, (Object) null), fields.get(3));
    assertEquals(new Schema.Field("f3", optional2, null, Schema.Field.NULL_DEFAULT_VALUE), fields.get(3));
  }

  @Test
  void doc() {
    Schema s = SchemaBuilder.fixed("myfixed").doc("mydoc").size(1);
    assertEquals("mydoc", s.getDoc());
  }

  @Test
  void props() {
    Schema s = SchemaBuilder.builder().intBuilder().prop("p1", "v1").prop("p2", "v2").prop("p2", "v2real") // overwrite
        .endInt();
    int size = s.getObjectProps().size();
    assertEquals(2, size);
    assertEquals("v1", s.getProp("p1"));
    assertEquals("v2real", s.getProp("p2"));
  }

  @Test
  void objectProps() {
    Schema s = SchemaBuilder.builder().intBuilder().prop("booleanProp", true).prop("intProp", Integer.MAX_VALUE)
        .prop("longProp", Long.MAX_VALUE).prop("floatProp", 1.0f).prop("doubleProp", Double.MAX_VALUE)
        .prop("byteProp", new byte[] { 0x41, 0x42, 0x43 }).prop("stringProp", "abc").endInt();

    // object properties
    assertEquals(7, s.getObjectProps().size());
    assertTrue(s.getObjectProp("booleanProp") instanceof Boolean);
    assertEquals(true, s.getObjectProp("booleanProp"));
    assertTrue(s.getObjectProp("intProp") instanceof Integer);
    assertEquals(Integer.MAX_VALUE, s.getObjectProp("intProp"));
    assertTrue(s.getObjectProp("intProp") instanceof Integer);
    assertTrue(s.getObjectProp("longProp") instanceof Long);
    assertEquals(Long.MAX_VALUE, s.getObjectProp("longProp"));
    assertTrue(s.getObjectProp("floatProp") instanceof Double);
    // float converts to double
    assertEquals(1.0d, s.getObjectProp("floatProp"));
    assertTrue(s.getObjectProp("doubleProp") instanceof Double);
    assertEquals(Double.MAX_VALUE, s.getObjectProp("doubleProp"));
    // byte[] converts to string
    assertTrue(s.getObjectProp("byteProp") instanceof String);
    assertEquals("ABC", s.getObjectProp("byteProp"));
    assertTrue(s.getObjectProp("stringProp") instanceof String);
    assertEquals("abc", s.getObjectProp("stringProp"));
  }

  @Test
  void fieldObjectProps() {
    Schema s = SchemaBuilder.builder().record("MyRecord").fields().name("myField").prop("booleanProp", true)
        .prop("intProp", Integer.MAX_VALUE).prop("longProp", Long.MAX_VALUE).prop("floatProp", 1.0f)
        .prop("doubleProp", Double.MAX_VALUE).prop("byteProp", new byte[] { 0x41, 0x42, 0x43 })
        .prop("stringProp", "abc").type().intType().noDefault().endRecord();

    Schema.Field f = s.getField("myField");

    // object properties
    assertEquals(7, f.getObjectProps().size());
    assertTrue(f.getObjectProp("booleanProp") instanceof Boolean);
    assertEquals(true, f.getObjectProp("booleanProp"));
    assertTrue(f.getObjectProp("intProp") instanceof Integer);
    assertEquals(Integer.MAX_VALUE, f.getObjectProp("intProp"));
    assertTrue(f.getObjectProp("intProp") instanceof Integer);
    assertTrue(f.getObjectProp("longProp") instanceof Long);
    assertEquals(Long.MAX_VALUE, f.getObjectProp("longProp"));
    assertTrue(f.getObjectProp("floatProp") instanceof Double);
    // float converts to double
    assertEquals(1.0d, f.getObjectProp("floatProp"));
    assertTrue(f.getObjectProp("doubleProp") instanceof Double);
    assertEquals(Double.MAX_VALUE, f.getObjectProp("doubleProp"));
    // byte[] converts to string
    assertTrue(f.getObjectProp("byteProp") instanceof String);
    assertEquals("ABC", f.getObjectProp("byteProp"));
    assertTrue(f.getObjectProp("stringProp") instanceof String);
    assertEquals("abc", f.getObjectProp("stringProp"));

    assertEquals("abc", f.getObjectProp("stringProp", "default"));
    assertEquals("default", f.getObjectProp("unknwon", "default"));
  }

  @Test
  void arrayObjectProp() {
    List<Object> values = new ArrayList<>();
    values.add(true);
    values.add(Integer.MAX_VALUE);
    values.add(Long.MAX_VALUE);
    values.add(1.0f);
    values.add(Double.MAX_VALUE);
    values.add(new byte[] { 0x41, 0x42, 0x43 });
    values.add("abc");

    Schema s = SchemaBuilder.builder().intBuilder().prop("arrayProp", values).endInt();

    // object properties
    assertEquals(1, s.getObjectProps().size());

    assertTrue(s.getObjectProp("arrayProp") instanceof Collection);
    @SuppressWarnings("unchecked")
    Collection<Object> valueCollection = (Collection<Object>) s.getObjectProp("arrayProp");
    Iterator<Object> iter = valueCollection.iterator();
    assertEquals(7, valueCollection.size());
    assertEquals(true, iter.next());
    assertEquals(Integer.MAX_VALUE, iter.next());
    assertEquals(Long.MAX_VALUE, iter.next());
    // float converts to double
    assertEquals(1.0d, iter.next());
    assertEquals(Double.MAX_VALUE, iter.next());
    // byte[] converts to string
    assertEquals("ABC", iter.next());
    assertEquals("abc", iter.next());
  }

  @Test
  void fieldArrayObjectProp() {
    List<Object> values = new ArrayList<>();
    values.add(true);
    values.add(Integer.MAX_VALUE);
    values.add(Long.MAX_VALUE);
    values.add(1.0f);
    values.add(Double.MAX_VALUE);
    values.add(new byte[] { 0x41, 0x42, 0x43 });
    values.add("abc");

    Schema s = SchemaBuilder.builder().record("MyRecord").fields().name("myField").prop("arrayProp", values).type()
        .intType().noDefault().endRecord();

    Schema.Field f = s.getField("myField");

    // object properties
    assertEquals(1, f.getObjectProps().size());

    assertTrue(f.getObjectProp("arrayProp") instanceof Collection);
    @SuppressWarnings("unchecked")
    Collection<Object> valueCollection = (Collection<Object>) f.getObjectProp("arrayProp");
    Iterator<Object> iter = valueCollection.iterator();
    assertEquals(7, valueCollection.size());
    assertEquals(true, iter.next());
    assertEquals(Integer.MAX_VALUE, iter.next());
    assertEquals(Long.MAX_VALUE, iter.next());
    // float converts to double
    assertEquals(1.0d, iter.next());
    assertEquals(Double.MAX_VALUE, iter.next());
    // byte[] converts to string
    assertEquals("ABC", iter.next());
    assertEquals("abc", iter.next());
  }

  @Test
  void mapObjectProp() {
    Map<String, Object> values = new HashMap<>();
    values.put("booleanKey", true);
    values.put("intKey", Integer.MAX_VALUE);
    values.put("longKey", Long.MAX_VALUE);
    values.put("floatKey", 1.0f);
    values.put("doubleKey", Double.MAX_VALUE);
    values.put("byteKey", new byte[] { 0x41, 0x42, 0x43 });
    values.put("stringKey", "abc");

    Schema s = SchemaBuilder.builder().intBuilder().prop("mapProp", values).endInt();

    // object properties
    assertTrue(s.getObjectProp("mapProp") instanceof Map);
    @SuppressWarnings("unchecked")
    Map<String, Object> valueMap = (Map<String, Object>) s.getObjectProp("mapProp");
    assertEquals(values.size(), valueMap.size());

    assertTrue(valueMap.get("booleanKey") instanceof Boolean);
    assertEquals(true, valueMap.get("booleanKey"));
    assertTrue(valueMap.get("intKey") instanceof Integer);
    assertEquals(Integer.MAX_VALUE, valueMap.get("intKey"));
    assertTrue(valueMap.get("longKey") instanceof Long);
    assertEquals(Long.MAX_VALUE, valueMap.get("longKey"));
    // float converts to double
    assertTrue(valueMap.get("floatKey") instanceof Double);
    assertEquals(1.0d, valueMap.get("floatKey"));
    assertTrue(valueMap.get("doubleKey") instanceof Double);
    assertEquals(Double.MAX_VALUE, valueMap.get("doubleKey"));
    // byte[] converts to string
    assertTrue(valueMap.get("byteKey") instanceof String);
    assertEquals("ABC", valueMap.get("byteKey"));
    assertTrue(valueMap.get("stringKey") instanceof String);
    assertEquals("abc", valueMap.get("stringKey"));
  }

  @Test
  void fieldMapObjectProp() {
    Map<String, Object> values = new HashMap<>();
    values.put("booleanKey", true);
    values.put("intKey", Integer.MAX_VALUE);
    values.put("longKey", Long.MAX_VALUE);
    values.put("floatKey", 1.0f);
    values.put("doubleKey", Double.MAX_VALUE);
    values.put("byteKey", new byte[] { 0x41, 0x42, 0x43 });
    values.put("stringKey", "abc");

    Schema s = SchemaBuilder.builder().record("MyRecord").fields().name("myField").prop("mapProp", values).type()
        .intType().noDefault().endRecord();

    Schema.Field f = s.getField("myField");

    // object properties
    assertTrue(f.getObjectProp("mapProp") instanceof Map);
    @SuppressWarnings("unchecked")
    Map<String, Object> valueMap = (Map<String, Object>) f.getObjectProp("mapProp");
    assertEquals(values.size(), valueMap.size());

    assertTrue(valueMap.get("booleanKey") instanceof Boolean);
    assertEquals(true, valueMap.get("booleanKey"));
    assertTrue(valueMap.get("intKey") instanceof Integer);
    assertEquals(Integer.MAX_VALUE, valueMap.get("intKey"));
    assertTrue(valueMap.get("longKey") instanceof Long);
    assertEquals(Long.MAX_VALUE, valueMap.get("longKey"));
    // float converts to double
    assertTrue(valueMap.get("floatKey") instanceof Double);
    assertEquals(1.0d, valueMap.get("floatKey"));
    assertTrue(valueMap.get("doubleKey") instanceof Double);
    assertEquals(Double.MAX_VALUE, valueMap.get("doubleKey"));
    // byte[] converts to string
    assertTrue(valueMap.get("byteKey") instanceof String);
    assertEquals("ABC", valueMap.get("byteKey"));
    assertTrue(valueMap.get("stringKey") instanceof String);
    assertEquals("abc", valueMap.get("stringKey"));
  }

  @Test
  void nullObjectProp() {
    assertThrows(AvroRuntimeException.class, () -> {
      SchemaBuilder.builder().intBuilder().prop("nullProp", (Object) null).endInt();
    });
  }

  @Test
  void fieldNullObjectProp() {
    assertThrows(AvroRuntimeException.class, () -> {
      SchemaBuilder.builder().record("MyRecord").fields().name("myField").prop("nullProp", (Object) null).type()
          .intType().noDefault().endRecord();
    });
  }

  @Test
  void namespaces() {
    Schema s1 = SchemaBuilder.record("myrecord").namespace("org.example").fields().name("myint").type().intType()
        .noDefault().endRecord();
    Schema s2 = SchemaBuilder.record("org.example.myrecord").fields().name("myint").type().intType().noDefault()
        .endRecord();
    Schema s3 = SchemaBuilder.record("org.example.myrecord").namespace("org.example2").fields().name("myint").type()
        .intType().noDefault().endRecord();
    Schema s4 = SchemaBuilder.builder("org.example").record("myrecord").fields().name("myint").type().intType()
        .noDefault().endRecord();

    assertEquals("myrecord", s1.getName());
    assertEquals("myrecord", s2.getName());
    assertEquals("myrecord", s3.getName());
    assertEquals("myrecord", s4.getName());

    assertEquals("org.example", s1.getNamespace());
    assertEquals("org.example", s2.getNamespace());
    assertEquals("org.example", s3.getNamespace()); // namespace call is ignored
    assertEquals("org.example", s4.getNamespace());

    assertEquals("org.example.myrecord", s1.getFullName());
    assertEquals("org.example.myrecord", s2.getFullName());
    assertEquals("org.example.myrecord", s3.getFullName());
    assertEquals("org.example.myrecord", s4.getFullName());
  }

  @Test
  void missingRecordName() {
    assertThrows(NullPointerException.class, () -> {
      SchemaBuilder.record(null).fields() // null name
          .name("f0").type().stringType().noDefault().endRecord();
    });
  }

  @Test
  void testBoolean() {
    Schema.Type type = Schema.Type.BOOLEAN;
    Schema simple = SchemaBuilder.builder().booleanType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().booleanBuilder().prop("p", "v").endBoolean();
    assertEquals(expected, built1);
  }

  @Test
  void testInt() {
    Schema.Type type = Schema.Type.INT;
    Schema simple = SchemaBuilder.builder().intType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().intBuilder().prop("p", "v").endInt();
    assertEquals(expected, built1);
  }

  @Test
  void testLong() {
    Schema.Type type = Schema.Type.LONG;
    Schema simple = SchemaBuilder.builder().longType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().longBuilder().prop("p", "v").endLong();
    assertEquals(expected, built1);
  }

  @Test
  void testFloat() {
    Schema.Type type = Schema.Type.FLOAT;
    Schema simple = SchemaBuilder.builder().floatType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().floatBuilder().prop("p", "v").endFloat();
    assertEquals(expected, built1);
  }

  @Test
  void duble() {
    Schema.Type type = Schema.Type.DOUBLE;
    Schema simple = SchemaBuilder.builder().doubleType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().doubleBuilder().prop("p", "v").endDouble();
    assertEquals(expected, built1);
  }

  @Test
  void string() {
    Schema.Type type = Schema.Type.STRING;
    Schema simple = SchemaBuilder.builder().stringType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().stringBuilder().prop("p", "v").endString();
    assertEquals(expected, built1);
  }

  @Test
  void bytes() {
    Schema.Type type = Schema.Type.BYTES;
    Schema simple = SchemaBuilder.builder().bytesType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().bytesBuilder().prop("p", "v").endBytes();
    assertEquals(expected, built1);
  }

  @Test
  void testNull() {
    Schema.Type type = Schema.Type.NULL;
    Schema simple = SchemaBuilder.builder().nullType();
    Schema expected = primitive(type, simple);
    Schema built1 = SchemaBuilder.builder().nullBuilder().prop("p", "v").endNull();
    assertEquals(expected, built1);
  }

  private Schema primitive(Schema.Type type, Schema bare) {
    // test creation of bare schema by name
    Schema bareByName = SchemaBuilder.builder().type(type.getName());
    assertEquals(Schema.create(type), bareByName);
    assertEquals(bareByName, bare);
    // return a schema with custom prop set
    Schema p = Schema.create(type);
    p.addProp("p", "v");
    return p;
  }

//  @Test
//  public void testError() {
//    Schema schema = SchemaBuilder
//        .errorType("myerror")
//        .requiredString("message")
//        .build();
//
//    Assert.assertEquals("myerror", schema.getName());
//    Assert.assertTrue(schema.isError());
//  }

  @Test
  void recursiveRecord() {
    Schema schema = SchemaBuilder.record("LongList").fields().name("value").type().longType().noDefault().name("next")
        .type().optional().type("LongList").endRecord();

    assertEquals("LongList", schema.getName());
    List<Schema.Field> fields = schema.getFields();
    assertEquals(2, fields.size());
    assertEquals(new Schema.Field("value", Schema.create(Schema.Type.LONG), null), fields.get(0));

    assertEquals(Schema.Type.UNION, fields.get(1).schema().getType());

    assertEquals(Schema.Type.NULL, fields.get(1).schema().getTypes().get(0).getType());
    Schema recordSchema = fields.get(1).schema().getTypes().get(1);
    assertEquals(Schema.Type.RECORD, recordSchema.getType());
    assertEquals("LongList", recordSchema.getName());
    assertEquals(NullNode.getInstance(), fields.get(1).defaultValue());
  }

  @Test
  void testEnum() {
    List<String> symbols = Arrays.asList("a", "b");
    Schema expected = Schema.createEnum("myenum", null, null, symbols);
    expected.addProp("p", "v");
    Schema schema = SchemaBuilder.enumeration("myenum").prop("p", "v").symbols("a", "b");
    assertEquals(expected, schema);
  }

  @Test
  void enumWithDefault() {
    List<String> symbols = Arrays.asList("a", "b");
    String enumDefault = "a";
    Schema expected = Schema.createEnum("myenum", null, null, symbols, enumDefault);
    expected.addProp("p", "v");
    Schema schema = SchemaBuilder.enumeration("myenum").prop("p", "v").defaultSymbol(enumDefault).symbols("a", "b");
    assertEquals(expected, schema);
  }

  @Test
  void fixed() {
    Schema expected = Schema.createFixed("myfixed", null, null, 16);
    expected.addAlias("myOldFixed");
    Schema schema = SchemaBuilder.fixed("myfixed").aliases("myOldFixed").size(16);
    assertEquals(expected, schema);
  }

  @Test
  void array() {
    Schema longSchema = Schema.create(Schema.Type.LONG);
    Schema expected = Schema.createArray(longSchema);

    Schema schema1 = SchemaBuilder.array().items().longType();
    assertEquals(expected, schema1);

    Schema schema2 = SchemaBuilder.array().items(longSchema);
    assertEquals(expected, schema2);

    Schema schema3 = SchemaBuilder.array().prop("p", "v").items().type("long");
    expected.addProp("p", "v");
    assertEquals(expected, schema3);
  }

  @Test
  void map() {
    Schema intSchema = Schema.create(Schema.Type.INT);
    Schema expected = Schema.createMap(intSchema);

    Schema schema1 = SchemaBuilder.map().values().intType();
    assertEquals(expected, schema1);

    Schema schema2 = SchemaBuilder.map().values(intSchema);
    assertEquals(expected, schema2);

    Schema schema3 = SchemaBuilder.map().prop("p", "v").values().type("int");
    expected.addProp("p", "v");
    assertEquals(expected, schema3);
  }

  @Test
  void unionAndNullable() {
    List<Schema> types = new ArrayList<>();
    types.add(Schema.create(Schema.Type.LONG));
    types.add(Schema.create(Schema.Type.NULL));
    Schema expected = Schema.createUnion(types);

    Schema schema = SchemaBuilder.unionOf().longType().and().nullType().endUnion();
    assertEquals(expected, schema);

    schema = SchemaBuilder.nullable().longType();
    assertEquals(expected, schema);
  }

  @Test
  void fields() {
    Schema rec = SchemaBuilder.record("Rec").fields().name("documented").doc("documented").type().nullType().noDefault()
        .name("ascending").orderAscending().type().booleanType().noDefault().name("descending").orderDescending().type()
        .floatType().noDefault().name("ignored").orderIgnore().type().doubleType().noDefault().name("aliased")
        .aliases("anAlias").type().stringType().noDefault().endRecord();
    assertEquals("documented", rec.getField("documented").doc());
    assertEquals(Order.ASCENDING, rec.getField("ascending").order());
    assertEquals(Order.DESCENDING, rec.getField("descending").order());
    assertEquals(Order.IGNORE, rec.getField("ignored").order());
    assertTrue(rec.getField("aliased").aliases().contains("anAlias"));
  }

  @Test
  void fieldShortcuts() {
    Schema full = SchemaBuilder.record("Blah").fields().name("rbool").type().booleanType().noDefault().name("obool")
        .type().optional().booleanType().name("nbool").type().nullable().booleanType().booleanDefault(true).name("rint")
        .type().intType().noDefault().name("oint").type().optional().intType().name("nint").type().nullable().intType()
        .intDefault(1).name("rlong").type().longType().noDefault().name("olong").type().optional().longType()
        .name("nlong").type().nullable().longType().longDefault(2L).name("rfloat").type().floatType().noDefault()
        .name("ofloat").type().optional().floatType().name("nfloat").type().nullable().floatType().floatDefault(-1.1f)
        .name("rdouble").type().doubleType().noDefault().name("odouble").type().optional().doubleType().name("ndouble")
        .type().nullable().doubleType().doubleDefault(99.9d).name("rstring").type().stringType().noDefault()
        .name("ostring").type().optional().stringType().name("nstring").type().nullable().stringType()
        .stringDefault("def").name("rbytes").type().bytesType().noDefault().name("obytes").type().optional().bytesType()
        .name("nbytes").type().nullable().bytesType().bytesDefault(new byte[] { 1, 2, 3 }).endRecord();

    Schema shortcut = SchemaBuilder.record("Blah").fields().requiredBoolean("rbool").optionalBoolean("obool")
        .nullableBoolean("nbool", true).requiredInt("rint").optionalInt("oint").nullableInt("nint", 1)
        .requiredLong("rlong").optionalLong("olong").nullableLong("nlong", 2L).requiredFloat("rfloat")
        .optionalFloat("ofloat").nullableFloat("nfloat", -1.1f).requiredDouble("rdouble").optionalDouble("odouble")
        .nullableDouble("ndouble", 99.9d).requiredString("rstring").optionalString("ostring")
        .nullableString("nstring", "def").requiredBytes("rbytes").optionalBytes("obytes")
        .nullableBytes("nbytes", new byte[] { 1, 2, 3 }).endRecord();

    assertEquals(full, shortcut);
  }

  @Test
  void names() {
    // no contextual namespace
    Schema r = SchemaBuilder.record("Rec").fields().name("f0").type().fixed("org.foo.MyFixed").size(1).noDefault()
        .name("f1").type("org.foo.MyFixed").noDefault().name("f2").type("org.foo.MyFixed", "").noDefault().name("f3")
        .type("org.foo.MyFixed", null).noDefault().name("f4").type("org.foo.MyFixed", "ignorethis").noDefault()
        .name("f5").type("MyFixed", "org.foo").noDefault().endRecord();
    Schema expected = Schema.createFixed("org.foo.MyFixed", null, null, 1);
    checkField(r, expected, "f0");
    checkField(r, expected, "f1");
    checkField(r, expected, "f2");
    checkField(r, expected, "f3");
    checkField(r, expected, "f4");
    checkField(r, expected, "f5");

    // context namespace
    Schema f = SchemaBuilder.builder("").fixed("Foo").size(1);
    assertEquals(Schema.createFixed("Foo", null, null, 1), f);

    // context namespace from record matches
    r = SchemaBuilder.record("Rec").namespace("org.foo").fields().name("f0").type().fixed("MyFixed").size(1).noDefault()
        .name("f1").type("org.foo.MyFixed").noDefault().name("f2").type("org.foo.MyFixed", "").noDefault().name("f3")
        .type("org.foo.MyFixed", null).noDefault().name("f4").type("org.foo.MyFixed", "ignorethis").noDefault()
        .name("f5").type("MyFixed", "org.foo").noDefault().name("f6").type("MyFixed", null).noDefault().name("f7")
        .type("MyFixed").noDefault().endRecord();
    checkField(r, expected, "f0");
    checkField(r, expected, "f1");
    checkField(r, expected, "f2");
    checkField(r, expected, "f3");
    checkField(r, expected, "f4");
    checkField(r, expected, "f5");
    checkField(r, expected, "f6");
    checkField(r, expected, "f7");

    // context namespace from record does not match
    r = SchemaBuilder.record("Rec").namespace("org.rec").fields().name("f0").type().fixed("MyFixed")
        .namespace("org.foo").size(1).noDefault().name("f1").type("org.foo.MyFixed").noDefault().name("f2")
        .type("org.foo.MyFixed", "").noDefault().name("f3").type("org.foo.MyFixed", null).noDefault().name("f4")
        .type("org.foo.MyFixed", "ignorethis").noDefault().name("f5").type("MyFixed", "org.foo").noDefault()
        .endRecord();
    checkField(r, expected, "f0");
    checkField(r, expected, "f1");
    checkField(r, expected, "f2");
    checkField(r, expected, "f3");
    checkField(r, expected, "f4");
    checkField(r, expected, "f5");

    // context namespace from record, nested has no namespace
    expected = Schema.createFixed("MyFixed", null, null, 1);
    r = SchemaBuilder.record("Rec").namespace("org.rec").fields().name("f0").type().fixed("MyFixed").namespace("")
        .size(1).noDefault().name("f1").type("MyFixed", "").noDefault().endRecord();
    checkField(r, expected, "f0");
    checkField(r, expected, "f1");

    // mimic names of primitives, but with a namesapce. This is OK
    SchemaBuilder.fixed("org.test.long").size(1);
    SchemaBuilder.fixed("long").namespace("org.test").size(1);
    SchemaBuilder.builder("org.test").fixed("long").size(1);

  }

  private void checkField(Schema r, Schema expected, String name) {
    assertEquals(expected, r.getField(name).schema());
  }

  @Test
  void namesFailRedefined() {
    assertThrows(SchemaParseException.class, () -> {
      SchemaBuilder.record("Rec").fields().name("f0").type().enumeration("MyEnum").symbols("A", "B").enumDefault("A")
          .name("f1").type().enumeration("MyEnum").symbols("X", "Y").noDefault().endRecord();
    });
  }

  @Test
  void namesFailAbsent() {
    assertThrows(SchemaParseException.class, () -> {
      SchemaBuilder.builder().type("notdefined");
    });
  }

  @Test
  void nameReserved() {
    assertThrows(AvroTypeException.class, () -> {
      SchemaBuilder.fixed("long").namespace("").size(1);
    });
  }

  @Test
  void fieldTypesAndDefaultValues() {
    byte[] bytedef = new byte[] { 3 };
    ByteBuffer bufdef = ByteBuffer.wrap(bytedef);
    String strdef = "\u0003";
    HashMap<String, String> mapdef = new HashMap<>();
    mapdef.put("a", "A");
    List<String> arrdef = Collections.singletonList("arr");

    Schema rec = SchemaBuilder.record("inner").fields().name("f").type().intType().noDefault().endRecord();

    Schema rec2 = SchemaBuilder.record("inner2").fields().name("f2").type().intType().noDefault().endRecord();

    GenericData.Record recdef = new GenericRecordBuilder(rec).set("f", 1).build();

    GenericData.Record recdef2 = new GenericRecordBuilder(rec2).set("f2", 2).build();

    Schema r = SchemaBuilder.record("r").fields().name("boolF").type().booleanType().booleanDefault(false).name("intF")
        .type().intType().intDefault(1).name("longF").type().longType().longDefault(2L).name("floatF").type()
        .floatType().floatDefault(3.0f).name("doubleF").type().doubleType().doubleDefault(4.0d).name("stringF").type()
        .stringType().stringDefault("def").name("bytesF1").type().bytesType().bytesDefault(bytedef).name("bytesF2")
        .type().bytesType().bytesDefault(bufdef).name("bytesF3").type().bytesType().bytesDefault(strdef).name("nullF")
        .type().nullType().nullDefault().name("fixedF1").type().fixed("F1").size(1).fixedDefault(bytedef)
        .name("fixedF2").type().fixed("F2").size(1).fixedDefault(bufdef).name("fixedF3").type().fixed("F3").size(1)
        .fixedDefault(strdef).name("enumF").type().enumeration("E1").symbols("S").enumDefault("S").name("mapF").type()
        .map().values().stringType().mapDefault(mapdef).name("arrayF").type().array().items().stringType()
        .arrayDefault(arrdef).name("recordF").type().record("inner").fields().name("f").type().intType().noDefault()
        .endRecord().recordDefault(recdef).name("byName").type("E1").withDefault("S")
        // union builders, one for each 'first type' in a union:
        .name("boolU").type().unionOf().booleanType().and().intType().endUnion().booleanDefault(false).name("intU")
        .type().unionOf().intType().and().longType().endUnion().intDefault(1).name("longU").type().unionOf().longType()
        .and().intType().endUnion().longDefault(2L).name("floatU").type().unionOf().floatType().and().intType()
        .endUnion().floatDefault(3.0f).name("doubleU").type().unionOf().doubleType().and().intType().endUnion()
        .doubleDefault(4.0d).name("stringU").type().unionOf().stringType().and().intType().endUnion()
        .stringDefault("def").name("bytesU").type().unionOf().bytesType().and().intType().endUnion()
        .bytesDefault(bytedef).name("nullU").type().unionOf().nullType().and().intType().endUnion().nullDefault()
        .name("fixedU").type().unionOf().fixed("F4").size(1).and().intType().endUnion().fixedDefault(bytedef)
        .name("enumU").type().unionOf().enumeration("E2").symbols("SS").and().intType().endUnion().enumDefault("SS")
        .name("mapU").type().unionOf().map().values().stringType().and().intType().endUnion().mapDefault(mapdef)
        .name("arrayU").type().unionOf().array().items().stringType().and().intType().endUnion().arrayDefault(arrdef)
        .name("recordU").type().unionOf().record("inner2").fields().name("f2").type().intType().noDefault().endRecord()
        .and().intType().endUnion().recordDefault(recdef2).endRecord();

    GenericData.Record newRec = new GenericRecordBuilder(r).build();

    assertEquals(false, newRec.get("boolF"));
    assertEquals(false, newRec.get("boolU"));
    assertEquals(1, newRec.get("intF"));
    assertEquals(1, newRec.get("intU"));
    assertEquals(2L, newRec.get("longF"));
    assertEquals(2L, newRec.get("longU"));
    assertEquals(3f, newRec.get("floatF"));
    assertEquals(3f, newRec.get("floatU"));
    assertEquals(4d, newRec.get("doubleF"));
    assertEquals(4d, newRec.get("doubleU"));
    assertEquals("def", newRec.get("stringF").toString());
    assertEquals("def", newRec.get("stringU").toString());
    assertEquals(bufdef, newRec.get("bytesF1"));
    assertEquals(bufdef, newRec.get("bytesF2"));
    assertEquals(bufdef, newRec.get("bytesF3"));
    assertEquals(bufdef, newRec.get("bytesU"));
    assertNull(newRec.get("nullF"));
    assertNull(newRec.get("nullU"));
    assertArrayEquals(bytedef, ((GenericData.Fixed) newRec.get("fixedF1")).bytes());
    assertArrayEquals(bytedef, ((GenericData.Fixed) newRec.get("fixedF2")).bytes());
    assertArrayEquals(bytedef, ((GenericData.Fixed) newRec.get("fixedF3")).bytes());
    assertArrayEquals(bytedef, ((GenericData.Fixed) newRec.get("fixedU")).bytes());
    assertEquals("S", newRec.get("enumF").toString());
    assertEquals("SS", newRec.get("enumU").toString());
    @SuppressWarnings("unchecked")
    Map<CharSequence, CharSequence> map = (Map<CharSequence, CharSequence>) newRec.get("mapF");
    assertEquals(mapdef.size(), map.size());
    for (Map.Entry<CharSequence, CharSequence> e : map.entrySet()) {
      assertEquals(mapdef.get(e.getKey().toString()), e.getValue().toString());
    }
    assertEquals(newRec.get("mapF"), newRec.get("mapU"));
    @SuppressWarnings("unchecked")
    GenericData.Array<CharSequence> arr = (GenericData.Array<CharSequence>) newRec.get("arrayF");
    assertEquals(arrdef.size(), arr.size());
    for (CharSequence c : arr) {
      assertTrue(arrdef.contains(c.toString()));
    }
    assertEquals(newRec.get("arrayF"), newRec.get("arrayU"));
    assertEquals(recdef, newRec.get("recordF"));
    assertEquals(recdef2, newRec.get("recordU"));
    assertEquals("S", newRec.get("byName").toString());
  }

  @Test
  void badDefault() {
    assertThrows(SchemaBuilderException.class, () -> {
      SchemaBuilder.record("r").fields().name("f").type(Schema.create(Schema.Type.INT)).withDefault(new Object())
          .endRecord();
    });
  }

  @Test
  void unionFieldBuild() {
    SchemaBuilder.record("r").fields().name("allUnion").type().unionOf().booleanType().and().intType().and().longType()
        .and().floatType().and().doubleType().and().stringType().and().bytesType().and().nullType().and().fixed("Fix")
        .size(1).and().enumeration("Enu").symbols("Q").and().array().items().intType().and().map().values().longType()
        .and().record("Rec").fields().name("one").type("Fix").noDefault().endRecord().endUnion().booleanDefault(false)
        .endRecord();
  }

  @Test
  void defaults() throws IOException {
    Schema writeSchema = SchemaBuilder.record("r").fields().name("requiredInt").type().intType().noDefault()
        .name("optionalInt").type().optional().intType().name("nullableIntWithDefault").type().nullable().intType()
        .intDefault(3).endRecord();

    GenericData.Record rec1 = new GenericRecordBuilder(writeSchema).set("requiredInt", 1).build();

    assertEquals(1, rec1.get("requiredInt"));
    assertNull(rec1.get("optionalInt"));
    assertEquals(3, rec1.get("nullableIntWithDefault"));

    GenericData.Record rec2 = new GenericRecordBuilder(writeSchema).set("requiredInt", 1).set("optionalInt", 2)
        .set("nullableIntWithDefault", 13).build();

    assertEquals(1, rec2.get("requiredInt"));
    assertEquals(2, rec2.get("optionalInt"));
    assertEquals(13, rec2.get("nullableIntWithDefault"));

    // write to file

    File file = new File(DIR.getPath(), "testDefaults.avro");

    try (DataFileWriter<Object> writer = new DataFileWriter<>(new GenericDatumWriter<>())) {
      writer.create(writeSchema, file);
      writer.append(rec1);
      writer.append(rec2);
    }

    Schema readSchema = SchemaBuilder.record("r").fields().name("requiredInt").type().intType().noDefault()
        .name("optionalInt").type().optional().intType().name("nullableIntWithDefault").type().nullable().intType()
        .intDefault(3).name("newOptionalInt").type().optional().intType().name("newNullableIntWithDefault").type()
        .nullable().intType().intDefault(5).endRecord();

    try (DataFileReader<GenericData.Record> reader = new DataFileReader<>(file,
        new GenericDatumReader<>(writeSchema, readSchema))) {

      GenericData.Record rec1read = reader.iterator().next();
      assertEquals(1, rec1read.get("requiredInt"));
      assertNull(rec1read.get("optionalInt"));
      assertEquals(3, rec1read.get("nullableIntWithDefault"));
      assertNull(rec1read.get("newOptionalInt"));
      assertEquals(5, rec1read.get("newNullableIntWithDefault"));

      GenericData.Record rec2read = reader.iterator().next();
      assertEquals(1, rec2read.get("requiredInt"));
      assertEquals(2, rec2read.get("optionalInt"));
      assertEquals(13, rec2read.get("nullableIntWithDefault"));
      assertNull(rec2read.get("newOptionalInt"));
      assertEquals(5, rec2read.get("newNullableIntWithDefault"));
    }

  }

  @Test
  void defaultTypes() {
    Integer intDef = 1;
    Long longDef = 2L;
    Float floatDef = 3F;
    Double doubleDef = 4D;
    Schema schema = SchemaBuilder.record("r").fields().name("int").type().intType().intDefault(intDef).name("long")
        .type().longType().longDefault(longDef).name("float").type().floatType().floatDefault(floatDef).name("double")
        .type().doubleType().doubleDefault(doubleDef).endRecord();

    assertEquals(intDef, schema.getField("int").defaultVal(), "int field default type or value mismatch");
    assertEquals(longDef, schema.getField("long").defaultVal(), "long field default type or value mismatch");
    assertEquals(floatDef, schema.getField("float").defaultVal(), "float field default type or value mismatch");
    assertEquals(doubleDef, schema.getField("double").defaultVal(), "double field default type or value mismatch");
  }

  @Test
  void validateDefaultsEnabled() {
    assertThrows(AvroRuntimeException.class, () -> {
      try {
        SchemaBuilder.record("ValidationRecord").fields().name("IntegerField").type("int").withDefault("Invalid")
            .endRecord();
      } catch (AvroRuntimeException e) {
        assertEquals("Invalid default for field IntegerField: \"Invalid\" not a \"int\"", e.getMessage(),
            "Default behavior is to raise an exception due to record having an invalid default");
        throw e;
      }
    });
  }

  @Test
  void validateDefaultsDisabled() {
    final String fieldName = "IntegerField";
    final String defaultValue = "foo";
    Schema schema = SchemaBuilder.record("ValidationRecord").fields().name(fieldName).notValidatingDefaults()
        .type("int").withDefault(defaultValue) // Would throw an exception on endRecord() if validations enabled
        .endRecord();
    assertNull(schema.getField(fieldName).defaultVal(), "Differing types, so this returns null");
    assertEquals(defaultValue, schema.getField(fieldName).defaultValue().asText(),
        "Schema is able to be successfully created as is without validation");
  }

  /**
   * https://issues.apache.org/jira/browse/AVRO-1965
   */
  @Test
  void namespaceDefaulting() {
    Schema d = SchemaBuilder.builder().intType();
    Schema c = SchemaBuilder.record("c").fields().name("d").type(d).noDefault().endRecord();
    Schema b = SchemaBuilder.record("b").fields().name("c").type(c).noDefault().endRecord();

    Schema a1 = SchemaBuilder.record("default.a").fields().name("b").type(b).noDefault().endRecord();
    Schema a2 = SchemaParser.parseSingle(a1.toString());

    assertEquals(a2, a1);
  }

  @Test
  void namesAcceptAll() throws InterruptedException {
    // Ensure that Schema.setNameValidator won't interfere with others unit tests.
    Runnable r = () -> {
      Schema.setNameValidator(NameValidator.NO_VALIDATION);
      final Schema schema = SchemaBuilder.record("7name").fields().name("123").type(Schema.create(Schema.Type.INT))
          .noDefault().endRecord();
      Assertions.assertNotNull(schema);
      Assertions.assertEquals("7name", schema.getName());
      final Schema.Field field = schema.getField("123");
      Assertions.assertEquals("123", field.name());
    };

    final Throwable[] exception = new Throwable[] { null };
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((Thread th, Throwable e) -> exception[0] = e);
    t.start();
    t.join();
    Assertions.assertNull(exception[0], () -> exception[0].getMessage());
  }
}
