/*
 * 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 com.datastax.oss.driver.api.core.data;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;

import com.datastax.oss.driver.api.core.type.codec.TypeCodecs;
import com.datastax.oss.driver.internal.SerializationHelper;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamException;
import java.time.LocalTime;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.assertj.core.util.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(DataProviderRunner.class)
public class CqlVectorTest {

  @DataProvider
  public static Object[][] dataProvider() {
    return new Object[][] {
      {new Float[] {1.0f, 2.5f}},
      {new LocalTime[] {LocalTime.of(1, 2), LocalTime.of(3, 4)}},
      {new List[] {Arrays.asList(1, 2), Arrays.asList(3, 4)}},
      {new CqlVector[] {CqlVector.newInstance("a", "bc"), CqlVector.newInstance("d", "ef")}}
    };
  }

  private void validate_built_vector(CqlVector<?> vec, Object[] expectedVals) {
    assertThat(vec.size()).isEqualTo(2);
    assertThat(vec.isEmpty()).isFalse();
    assertThat(vec.get(0)).isEqualTo(expectedVals[0]);
    assertThat(vec.get(1)).isEqualTo(expectedVals[1]);
  }

  @UseDataProvider("dataProvider")
  @Test
  public void should_build_vector_from_elements(Object[] vals) {
    validate_built_vector(CqlVector.newInstance(vals), vals);
  }

  @Test
  @UseDataProvider("dataProvider")
  public void should_build_vector_from_list(Object[] vals) {
    validate_built_vector(CqlVector.newInstance(Lists.newArrayList(vals)), vals);
  }

  @Test
  public void should_throw_from_null_string() {
    assertThatThrownBy(
            () -> {
              CqlVector.from(null, TypeCodecs.FLOAT);
            })
        .isInstanceOf(IllegalArgumentException.class);
  }

  @Test
  public void should_throw_from_empty_string() {

    assertThatThrownBy(
            () -> {
              CqlVector.from("", TypeCodecs.FLOAT);
            })
        .isInstanceOf(IllegalArgumentException.class);
  }

  @Test
  public void should_throw_when_building_with_nulls() {

    assertThatThrownBy(
            () -> {
              CqlVector.newInstance(1.1f, null, 2.2f);
            })
        .isInstanceOf(IllegalArgumentException.class);

    Float[] theArray = new Float[] {1.1f, null, 2.2f};
    assertThatThrownBy(
            () -> {
              CqlVector.newInstance(theArray);
            })
        .isInstanceOf(IllegalArgumentException.class);

    List<Float> theList = Lists.newArrayList(1.1f, null, 2.2f);
    assertThatThrownBy(
            () -> {
              CqlVector.newInstance(theList);
            })
        .isInstanceOf(IllegalArgumentException.class);
  }

  @Test
  public void should_build_empty_vector() {
    CqlVector<Float> vector = CqlVector.newInstance();
    assertThat(vector.isEmpty()).isTrue();
    assertThat(vector.size()).isEqualTo(0);
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_behave_mostly_like_a_list(T[] vals) {
    T[] theArray = Arrays.copyOf(vals, vals.length);
    CqlVector<T> vector = CqlVector.newInstance(theArray);
    assertThat(vector.get(0)).isEqualTo(theArray[0]);
    vector.set(0, theArray[1]);
    assertThat(vector.get(0)).isEqualTo(theArray[1]);
    assertThat(vector.isEmpty()).isFalse();
    assertThat(vector.size()).isEqualTo(2);
    Iterator<?> iterator = vector.iterator();
    assertThat(iterator.next()).isEqualTo(theArray[1]);
    assertThat(iterator.next()).isEqualTo(theArray[1]);
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_play_nicely_with_streams(T[] vals) {
    CqlVector<T> vector = CqlVector.newInstance(vals);
    List<String> results =
        vector.stream()
            .map(Object::toString)
            .collect(Collectors.toCollection(() -> new ArrayList<String>()));
    for (int i = 0; i < vector.size(); ++i) {
      assertThat(results.get(i)).isEqualTo(vector.get(i).toString());
    }
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_reflect_changes_to_mutable_list(T[] vals) {
    List<T> theList = Lists.newArrayList(vals);
    CqlVector<T> vector = CqlVector.newInstance(theList);
    assertThat(vector.size()).isEqualTo(2);
    assertThat(vector.get(1)).isEqualTo(vals[1]);

    T newVal = vals[0];
    theList.set(1, newVal);
    assertThat(vector.size()).isEqualTo(2);
    assertThat(vector.get(1)).isEqualTo(newVal);
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_reflect_changes_to_array(T[] vals) {
    T[] theArray = Arrays.copyOf(vals, vals.length);
    CqlVector<T> vector = CqlVector.newInstance(theArray);
    assertThat(vector.size()).isEqualTo(2);
    assertThat(vector.get(1)).isEqualTo(theArray[1]);

    T newVal = theArray[0];
    theArray[1] = newVal;
    assertThat(vector.size()).isEqualTo(2);
    assertThat(vector.get(1)).isEqualTo(newVal);
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_correctly_compare_vectors(T[] vals) {
    CqlVector<T> vector1 = CqlVector.newInstance(vals);
    CqlVector<T> vector2 = CqlVector.newInstance(vals);
    CqlVector<T> vector3 = CqlVector.newInstance(Lists.newArrayList(vals));
    assertThat(vector1).isNotSameAs(vector2);
    assertThat(vector1).isEqualTo(vector2);
    assertThat(vector1).isNotSameAs(vector3);
    assertThat(vector1).isEqualTo(vector3);

    T[] differentArgs = Arrays.copyOf(vals, vals.length);
    T newVal = differentArgs[1];
    differentArgs[0] = newVal;
    CqlVector<T> vector4 = CqlVector.newInstance(differentArgs);
    assertThat(vector1).isNotSameAs(vector4);
    assertThat(vector1).isNotEqualTo(vector4);

    T[] biggerArgs = Arrays.copyOf(vals, vals.length + 1);
    biggerArgs[biggerArgs.length - 1] = newVal;
    CqlVector<T> vector5 = CqlVector.newInstance(biggerArgs);
    assertThat(vector1).isNotSameAs(vector5);
    assertThat(vector1).isNotEqualTo(vector5);
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_serialize_and_deserialize(T[] vals) throws Exception {
    CqlVector<T> initial = CqlVector.newInstance(vals);
    CqlVector<T> deserialized = SerializationHelper.serializeAndDeserialize(initial);
    assertThat(deserialized).isEqualTo(initial);
  }

  @Test
  public void should_serialize_and_deserialize_empty_vector() throws Exception {
    CqlVector<Float> initial = CqlVector.newInstance(Collections.emptyList());
    CqlVector<Float> deserialized = SerializationHelper.serializeAndDeserialize(initial);
    assertThat(deserialized).isEqualTo(initial);
  }

  @Test
  @UseDataProvider("dataProvider")
  public <T> void should_serialize_and_deserialize_unserializable_list(T[] vals) throws Exception {
    CqlVector<T> initial =
        CqlVector.newInstance(
            new AbstractList<T>() {
              @Override
              public T get(int index) {
                return vals[index];
              }

              @Override
              public int size() {
                return vals.length;
              }
            });
    CqlVector<T> deserialized = SerializationHelper.serializeAndDeserialize(initial);
    assertThat(deserialized).isEqualTo(initial);
  }

  @Test
  public void should_not_use_preallocate_serialized_size() throws DecoderException {
    // serialized CqlVector<Float>(1.0f, 2.5f, 3.0f) with size field adjusted to Integer.MAX_VALUE
    byte[] suspiciousBytes =
        Hex.decodeHex(
            "aced000573720042636f6d2e64617461737461782e6f73732e6472697665722e6170692e636f72652e646174612e43716c566563746f722453657269616c697a6174696f6e50726f78790000000000000001030000787077047fffffff7372000f6a6176612e6c616e672e466c6f6174daedc9a2db3cf0ec02000146000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b02000078703f8000007371007e0002402000007371007e00024040000078"
                .toCharArray());
    try {
      new ObjectInputStream(new ByteArrayInputStream(suspiciousBytes)).readObject();
      fail("Should not be able to deserialize bytes with incorrect size field");
    } catch (Exception e) {
      // check we fail to deserialize, rather than OOM
      assertThat(e).isInstanceOf(ObjectStreamException.class);
    }
  }
}
