/*
 * Copyright 2018 LinkedIn Corp.
 * Licensed under the BSD 2-Clause License (the "License").

 * See License in the project root for license information.
 */

package com.linkedin.avro.compatibility;

import com.acme.generatedbylatest.EnumType;
import com.acme.generatedbylatest.Event;
import com.acme.generatedbylatest.Guid;
import com.acme.generatedbylatest.Header;
import com.linkedin.avro.TestUtil;
import com.linkedin.avro.legacy.LegacyAvroSchema;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.BinaryDecoder;
import org.apache.avro.io.BinaryEncoder;
import org.apache.avro.util.Utf8;
import org.testng.Assert;
import org.testng.SkipException;
import org.testng.annotations.Test;


public class AvroCompatibilityHelperTest {
  private Schema _schema = Schema.parse(
           "{\n"
          + "  \"type\": \"record\",\n"
          + "  \"name\": \"Event\",\n"
          + "  \"namespace\": \"com.acme\",\n"
          + "  \"fields\" : [\n"
          + "    {\"name\":\"header\",\n"
          + "     \"type\":{\n"
          + "        \"name\": \"Header\",\n"
          + "        \"type\": \"record\",\n"
          + "        \"namespace\": \"com.acme\",\n"
          + "        \"fields\" : [\n"
          + "          {\"name\":\"intField\", \"type\":\"int\"},\n"
          + "          {\"name\":\"guid\", \n"
          + "           \"type\":{\n"
          + "              \"name\": \"Guid\",\n"
          + "              \"type\":\"fixed\",\n"
          + "              \"size\":16\n"
          + "           }\n"
          + "          }\n"
          + "        ]\n"
          + "      }\n"
          + "    },\n"
          + "    {\"name\":\"enumField\", \n"
          + "     \"type\":{\n"
          + "       \"type\":\"enum\", \n"
          + "       \"symbols\":[\"A\", \"B\", \"C\"], \n"
          + "       \"name\":\"EnumType\"\n"
          + "     }\n"
          + "    },\n"
          + "    {\"name\":\"strField\", \"type\":\"string\", \"default\":\"str\"}\n"
          + "  ]\n"
          + "}"
  );

  @Test
  public void testFixedField() throws Exception {
    Schema fixedTypeSchema = Schema.parse("{\"name\": \"UUID\", \"type\":\"fixed\", \"size\":16}");
    GenericData.Fixed fixed = AvroCompatibilityHelper.newFixedField(fixedTypeSchema);
    Assert.assertNotNull(fixed);
  }

  @Test
  public void testGetDefaultValue() {
    Object defaultValue = AvroCompatibilityHelper.getDefaultValue(_schema.getField("strField"));
    Assert.assertEquals(defaultValue, new Utf8("str"));
  }

  @Test
  public void testBinaryEncoder() throws Exception {
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    BinaryEncoder binaryEncoder = AvroCompatibilityHelper.newBinaryEncoder(os);
    Assert.assertNotNull(binaryEncoder);
  }

  @Test
  public void testBinaryDecoder() {
    ByteArrayInputStream is = new ByteArrayInputStream(new byte[0]);
    BinaryDecoder binaryDecoder = AvroCompatibilityHelper.newBinaryDecoder(is);
    Assert.assertNotNull(binaryDecoder);
  }

  @Test
  public void testCompilerCurrent() throws Exception {
    Collection<AvroGeneratedSourceCode> compiled = AvroCompatibilityHelper.compile(Collections.singletonList(_schema), AvroCompatibilityHelper
      .getRuntimeAvroVersion());
    Assert.assertEquals(4, compiled.size()); //Event, Header, EnumType and Guid
    assertCompiles(compiled);
  }

  @Test
  public void testCompilerCompatible() throws Exception {
    Collection<AvroGeneratedSourceCode> compiled = AvroCompatibilityHelper.compile(Collections.singletonList(_schema), AvroVersion.AVRO_1_4);
    Assert.assertEquals(4, compiled.size()); //Event, Header, EnumType and Guid
    assertCompiles(compiled);
  }

  @Test
  public void test17Compatibility() throws Exception {
    //exercise code generated by helper under 1.8 + compatibility under whatever avro this build runs on
    //(1.4 at the time of this writing)
    Guid guid = new Guid(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16});
    Header header = new Header();
    header.intField = 42;
    header.guid = guid;
    Event event = new Event(header, EnumType.B, "some string");
  }

  @Test
  public void testCompileLargeSchema() throws Exception {
    String schemaJson = TestUtil.load("VeryLong.avsc");
    Schema schema = Schema.parse(schemaJson);
    Collection<AvroGeneratedSourceCode> compiled = AvroCompatibilityHelper.compile(Collections.singletonList(schema), AvroVersion.AVRO_1_4);
    Assert.assertEquals(1, compiled.size()); //no inner classes
    assertCompiles(compiled);
  }

  @Test
  public void testFixShortUnionBranches() throws Exception {
    if (!AvroCompatibilityHelper.getRuntimeAvroVersion().laterThan(AvroVersion.AVRO_1_4)) {
      throw new SkipException("test only valid under modern avro");
    }
    String avsc = TestUtil.load("HasUnions.avsc");
    String payload14 = TestUtil.load("HasUnions14.json");
    String payload17 = TestUtil.load("HasUnions17.json");
    LegacyAvroSchema schema = new LegacyAvroSchema("1234", avsc);
    GenericRecord deserialized14 = schema.deserializeJson(payload14);
    Assert.assertNotNull(deserialized14);
    Assert.assertNotNull(deserialized14.get("f1"));
    GenericRecord deserialized17 = schema.deserializeJson(payload17);
    Assert.assertNotNull(deserialized17);
    Assert.assertNotNull(deserialized17.get("f1"));
  }

  @Test
  public void testFixFullUnionBranches() throws Exception {
    if (AvroCompatibilityHelper.getRuntimeAvroVersion().laterThan(AvroVersion.AVRO_1_4)) {
      throw new SkipException("test only valid under avro 1.4 and earlier");
    }
    String avsc = TestUtil.load("HasUnions.avsc");
    String payload14 = TestUtil.load("HasUnions14.json");
    String payload17 = TestUtil.load("HasUnions17.json");
    LegacyAvroSchema schema = new LegacyAvroSchema("1234", avsc);
    GenericRecord deserialized14 = schema.deserializeJson(payload14);
    Assert.assertNotNull(deserialized14);
    Assert.assertNotNull(deserialized14.get("f1"));
    GenericRecord deserialized17 = schema.deserializeJson(payload17);
    Assert.assertNotNull(deserialized17);
    Assert.assertNotNull(deserialized17.get("f1"));
  }

  @Test
  public void testIsSpecificRecord() {
    GenericRecord genericRecord = new GenericData.Record(_schema);
    Assert.assertFalse(AvroCompatibilityHelper.isSpecificRecord(genericRecord));
    Assert.assertTrue(AvroCompatibilityHelper.isGenericRecord(genericRecord));
  }

  private void assertCompiles(Collection<AvroGeneratedSourceCode> generatedCode) throws Exception {

    //write out generated code into a source tree
    Path tempRootFolder = Files.createTempDirectory(null);
    File[] fileArray = new File[generatedCode.size()];
    int i=0;
    for (AvroGeneratedSourceCode avroGeneratedSourceCode : generatedCode) {
      fileArray[i++] = avroGeneratedSourceCode.writeToDestination(tempRootFolder.toFile());
    }

    //spin up a java compiler task, use current runtime classpath, point at source tree created above
    List<String> optionList = new ArrayList<>();
    optionList.addAll(Arrays.asList("-classpath", System.getProperty("java.class.path")));
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
    Iterable<? extends JavaFileObject> javaFileObjects = fileManager.getJavaFileObjects(fileArray);
    StringWriter compilerOutput = new StringWriter();
    JavaCompiler.CompilationTask task = compiler.getTask(compilerOutput, fileManager, null, optionList, null, javaFileObjects);
    compilerOutput.flush();

    //compile, assert no errors
    Assert.assertTrue(task.call(), "compilation failed with " + compilerOutput.toString());
  }
}