/*
 * Copyright DataStax, Inc.
 *
 * Licensed 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.dse.driver.internal.core.graph;

import static com.datastax.dse.driver.internal.core.graph.GraphProtocol.GRAPHSON_1_0;
import static com.datastax.dse.driver.internal.core.graph.GraphProtocol.GRAPHSON_2_0;
import static com.datastax.dse.driver.internal.core.graph.GraphProtocol.GRAPH_BINARY_1_0;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.datastax.dse.driver.api.core.DseProtocolVersion;
import com.datastax.dse.driver.api.core.graph.GraphNode;
import com.datastax.dse.driver.internal.core.graph.binary.GraphBinaryModule;
import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
import com.datastax.oss.driver.internal.core.context.DefaultDriverContext;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraverser;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyPath;
import org.apache.tinkerpop.gremlin.structure.io.Buffer;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedProperty;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(DataProviderRunner.class)
public class GraphNodeTest {

  private GraphBinaryModule graphBinaryModule;

  @Before
  public void setup() {
    DefaultDriverContext dseDriverContext = mock(DefaultDriverContext.class);
    when(dseDriverContext.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT);
    when(dseDriverContext.getProtocolVersion()).thenReturn(DseProtocolVersion.DSE_V2);

    TypeSerializerRegistry registry =
        GraphBinaryModule.createDseTypeSerializerRegistry(dseDriverContext);
    graphBinaryModule =
        new GraphBinaryModule(new GraphBinaryReader(registry), new GraphBinaryWriter(registry));
  }

  @Test
  public void should_not_support_set_for_graphson_2_0() throws IOException {
    // when
    GraphNode graphNode = serdeAndCreateGraphNode(ImmutableSet.of("value"), GRAPHSON_2_0);

    // then
    assertThat(graphNode.isSet()).isFalse();
  }

  @Test
  public void should_throw_for_set_for_graphson_1_0() throws IOException {
    // when
    GraphNode graphNode = serdeAndCreateGraphNode(ImmutableSet.of("value"), GRAPHSON_1_0);

    // then
    assertThat(graphNode.isSet()).isFalse();
    assertThatThrownBy(graphNode::asSet).isExactlyInstanceOf(UnsupportedOperationException.class);
  }

  @Test
  @UseDataProvider(value = "allGraphProtocols")
  public void should_create_graph_node_for_list(GraphProtocol graphVersion) throws IOException {
    // when
    GraphNode graphNode = serdeAndCreateGraphNode(ImmutableList.of("value"), graphVersion);

    // then
    assertThat(graphNode.isList()).isTrue();
    List<String> result = graphNode.asList();
    assertThat(result).isEqualTo(ImmutableList.of("value"));
  }

  @Test
  @UseDataProvider("allGraphProtocols")
  public void should_create_graph_node_for_map(GraphProtocol graphProtocol) throws IOException {
    // when
    GraphNode graphNode = serdeAndCreateGraphNode(ImmutableMap.of("value", 1234), graphProtocol);

    // then
    assertThat(graphNode.isMap()).isTrue();
    Map<String, Integer> result = graphNode.asMap();
    assertThat(result).isEqualTo(ImmutableMap.of("value", 1234));
  }

  @Test
  @UseDataProvider("graphson1_0and2_0")
  public void should_create_graph_node_for_map_for_non_string_key(GraphProtocol graphProtocol)
      throws IOException {
    // when
    GraphNode graphNode = serdeAndCreateGraphNode(ImmutableMap.of(12, 1234), graphProtocol);

    // then
    assertThat(graphNode.isMap()).isTrue();
    Map<String, Integer> result = graphNode.asMap();
    assertThat(result).isEqualTo(ImmutableMap.of("12", 1234));
  }

  @Test
  @UseDataProvider(value = "allGraphProtocols")
  public void should_calculate_size_of_collection_types(GraphProtocol graphProtocol)
      throws IOException {
    // when
    GraphNode mapNode = serdeAndCreateGraphNode(ImmutableMap.of(12, 1234), graphProtocol);
    GraphNode setNode = serdeAndCreateGraphNode(ImmutableSet.of(12, 1234), graphProtocol);
    GraphNode listNode = serdeAndCreateGraphNode(ImmutableList.of(12, 1234, 99999), graphProtocol);

    // then
    assertThat(mapNode.size()).isEqualTo(1);
    assertThat(setNode.size()).isEqualTo(2);
    assertThat(listNode.size()).isEqualTo(3);
  }

  @Test
  @UseDataProvider(value = "allGraphProtocols")
  public void should_return_is_value_only_for_scalar_value(GraphProtocol graphProtocol)
      throws IOException {
    // when
    GraphNode mapNode = serdeAndCreateGraphNode(ImmutableMap.of(12, 1234), graphProtocol);
    GraphNode setNode = serdeAndCreateGraphNode(ImmutableMap.of(12, 1234), graphProtocol);
    GraphNode listNode = serdeAndCreateGraphNode(ImmutableMap.of(12, 1234), graphProtocol);
    GraphNode vertexNode =
        serdeAndCreateGraphNode(new DetachedVertex("a", "l", null), graphProtocol);
    GraphNode edgeNode =
        serdeAndCreateGraphNode(
            new DetachedEdge("a", "l", Collections.emptyMap(), "v1", "l1", "v2", "l2"),
            graphProtocol);
    GraphNode pathNode = serdeAndCreateGraphNode(EmptyPath.instance(), graphProtocol);
    GraphNode propertyNode = serdeAndCreateGraphNode(new DetachedProperty<>("a", 1), graphProtocol);
    GraphNode vertexPropertyNode =
        serdeAndCreateGraphNode(
            new DetachedVertexProperty<>("id", "l", "v", null, new DetachedVertex("a", "l", null)),
            graphProtocol);
    GraphNode scalarValueNode = serdeAndCreateGraphNode(true, graphProtocol);

    // then
    assertThat(mapNode.isValue()).isFalse();
    assertThat(setNode.isValue()).isFalse();
    assertThat(listNode.isValue()).isFalse();
    assertThat(vertexNode.isValue()).isFalse();
    assertThat(edgeNode.isValue()).isFalse();
    assertThat(pathNode.isValue()).isFalse();
    assertThat(propertyNode.isValue()).isFalse();
    assertThat(vertexPropertyNode.isValue()).isFalse();
    assertThat(scalarValueNode.isValue()).isTrue();
  }

  @Test
  @UseDataProvider("objectGraphNodeProtocols")
  public void should_check_if_node_is_property_not_map(GraphProtocol graphProtocol)
      throws IOException {
    // when
    GraphNode propertyNode = serdeAndCreateGraphNode(new DetachedProperty<>("a", 1), graphProtocol);

    // then
    assertThat(propertyNode.isProperty()).isTrue();
    assertThat(propertyNode.isMap()).isFalse();
    assertThat(propertyNode.asProperty()).isNotNull();
  }

  @Test
  public void should_check_if_node_is_property_or_map_for_1_0() throws IOException {
    // when
    GraphNode propertyNode = serdeAndCreateGraphNode(new DetachedProperty<>("a", 1), GRAPHSON_1_0);

    // then
    assertThat(propertyNode.isProperty()).isTrue();
    assertThat(propertyNode.isMap()).isTrue();
    assertThat(propertyNode.asProperty()).isNotNull();
  }

  @Test
  @UseDataProvider("allGraphProtocols")
  public void should_check_if_node_is_vertex_property(GraphProtocol graphProtocol)
      throws IOException {
    // when
    GraphNode vertexPropertyNode =
        serdeAndCreateGraphNode(
            new DetachedVertexProperty<>("id", "l", "v", null, new DetachedVertex("a", "l", null)),
            graphProtocol);

    // then
    assertThat(vertexPropertyNode.isVertexProperty()).isTrue();
    assertThat(vertexPropertyNode.isVertexProperty()).isNotNull();
  }

  @Test
  public void should_check_if_node_is_path_for_graphson_1_0() throws IOException {
    // when
    GraphNode pathNode = serdeAndCreateGraphNode(EmptyPath.instance(), GRAPHSON_1_0);

    // then
    assertThat(pathNode.isPath()).isFalse();
    assertThatThrownBy(pathNode::asPath).isExactlyInstanceOf(UnsupportedOperationException.class);
  }

  @Test
  @UseDataProvider("objectGraphNodeProtocols")
  public void should_check_if_node_is_path(GraphProtocol graphProtocol) throws IOException {
    // when
    GraphNode pathNode = serdeAndCreateGraphNode(EmptyPath.instance(), graphProtocol);

    // then
    assertThat(pathNode.isPath()).isTrue();
    assertThat(pathNode.asPath()).isNotNull();
  }

  @Test
  @UseDataProvider("allGraphProtocols")
  public void should_check_if_node_is_vertex(GraphProtocol graphProtocol) throws IOException {
    // when
    GraphNode vertexNode =
        serdeAndCreateGraphNode(new DetachedVertex("a", "l", null), graphProtocol);

    // then
    assertThat(vertexNode.isVertex()).isTrue();
    assertThat(vertexNode.asVertex()).isNotNull();
  }

  @Test
  @UseDataProvider("allGraphProtocols")
  public void should_check_if_node_is_edge(GraphProtocol graphProtocol) throws IOException {
    // when
    GraphNode edgeNode =
        serdeAndCreateGraphNode(
            new DetachedEdge("a", "l", Collections.emptyMap(), "v1", "l1", "v2", "l2"),
            graphProtocol);

    // then
    assertThat(edgeNode.isEdge()).isTrue();
    assertThat(edgeNode.asEdge()).isNotNull();
  }

  private GraphNode serdeAndCreateGraphNode(Object inputValue, GraphProtocol graphProtocol)
      throws IOException {
    if (graphProtocol.isGraphBinary()) {
      Buffer tinkerBuf = graphBinaryModule.serialize(new DefaultRemoteTraverser<>(inputValue, 0L));
      ByteBuffer nioBuffer = tinkerBuf.nioBuffer();
      tinkerBuf.release();
      return new ObjectGraphNode(
          GraphConversions.createGraphBinaryGraphNode(
                  ImmutableList.of(nioBuffer), graphBinaryModule)
              .as(Traverser.class)
              .get());
    } else {
      return GraphSONUtils.createGraphNode(
          ImmutableList.of(GraphSONUtils.serializeToByteBuffer(inputValue, graphProtocol)),
          graphProtocol);
    }
  }

  @DataProvider
  public static Object[][] allGraphProtocols() {
    return new Object[][] {{GRAPHSON_1_0}, {GRAPHSON_2_0}, {GRAPH_BINARY_1_0}};
  }

  @DataProvider
  public static Object[][] graphson1_0and2_0() {
    return new Object[][] {{GRAPHSON_1_0}, {GRAPHSON_2_0}};
  }

  @DataProvider
  public static Object[][] objectGraphNodeProtocols() {
    return new Object[][] {{GRAPHSON_2_0}, {GRAPH_BINARY_1_0}};
  }
}
