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

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import org.apache.trevni.ValueType;
import org.apache.trevni.ColumnMetaData;
import org.apache.trevni.ColumnFileMetaData;

import org.apache.avro.Schema;

import org.junit.Test;
import static org.junit.Assert.*;

public class TestShredder {

  private static final int COUNT = 100;
  private static final File FILE = new File("target", "test.trv");

  @Test public void testPrimitives() throws Exception {
    check(Schema.create(Schema.Type.NULL),
          new ColumnMetaData("null", ValueType.NULL));
    check(Schema.create(Schema.Type.BOOLEAN),
          new ColumnMetaData("boolean", ValueType.BOOLEAN));

    check(Schema.create(Schema.Type.INT),
          new ColumnMetaData("int", ValueType.INT));
    check(Schema.create(Schema.Type.LONG),
          new ColumnMetaData("long", ValueType.LONG));

    check(Schema.create(Schema.Type.FLOAT),
          new ColumnMetaData("float", ValueType.FLOAT));
    check(Schema.create(Schema.Type.DOUBLE),
          new ColumnMetaData("double", ValueType.DOUBLE));

    check(Schema.create(Schema.Type.BYTES),
          new ColumnMetaData("bytes", ValueType.BYTES));
    check(Schema.create(Schema.Type.STRING),
          new ColumnMetaData("string", ValueType.STRING));

    check(Schema.createEnum("E", null, null, Arrays.asList("X","Y","Z")),
          new ColumnMetaData("E", ValueType.INT));
    check(Schema.createFixed("F", null, null, 5),
          new ColumnMetaData("F", ValueType.BYTES));
  }

  private static final String SIMPLE_FIELDS =
    "{\"name\":\"x\",\"type\":\"int\"},"+
    "{\"name\":\"y\",\"type\":\"string\"}";

  private static final String SIMPLE_RECORD =
    "{\"type\":\"record\",\"name\":\"R\",\"fields\":["
    +SIMPLE_FIELDS
    +"]}";

  @Test public void testSimpleRecord() throws Exception {
    check(Schema.parse(SIMPLE_RECORD),
          new ColumnMetaData("x", ValueType.INT),
          new ColumnMetaData("y", ValueType.STRING));
  }

  @Test public void testDefaultValue() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"R\",\"fields\":["
      +SIMPLE_FIELDS+","
      +"{\"name\":\"z\",\"type\":\"int\","
      +"\"default\":1,\""+RandomData.USE_DEFAULT+"\":true}"
      +"]}";
    checkWrite(Schema.parse(SIMPLE_RECORD));
    checkRead(Schema.parse(s));
  }

  @Test public void testNestedRecord() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"S\",\"fields\":["
      +"{\"name\":\"x\",\"type\":\"int\"},"
      +"{\"name\":\"R\",\"type\":"+SIMPLE_RECORD+"},"
      +"{\"name\":\"y\",\"type\":\"string\"}"
      +"]}";
    check(Schema.parse(s),
          new ColumnMetaData("x", ValueType.INT),
          new ColumnMetaData("R#x", ValueType.INT),
          new ColumnMetaData("R#y", ValueType.STRING),
          new ColumnMetaData("y", ValueType.STRING));
  }

  @Test public void testNamedRecord() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"S\",\"fields\":["
      +"{\"name\":\"R1\",\"type\":"+SIMPLE_RECORD+"},"
      +"{\"name\":\"R2\",\"type\":\"R\"}"
      +"]}";
    check(Schema.parse(s),
      new ColumnMetaData("R1#x", ValueType.INT),
      new ColumnMetaData("R1#y", ValueType.STRING),
      new ColumnMetaData("R2#x", ValueType.INT),
      new ColumnMetaData("R2#y", ValueType.STRING));
  }

  @Test public void testSimpleArray() throws Exception {
    String s = "{\"type\":\"array\",\"items\":\"long\"}";
    check(Schema.parse(s),
          new ColumnMetaData("[]", ValueType.LONG).isArray(true));
  }

  private static final String RECORD_ARRAY =
    "{\"type\":\"array\",\"items\":"+SIMPLE_RECORD+"}";

  @Test public void testArray() throws Exception {
    ColumnMetaData p = new ColumnMetaData("[]", ValueType.NULL).isArray(true);
    check(Schema.parse(RECORD_ARRAY),
          p,
          new ColumnMetaData("[]#x", ValueType.INT).setParent(p),
          new ColumnMetaData("[]#y", ValueType.STRING).setParent(p));
  }

  @Test public void testSimpleUnion() throws Exception {
    String s = "[\"int\",\"string\"]";
    check(Schema.parse(s),
          new ColumnMetaData("int", ValueType.INT).isArray(true),
          new ColumnMetaData("string", ValueType.STRING).isArray(true));
  }

  @Test public void testSimpleOptional() throws Exception {
    String s = "[\"null\",\"string\"]";
    check(Schema.parse(s),
          new ColumnMetaData("string", ValueType.STRING).isArray(true));
  }

  private static final String UNION = "[\"null\",\"int\","+SIMPLE_RECORD+"]";

  @Test public void testUnion() throws Exception {
    ColumnMetaData p = new ColumnMetaData("R", ValueType.NULL).isArray(true);
    check(Schema.parse(UNION),
          new ColumnMetaData("int", ValueType.INT).isArray(true),
          p,
          new ColumnMetaData("R#x", ValueType.INT).setParent(p),
          new ColumnMetaData("R#y", ValueType.STRING).setParent(p));
  }

  @Test public void testNestedArray() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"S\",\"fields\":["
      +"{\"name\":\"x\",\"type\":\"int\"},"
      +"{\"name\":\"A\",\"type\":"+RECORD_ARRAY+"},"
      +"{\"name\":\"y\",\"type\":\"string\"}"
      +"]}";
    ColumnMetaData p = new ColumnMetaData("A[]", ValueType.NULL).isArray(true);
    check(Schema.parse(s),
          new ColumnMetaData("x", ValueType.INT),
          p,
          new ColumnMetaData("A[]#x", ValueType.INT).setParent(p),
          new ColumnMetaData("A[]#y", ValueType.STRING).setParent(p),
          new ColumnMetaData("y", ValueType.STRING));
  }

  @Test public void testNestedUnion() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"S\",\"fields\":["
      +"{\"name\":\"x\",\"type\":\"int\"},"
      +"{\"name\":\"u\",\"type\":"+UNION+"},"
      +"{\"name\":\"y\",\"type\":\"string\"}"
      +"]}";
    ColumnMetaData p = new ColumnMetaData("u/R", ValueType.NULL).isArray(true);
    check(Schema.parse(s),
          new ColumnMetaData("x", ValueType.INT),
          new ColumnMetaData("u/int", ValueType.INT).isArray(true),
          p,
          new ColumnMetaData("u/R#x", ValueType.INT).setParent(p),
          new ColumnMetaData("u/R#y", ValueType.STRING).setParent(p),
          new ColumnMetaData("y", ValueType.STRING));
  }

  @Test public void testUnionInArray() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"S\",\"fields\":["
      +"{\"name\":\"a\",\"type\":{\"type\":\"array\",\"items\":"+UNION+"}}"
      +"]}";
    ColumnMetaData p = new ColumnMetaData("a[]",ValueType.NULL).isArray(true);
    ColumnMetaData r = new ColumnMetaData("a[]/R", ValueType.NULL)
      .setParent(p)
      .isArray(true);
      check(Schema.parse(s),
          p,
          new ColumnMetaData("a[]/int", ValueType.INT)
            .setParent(p)
            .isArray(true),
          r,
          new ColumnMetaData("a[]/R#x", ValueType.INT).setParent(r),
          new ColumnMetaData("a[]/R#y", ValueType.STRING).setParent(r));
  }

  @Test public void testArrayInUnion() throws Exception {
    String s =
      "{\"type\":\"record\",\"name\":\"S\",\"fields\":["
      +"{\"name\":\"a\",\"type\":[\"int\","+RECORD_ARRAY+"]}]}";
    ColumnMetaData q = new ColumnMetaData("a/array",ValueType.NULL)
      .isArray(true);
    ColumnMetaData r = new ColumnMetaData("a/array[]", ValueType.NULL)
      .setParent(q)
      .isArray(true);
    check(Schema.parse(s),
          new ColumnMetaData("a/int", ValueType.INT).isArray(true),
          q,
          r,
          new ColumnMetaData("a/array[]#x", ValueType.INT).setParent(r),
          new ColumnMetaData("a/array[]#y", ValueType.STRING).setParent(r));
  }

  @Test public void testSimpleMap() throws Exception {
    String s = "{\"type\":\"map\",\"values\":\"long\"}";
    ColumnMetaData p = new ColumnMetaData(">", ValueType.NULL).isArray(true);
    check(Schema.parse(s),
          p,
          new ColumnMetaData(">key", ValueType.STRING).setParent(p),
          new ColumnMetaData(">value", ValueType.LONG).setParent(p));
  }

  @Test public void testMap() throws Exception {
    String s = "{\"type\":\"map\",\"values\":"+SIMPLE_RECORD+"}";
    ColumnMetaData p = new ColumnMetaData(">", ValueType.NULL).isArray(true);
    check(Schema.parse(s),
          p,
          new ColumnMetaData(">key", ValueType.STRING).setParent(p),
          new ColumnMetaData(">value#x", ValueType.INT).setParent(p),
          new ColumnMetaData(">value#y", ValueType.STRING).setParent(p));
  }

  private void check(Schema s, ColumnMetaData... expected) throws Exception {
    ColumnMetaData[] shredded = new AvroColumnator(s).getColumns();
    assertEquals(expected.length, shredded.length);
    for (int i = 0; i < expected.length; i++)
      assertEquals(expected[i].toString(), shredded[i].toString());
    checkWrite(s);
    checkRead(s);
  }

  private void checkWrite(Schema schema) throws IOException {
    AvroColumnWriter<Object> writer =
      new AvroColumnWriter<Object>(schema, new ColumnFileMetaData());
    int count = 0;
    for (Object datum : new RandomData(schema, COUNT)) {
      //System.out.println("datum="+datum);
      writer.write(datum);
    }
    writer.writeTo(FILE);
  }

  private void checkRead(Schema schema) throws IOException {
    AvroColumnReader<Object> reader =
      new AvroColumnReader<Object>(new AvroColumnReader.Params(FILE)
                                   .setSchema(schema));
    for (Object expected : new RandomData(schema, COUNT))
      assertEquals(expected, reader.next());
    reader.close();
  }

}
