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

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Random;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.runners.Parameterized.Parameters;

public class TestColumnFile {

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

  @Parameters
  public static Stream<Arguments> codecs() {
    return Stream.of(Arguments.of(createFileMeta("null", "null")), Arguments.of(createFileMeta("snappy", "crc32")),
        Arguments.of(createFileMeta("deflate", "crc32")));
  }

  private static ColumnFileMetaData createFileMeta(String codec, String checksum) {
    return new ColumnFileMetaData().setCodec(codec).setChecksum(checksum);
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void emptyFile(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();
    ColumnFileWriter out = new ColumnFileWriter(fileMeta);
    out.writeTo(FILE);
    ColumnFileReader in = new ColumnFileReader(FILE);
    Assertions.assertEquals(0, in.getRowCount());
    Assertions.assertEquals(0, in.getColumnCount());
    in.close();
  }

  /** Byte offset of the little-endian 4-byte columnCount field in the header. */
  private static final int COLUMN_COUNT_OFFSET = ColumnFileWriter.MAGIC.length + Long.BYTES; // MAGIC + rowCount fixed64

  /**
   * A header column count larger than the data present (from a malformed,
   * corrupted, or truncated file) must be rejected before allocating, rather than
   * attempting an oversized allocation or failing later.
   */
  @Test
  void oversizedColumnCountIsRejected() throws Exception {
    FILE.delete();
    // A valid, minimal file (no columns) written by Trevni's own writer.
    new ColumnFileWriter(new ColumnFileMetaData()).writeTo(FILE);

    // Overwrite the columnCount field with Integer.MAX_VALUE.
    try (RandomAccessFile raf = new RandomAccessFile(FILE, "rw")) {
      raf.seek(COLUMN_COUNT_OFFSET);
      raf.write(0xFF);
      raf.write(0xFF);
      raf.write(0xFF);
      raf.write(0x7F);
    }

    IOException e = Assertions.assertThrows(IOException.class, () -> new ColumnFileReader(FILE).close());
    Assertions.assertNotNull(e.getMessage());
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void emptyColumn(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();
    ColumnFileWriter out = new ColumnFileWriter(fileMeta, new ColumnMetaData("test", ValueType.INT));
    out.writeTo(FILE);
    ColumnFileReader in = new ColumnFileReader(FILE);
    Assertions.assertEquals(0, in.getRowCount());
    Assertions.assertEquals(1, in.getColumnCount());
    ColumnValues<Integer> values = in.getValues("test");
    for (int i : values)
      throw new Exception("no value should be found");
    in.close();
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void ints(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();

    ColumnFileWriter out = new ColumnFileWriter(fileMeta, new ColumnMetaData("test", ValueType.INT));
    Random random = TestUtil.createRandom();
    for (int i = 0; i < COUNT; i++)
      out.writeRow(TestUtil.randomLength(random));
    out.writeTo(FILE);

    random = TestUtil.createRandom();
    ColumnFileReader in = new ColumnFileReader(FILE);
    Assertions.assertEquals(COUNT, in.getRowCount());
    Assertions.assertEquals(1, in.getColumnCount());
    Iterator<Integer> i = in.getValues("test");
    int count = 0;
    while (i.hasNext()) {
      Assertions.assertEquals(TestUtil.randomLength(random), (int) i.next());
      count++;
    }
    Assertions.assertEquals(COUNT, count);
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void longs(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();

    ColumnFileWriter out = new ColumnFileWriter(fileMeta, new ColumnMetaData("test", ValueType.LONG));
    Random random = TestUtil.createRandom();
    for (int i = 0; i < COUNT; i++)
      out.writeRow(random.nextLong());
    out.writeTo(FILE);

    random = TestUtil.createRandom();
    ColumnFileReader in = new ColumnFileReader(FILE);
    Assertions.assertEquals(COUNT, in.getRowCount());
    Assertions.assertEquals(1, in.getColumnCount());
    Iterator<Long> i = in.getValues("test");
    int count = 0;
    while (i.hasNext()) {
      Assertions.assertEquals(random.nextLong(), (long) i.next());
      count++;
    }
    Assertions.assertEquals(COUNT, count);
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void strings(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();

    ColumnFileWriter out = new ColumnFileWriter(fileMeta, new ColumnMetaData("test", ValueType.STRING));
    Random random = TestUtil.createRandom();
    for (int i = 0; i < COUNT; i++)
      out.writeRow(TestUtil.randomString(random));
    out.writeTo(FILE);

    random = TestUtil.createRandom();
    ColumnFileReader in = new ColumnFileReader(FILE);
    Assertions.assertEquals(COUNT, in.getRowCount());
    Assertions.assertEquals(1, in.getColumnCount());
    Iterator<String> i = in.getValues("test");
    int count = 0;
    while (i.hasNext()) {
      Assertions.assertEquals(TestUtil.randomString(random), i.next());
      count++;
    }
    Assertions.assertEquals(COUNT, count);
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void twoColumn(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();
    ColumnFileWriter out = new ColumnFileWriter(fileMeta, new ColumnMetaData("a", ValueType.FIXED32),
        new ColumnMetaData("b", ValueType.STRING));
    Random random = TestUtil.createRandom();
    for (int i = 0; i < COUNT; i++)
      out.writeRow(random.nextInt(), TestUtil.randomString(random));
    out.writeTo(FILE);

    random = TestUtil.createRandom();
    ColumnFileReader in = new ColumnFileReader(FILE);
    Assertions.assertEquals(COUNT, in.getRowCount());
    Assertions.assertEquals(2, in.getColumnCount());
    Iterator<String> i = in.getValues("a");
    Iterator<String> j = in.getValues("b");
    int count = 0;
    while (i.hasNext() && j.hasNext()) {
      Assertions.assertEquals(random.nextInt(), i.next());
      Assertions.assertEquals(TestUtil.randomString(random), j.next());
      count++;
    }
    Assertions.assertEquals(COUNT, count);
  }

  @ParameterizedTest
  @MethodSource("codecs")
  void seekLongs(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();

    ColumnFileWriter out = new ColumnFileWriter(fileMeta, new ColumnMetaData("test", ValueType.LONG));
    Random random = TestUtil.createRandom();

    int seekCount = COUNT / 1024;
    int[] seekRows = new int[seekCount];
    Map<Integer, Integer> seekRowMap = new HashMap<>(seekCount);
    while (seekRowMap.size() < seekCount) {
      int row = random.nextInt(COUNT);
      if (!seekRowMap.containsKey(row)) {
        seekRows[seekRowMap.size()] = row;
        seekRowMap.put(row, seekRowMap.size());
      }
    }

    Long[] seekValues = new Long[seekCount];
    for (int i = 0; i < COUNT; i++) {
      long l = random.nextLong();
      out.writeRow(l);
      if (seekRowMap.containsKey(i))
        seekValues[seekRowMap.get(i)] = l;
    }
    out.writeTo(FILE);

    ColumnFileReader in = new ColumnFileReader(FILE);
    ColumnValues<Long> v = in.getValues("test");

    for (int i = 0; i < seekCount; i++) {
      v.seek(seekRows[i]);
      Assertions.assertEquals(seekValues[i], v.next());
    }

  }

  @ParameterizedTest
  @MethodSource("codecs")
  void seekStrings(ColumnFileMetaData fileMeta) throws Exception {
    FILE.delete();

    ColumnFileWriter out = new ColumnFileWriter(fileMeta,
        new ColumnMetaData("test", ValueType.STRING).hasIndexValues(true));

    Random random = TestUtil.createRandom();

    int seekCount = COUNT / 1024;
    Map<Integer, Integer> seekRowMap = new HashMap<>(seekCount);
    while (seekRowMap.size() < seekCount) {
      int row = random.nextInt(COUNT);
      if (!seekRowMap.containsKey(row))
        seekRowMap.put(row, seekRowMap.size());
    }

    String[] values = new String[COUNT];
    for (int i = 0; i < COUNT; i++)
      values[i] = TestUtil.randomString(random);
    Arrays.sort(values);

    String[] seekValues = new String[seekCount];
    for (int i = 0; i < COUNT; i++) {
      out.writeRow(values[i]);
      if (seekRowMap.containsKey(i))
        seekValues[seekRowMap.get(i)] = values[i];
    }
    out.writeTo(FILE);

    ColumnFileReader in = new ColumnFileReader(FILE);
    ColumnValues<String> v = in.getValues("test");

    for (int i = 0; i < seekCount; i++) {
      v.seek(seekValues[i]);
      Assertions.assertEquals(seekValues[i], v.next());
    }

  }

}
