/*
 * 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.compiler.specific;

import static org.junit.jupiter.api.Assertions.*;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;

import org.apache.avro.Protocol;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Field;
import org.apache.avro.Schema.Type;
import org.apache.avro.SchemaParser;
import org.apache.avro.TestProtocolParsing;
import org.apache.avro.TestSchema;
import org.apache.avro.TestAnnotation;
import org.apache.avro.generic.GenericData.StringType;

import org.apache.avro.test.Simple;
import org.apache.avro.test.TestRecord;
import org.apache.avro.test.MD5;
import org.apache.avro.test.Kind;

import org.apache.avro.compiler.specific.SpecificCompiler.OutputFile;
import org.junit.Rule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.io.TempDir;
import org.junit.rules.TestName;

public class TestSpecificCompiler {

  @TempDir
  public File INPUT_DIR;

  @TempDir
  public File OUTPUT_DIR;

  static final String PROTOCOL = "" + "{ \"protocol\": \"default\",\n" + "  \"types\":\n" + "    [\n" + "      {\n"
      + "       \"name\": \"finally\",\n" + "       \"type\": \"error\",\n"
      + "       \"fields\": [{\"name\": \"catch\", \"type\": \"boolean\"}]\n" + "      }\n" + "    ],\n"
      + "  \"messages\": { \"goto\":\n" + "    { \"request\": [{\"name\": \"break\", \"type\": \"string\"}],\n"
      + "      \"response\": \"string\",\n" + "      \"errors\": [\"finally\"]\n" + "    }" + "   }\n" + "}\n";

  @Test
  void esc() {
    assertEquals("\\\"", SpecificCompiler.escapeForJavaString("\""));
  }

  @Test
  void makePath() {
    SpecificCompiler compiler = new SpecificCompiler();
    assertEquals("foo/bar/Baz.java".replace("/", File.separator), compiler.makePath("Baz", "foo.bar"));
    assertEquals("baz.java", compiler.makePath("baz", ""));
  }

  @Test
  void primitiveSchemaGeneratesNothing() {
    assertEquals(0, new SpecificCompiler(SchemaParser.parseSingle("\"double\"")).compile().size());
  }

  @Test
  void simpleEnumSchema(TestInfo testInfo) throws IOException {
    Collection<OutputFile> outputs = new SpecificCompiler(SchemaParser.parseSingle(TestSchema.BASIC_ENUM_SCHEMA))
        .compile();
    assertEquals(1, outputs.size());
    OutputFile o = outputs.iterator().next();
    assertEquals(o.path, "Test.java");
    assertTrue(o.contents.contains("public enum Test"));
    assertCompilesWithJavaCompiler(new File(INPUT_DIR, testInfo.getTestMethod().get().getName()), outputs);
  }

  @Test
  void mangleIfReserved() {
    assertEquals("foo", SpecificCompiler.mangle("foo"));
    assertEquals("goto$", SpecificCompiler.mangle("goto"));
  }

  @Test
  void manglingForProtocols(TestInfo testInfo) throws IOException {
    Collection<OutputFile> outputs = new SpecificCompiler(Protocol.parse(PROTOCOL)).compile();
    Iterator<OutputFile> i = outputs.iterator();
    String errType = i.next().contents;
    String protocol = i.next().contents;

    assertTrue(errType.contains("public class finally$ extends org.apache.avro.specific.SpecificExceptionBase"));
    assertTrue(errType.contains("private boolean catch$;"));

    assertTrue(protocol.contains("java.lang.CharSequence goto$(java.lang.CharSequence break$)"));
    assertTrue(protocol.contains("public interface default$"));
    assertTrue(protocol.contains(" finally$"));

    assertCompilesWithJavaCompiler(new File(INPUT_DIR, testInfo.getTestMethod().get().getName()), outputs);

  }

  private static String SCHEMA = "{ \"name\": \"volatile\", \"type\": \"record\", "
      + "  \"fields\": [ {\"name\": \"package\", \"type\": \"string\" },"
      + "                {\"name\": \"data\", \"type\": \"int\" },"
      + "                {\"name\": \"value\", \"type\": \"int\" },"
      + "                {\"name\": \"defaultValue\", \"type\": \"int\" },"
      + "                {\"name\": \"other\", \"type\": \"int\" },"
      + "                {\"name\": \"short\", \"type\": \"volatile\" } ] }";

  @Test
  void manglingForRecords(TestInfo testInfo) throws IOException {
    Collection<OutputFile> outputs = new SpecificCompiler(SchemaParser.parseSingle(SCHEMA)).compile();
    assertEquals(1, outputs.size());
    String contents = outputs.iterator().next().contents;

    assertTrue(contents.contains("private java.lang.CharSequence package$;"));
    assertTrue(contents.contains("class volatile$ extends"));
    assertTrue(contents.contains("volatile$ short$;"));

    assertCompilesWithJavaCompiler(new File(INPUT_DIR, testInfo.getTestMethod().get().getName()), outputs);
  }

  @Test
  void manglingForEnums(TestInfo testInfo) throws IOException {
    String enumSchema = "" + "{ \"name\": \"instanceof\", \"type\": \"enum\","
        + "  \"symbols\": [\"new\", \"super\", \"switch\"] }";
    Collection<OutputFile> outputs = new SpecificCompiler(SchemaParser.parseSingle(enumSchema)).compile();
    assertEquals(1, outputs.size());
    String contents = outputs.iterator().next().contents;

    assertTrue(contents.contains("new$"));

    assertCompilesWithJavaCompiler(new File(INPUT_DIR, testInfo.getTestMethod().get().getName()), outputs);
  }

  @Test
  void schemaSplit(TestInfo testInfo) throws IOException {
    SpecificCompiler compiler = new SpecificCompiler(SchemaParser.parseSingle(SCHEMA));
    compiler.maxStringChars = 10;
    Collection<OutputFile> files = compiler.compile();
    assertCompilesWithJavaCompiler(new File(INPUT_DIR, testInfo.getTestMethod().get().getName()), files);
  }

  @Test
  void protocolSplit(TestInfo testInfo) throws IOException {
    SpecificCompiler compiler = new SpecificCompiler(Protocol.parse(PROTOCOL));
    compiler.maxStringChars = 10;
    Collection<OutputFile> files = compiler.compile();
    assertCompilesWithJavaCompiler(new File(INPUT_DIR, testInfo.getTestMethod().get().getName()), files);
  }

  @Test
  void schemaWithDocs() {
    Collection<OutputFile> outputs = new SpecificCompiler(SchemaParser.parseSingle(TestSchema.SCHEMA_WITH_DOC_TAGS))
        .compile();
    assertEquals(3, outputs.size());
    int count = 0;
    for (OutputFile o : outputs) {
      if (o.path.endsWith("outer_record.java")) {
        count++;
        assertTrue(o.contents.contains("/** This is not a world record. */"));
        assertTrue(o.contents.contains("/** Inner Fixed */"));
        assertTrue(o.contents.contains("/** Inner Enum */"));
        assertTrue(o.contents.contains("/** Inner String */"));
      }
      if (o.path.endsWith("very_inner_fixed.java")) {
        count++;
        assertTrue(o.contents.contains("/** Very Inner Fixed */"));
        assertTrue(o.contents.contains("@org.apache.avro.specific.FixedSize(1)"));
      }
      if (o.path.endsWith("very_inner_enum.java")) {
        count++;
        assertTrue(o.contents.contains("/** Very Inner Enum */"));
      }
    }

    assertEquals(3, count);
  }

  @Test
  void protocolWithDocs() throws IOException {
    Protocol protocol = TestProtocolParsing.getSimpleProtocol();
    Collection<OutputFile> out = new SpecificCompiler(protocol).compile();
    assertEquals(6, out.size());
    int count = 0;
    for (OutputFile o : out) {
      if (o.path.endsWith("Simple.java")) {
        count++;
        assertTrue(o.contents.contains("/** Protocol used for testing. */"));
        assertTrue(o.contents.contains("* Send a greeting"));
      }
    }
    assertEquals(1, count, "Missed generated protocol!");
  }

  @Test
  void needCompile() throws IOException, InterruptedException {
    String schema = "" + "{ \"name\": \"Foo\", \"type\": \"record\", "
        + "  \"fields\": [ {\"name\": \"package\", \"type\": \"string\" },"
        + "                {\"name\": \"short\", \"type\": \"Foo\" } ] }";
    File inputFile = new File(INPUT_DIR.getPath(), "input.avsc");
    try (FileWriter fw = new FileWriter(inputFile)) {
      fw.write(schema);
    }

    File outputDir = OUTPUT_DIR;

    File outputFile = new File(outputDir, "Foo.java");
    outputFile.delete();
    assertFalse(outputFile.exists());
    outputDir.delete();
    assertFalse(outputDir.exists());
    SpecificCompiler.compileSchema(inputFile, outputDir);
    assertTrue(outputDir.exists());
    assertTrue(outputFile.exists());

    long lastModified = outputFile.lastModified();
    Thread.sleep(1000); // granularity of JVM doesn't seem to go below 1 sec
    SpecificCompiler.compileSchema(inputFile, outputDir);
    assertEquals(lastModified, outputFile.lastModified());

    try (FileWriter fw = new FileWriter(inputFile)) {
      fw.write(schema);
    }
    SpecificCompiler.compileSchema(inputFile, outputDir);
    assertTrue(lastModified != outputFile.lastModified());
  }

  /**
   * Creates a record with the given name, error status, and fields.
   *
   * @param name    the name of the schema.
   * @param isError true if the schema represents an error; false otherwise.
   * @param fields  the field(s) to add to the schema.
   * @return the schema.
   */
  private Schema createRecord(String name, boolean isError, Field... fields) {
    Schema record = Schema.createRecord(name, null, null, isError);
    record.setFields(Arrays.asList(fields));
    return record;
  }

  @Test
  void generateGetMethod() {
    Field height = new Field("height", Schema.create(Type.INT), null, null);
    Field Height = new Field("Height", Schema.create(Type.INT), null, null);
    Field height_and_width = new Field("height_and_width", Schema.create(Type.STRING), null, null);
    Field message = new Field("message", Schema.create(Type.STRING), null, null);
    Field Message = new Field("Message", Schema.create(Type.STRING), null, null);
    Field cause = new Field("cause", Schema.create(Type.STRING), null, null);
    Field clasz = new Field("class", Schema.create(Type.STRING), null, null);
    Field schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Field Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);

    assertEquals("getHeight", SpecificCompiler.generateGetMethod(createRecord("test", false, height), height));

    assertEquals("getHeightAndWidth",
        SpecificCompiler.generateGetMethod(createRecord("test", false, height_and_width), height_and_width));

    assertEquals("getMessage", SpecificCompiler.generateGetMethod(createRecord("test", false, message), message));
    message = new Field("message", Schema.create(Type.STRING), null, null);
    assertEquals("getMessage$", SpecificCompiler.generateGetMethod(createRecord("test", true, message), message));

    assertEquals("getCause", SpecificCompiler.generateGetMethod(createRecord("test", false, cause), cause));
    cause = new Field("cause", Schema.create(Type.STRING), null, null);
    assertEquals("getCause$", SpecificCompiler.generateGetMethod(createRecord("test", true, cause), cause));

    assertEquals("getClass$", SpecificCompiler.generateGetMethod(createRecord("test", false, clasz), clasz));
    clasz = new Field("class", Schema.create(Type.STRING), null, null);
    assertEquals("getClass$", SpecificCompiler.generateGetMethod(createRecord("test", true, clasz), clasz));

    assertEquals("getSchema$", SpecificCompiler.generateGetMethod(createRecord("test", false, schema), schema));
    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    assertEquals("getSchema$", SpecificCompiler.generateGetMethod(createRecord("test", true, schema), schema));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("getHeight", SpecificCompiler.generateGetMethod(createRecord("test", false, Height), Height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("getHeight", SpecificCompiler.generateGetMethod(createRecord("test", false, height, Height), height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("getHeight$0",
        SpecificCompiler.generateGetMethod(createRecord("test", false, height, Height), Height));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("getMessage$", SpecificCompiler.generateGetMethod(createRecord("test", true, Message), Message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("getMessage$",
        SpecificCompiler.generateGetMethod(createRecord("test", true, message, Message), message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("getMessage$0",
        SpecificCompiler.generateGetMethod(createRecord("test", true, message, Message), Message));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("getSchema$", SpecificCompiler.generateGetMethod(createRecord("test", false, Schema$), Schema$));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("getSchema$",
        SpecificCompiler.generateGetMethod(createRecord("test", false, schema, Schema$), schema));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("getSchema$0",
        SpecificCompiler.generateGetMethod(createRecord("test", false, schema, Schema$), Schema$));
  }

  @Test
  void generateSetMethod() {
    Field height = new Field("height", Schema.create(Type.INT), null, null);
    Field Height = new Field("Height", Schema.create(Type.INT), null, null);
    Field height_and_width = new Field("height_and_width", Schema.create(Type.STRING), null, null);
    Field message = new Field("message", Schema.create(Type.STRING), null, null);
    Field Message = new Field("Message", Schema.create(Type.STRING), null, null);
    Field cause = new Field("cause", Schema.create(Type.STRING), null, null);
    Field clasz = new Field("class", Schema.create(Type.STRING), null, null);
    Field schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Field Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);

    assertEquals("setHeight", SpecificCompiler.generateSetMethod(createRecord("test", false, height), height));

    assertEquals("setHeightAndWidth",
        SpecificCompiler.generateSetMethod(createRecord("test", false, height_and_width), height_and_width));

    assertEquals("setMessage", SpecificCompiler.generateSetMethod(createRecord("test", false, message), message));
    message = new Field("message", Schema.create(Type.STRING), null, null);
    assertEquals("setMessage$", SpecificCompiler.generateSetMethod(createRecord("test", true, message), message));

    assertEquals("setCause", SpecificCompiler.generateSetMethod(createRecord("test", false, cause), cause));
    cause = new Field("cause", Schema.create(Type.STRING), null, null);
    assertEquals("setCause$", SpecificCompiler.generateSetMethod(createRecord("test", true, cause), cause));

    assertEquals("setClass$", SpecificCompiler.generateSetMethod(createRecord("test", false, clasz), clasz));
    clasz = new Field("class", Schema.create(Type.STRING), null, null);
    assertEquals("setClass$", SpecificCompiler.generateSetMethod(createRecord("test", true, clasz), clasz));

    assertEquals("setSchema$", SpecificCompiler.generateSetMethod(createRecord("test", false, schema), schema));
    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    assertEquals("setSchema$", SpecificCompiler.generateSetMethod(createRecord("test", true, schema), schema));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("setHeight", SpecificCompiler.generateSetMethod(createRecord("test", false, Height), Height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("setHeight", SpecificCompiler.generateSetMethod(createRecord("test", false, height, Height), height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("setHeight$0",
        SpecificCompiler.generateSetMethod(createRecord("test", false, height, Height), Height));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("setMessage$", SpecificCompiler.generateSetMethod(createRecord("test", true, Message), Message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("setMessage$",
        SpecificCompiler.generateSetMethod(createRecord("test", true, message, Message), message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("setMessage$0",
        SpecificCompiler.generateSetMethod(createRecord("test", true, message, Message), Message));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("setSchema$", SpecificCompiler.generateSetMethod(createRecord("test", false, Schema$), Schema$));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("setSchema$",
        SpecificCompiler.generateSetMethod(createRecord("test", false, schema, Schema$), schema));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("setSchema$0",
        SpecificCompiler.generateSetMethod(createRecord("test", false, schema, Schema$), Schema$));
  }

  @Test
  void generateHasMethod() {
    Field height = new Field("height", Schema.create(Type.INT), null, null);
    Field Height = new Field("Height", Schema.create(Type.INT), null, null);
    Field height_and_width = new Field("height_and_width", Schema.create(Type.STRING), null, null);
    Field message = new Field("message", Schema.create(Type.STRING), null, null);
    Field Message = new Field("Message", Schema.create(Type.STRING), null, null);
    Field cause = new Field("cause", Schema.create(Type.STRING), null, null);
    Field clasz = new Field("class", Schema.create(Type.STRING), null, null);
    Field schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Field Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);

    assertEquals("hasHeight", SpecificCompiler.generateHasMethod(createRecord("test", false, height), height));

    assertEquals("hasHeightAndWidth",
        SpecificCompiler.generateHasMethod(createRecord("test", false, height_and_width), height_and_width));

    assertEquals("hasMessage", SpecificCompiler.generateHasMethod(createRecord("test", false, message), message));
    message = new Field("message", Schema.create(Type.STRING), null, null);
    assertEquals("hasMessage$", SpecificCompiler.generateHasMethod(createRecord("test", true, message), message));

    assertEquals("hasCause", SpecificCompiler.generateHasMethod(createRecord("test", false, cause), cause));
    cause = new Field("cause", Schema.create(Type.STRING), null, null);
    assertEquals("hasCause$", SpecificCompiler.generateHasMethod(createRecord("test", true, cause), cause));

    assertEquals("hasClass$", SpecificCompiler.generateHasMethod(createRecord("test", false, clasz), clasz));
    clasz = new Field("class", Schema.create(Type.STRING), null, null);
    assertEquals("hasClass$", SpecificCompiler.generateHasMethod(createRecord("test", true, clasz), clasz));

    assertEquals("hasSchema$", SpecificCompiler.generateHasMethod(createRecord("test", false, schema), schema));
    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    assertEquals("hasSchema$", SpecificCompiler.generateHasMethod(createRecord("test", true, schema), schema));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("hasHeight", SpecificCompiler.generateHasMethod(createRecord("test", false, Height), Height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("hasHeight", SpecificCompiler.generateHasMethod(createRecord("test", false, height, Height), height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("hasHeight$0",
        SpecificCompiler.generateHasMethod(createRecord("test", false, height, Height), Height));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("hasMessage$", SpecificCompiler.generateHasMethod(createRecord("test", true, Message), Message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("hasMessage$",
        SpecificCompiler.generateHasMethod(createRecord("test", true, message, Message), message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("hasMessage$0",
        SpecificCompiler.generateHasMethod(createRecord("test", true, message, Message), Message));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("hasSchema$", SpecificCompiler.generateHasMethod(createRecord("test", false, Schema$), Schema$));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("hasSchema$",
        SpecificCompiler.generateHasMethod(createRecord("test", false, schema, Schema$), schema));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("hasSchema$0",
        SpecificCompiler.generateHasMethod(createRecord("test", false, schema, Schema$), Schema$));
  }

  @Test
  void generateClearMethod() {
    Field height = new Field("height", Schema.create(Type.INT), null, null);
    Field Height = new Field("Height", Schema.create(Type.INT), null, null);
    Field height_and_width = new Field("height_and_width", Schema.create(Type.STRING), null, null);
    Field message = new Field("message", Schema.create(Type.STRING), null, null);
    Field Message = new Field("Message", Schema.create(Type.STRING), null, null);
    Field cause = new Field("cause", Schema.create(Type.STRING), null, null);
    Field clasz = new Field("class", Schema.create(Type.STRING), null, null);
    Field schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Field Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);

    assertEquals("clearHeight", SpecificCompiler.generateClearMethod(createRecord("test", false, height), height));

    assertEquals("clearHeightAndWidth",
        SpecificCompiler.generateClearMethod(createRecord("test", false, height_and_width), height_and_width));

    assertEquals("clearMessage", SpecificCompiler.generateClearMethod(createRecord("test", false, message), message));
    message = new Field("message", Schema.create(Type.STRING), null, null);
    assertEquals("clearMessage$", SpecificCompiler.generateClearMethod(createRecord("test", true, message), message));

    assertEquals("clearCause", SpecificCompiler.generateClearMethod(createRecord("test", false, cause), cause));
    cause = new Field("cause", Schema.create(Type.STRING), null, null);
    assertEquals("clearCause$", SpecificCompiler.generateClearMethod(createRecord("test", true, cause), cause));

    assertEquals("clearClass$", SpecificCompiler.generateClearMethod(createRecord("test", false, clasz), clasz));
    clasz = new Field("class", Schema.create(Type.STRING), null, null);
    assertEquals("clearClass$", SpecificCompiler.generateClearMethod(createRecord("test", true, clasz), clasz));

    assertEquals("clearSchema$", SpecificCompiler.generateClearMethod(createRecord("test", false, schema), schema));
    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    assertEquals("clearSchema$", SpecificCompiler.generateClearMethod(createRecord("test", true, schema), schema));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("clearHeight", SpecificCompiler.generateClearMethod(createRecord("test", false, Height), Height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("clearHeight",
        SpecificCompiler.generateClearMethod(createRecord("test", false, height, Height), height));

    height = new Field("height", Schema.create(Type.INT), null, null);
    Height = new Field("Height", Schema.create(Type.INT), null, null);
    assertEquals("clearHeight$0",
        SpecificCompiler.generateClearMethod(createRecord("test", false, height, Height), Height));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("clearMessage$", SpecificCompiler.generateClearMethod(createRecord("test", true, Message), Message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("clearMessage$",
        SpecificCompiler.generateClearMethod(createRecord("test", true, message, Message), message));

    message = new Field("message", Schema.create(Type.STRING), null, null);
    Message = new Field("Message", Schema.create(Type.STRING), null, null);
    assertEquals("clearMessage$0",
        SpecificCompiler.generateClearMethod(createRecord("test", true, message, Message), Message));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("clearSchema$", SpecificCompiler.generateClearMethod(createRecord("test", false, Schema$), Schema$));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("clearSchema$",
        SpecificCompiler.generateClearMethod(createRecord("test", false, schema, Schema$), schema));

    schema = new Field("schema", Schema.create(Type.STRING), null, null);
    Schema$ = new Field("Schema", Schema.create(Type.STRING), null, null);
    assertEquals("clearSchema$0",
        SpecificCompiler.generateClearMethod(createRecord("test", false, schema, Schema$), Schema$));
  }

  @Test
  void annotations() throws Exception {
    // an interface generated for protocol
    assertNotNull(Simple.class.getAnnotation(TestAnnotation.class));
    // a class generated for a record
    assertNotNull(TestRecord.class.getAnnotation(TestAnnotation.class));
    // a class generated for a fixed
    assertNotNull(MD5.class.getAnnotation(TestAnnotation.class));
    // a class generated for an enum
    assertNotNull(Kind.class.getAnnotation(TestAnnotation.class));

    // a field
    assertNotNull(TestRecord.class.getDeclaredField("name").getAnnotation(TestAnnotation.class));
    // a method
    assertNotNull(Simple.class.getMethod("ack").getAnnotation(TestAnnotation.class));
  }

  @Test
  void aliases() throws IOException {
    Schema s = SchemaParser.parseSingle("{\"name\":\"X\",\"type\":\"record\",\"aliases\":[\"Y\"],\"fields\":["
        + "{\"name\":\"f\",\"type\":\"int\",\"aliases\":[\"g\"]}]}");
    SpecificCompiler compiler = new SpecificCompiler(s);
    compiler.setStringType(StringType.valueOf("String"));
    Collection<OutputFile> outputs = compiler.compile();
    assertEquals(1, outputs.size());
    OutputFile o = outputs.iterator().next();
    assertEquals(o.path, "X.java");
    assertTrue(o.contents.contains("[\\\"Y\\\"]"));
    assertTrue(o.contents.contains("[\\\"g\\\"]"));
  }

  /**
   * Checks that a schema passes through the SpecificCompiler, and, optionally,
   * uses the system's Java compiler to check that the generated code is valid.
   */
  public static void assertCompiles(File dstDir, Schema schema, boolean useJavaCompiler) throws IOException {
    Collection<OutputFile> outputs = new SpecificCompiler(schema).compile();
    assertNotNull(outputs);
    if (useJavaCompiler) {
      assertCompilesWithJavaCompiler(dstDir, outputs);
    }
  }

  /**
   * Checks that a protocol passes through the SpecificCompiler, and, optionally,
   * uses the system's Java compiler to check that the generated code is valid.
   */
  public static void assertCompiles(File dstDir, Protocol protocol, boolean useJavaCompiler) throws IOException {
    Collection<OutputFile> outputs = new SpecificCompiler(protocol).compile();
    assertNotNull(outputs);
    if (useJavaCompiler) {
      assertCompilesWithJavaCompiler(dstDir, outputs);
    }
  }

  /** Uses the system's java compiler to actually compile the generated code. */
  static void assertCompilesWithJavaCompiler(File dstDir, Collection<OutputFile> outputs) throws IOException {
    if (outputs.isEmpty()) {
      return; // Nothing to compile!
    }

    List<File> javaFiles = new ArrayList<>();
    for (OutputFile o : outputs) {
      javaFiles.add(o.writeToDestination(null, dstDir));
    }

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

    CompilationTask cTask = compiler.getTask(null, fileManager, null, null, null,
        fileManager.getJavaFileObjects(javaFiles.toArray(new File[0])));
    assertTrue(cTask.call());
  }

  private static String SCHEMA1 = "{ \"name\": \"volatile\", \"type\": \"record\", "
      + "  \"fields\": [{\"name\": \"ownerAddress\", \"type\": [\"null\",{ \"type\": \"string\",\"java-class\": \"java.net.URI\"}], \"default\": null},"
      + "                {\"name\": \"ownerURL\", \"type\": [\"null\",{ \"type\": \"string\",\"java-class\": \"java.net.URL\"}], \"default\": null}]}";

  @Test
  void generateExceptionCodeBlock() throws IOException {
    Collection<OutputFile> outputs = new SpecificCompiler(SchemaParser.parseSingle(SCHEMA1)).compile();
    assertEquals(1, outputs.size());
    String contents = outputs.iterator().next().contents;

    assertTrue(contents.contains("private java.net.URI"));
    assertTrue(contents.contains("catch (java.net.URISyntaxException e)"));
    assertTrue(contents.contains("private java.net.URL"));
    assertTrue(contents.contains("catch (java.net.MalformedURLException e)"));
  }
}
