/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.hive.serde2.avro;

import org.apache.avro.Schema;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;
import org.apache.hive.maprminicluster.MapRMiniDFSCluster;
import org.junit.Test;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import static org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.EXCEPTION_MESSAGE;
import static org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.SCHEMA_NONE;
import org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.AvroTableProperties;
import static org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.determineSchemaOrThrowException;
import static org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.getOtherTypeFromNullableType;
import static org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.isNullableType;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

public class TestAvroSerdeUtils {
  private final String NULLABLE_UNION = "{\n" +
      "  \"type\": \"record\", \n" +
      "  \"name\": \"nullTest\",\n" +
      "  \"fields\" : [\n" +
      "    {\"name\":\"mayBeNull\", \"type\":[\"string\", \"null\"]}\n" +
      "  ]\n" +
      "}";
  // Same union, order reveresed
  private final String NULLABLE_UNION2 = "{\n" +
    "  \"type\": \"record\", \n" +
    "  \"name\": \"nullTest\",\n" +
    "  \"fields\" : [\n" +
    "    {\"name\":\"mayBeNull\", \"type\":[\"null\", \"string\"]}\n" +
    "  ]\n" +
    "}";

  private void testField(String schemaString, String fieldName, boolean shouldBeNullable) {
    Schema s = AvroSerdeUtils.getSchemaFor(schemaString);
    assertEquals(shouldBeNullable, isNullableType(s.getField(fieldName).schema()));
  }

  @Test
  public void isNullableTypeAcceptsNullableUnions() {
    testField(NULLABLE_UNION, "mayBeNull", true);
    testField(NULLABLE_UNION2, "mayBeNull", true);
  }

  @Test
  public void isNullableTypeIdentifiesUnionsOfMoreThanTwoTypes() {
    List<String> schemaStrings = Arrays.asList(
        "{\n" +
            "  \"type\": \"record\", \n" +
            "  \"name\": \"shouldNotPass\",\n" +
            "  \"fields\" : [\n" +
            "    {\"name\":\"mayBeNull\", \"type\":[\"string\", \"int\", \"null\"]}\n" +
            "  ]\n" +
            "}",
        "{\n" +
            "  \"type\": \"record\", \n" +
            "  \"name\": \"shouldNotPass\",\n" +
            "  \"fields\" : [\n" +
            "    {\"name\":\"mayBeNull\", \"type\":[\"string\", \"null\", \"int\"]}\n" +
            "  ]\n" +
            "}",
        "{\n" +
            "  \"type\": \"record\", \n" +
            "  \"name\": \"shouldNotPass\",\n" +
            "  \"fields\" : [\n" +
            "    {\"name\":\"mayBeNull\", \"type\":[\"null\", \"string\", \"int\"]}\n" +
            "  ]\n" +
            "}"
    );
    for (String schemaString : schemaStrings) {
      testField(schemaString, "mayBeNull", true);
    }
  }

  @Test
  public void isNullableTypeIdentifiesUnionsWithoutNulls() {
    String s = "{\n" +
      "  \"type\": \"record\", \n" +
      "  \"name\": \"unionButNoNull\",\n" +
      "  \"fields\" : [\n" +
      "    {\"name\":\"a\", \"type\":[\"int\", \"string\"]}\n" +
      "  ]\n" +
      "}";
    testField(s, "a", false);
  }

  @Test
  public void isNullableTypeIdentifiesNonUnionTypes() {
    String schemaString = "{\n" +
      "  \"type\": \"record\", \n" +
      "  \"name\": \"nullTest2\",\n" +
      "  \"fields\" : [\n" +
      "    {\"name\":\"justAnInt\", \"type\":\"int\"}\n" +
      "  ]\n" +
      "}";
    testField(schemaString, "justAnInt", false);
  }

  @Test
  public void getTypeFromNullableTypePositiveCase() {
    Schema s = AvroSerdeUtils.getSchemaFor(NULLABLE_UNION);
    Schema typeFromNullableType = getOtherTypeFromNullableType(s.getField("mayBeNull").schema());
    assertEquals(Schema.Type.STRING, typeFromNullableType.getType());

    s = AvroSerdeUtils.getSchemaFor(NULLABLE_UNION2);
    typeFromNullableType = getOtherTypeFromNullableType(s.getField("mayBeNull").schema());
    assertEquals(Schema.Type.STRING, typeFromNullableType.getType());
  }

  @Test(expected=AvroSerdeException.class)
  public void determineSchemaThrowsExceptionIfNoSchema() throws IOException, AvroSerdeException {
    Configuration conf = new Configuration();
    Properties prop = new Properties();
    AvroSerdeUtils.determineSchemaOrThrowException(conf, prop);
  }

  @Test
  public void determineSchemaFindsLiterals() throws Exception {
    String schema = TestAvroObjectInspectorGenerator.RECORD_SCHEMA;
    Configuration conf = new Configuration();
    Properties props = new Properties();
    props.put(AvroTableProperties.SCHEMA_LITERAL.getPropName(), schema);
    Schema expected = AvroSerdeUtils.getSchemaFor(schema);
    assertEquals(expected, AvroSerdeUtils.determineSchemaOrThrowException(conf, props));
  }

  @Test
  public void detemineSchemaTriesToOpenUrl() throws AvroSerdeException, IOException {
    Configuration conf = new Configuration();
    Properties props = new Properties();
    props.put(AvroTableProperties.SCHEMA_URL.getPropName(), "not:///a.real.url");

    try {
      AvroSerdeUtils.determineSchemaOrThrowException(conf, props);
      fail("Should have tried to open that URL");
    } catch (AvroSerdeException e) {
      assertEquals("Unable to read schema from given path: not:///a.real.url", e.getMessage());
    }
  }

  @Test
  public void noneOptionWorksForSpecifyingSchemas() throws IOException, AvroSerdeException {
    Configuration conf = new Configuration();
    Properties props = new Properties();

    // Combo 1: Both set to none
    props.put(AvroTableProperties.SCHEMA_URL.getPropName(), SCHEMA_NONE);
    props.put(AvroTableProperties.SCHEMA_LITERAL.getPropName(), SCHEMA_NONE);
    try {
      determineSchemaOrThrowException(conf, props);
      fail("Should have thrown exception with none set for both url and literal");
    } catch(AvroSerdeException he) {
      assertEquals(EXCEPTION_MESSAGE, he.getMessage());
    }

    // Combo 2: Literal set, url set to none
    props.put(AvroTableProperties.SCHEMA_LITERAL.getPropName(), TestAvroObjectInspectorGenerator.RECORD_SCHEMA);
    Schema s;
    try {
      s = determineSchemaOrThrowException(conf, props);
      assertNotNull(s);
      assertEquals(AvroSerdeUtils.getSchemaFor(TestAvroObjectInspectorGenerator.RECORD_SCHEMA), s);
    } catch(AvroSerdeException he) {
      fail("Should have parsed schema literal, not thrown exception.");
    }

    // Combo 3: url set, literal set to none
    props.put(AvroTableProperties.SCHEMA_LITERAL.getPropName(), SCHEMA_NONE);
    props.put(AvroTableProperties.SCHEMA_URL.getPropName(), "not:///a.real.url");
    try {
      determineSchemaOrThrowException(conf, props);
      fail("Should have tried to open that bogus URL");
    } catch (AvroSerdeException e) {
      assertEquals("Unable to read schema from given path: not:///a.real.url", e.getMessage());
    }
  }

  @Test
  public void determineSchemaCanReadSchemaFromHDFS() throws IOException, AvroSerdeException,
                                                            URISyntaxException{
    String schemaString = TestAvroObjectInspectorGenerator.RECORD_SCHEMA;
    MapRMiniDFSCluster miniDfs = null;
    try {
      // MiniDFSCluster litters files and folders all over the place.
      miniDfs = new MapRMiniDFSCluster();

      miniDfs.getFileSystem().mkdirs(new Path("./path/to/schema"));
      FSDataOutputStream out = miniDfs.getFileSystem()
              .create(new Path("./path/to/schema/schema.avsc"));
      out.writeBytes(schemaString);
      out.close();
      String onHDFS = miniDfs.getFileSystem().getUri() + miniDfs.getFileSystem().getWorkingDirectory().toString() + "/path/to/schema/schema.avsc";

      Schema schemaFromHDFS =
              AvroSerdeUtils.getSchemaFromFS(onHDFS, miniDfs.getFileSystem().getConf());
      Schema expectedSchema = AvroSerdeUtils.getSchemaFor(schemaString);
      assertEquals(expectedSchema, schemaFromHDFS);
    } finally {
      if(miniDfs != null) miniDfs.shutdown();
    }
  }
}
