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

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

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

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hadoop.io.DataInputBuffer;
import org.apache.hadoop.io.DataOutputBuffer;
import org.apache.hadoop.io.WritableComparator;
import org.apache.hadoop.io.WritableUtils;

public class TestGridmixRecord {
  private static final Logger LOG = LoggerFactory.getLogger(TestGridmixRecord.class);

  static void lengthTest(GridmixRecord x, GridmixRecord y, int min,
      int max) throws Exception {
    final Random r = new Random();
    final long seed = r.nextLong();
    r.setSeed(seed);
    LOG.info("length: " + seed);
    final DataInputBuffer in = new DataInputBuffer();
    final DataOutputBuffer out1 = new DataOutputBuffer();
    final DataOutputBuffer out2 = new DataOutputBuffer();
    for (int i = min; i < max; ++i) {
      setSerialize(x, r.nextLong(), i, out1);
      // check write
      assertEquals(i, out1.getLength());
      // write to stream
      x.write(out2);
      // check read
      in.reset(out1.getData(), 0, out1.getLength());
      y.readFields(in);
      assertEquals(i, x.getSize());
      assertEquals(i, y.getSize());
    }
    // check stream read
    in.reset(out2.getData(), 0, out2.getLength());
    for (int i = min; i < max; ++i) {
      y.readFields(in);
      assertEquals(i, y.getSize());
    }
  }

  static void randomReplayTest(GridmixRecord x, GridmixRecord y, int min,
      int max) throws Exception {
    final Random r = new Random();
    final long seed = r.nextLong();
    r.setSeed(seed);
    LOG.info("randReplay: " + seed);
    final DataOutputBuffer out1 = new DataOutputBuffer();
    for (int i = min; i < max; ++i) {
      final int s = out1.getLength();
      x.setSeed(r.nextLong());
      x.setSize(i);
      x.write(out1);
      assertEquals(i, out1.getLength() - s);
    }
    final DataInputBuffer in = new DataInputBuffer();
    in.reset(out1.getData(), 0, out1.getLength());
    final DataOutputBuffer out2 = new DataOutputBuffer();
    // deserialize written records, write to separate buffer
    for (int i = min; i < max; ++i) {
      final int s = in.getPosition();
      y.readFields(in);
      assertEquals(i, in.getPosition() - s);
      y.write(out2);
    }
    // verify written contents match
    assertEquals(out1.getLength(), out2.getLength());
    // assumes that writes will grow buffer deterministically
    assertEquals(out1.getData().length, out2.getData().length, "Bad test");
    assertArrayEquals(out1.getData(), out2.getData());
  }

  static void eqSeedTest(GridmixRecord x, GridmixRecord y, int max)
      throws Exception {
    final Random r = new Random();
    final long s = r.nextLong();
    r.setSeed(s);
    LOG.info("eqSeed: " + s);
    assertEquals(x.fixedBytes(), y.fixedBytes());
    final int min = x.fixedBytes() + 1;
    final DataOutputBuffer out1 = new DataOutputBuffer();
    final DataOutputBuffer out2 = new DataOutputBuffer();
    for (int i = min; i < max; ++i) {
      final long seed = r.nextLong();
      setSerialize(x, seed, i, out1);
      setSerialize(y, seed, i, out2);
      assertEquals(x, y);
      assertEquals(x.hashCode(), y.hashCode());

      // verify written contents match
      assertEquals(out1.getLength(), out2.getLength());
      // assumes that writes will grow buffer deterministically
      assertEquals(out1.getData().length, out2.getData().length, "Bad test");
      assertArrayEquals(out1.getData(), out2.getData());
    }
  }

  static void binSortTest(GridmixRecord x, GridmixRecord y, int min,
      int max, WritableComparator cmp) throws Exception {
    final Random r = new Random();
    final long s = r.nextLong();
    r.setSeed(s);
    LOG.info("sort: " + s);
    final DataOutputBuffer out1 = new DataOutputBuffer();
    final DataOutputBuffer out2 = new DataOutputBuffer();
    for (int i = min; i < max; ++i) {
      final long seed1 = r.nextLong();
      setSerialize(x, seed1, i, out1);
      assertEquals(0, x.compareSeed(seed1, Math.max(0, i - x.fixedBytes())));

      final long seed2 = r.nextLong();
      setSerialize(y, seed2, i, out2);
      assertEquals(0, y.compareSeed(seed2, Math.max(0, i - x.fixedBytes())));

      // for eq sized records, ensure byte cmp where req
      final int chk = WritableComparator.compareBytes(
          out1.getData(), 0, out1.getLength(),
          out2.getData(), 0, out2.getLength());
      assertEquals(Integer.signum(chk), Integer.signum(x.compareTo(y)));
      assertEquals(Integer.signum(chk), Integer.signum(cmp.compare(
            out1.getData(), 0, out1.getLength(),
            out2.getData(), 0, out2.getLength())));
      // write second copy, compare eq
      final int s1 = out1.getLength();
      x.write(out1);
      assertEquals(0, cmp.compare(out1.getData(), 0, s1,
            out1.getData(), s1, out1.getLength() - s1));
      final int s2 = out2.getLength();
      y.write(out2);
      assertEquals(0, cmp.compare(out2.getData(), 0, s2,
            out2.getData(), s2, out2.getLength() - s2));
      assertEquals(Integer.signum(chk), Integer.signum(cmp.compare(out1.getData(), 0, s1,
            out2.getData(), s2, out2.getLength() - s2)));
    }
  }

  static void checkSpec(GridmixKey a, GridmixKey b) throws Exception {
    final Random r = new Random();
    final long s = r.nextLong();
    r.setSeed(s);
    LOG.info("spec: " + s);
    final DataInputBuffer in = new DataInputBuffer();
    final DataOutputBuffer out = new DataOutputBuffer();
    a.setType(GridmixKey.REDUCE_SPEC);
    b.setType(GridmixKey.REDUCE_SPEC);
    for (int i = 0; i < 100; ++i) {
      final int in_rec = r.nextInt(Integer.MAX_VALUE);
      a.setReduceInputRecords(in_rec);
      final int out_rec = r.nextInt(Integer.MAX_VALUE);
      a.setReduceOutputRecords(out_rec);
      final int out_bytes = r.nextInt(Integer.MAX_VALUE);
      a.setReduceOutputBytes(out_bytes);
      final int min = WritableUtils.getVIntSize(in_rec)
                    + WritableUtils.getVIntSize(out_rec)
                    + WritableUtils.getVIntSize(out_bytes)
                    + WritableUtils.getVIntSize(0);
      assertEquals(min + 2, a.fixedBytes()); // meta + vint min
      final int size = r.nextInt(1024) + a.fixedBytes() + 1;
      setSerialize(a, r.nextLong(), size, out);
      assertEquals(size, out.getLength());
      assertTrue(a.equals(a));
      assertEquals(0, a.compareTo(a));

      in.reset(out.getData(), 0, out.getLength());

      b.readFields(in);
      assertEquals(size, b.getSize());
      assertEquals(in_rec, b.getReduceInputRecords());
      assertEquals(out_rec, b.getReduceOutputRecords());
      assertEquals(out_bytes, b.getReduceOutputBytes());
      assertTrue(a.equals(b));
      assertEquals(0, a.compareTo(b));
      assertEquals(a.hashCode(), b.hashCode());
    }
  }

  static void setSerialize(GridmixRecord x, long seed, int size,
      DataOutputBuffer out) throws IOException {
    x.setSeed(seed);
    x.setSize(size);
    out.reset();
    x.write(out);
  }

  @Test
  public void testKeySpec() throws Exception {
    final int min = 6;
    final int max = 300;
    final GridmixKey a = new GridmixKey(GridmixKey.REDUCE_SPEC, 1, 0L);
    final GridmixKey b = new GridmixKey(GridmixKey.REDUCE_SPEC, 1, 0L);
    lengthTest(a, b, min, max);
    randomReplayTest(a, b, min, max);
    binSortTest(a, b, min, max, new GridmixKey.Comparator());
    // 2 fixed GR bytes, 1 type, 3 spec
    eqSeedTest(a, b, max);
    checkSpec(a, b);
  }

  @Test
  public void testKeyData() throws Exception {
    final int min = 2;
    final int max = 300;
    final GridmixKey a = new GridmixKey(GridmixKey.DATA, 1, 0L);
    final GridmixKey b = new GridmixKey(GridmixKey.DATA, 1, 0L);
    lengthTest(a, b, min, max);
    randomReplayTest(a, b, min, max);
    binSortTest(a, b, min, max, new GridmixKey.Comparator());
    // 2 fixed GR bytes, 1 type
    eqSeedTest(a, b, 300);
  }

  @Test
  public void testBaseRecord() throws Exception {
    final int min = 1;
    final int max = 300;
    final GridmixRecord a = new GridmixRecord();
    final GridmixRecord b = new GridmixRecord();
    lengthTest(a, b, min, max);
    randomReplayTest(a, b, min, max);
    binSortTest(a, b, min, max, new GridmixRecord.Comparator());
    // 2 fixed GR bytes
    eqSeedTest(a, b, 300);
  }

  public static void main(String[] argv) throws Exception {
    boolean fail = false;
    final TestGridmixRecord test = new TestGridmixRecord();
    try { test.testKeySpec(); } catch (Exception e) {
      fail = true;
      e.printStackTrace();
    }
    try {test.testKeyData(); } catch (Exception e) {
      fail = true;
      e.printStackTrace();
    }
    try {test.testBaseRecord(); } catch (Exception e) {
      fail = true;
      e.printStackTrace();
    }
    System.exit(fail ? -1 : 0);
  }

  static void printDebug(GridmixRecord a, GridmixRecord b) throws IOException {
    DataOutputBuffer out = new DataOutputBuffer();
    a.write(out);
    System.out.println("A " +
        Arrays.toString(Arrays.copyOf(out.getData(), out.getLength())));
    out.reset();
    b.write(out);
    System.out.println("B " +
        Arrays.toString(Arrays.copyOf(out.getData(), out.getLength())));
  }

}
