/*
 * 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.calcite.avatica.util;

import org.apache.calcite.avatica.ColumnMetaData;
import org.apache.calcite.avatica.MetaImpl;
import org.apache.calcite.avatica.util.Cursor.Accessor;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.sql.SQLException;
import java.sql.Struct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static org.junit.Assert.assertArrayEquals;

/**
 * Test class for StructImpl.
 */
@RunWith(Parameterized.class)
public class StructImplTest {

  /**
   * A class holding the necessary information for performing tests related with one table column.
   *
   * @param <T> the type of the expected values for the column
   */
  private static class ColumnInputBundle<T> {
    private final ColumnMetaData metaData;

    /**
     * The input values for the given column.
     *
     * <p>These values are used to construct the rows of a ResultSet.
     */
    private final List<Object> inputValues;

    /**
     * The expected values for the given column.
     *
     * <p>These values are used to verify that the result obtained from a
     * ResultSet is correct. Note that inputValues and expectedValues are not
     * necessarily equal.
     */
    private final List<T> expectedValues;

    ColumnInputBundle(List<Object> inputValues, List<T> expected, ColumnMetaData metaData) {
      this.inputValues = inputValues;
      this.expectedValues = expected;
      this.metaData = metaData;
    }

    @Override public String toString() {
      return "column=" + metaData.columnName + ", inputType=" + inputValues.get(0).getClass();
    }
  }

  private final ColumnInputBundle<Struct> columnInputBundle;

  public StructImplTest(ColumnInputBundle<Struct> columnInputBundle) {
    this.columnInputBundle = columnInputBundle;
  }

  @Parameterized.Parameters(name = "{0}")
  public static Collection<ColumnInputBundle<Struct>> data() throws SQLException {
    List<ColumnInputBundle<Struct>> data = new ArrayList<>();
    final int numRows = 5;

    ColumnMetaData oneAttrStructMeta = MetaImpl.columnMetaData(
        "ONE_ATTRIBUTE_STRUCT",
        0,
        ColumnMetaData.struct(
            Arrays.asList(
                MetaImpl.columnMetaData("INT", 0, int.class, false))),
        false);

    ColumnMetaData twoAttrStructMeta = MetaImpl.columnMetaData(
        "TWO_ATTRIBUTES_STRUCT",
        0,
        ColumnMetaData.struct(
            Arrays.asList(
                MetaImpl.columnMetaData("INT", 0, int.class, false),
                MetaImpl.columnMetaData("INT", 1, int.class, false))),
        false);

    List<Struct> oneAttrStructData = new ArrayList<>(numRows);
    List<Struct> twoAttrStructData = new ArrayList<>(numRows);
    for (int i = 0; i < numRows; i++) {
      oneAttrStructData.add(new StructImpl(Arrays.asList(i)));
      twoAttrStructData.add(new StructImpl(Arrays.asList(i, i)));
    }

    // A struct has various internal representations. The most common are the following:
    Class[] structTypes = new Class[]{Object[].class, Struct.class, List.class};
    // Generate column bundles for every possible representation that is supported currently.
    for (Class<?> type : structTypes) {
      data.add(newStructColBundle(oneAttrStructData, type, oneAttrStructMeta));
      data.add(newStructColBundle(twoAttrStructData, type, twoAttrStructMeta));
    }

    return data;
  }

  private static ColumnInputBundle newStructColBundle(
      List<Struct> data, Class<?> structType, ColumnMetaData meta) throws SQLException {
    List<Object> input = new ArrayList<>();
    List<Struct> expected = new ArrayList<>();
    for (Struct struct : data) {
      input.add(structOf(structType, struct));
      // The result obtained from StructAccessor is always of type Struct.class
      expected.add(structOf(Struct.class, struct));
    }
    return new ColumnInputBundle(input, expected, meta);
  }

  /**
   * Creates a struct in the representation dictated by the <code>structClass</code> parameter.
   */
  private static <T> T structOf(Class<T> structClass, Struct structData) throws SQLException {
    Object[] fieldValues = structData.getAttributes();
    if (structClass.equals(Object[].class)) {
      Object[] arrayStruct = new Object[fieldValues.length];
      for (int i = 0; i < fieldValues.length; i++) {
        arrayStruct[i] = fieldValues[i];
      }
      return (T) arrayStruct;
    } else if (structClass.equals(Struct.class)) {
      List<Object> listStruct = new ArrayList<>();
      for (int i = 0; i < fieldValues.length; i++) {
        listStruct.add(fieldValues[i]);
      }
      return (T) new StructImpl(listStruct);
    } else if (structClass.equals(List.class)) {
      List<Object> listStruct = new ArrayList<>();
      for (int i = 0; i < fieldValues.length; i++) {
        listStruct.add(fieldValues[i]);
      }
      return (T) listStruct;
    } else {
      throw new IllegalStateException("Cannot create struct of type" + structClass);
    }
  }

  @Test public void testStructAccessor() throws Exception {
    // Create rows based on the inputValues data
    List<List<Object>> rows = new ArrayList<>();
    for (Object o : columnInputBundle.inputValues) {
      rows.add(Collections.singletonList(o));
    }
    try (Cursor cursor = new ListIteratorCursor(rows.iterator())) {
      List<Accessor> accessors =
          cursor.createAccessors(
              Collections.singletonList(columnInputBundle.metaData), Unsafe.localCalendar(), null);
      Accessor accessor = accessors.get(0);
      int i = 0;
      while (cursor.next()) {
        Struct s = accessor.getObject(Struct.class);
        Object[] expectedStructAttributes = columnInputBundle.expectedValues.get(i).getAttributes();
        Object[] actualStructAttributes = s.getAttributes();
        assertArrayEquals(expectedStructAttributes, actualStructAttributes);
        i++;
      }
    }
  }
}

// End StructImplTest.java
