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

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import org.apache.avro.Schema;
import org.apache.avro.util.Utf8;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RunWith(Parameterized.class)
public class TestValidatingIO {
  enum Encoding {
    BINARY,
    BLOCKING_BINARY,
    JSON,
  }
  
  private static final Logger LOG =
      LoggerFactory.getLogger(TestValidatingIO.class);
  
  private Encoding eEnc;
  private int iSkipL;
  private String sJsSch;
  private String sCl;

  public TestValidatingIO (Encoding enc, int skip, String js, String cls) {
    this.eEnc = enc;
    this.iSkipL = skip;
    this.sJsSch = js;
    this.sCl = cls;
  }
  private static final int COUNT = 1;
  
  @Test
  public void testMain() throws IOException {
    for (int i = 0; i < COUNT; i++) {
      testOnce(new Schema.Parser().parse(sJsSch), sCl, iSkipL, eEnc);
    }
  }

  private void testOnce(Schema schema, String calls,
      int skipLevel,
      Encoding encoding)
    throws IOException {
    Object[] values = randomValues(calls);
    print(eEnc, iSkipL, schema, schema, values, values);
    byte[] bytes = make(schema, calls, values, encoding);
    check(schema, bytes, calls, values, skipLevel, encoding);
  }

  public static byte[] make(Schema sc, String calls,
      Object[] values, Encoding encoding) throws IOException {
    EncoderFactory factory = EncoderFactory.get();
    ByteArrayOutputStream ba = new ByteArrayOutputStream();
    Encoder bvo = null;
    switch (encoding) {
    case BINARY:
      bvo = factory.binaryEncoder(ba, null);
      break;
    case BLOCKING_BINARY:
      bvo = factory.blockingBinaryEncoder(ba, null);
      break;
    case JSON:
      bvo = factory.jsonEncoder(sc, ba);
      break;
    }
        
    Encoder vo = factory.validatingEncoder(sc, bvo);
    generate(vo, calls, values);
    vo.flush();
    return ba.toByteArray();
  }

  public static class InputScanner {
    private final char[] chars;
    private int cpos = 0;
    
    public InputScanner(char[] chars) {
      this.chars = chars;
    }
    
    public boolean next() {
      if (cpos < chars.length) {
        cpos++;
      }
      return cpos != chars.length;
    }
    
    public char cur() {
      return chars[cpos];
    }
    
    public boolean isDone() {
      return cpos == chars.length;
    }
  }
  public static void generate(Encoder vw, String calls,
      Object[] values) throws IOException {
    InputScanner cs = new InputScanner(calls.toCharArray());
    int p = 0;
    while (! cs.isDone()) {
      char c = cs.cur();
      cs.next();
      switch (c) {
      case 'N':
        vw.writeNull();
        break;
      case 'B':
        boolean b = (Boolean) values[p++];
        vw.writeBoolean(b);
        break;
      case 'I':
        int ii = (Integer) values[p++];
        vw.writeInt(ii);
        break;
      case 'L':
        long l = (Long) values[p++];
        vw.writeLong(l);
        break;
      case 'F':
        float f = (Float) values[p++];
        vw.writeFloat(f);
        break;
      case 'D':
        double d = (Double) values[p++];
        vw.writeDouble(d);
        break;
      case 'S':
        {
          extractInt(cs);
          String s = (String) values[p++];
          vw.writeString(new Utf8(s));
          break;
        }
      case 'K':
      {
        extractInt(cs);
        String s = (String) values[p++];
        vw.writeString(s);
        break;
      }
      case 'b':
        {
          extractInt(cs);
          byte[] bb = (byte[]) values[p++];
          vw.writeBytes(bb);
          break;
        }
      case 'f':
        {
          extractInt(cs);
          byte[] bb = (byte[]) values[p++];
          vw.writeFixed(bb);
          break;
        }
      case 'e':
        {
          int e = extractInt(cs);
          vw.writeEnum(e);
          break;
        }
      case '[':
        vw.writeArrayStart();
        break;
      case ']':
        vw.writeArrayEnd();
        break;
      case '{':
        vw.writeMapStart();
        break;
      case '}':
        vw.writeMapEnd();
        break;
      case 'c':
        vw.setItemCount(extractInt(cs));
        break;
      case 's':
        vw.startItem();
        break;
      case 'U':
        {
          vw.writeIndex(extractInt(cs));
          break;
        }
      default:
        fail();
        break;
      }
    }
  }

  public static Object[] randomValues(String calls) {
    Random r = new Random(0L);
    InputScanner cs = new InputScanner(calls.toCharArray());
    List<Object> result = new ArrayList<Object>();
    while (! cs.isDone()) {
      char c = cs.cur();
      cs.next();
      switch (c) {
      case 'N':
        break;
      case 'B':
        result.add(r.nextBoolean());
        break;
      case 'I':
        result.add(r.nextInt());
        break;
      case 'L':
        result.add(new Long(r.nextInt()));
        break;
      case 'F':
        result.add(new Float(r.nextInt()));
        break;
      case 'D':
        result.add(new Double(r.nextInt()));
        break;
      case 'S':
      case 'K':
        result.add(nextString(r, extractInt(cs)));
        break;
      case 'b':
      case 'f':
        result.add(nextBytes(r, extractInt(cs)));
        break;
      case 'e':
      case 'c':
      case 'U':
        extractInt(cs);
      case '[':
      case ']':
      case '{':
      case '}':
      case 's':
        break;
      default:
        fail();
        break;
      }
    }
    return result.toArray();
  }

  private static int extractInt(InputScanner sc) {
    int r = 0;
    while (! sc.isDone()) {
      if (Character.isDigit(sc.cur())) {
        r = r * 10 + sc.cur() - '0';
        sc.next();
      } else {
        break;
      }
    }
    return r;
  }

  private static byte[] nextBytes(Random r, int length) {
    byte[] bb = new byte[length];
    r.nextBytes(bb);
    return bb;
  }

  private static String nextString(Random r, int length) {
    char[] cc = new char[length];
    for (int i = 0; i < length; i++) {
      cc[i] = (char) ('A' + r.nextInt(26));
    }
    return new String(cc);
  }

  private static void check(Schema sc, byte[] bytes, String calls,
      Object[] values, final int skipLevel, Encoding encoding)
    throws IOException {
    // dump(bytes);
    // System.out.println(new String(bytes, "UTF-8"));
    Decoder bvi = null;
    switch (encoding) {
    case BINARY:
    case BLOCKING_BINARY:
      bvi = DecoderFactory.get().binaryDecoder(bytes, null);
      break;
    case JSON:
      InputStream in = new ByteArrayInputStream(bytes);
      bvi = new JsonDecoder(sc, in);
    }
    Decoder vi = new ValidatingDecoder(sc, bvi);
    check(vi, calls, values, skipLevel);
  }
  
  public static void check(Decoder vi, String calls,
      Object[] values, final int skipLevel) throws IOException {
    InputScanner cs = new InputScanner(calls.toCharArray());
    int p = 0;
    int level = 0;
    long[] counts = new long[100];
    boolean[] isArray = new boolean[100];
    boolean[] isEmpty = new boolean[100];
    while (! cs.isDone()) {
      final char c = cs.cur();
      cs.next();
      switch (c) {
      case 'N':
        vi.readNull();
        break;
      case 'B':
        assertEquals(values[p++], vi.readBoolean());
        break;
      case 'I':
        assertEquals(values[p++], vi.readInt());
        break;
      case 'L':
        assertEquals(values[p++], vi.readLong());
        break;
      case 'F':
        if (!(values[p] instanceof Float)) fail();
        float f = (Float) values[p++];
        assertEquals(f, vi.readFloat(), Math.abs(f / 1000));
        break;
      case 'D':
        if (!(values[p] instanceof Double)) fail();
        double d = (Double) values[p++];
        assertEquals(d, vi.readDouble(), Math.abs(d / 1000));
        break;
      case 'S':
        extractInt(cs);
        if (level == skipLevel) {
          vi.skipString();
          p++;
        } else {
          String s = (String) values[p++];
          assertEquals(new Utf8(s), vi.readString(null));
        }
        break;
      case 'K':
        extractInt(cs);
        if (level == skipLevel) {
          vi.skipString();
          p++;
        } else {
          String s = (String) values[p++];
          assertEquals(new Utf8(s), vi.readString(null));
        }
        break;
      case 'b':
        extractInt(cs);
        if (level == skipLevel) {
          vi.skipBytes();
          p++;
        } else {
          byte[] bb = (byte[]) values[p++];
          ByteBuffer bb2 = vi.readBytes(null);
          byte[] actBytes = new byte[bb2.remaining()];
          System.arraycopy(bb2.array(), bb2.position(), actBytes,
              0, bb2.remaining());
          assertArrayEquals(bb, actBytes);
        }
        break;
      case 'f':
        {
          int len = extractInt(cs);
          if (level == skipLevel) {
            vi.skipFixed(len);
            p++;
          } else {
            byte[] bb = (byte[]) values[p++];
            byte[] actBytes = new byte[len];
            vi.readFixed(actBytes);
            assertArrayEquals(bb, actBytes);
          }
        }
        break;
      case 'e':
      {
        int e = extractInt(cs);
        if (level == skipLevel) {
          vi.readEnum();
        } else {
          assertEquals(e, vi.readEnum());
        }
      }
      break;
      case '[':
        if (level == skipLevel) {
          p += skip(cs, vi, true);
          break;
        } else {
          level++;
          counts[level] = vi.readArrayStart();
          isArray[level] = true;
          isEmpty[level] = counts[level] == 0;
          continue;
        }
      case '{':
        if (level == skipLevel) {
          p += skip(cs, vi, false);
          break;
        } else {
          level++;
          counts[level] = vi.readMapStart();
          isArray[level] = false;
          isEmpty[level] = counts[level] == 0;
          continue;
        }
      case ']':
        assertEquals(0, counts[level]);
        if (! isEmpty[level]) {
          assertEquals(0, vi.arrayNext());
        }
        level--;
        break;
      case '}':
        assertEquals(0, counts[level]);
        if (! isEmpty[level]) {
          assertEquals(0, vi.mapNext());
        }
        level--;
        break;
      case 's':
        if (counts[level] == 0) {
          if (isArray[level]) {
            counts[level] = vi.arrayNext();
          } else {
            counts[level] = vi.mapNext();
          }
        }
        counts[level]--;
        continue;
      case 'c':
        extractInt(cs);
        continue;
      case 'U':
        {
          int idx = extractInt(cs);
          assertEquals(idx, vi.readIndex());
          continue;
        }
      case 'R':
          ((ResolvingDecoder) vi).readFieldOrder();
          continue;
      default:
        fail();
      }
    }
    assertEquals(values.length, p);
  }

  private static int skip(InputScanner cs, Decoder vi, boolean isArray)
    throws IOException {
    final char end = isArray ? ']' : '}';
    if (isArray) {
      assertEquals(0, vi.skipArray());
    } else if (end == '}'){
      assertEquals(0, vi.skipMap());
    }
    int level = 0;
    int p = 0;
    while (! cs.isDone()) {
      char c = cs.cur();
      cs.next();
      switch (c) {
      case '[':
      case '{':
        ++level;
        break;
      case ']':
      case '}':
        if (c == end && level == 0) {
          return p;
        }
        level--;
        break;
      case 'B':
      case 'I':
      case 'L':
      case 'F':
      case 'D':
      case 'S':
      case 'K':
      case 'b':
      case 'f':
      case 'e':
        p++;
        break;
      }
    }
    throw new RuntimeException("Don't know how to skip");
  }

  @Parameterized.Parameters
  public static Collection<Object[]> data() {
    return Arrays.asList(convertTo2dArray(encodings, skipLevels, testSchemas()));
  }
  
  private static Object[][] encodings = new Object[][] {
      { Encoding.BINARY }, { Encoding.BLOCKING_BINARY },
      { Encoding.JSON }
    }; 

  private static Object[][] skipLevels = new Object[][] {
      { -1 }, { 0 }, { 1 }, { 2 },
  };
  
  public static Object[][] convertTo2dArray(final Object[][]... values) {
    ArrayList<Object[]> ret = new ArrayList<Object[]>();

    Iterator<Object[]> iter = cartesian(values);
    while (iter.hasNext()) {
      Object[] objects = iter.next();
      ret.add(objects);
    }
    Object[][] retArrays = new Object[ret.size()][];
    for (int i = 0; i < ret.size(); i++) {
      retArrays[i] = ret.get(i);
    }
    return retArrays;
  }
  /**
   * Returns the Cartesian product of input sequences.
   */
  public static Iterator<Object[]> cartesian(final Object[][]... values) {
    return new Iterator<Object[]>() {
      private int[] pos = new int[values.length];
      @Override
      public boolean hasNext() {
        return pos[0] < values[0].length;
      }

      @Override
      public Object[] next() {
        Object[][] v = new Object[values.length][];
        for (int i = 0; i < v.length; i++) {
          v[i] = values[i][pos[i]];
        }
        for (int i = v.length - 1; i >= 0; i--) {
          if (++pos[i] == values[i].length) {
            if (i != 0) {
              pos[i] = 0;
            }
          } else {
            break;
          }
        }
        return concat(v);
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }
  
  /**
   * Concatenates the input sequences in order and forms a longer sequence.
   */
  public static Object[] concat(Object[]... oo) {
    int l = 0;
    for (Object[] o : oo) {
      l += o.length;
    }
    Object[] result = new Object[l];
    l = 0;
    for (Object[] o : oo) {
      System.arraycopy(o, 0, result, l, o.length);
      l += o.length;
    }
    return result;
  }

  /**
   * Pastes incoming tables to form a wider table. All incoming tables
   * should be of same height.
   */
  static Object[][] paste(Object[][]... in) {
    Object[][] result = new Object[in[0].length][];
    Object[][] cc = new Object[in.length][];
    for (int i = 0; i < result.length; i++) {
      for (int j = 0; j < cc.length; j++) {
        cc[j] = in[j][i];
      }
      result[i] = concat(cc);
    }
    return result;
  }

  public static Object[][] testSchemas() {
    /**
     * The first argument is a schema.
     * The second one is a sequence of (single character) mnemonics:
     * N  null
     * B  boolean
     * I  int
     * L  long
     * F  float
     * D  double
     * K followed by integer - key-name (and its length) in a map
     * S followed by integer - string and its length
     * b followed by integer - bytes and length
     * f followed by integer - fixed and length
     * c  Number of items to follow in an array/map.
     * U followed by integer - Union and its branch
     * e followed by integer - Enum and its value
     * [  Start array
     * ]  End array
     * {  Start map
     * }  End map
     * s  start item
     */
    return new Object[][] {
        { "\"null\"", "N" },
        { "\"boolean\"", "B" },
        { "\"int\"", "I" },
        { "\"long\"", "L" },
        { "\"float\"", "F" },
        { "\"double\"", "D" },
        { "\"string\"", "S0" },
        { "\"string\"", "S10" },
        { "\"bytes\"", "b0" },
        { "\"bytes\"", "b10" },
        { "{\"type\":\"fixed\", \"name\":\"fi\", \"size\": 1}", "f1" },
        { "{\"type\":\"fixed\", \"name\":\"fi\", \"size\": 10}", "f10" },
        { "{\"type\":\"enum\", \"name\":\"en\", \"symbols\":[\"v1\", \"v2\"]}",
            "e1" },

        { "{\"type\":\"array\", \"items\": \"boolean\"}", "[]", },
        { "{\"type\":\"array\", \"items\": \"int\"}", "[]", },
        { "{\"type\":\"array\", \"items\": \"long\"}", "[]", },
        { "{\"type\":\"array\", \"items\": \"float\"}", "[]", },
        { "{\"type\":\"array\", \"items\": \"double\"}", "[]", },
        { "{\"type\":\"array\", \"items\": \"string\"}", "[]", },
        { "{\"type\":\"array\", \"items\": \"bytes\"}", "[]", },
        { "{\"type\":\"array\", \"items\":{\"type\":\"fixed\", "
          + "\"name\":\"fi\", \"size\": 10}}", "[]" },

        { "{\"type\":\"array\", \"items\": \"boolean\"}", "[c1sB]" },
        { "{\"type\":\"array\", \"items\": \"int\"}", "[c1sI]" },
        { "{\"type\":\"array\", \"items\": \"long\"}", "[c1sL]" },
        { "{\"type\":\"array\", \"items\": \"float\"}", "[c1sF]" },
        { "{\"type\":\"array\", \"items\": \"double\"}", "[c1sD]" },
        { "{\"type\":\"array\", \"items\": \"string\"}", "[c1sS10]" },
        { "{\"type\":\"array\", \"items\": \"bytes\"}", "[c1sb10]" },
        { "{\"type\":\"array\", \"items\": \"int\"}", "[c1sIc1sI]" },
        { "{\"type\":\"array\", \"items\": \"int\"}", "[c2sIsI]" },
        { "{\"type\":\"array\", \"items\":{\"type\":\"fixed\", "
          + "\"name\":\"fi\", \"size\": 10}}", "[c2sf10sf10]" },

        { "{\"type\":\"map\", \"values\": \"boolean\"}", "{}" },
        { "{\"type\":\"map\", \"values\": \"int\"}", "{}" },
        { "{\"type\":\"map\", \"values\": \"long\"}", "{}" },
        { "{\"type\":\"map\", \"values\": \"float\"}", "{}" },
        { "{\"type\":\"map\", \"values\": \"double\"}", "{}" },
        { "{\"type\":\"map\", \"values\": \"string\"}", "{}" },
        { "{\"type\":\"map\", \"values\": \"bytes\"}", "{}" },
        { "{\"type\":\"map\", \"values\": "
          + "{\"type\":\"array\", \"items\":\"int\"}}", "{}" },

        { "{\"type\":\"map\", \"values\": \"boolean\"}", "{c1sK5B}" },
        { "{\"type\":\"map\", \"values\": \"int\"}", "{c1sK5I}" },
        { "{\"type\":\"map\", \"values\": \"long\"}", "{c1sK5L}" },
        { "{\"type\":\"map\", \"values\": \"float\"}", "{c1sK5F}" },
        { "{\"type\":\"map\", \"values\": \"double\"}", "{c1sK5D}" },
        { "{\"type\":\"map\", \"values\": \"string\"}", "{c1sK5S10}" },
        { "{\"type\":\"map\", \"values\": \"bytes\"}", "{c1sK5b10}" },
        { "{\"type\":\"map\", \"values\": "
          + "{\"type\":\"array\", \"items\":\"int\"}}", "{c1sK5[c3sIsIsI]}" },

        { "{\"type\":\"map\", \"values\": \"boolean\"}",
            "{c1sK5Bc2sK5BsK5B}" },

        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"boolean\"}]}", "B" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"int\"}]}", "I" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"long\"}]}", "L" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"float\"}]}", "F" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"double\"}]}", "D" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"string\"}]}", "S10" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f\", \"type\":\"bytes\"}]}", "b10" },

        // multi-field records
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"int\"},"
          + "{\"name\":\"f2\", \"type\":\"double\"},"
          + "{\"name\":\"f3\", \"type\":\"string\"}]}", "IDS10" },
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f0\", \"type\":\"null\"},"
          + "{\"name\":\"f1\", \"type\":\"boolean\"},"
          + "{\"name\":\"f2\", \"type\":\"int\"},"
          + "{\"name\":\"f3\", \"type\":\"long\"},"
          + "{\"name\":\"f4\", \"type\":\"float\"},"
          + "{\"name\":\"f5\", \"type\":\"double\"},"
          + "{\"name\":\"f6\", \"type\":\"string\"},"
          + "{\"name\":\"f7\", \"type\":\"bytes\"}]}",
            "NBILFDS10b25" },
        
        // record of records
        { "{\"type\":\"record\",\"name\":\"outer\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":{\"type\":\"record\", "
          + "\"name\":\"inner\", \"fields\":["
          + "{\"name\":\"g1\", \"type\":\"int\"}, {\"name\":\"g2\", "
          + "\"type\":\"double\"}]}},"
          + "{\"name\":\"f2\", \"type\":\"string\"},"
          + "{\"name\":\"f3\", \"type\":\"inner\"}]}",
          "IDS10ID" },
        // record with array
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"long\"},"
          + "{\"name\":\"f2\", "
          + "\"type\":{\"type\":\"array\", \"items\":\"int\"}}]}",
          "L[c1sI]" },

        // record with map
        { "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"long\"},"
          + "{\"name\":\"f2\", "
          + "\"type\":{\"type\":\"map\", \"values\":\"int\"}}]}",
          "L{c1sK5I}" },

        // array of records
        { "{\"type\":\"array\", \"items\":"
            + "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"long\"},"
          + "{\"name\":\"f2\", \"type\":\"null\"}]}}",
            "[c2sLNsLN]" },

        { "{\"type\":\"array\", \"items\":"
            + "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"long\"},"
          + "{\"name\":\"f2\", "
          + "\"type\":{\"type\":\"array\", \"items\":\"int\"}}]}}",
            "[c2sL[c1sI]sL[c2sIsI]]" },
        { "{\"type\":\"array\", \"items\":"
            + "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"long\"},"
          + "{\"name\":\"f2\", "
          + "\"type\":{\"type\":\"map\", \"values\":\"int\"}}]}}",
            "[c2sL{c1sK5I}sL{c2sK5IsK5I}]" },
        { "{\"type\":\"array\", \"items\":"
            + "{\"type\":\"record\",\"name\":\"r\",\"fields\":["
          + "{\"name\":\"f1\", \"type\":\"long\"},"
          + "{\"name\":\"f2\", "
          + "\"type\":[\"null\", \"int\"]}]}}",
            "[c2sLU0NsLU1I]" },

        { "[\"boolean\"]", "U0B" },
        { "[\"int\"]", "U0I" },
        { "[\"long\"]", "U0L" },
        { "[\"float\"]", "U0F" },
        { "[\"double\"]", "U0D" },
        { "[\"string\"]", "U0S10" },
        { "[\"bytes\"]", "U0b10" },

        { "[\"null\", \"int\"]", "U0N" },
        { "[\"boolean\", \"int\"]", "U0B" },
        { "[\"boolean\", \"int\"]", "U1I" },
        { "[\"boolean\", {\"type\":\"array\", \"items\":\"int\"} ]",
          "U0B" },

        { "[\"boolean\", {\"type\":\"array\", \"items\":\"int\"} ]",
            "U1[c1sI]" },
          
        // Recursion
        { "{\"type\": \"record\", \"name\": \"Node\", \"fields\": ["
          + "{\"name\":\"label\", \"type\":\"string\"},"
          + "{\"name\":\"children\", \"type\":"
          + "{\"type\": \"array\", \"items\": \"Node\" }}]}",
          "S10[c1sS10[]]" },
          
        { "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": ["
          + "{\"name\":\"value\", \"type\":[\"null\", \"string\","
          + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": ["
          + "{\"name\":\"car\", \"type\":\"Lisp\"},"
          + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}",
          "U0N"},
        { "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": ["
          + "{\"name\":\"value\", \"type\":[\"null\", \"string\","
          + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": ["
          + "{\"name\":\"car\", \"type\":\"Lisp\"},"
          + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}",
          "U1S10"},
        { "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": ["
          + "{\"name\":\"value\", \"type\":[\"null\", \"string\","
          + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": ["
          + "{\"name\":\"car\", \"type\":\"Lisp\"},"
          + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}",
          "U2U1S10U0N"},
          
        // Deep recursion
        { "{\"type\": \"record\", \"name\": \"Node\", \"fields\": ["
          + "{\"name\":\"children\", \"type\":"
          + "{\"type\": \"array\", \"items\": \"Node\" }}]}",
          "[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[]]]]]]]]]]]]" },
              
    };
  }
  
  static void dump(byte[] bb) {
    int col = 0;
    for (byte b : bb) {
      if (col % 16 == 0) {
        System.out.println();
      }
      col++;
      System.out.print(Integer.toHexString(b & 0xff) + " ");
    }
    System.out.println();
  }

  static void print(Encoding encoding, int skipLevel, Schema writerSchema, 
      Schema readerSchema, Object[] writtenValues, Object[] expectedValues) {
    LOG.debug("{} Skip Level {}", encoding, skipLevel); 
    printSchemaAndValues("Writer", writerSchema, writtenValues);
    printSchemaAndValues("Reader", readerSchema, expectedValues);
  }

  private static void printSchemaAndValues(String schemaType, Schema schema, Object[] values) {
    LOG.debug("{} Schema {}", schemaType, schema); 
    for (Object value : values) {
      LOG.debug("{} -> {}", value, value.getClass().getSimpleName());
    }
  }  
}
