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

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.BufferedReader;

import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * Verifies that the SpecificCompilerTool generates Java source properly
 */
public class TestSpecificCompilerTool {

  // where test input/expected output comes from
  private static final File TEST_DIR = new File(System.getProperty("test.compile.schema.dir", "src/test/compiler"));

  // where test input comes from
  private static final File TEST_INPUT_DIR = new File(TEST_DIR, "input");

  // where test expected output comes from
  private static final File TEST_EXPECTED_OUTPUT_DIR = new File(TEST_DIR, "output");
  private static final File TEST_EXPECTED_POSITION = new File(TEST_EXPECTED_OUTPUT_DIR, "Position.java");
  private static final File TEST_EXPECTED_PLAYER = new File(TEST_EXPECTED_OUTPUT_DIR, "Player.java");
  private static final File TEST_EXPECTED_NO_SETTERS = new File(TEST_EXPECTED_OUTPUT_DIR, "NoSettersTest.java");
  private static final File TEST_EXPECTED_OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS = new File(TEST_EXPECTED_OUTPUT_DIR,
      "OptionalGettersNullableFieldsTest.java");
  private static final File TEST_EXPECTED_OPTIONAL_GETTERS_FOR_ALL_FIELDS = new File(TEST_EXPECTED_OUTPUT_DIR,
      "OptionalGettersAllFieldsTest.java");
  private static final File TEST_EXPECTED_ADD_EXTRA_OPTIONAL_GETTERS = new File(TEST_EXPECTED_OUTPUT_DIR,
      "AddExtraOptionalGettersTest.java");

  private static final File TEST_EXPECTED_STRING_OUTPUT_DIR = new File(TEST_DIR, "output-string");
  private static final File TEST_EXPECTED_STRING_POSITION = new File(TEST_EXPECTED_STRING_OUTPUT_DIR,
      "avro/examples/baseball/Position.java");
  private static final File TEST_EXPECTED_STRING_PLAYER = new File(TEST_EXPECTED_STRING_OUTPUT_DIR,
      "avro/examples/baseball/Player.java");
  private static final File TEST_EXPECTED_STRING_JET_BRAINS_NULL_SAFE_ANNOTATIONS_TEST = new File(
      TEST_EXPECTED_STRING_OUTPUT_DIR, "avro/examples/baseball/JetBrainsNullSafeAnnotationsFieldsTest.java");
  private static final File TEST_EXPECTED_STRING_JSPECIFY_NULL_SAFE_ANNOTATIONS_TEST = new File(
      TEST_EXPECTED_STRING_OUTPUT_DIR, "avro/examples/baseball/JSpecifyNullSafeAnnotationsFieldsTest.java");
  private static final File TEST_EXPECTED_STRING_FIELDTEST = new File(TEST_EXPECTED_STRING_OUTPUT_DIR,
      "avro/examples/baseball/FieldTest.java");
  private static final File TEST_EXPECTED_STRING_PROTO = new File(TEST_EXPECTED_STRING_OUTPUT_DIR,
      "avro/examples/baseball/Proto.java");

  // where test output goes
  private static final File TEST_OUTPUT_DIR = new File("target/compiler/output");
  private static final File TEST_OUTPUT_PLAYER = new File(TEST_OUTPUT_DIR, "avro/examples/baseball/Player.java");
  private static final File TEST_OUTPUT_POSITION = new File(TEST_OUTPUT_DIR, "avro/examples/baseball/Position.java");
  private static final File TEST_OUTPUT_NO_SETTERS = new File(TEST_OUTPUT_DIR,
      "avro/examples/baseball/NoSettersTest.java");
  private static final File TEST_OUTPUT_OPTIONAL_GETTERS_NULLABLE_FIELDS = new File(TEST_OUTPUT_DIR,
      "avro/examples/baseball/OptionalGettersNullableFieldsTest.java");
  private static final File TEST_OUTPUT_OPTIONAL_GETTERS_ALL_FIELDS = new File(TEST_OUTPUT_DIR,
      "avro/examples/baseball/OptionalGettersAllFieldsTest.java");
  private static final File TEST_OUTPUT_ADD_EXTRA_OPTIONAL_GETTERS = new File(TEST_OUTPUT_DIR,
      "avro/examples/baseball/AddExtraOptionalGettersTest.java");

  private static final File TEST_OUTPUT_STRING_DIR = new File("target/compiler/output-string");
  private static final File TEST_OUTPUT_STRING_PLAYER = new File(TEST_OUTPUT_STRING_DIR,
      "avro/examples/baseball/Player.java");
  private static final File TEST_OUTPUT_STRING_POSITION = new File(TEST_OUTPUT_STRING_DIR,
      "avro/examples/baseball/Position.java");
  private static final File TEST_OUTPUT_STRING_FIELDTEST = new File(TEST_OUTPUT_STRING_DIR,
      "avro/examples/baseball/FieldTest.java");
  private static final File TEST_OUTPUT_STRING_JET_BRAINS_NULL_SAFE_ANNOTATIONS_TEST = new File(TEST_OUTPUT_STRING_DIR,
      "avro/examples/baseball/JetBrainsNullSafeAnnotationsFieldsTest.java");
  private static final File TEST_OUTPUT_STRING_JSPECIFY_NULL_SAFE_ANNOTATIONS_TEST = new File(TEST_OUTPUT_STRING_DIR,
      "avro/examples/baseball/JSpecifyNullSafeAnnotationsFieldsTest.java");
  private static final File TEST_OUTPUT_STRING_PROTO = new File(TEST_OUTPUT_STRING_DIR,
      "avro/examples/baseball/Proto.java");

  @BeforeEach
  public void setUp() {
    TEST_OUTPUT_DIR.delete();
  }

  @Test
  void compileSchemaWithExcludedSetters() throws Exception {

    TEST_OUTPUT_NO_SETTERS.delete();
    doCompile(new String[] { "-encoding", "UTF-8", "-noSetters", "schema",
        TEST_INPUT_DIR.toString() + "/nosetterstest.avsc", TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_NO_SETTERS, TEST_OUTPUT_NO_SETTERS);
  }

  @Test
  void compileSchemaWithOptionalGettersForNullableFieldsOnly() throws Exception {

    TEST_OUTPUT_OPTIONAL_GETTERS_NULLABLE_FIELDS.delete();
    doCompile(new String[] { "-encoding", "UTF-8", "-optionalGetters", "only_nullable_fields", "schema",
        TEST_INPUT_DIR.toString() + "/optionalgettersnullablefieldstest.avsc", TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_OPTIONAL_GETTERS_FOR_NULLABLE_FIELDS, TEST_OUTPUT_OPTIONAL_GETTERS_NULLABLE_FIELDS);
  }

  @Test
  void compileSchemaWithJetBrainsNullSafeAnnotationsFields() throws Exception {

    TEST_OUTPUT_STRING_JET_BRAINS_NULL_SAFE_ANNOTATIONS_TEST.delete();
    doCompile(new String[] { "-encoding", "UTF-8", "-nullSafeAnnotations", "-string", "schema",
        TEST_INPUT_DIR.toString() + "/jetbrainsnullsafeannotationsfieldstest.avsc", TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_JET_BRAINS_NULL_SAFE_ANNOTATIONS_TEST,
        TEST_OUTPUT_STRING_JET_BRAINS_NULL_SAFE_ANNOTATIONS_TEST);
  }

  @Test
  void compileSchemaWithJSpecifyNullSafeAnnotationsFields() throws Exception {

    TEST_OUTPUT_STRING_JSPECIFY_NULL_SAFE_ANNOTATIONS_TEST.delete();
    doCompile(new String[] { "-encoding", "UTF-8", "-nullSafeAnnotations", "-nullSafeAnnotationNullable",
        "org.jspecify.annotations.Nullable", "-nullSafeAnnotationNotNull", "org.jspecify.annotations.NonNull",
        "-string", "schema", TEST_INPUT_DIR.toString() + "/jspecifynullsafeannotationsfieldstest.avsc",
        TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_JSPECIFY_NULL_SAFE_ANNOTATIONS_TEST,
        TEST_OUTPUT_STRING_JSPECIFY_NULL_SAFE_ANNOTATIONS_TEST);
  }

  @Test
  void compileSchemaWithOptionalGettersForAllFields() throws Exception {

    TEST_OUTPUT_OPTIONAL_GETTERS_ALL_FIELDS.delete();
    doCompile(new String[] { "-encoding", "UTF-8", "-optionalGetters", "all_fields", "schema",
        TEST_INPUT_DIR.toString() + "/optionalgettersallfieldstest.avsc", TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_OPTIONAL_GETTERS_FOR_ALL_FIELDS, TEST_OUTPUT_OPTIONAL_GETTERS_ALL_FIELDS);
  }

  @Test
  void compileSchemaWithAddExtraOptionalGetters() throws Exception {

    TEST_OUTPUT_ADD_EXTRA_OPTIONAL_GETTERS.delete();
    doCompile(new String[] { "-encoding", "UTF-8", "-addExtraOptionalGetters", "schema",
        TEST_INPUT_DIR.toString() + "/addextraoptionalgetterstest.avsc", TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_ADD_EXTRA_OPTIONAL_GETTERS, TEST_OUTPUT_ADD_EXTRA_OPTIONAL_GETTERS);
  }

  @Test
  void compileSchemaSingleFile() throws Exception {

    doCompile(new String[] { "-encoding", "UTF-8", "schema", TEST_INPUT_DIR.toString() + "/position.avsc",
        TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_POSITION, TEST_OUTPUT_POSITION);
  }

  @Test
  void compileSchemaTwoFiles() throws Exception {

    doCompile(new String[] { "-encoding", "UTF-8", "schema", TEST_INPUT_DIR.toString() + "/position.avsc",
        TEST_INPUT_DIR.toString() + "/player.avsc", TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_POSITION, TEST_OUTPUT_POSITION);
    assertFileMatch(TEST_EXPECTED_PLAYER, TEST_OUTPUT_PLAYER);
  }

  @Test
  void compileSchemaFileAndDirectory() throws Exception {

    doCompile(new String[] { "-encoding", "UTF-8", "schema", TEST_INPUT_DIR.toString() + "/position.avsc",
        TEST_INPUT_DIR.toString(), TEST_OUTPUT_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_POSITION, TEST_OUTPUT_POSITION);
    assertFileMatch(TEST_EXPECTED_PLAYER, TEST_OUTPUT_PLAYER);
  }

  @Test
  void compileSchemasUsingString() throws Exception {

    doCompile(new String[] { "-encoding", "UTF-8", "-string", "schema", TEST_INPUT_DIR.toString() + "/position.avsc",
        TEST_INPUT_DIR.toString() + "/player.avsc", TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_POSITION, TEST_OUTPUT_STRING_POSITION);
    assertFileMatch(TEST_EXPECTED_STRING_PLAYER, TEST_OUTPUT_STRING_PLAYER);
  }

  @Test
  void compileSchemasWithVariousFieldTypes() throws Exception {

    doCompile(new String[] { "-encoding", "UTF-8", "-string", "schema", TEST_INPUT_DIR.toString() + "/fieldtest.avsc",
        TEST_INPUT_DIR.toString() + "/fieldtest.avsc", TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_FIELDTEST, TEST_OUTPUT_STRING_FIELDTEST);
  }

  @Test
  void orderingOfFlags() throws Exception {

    // Order of Flags as per initial implementation
    doCompile(new String[] { "-encoding", "UTF-8", "-string", "-bigDecimal", "schema",
        TEST_INPUT_DIR.toString() + "/fieldtest.avsc", TEST_INPUT_DIR.toString() + "/fieldtest.avsc",
        TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_FIELDTEST, TEST_OUTPUT_STRING_FIELDTEST);

    // Change order of encoding and string
    doCompile(new String[] { "-string", "-encoding", "UTF-8", "-bigDecimal", "schema",
        TEST_INPUT_DIR.toString() + "/fieldtest.avsc", TEST_INPUT_DIR.toString() + "/fieldtest.avsc",
        TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_FIELDTEST, TEST_OUTPUT_STRING_FIELDTEST);

    // Change order of -string and -bigDecimal
    doCompile(new String[] { "-bigDecimal", "-encoding", "UTF-8", "-string", "schema",
        TEST_INPUT_DIR.toString() + "/fieldtest.avsc", TEST_INPUT_DIR.toString() + "/fieldtest.avsc",
        TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_FIELDTEST, TEST_OUTPUT_STRING_FIELDTEST);

    // Keep encoding at the end
    doCompile(new String[] { "-bigDecimal", "-string", "-encoding", "UTF-8", "schema",
        TEST_INPUT_DIR.toString() + "/fieldtest.avsc", TEST_INPUT_DIR.toString() + "/fieldtest.avsc",
        TEST_OUTPUT_STRING_DIR.getPath() });
    assertFileMatch(TEST_EXPECTED_STRING_FIELDTEST, TEST_OUTPUT_STRING_FIELDTEST);
  }

  @Test
  void compileProtocol() throws Exception {

    doCompile(new String[] { "-encoding", "UTF-8", "protocol", TEST_INPUT_DIR.toString() + "/proto.avpr",
        TEST_OUTPUT_STRING_DIR.getPath() });

    assertFileMatch(TEST_EXPECTED_STRING_PROTO, TEST_OUTPUT_STRING_PROTO);
  }

  // Runs the actual compiler tool with the given input args
  private void doCompile(String[] args) throws Exception {
    SpecificCompilerTool tool = new SpecificCompilerTool();
    tool.run(null, null, null, Arrays.asList((args)));
  }

  /**
   * Verify that the generated Java files match the expected. This approach has
   * room for improvement, since we're currently just verify that the text
   * matches, which can be brittle if the code generation formatting or method
   * ordering changes for example. A better approach would be to compile the
   * sources and do a deeper comparison.
   *
   * See
   * https://download.oracle.com/javase/6/docs/api/javax/tools/JavaCompiler.html
   */
  private static void assertFileMatch(File expected, File found) throws IOException {
    assertEquals(readFile(expected), readFile(found),
        "Found file: " + found + " does not match expected file: " + expected);
  }

  /**
   * Not the best implementation, but does the job. Building full strings of the
   * file content and comparing provides nice diffs via JUnit when failures occur.
   */
  private static String readFile(File file) throws IOException {
    BufferedReader reader = new BufferedReader(
        new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
    StringBuilder sb = new StringBuilder();
    String line = null;
    boolean first = true;
    while ((line = reader.readLine()) != null) {
      if (!first) {
        sb.append("\n");
        first = false;
      }
      sb.append(line);
    }
    reader.close();
    return sb.toString();
  }
}
