/*
 * 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.tinkerpop.gremlin.structure.util;

import org.apache.tinkerpop.gremlin.TestHelper;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.javatuples.Pair;
import org.junit.Test;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

/**
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public class ElementHelperTest {
    @Test
    public void shouldBeUtilityClass() throws Exception {
        TestHelper.assertIsUtilityClass(ElementHelper.class);
    }

    @Test
    public void shouldValidatePropertyAndNotAllowNullValue() {
        try {
            ElementHelper.validateProperty("test", null);
            fail("Should fail as property value cannot be null");
        } catch (IllegalArgumentException iae) {
            assertEquals(Property.Exceptions.propertyValueCanNotBeNull().getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldValidatePropertyAndNotAllowNullKey() {
        try {
            ElementHelper.validateProperty(null, "test");
            fail("Should fail as property key cannot be null");
        } catch (IllegalArgumentException iae) {
            assertEquals(Property.Exceptions.propertyKeyCanNotBeNull().getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldValidatePropertyAndNotAllowEmptyKey() {
        try {
            ElementHelper.validateProperty("", "test");
            fail("Should fail as property key cannot be empty");
        } catch (IllegalArgumentException iae) {
            assertEquals(Property.Exceptions.propertyKeyCanNotBeEmpty().getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldValidatePropertyAndNotAllowHiddenKey() {
        final String key = Graph.Hidden.hide("key");
        try {
            ElementHelper.validateProperty(key, "test");
            fail("Should fail as property key cannot be hidden");
        } catch (IllegalArgumentException iae) {
            assertEquals(Property.Exceptions.propertyKeyCanNotBeAHiddenKey(key).getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldHaveValidProperty() {
        ElementHelper.validateProperty("aKey", "value");
    }

    @Test
    public void shouldAllowEvenNumberOfKeyValues() {
        try {
            ElementHelper.legalPropertyKeyValueArray("aKey", "test", "no-value-for-this-one");
            fail("Should fail as there is an odd number of key-values");
        } catch (IllegalArgumentException iae) {
            assertEquals(Element.Exceptions.providedKeyValuesMustBeAMultipleOfTwo().getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldNotAllowEvenNumberOfKeyValuesAndInvalidKeys() {
        try {
            ElementHelper.legalPropertyKeyValueArray("aKey", "test", "value-for-this-one", 1, 1, "none");
            fail("Should fail as there is an even number of key-values, but a bad key");
        } catch (IllegalArgumentException iae) {
            assertEquals(Element.Exceptions.providedKeyValuesMustHaveALegalKeyOnEvenIndices().getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldAllowEvenNumberOfKeyValuesAndValidKeys() {
        ElementHelper.legalPropertyKeyValueArray("aKey", "test", "value-for-this-one", 1, "1", "none");
    }

    @Test
    public void shouldNotAllowEvenNumberOfKeyValuesAndInvalidValues() {
        try {
            ElementHelper.legalPropertyKeyValueArray("aKey", "test", "value-for-this-one", 1, "1", null);
        } catch (IllegalArgumentException iae) {
            assertEquals(Property.Exceptions.propertyValueCanNotBeNull().getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldFindTheIdValueAlone() {
        assertEquals(123l, ElementHelper.getIdValue(T.id, 123l).get());
    }

    @Test
    public void shouldFindTheIdValueInSet() {
        assertEquals(123l, ElementHelper.getIdValue("test", 321, T.id, 123l, "testagain", "that").get());
    }

    @Test
    public void shouldNotFindAnIdValue() {
        assertFalse(ElementHelper.getIdValue("test", 321, "xyz", 123l, "testagain", "that").isPresent());
    }

    @Test(expected = NullPointerException.class)
    public void shouldNotFindAnIdValueBecauseItIsNull() {
        ElementHelper.getIdValue("test", 321, T.id, null, "testagain", "that");
    }

    @Test
    public void shouldFindTheLabelValueAlone() {
        assertEquals("friend", ElementHelper.getLabelValue(T.label, "friend").get());
    }

    @Test
    public void shouldFindTheLabelValueInSet() {
        assertEquals("friend", ElementHelper.getLabelValue("test", 321, T.label, "friend", "testagain", "that").get());
    }

    @Test
    public void shouldNotFindTheLabelValue() {
        assertFalse(ElementHelper.getLabelValue("test", 321, "xyz", "friend", "testagain", "that").isPresent());
    }

    @Test(expected = ClassCastException.class)
    public void shouldNotFindTheLabelBecauseItIsNotString() {
        ElementHelper.getLabelValue("test", 321, T.label, 4545, "testagain", "that");
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldFailCauseNullLabelsAreNotAllowed() {
        ElementHelper.getLabelValue("test", 321, T.label, null, "testagain", "that");
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldFailCauseSystemLabelsAreNotAllowed() {
        ElementHelper.getLabelValue("test", 321, T.label, Graph.Hidden.hide("systemLabel"), "testagain", "that");
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldFailCauseEmptyLabelsAreNotAllowed() {
        ElementHelper.getLabelValue("test", 321, T.label, "", "testagain", "that");
    }

    @Test
    public void shouldAttachPropertiesButNotLabelsOrId() {
        final Element mockElement = mock(Element.class);
        ElementHelper.attachProperties(mockElement, "test", 123, T.id, 321, T.label, "friends");
        verify(mockElement, times(1)).property("test", 123);
        verify(mockElement, times(0)).property(T.id.getAccessor(), 321);
        verify(mockElement, times(0)).property(T.label.getAccessor(), "friends");
    }

    @Test(expected = ClassCastException.class)
    public void shouldFailTryingToAttachPropertiesNonStringKey() {
        final Element mockElement = mock(Element.class);
        ElementHelper.attachProperties(mockElement, "test", 123, 321, "test");
    }

    @Test
    public void shouldFailTryingToAttachPropertiesToNullElement() {
        try {
            ElementHelper.attachProperties(null, "test", 123, 321, "test");
            fail("Should throw exception since the element argument is null");
        } catch (IllegalArgumentException iae) {
            assertTrue(Graph.Exceptions.argumentCanNotBeNull("vertex").getMessage().equals(iae.getMessage()) || Graph.Exceptions.argumentCanNotBeNull("element").getMessage().equals(iae.getMessage()));
        }
    }

    @Test
    public void shouldAttachPropertiesWithCardinalityButNotLabelsOrId() {
        final Vertex mockElement = mock(Vertex.class);
        ElementHelper.attachProperties(mockElement, VertexProperty.Cardinality.single, "test", 123, T.id, 321, T.label, "friends");
        verify(mockElement, times(1)).property(VertexProperty.Cardinality.single, "test", 123);
        verify(mockElement, times(0)).property(VertexProperty.Cardinality.single, T.id.getAccessor(), 321);
        verify(mockElement, times(0)).property(VertexProperty.Cardinality.single, T.label.getAccessor(), "friends");
    }

    @Test(expected = ClassCastException.class)
    public void shouldFailTryingToAttachPropertiesWithCardinalityNonStringKey() {
        final Element mockElement = mock(Vertex.class);
        ElementHelper.attachProperties(mockElement, VertexProperty.Cardinality.single, "test", 123, 321, "test");
    }

    @Test
    public void shouldFailTryingToAttachPropertiesWithCardinalityToNullElement() {
        try {
            ElementHelper.attachProperties((Vertex) null, VertexProperty.Cardinality.single, "test", 123, 321, "test");
            fail("Should throw exception since the element argument is null");
        } catch (IllegalArgumentException iae) {
            assertEquals(Graph.Exceptions.argumentCanNotBeNull("vertex").getMessage(), iae.getMessage());
        }
    }

    @Test
    public void shouldDetermineElementsAreEqualAsTheyAreSameObject() {
        final Element mockElement = mock(Element.class);
        assertTrue(ElementHelper.areEqual(mockElement, mockElement));
    }

    @Test
    public void shouldDetermineElementsAreNotEqualAsAIsVertexAndBIsEdge() {
        final Element mockVertex = mock(Vertex.class);
        final Element mockEdge = mock(Edge.class);
        assertFalse(ElementHelper.areEqual(mockVertex, mockEdge));
    }

    @Test
    public void shouldDetermineElementsAreNotEqualAsBIsVertexAndAIsEdge() {
        final Element mockVertex = mock(Vertex.class);
        final Element mockEdge = mock(Edge.class);
        assertFalse(ElementHelper.areEqual(mockEdge, mockVertex));
    }

    @Test
    public void shouldDetermineVerticesAreEqual() {
        final Element mockVertexA = mock(Vertex.class);
        final Element mockVertexB = mock(Vertex.class);
        when(mockVertexA.id()).thenReturn("1");
        when(mockVertexB.id()).thenReturn("1");
        assertTrue(ElementHelper.areEqual(mockVertexA, mockVertexB));
    }

    @Test
    public void shouldDetermineVerticesAreNotEqual() {
        final Element mockVertexA = mock(Vertex.class);
        final Element mockVertexB = mock(Vertex.class);
        when(mockVertexA.id()).thenReturn("1");
        when(mockVertexB.id()).thenReturn("2");
        assertFalse(ElementHelper.areEqual(mockVertexA, mockVertexB));
    }

    @Test
    public void shouldDetermineElementsAreNotEqualWhenBothNull() {
        assertFalse(ElementHelper.areEqual((Element) null, null));
    }

    @Test
    public void shouldDetermineElementsAreNotEqualBecauseFirstArgumentIsNull() {
        Element v = mock(Element.class);
        assertFalse(ElementHelper.areEqual((Element) null, v));
    }

    @Test
    public void shouldDetermineElementsAreNotEqualBecauseSecondArgumentIsNull() {
        Element v = mock(Element.class);
        assertFalse(ElementHelper.areEqual(v, null));
    }

    @Test
    public void shouldDetermineEdgesAreEqual() {
        final Element mockEdgeA = mock(Edge.class);
        final Element mockEdgeB = mock(Edge.class);
        when(mockEdgeA.id()).thenReturn("1");
        when(mockEdgeB.id()).thenReturn("1");
        assertTrue(ElementHelper.areEqual(mockEdgeA, mockEdgeB));
    }

    @Test
    public void shouldDetermineEdgesAreNotEqual() {
        final Element mockEdgeA = mock(Edge.class);
        final Element mockEdgeB = mock(Edge.class);
        when(mockEdgeA.id()).thenReturn("1");
        when(mockEdgeB.id()).thenReturn("2");
        assertFalse(ElementHelper.areEqual(mockEdgeA, mockEdgeB));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualBecauseBothAreNull() {
        assertFalse(ElementHelper.areEqual((Property) null, null));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualBecauseFirstArgumentIsNull() {
        assertFalse(ElementHelper.areEqual((Property) null, "some object"));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualTestBecauseSecondArgumentIsNull() {
        final Property mockProperty = mock(Property.class);
        assertFalse(ElementHelper.areEqual(mockProperty, null));
    }

    @Test
    public void shouldFailPropertyAreEqualTestBecauseSecondArgumentIsNotProperty() {
        final Property mockProperty = mock(Property.class);
        assertFalse(ElementHelper.areEqual(mockProperty, "i'm a string"));
    }

    @Test
    public void shouldDeterminePropertiesAreEqualAsTheyAreSameObject() {
        final Property mockProperty = mock(Property.class);
        assertTrue(ElementHelper.areEqual(mockProperty, mockProperty));
    }

    @Test
    public void shouldDeterminePropertiesAreEqualAsTheyAreBothEmpty() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        when(mockPropertyA.isPresent()).thenReturn(false);
        when(mockPropertyB.isPresent()).thenReturn(false);
        assertTrue(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualAsAIsEmptyAndBIsNot() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        when(mockPropertyA.isPresent()).thenReturn(false);
        when(mockPropertyB.isPresent()).thenReturn(true);
        assertFalse(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualAsBIsEmptyAndAIsNot() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        when(mockPropertyA.isPresent()).thenReturn(true);
        when(mockPropertyB.isPresent()).thenReturn(false);
        assertFalse(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDeterminePropertiesAreEqual() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        final Element mockElement = mock(Element.class);
        when(mockPropertyA.isPresent()).thenReturn(true);
        when(mockPropertyB.isPresent()).thenReturn(true);
        when(mockPropertyA.element()).thenReturn(mockElement);
        when(mockPropertyB.element()).thenReturn(mockElement);
        when(mockPropertyA.key()).thenReturn("k");
        when(mockPropertyB.key()).thenReturn("k");
        when(mockPropertyA.value()).thenReturn("v");
        when(mockPropertyB.value()).thenReturn("v");

        assertTrue(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualWhenElementsAreDifferent() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        final Element mockElement = mock(Element.class);
        final Element mockElementDifferent = mock(Element.class);
        when(mockPropertyA.isPresent()).thenReturn(true);
        when(mockPropertyB.isPresent()).thenReturn(true);
        when(mockPropertyA.element()).thenReturn(mockElement);
        when(mockPropertyB.element()).thenReturn(mockElementDifferent);
        when(mockPropertyA.key()).thenReturn("k");
        when(mockPropertyB.key()).thenReturn("k");
        when(mockPropertyA.value()).thenReturn("v");
        when(mockPropertyB.value()).thenReturn("v");

        assertFalse(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualWhenKeysAreDifferent() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        final Element mockElement = mock(Element.class);
        when(mockPropertyA.isPresent()).thenReturn(true);
        when(mockPropertyB.isPresent()).thenReturn(true);
        when(mockPropertyA.element()).thenReturn(mockElement);
        when(mockPropertyB.element()).thenReturn(mockElement);
        when(mockPropertyA.key()).thenReturn("k");
        when(mockPropertyB.key()).thenReturn("k1");
        when(mockPropertyA.value()).thenReturn("v");
        when(mockPropertyB.value()).thenReturn("v");

        assertFalse(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDeterminePropertiesAreNotEqualWhenValuesAreDifferent() {
        final Property mockPropertyA = mock(Property.class);
        final Property mockPropertyB = mock(Property.class);
        final Element mockElement = mock(Element.class);
        when(mockPropertyA.isPresent()).thenReturn(true);
        when(mockPropertyB.isPresent()).thenReturn(true);
        when(mockPropertyA.element()).thenReturn(mockElement);
        when(mockPropertyB.element()).thenReturn(mockElement);
        when(mockPropertyA.key()).thenReturn("k");
        when(mockPropertyB.key()).thenReturn("k");
        when(mockPropertyA.value()).thenReturn("v");
        when(mockPropertyB.value()).thenReturn("v1");

        assertFalse(ElementHelper.areEqual(mockPropertyA, mockPropertyB));
    }

    @Test
    public void shouldDetermineAbsentPropertiesEqual() {
        Property<?> p1 = mock(Property.class);
        Property<?> p2 = mock(Property.class);

        when(p1.isPresent()).thenReturn(false);
        when(p2.isPresent()).thenReturn(false);

        assertTrue(ElementHelper.areEqual(p1, p2));
    }

    @Test
    public void shouldDetermineAbsentPropertyNotEqualToPresent() {
        Property<?> p1 = mock(Property.class);
        Property<?> p2 = mock(Property.class);

        when(p1.isPresent()).thenReturn(false);
        when(p2.isPresent()).thenReturn(true);

        assertFalse(ElementHelper.areEqual(p1, p2));
    }

    @Test
    public void shouldDeterminePresentPropertyNotEqualToAbsent() {
        Property<?> p1 = mock(Property.class);
        Property<?> p2 = mock(Property.class);

        when(p1.isPresent()).thenReturn(true);
        when(p2.isPresent()).thenReturn(false);

        assertFalse(ElementHelper.areEqual(p1, p2));
    }

    @Test
    public void shouldExtractKeys() {
        final Set<String> keys = ElementHelper.getKeys("test1", "something", "test2", "something else");
        assertTrue(keys.contains("test1"));
        assertTrue(keys.contains("test2"));
    }

    @Test
    public void shouldGetPairs() {
        final List<Pair<String, Object>> pairs = ElementHelper.asPairs("1", "this", "2", 6l, "3", "other", "4", 1);
        assertEquals(4, pairs.size());
        assertEquals("1", pairs.get(0).getValue0());
        assertEquals("this", pairs.get(0).getValue1());
        assertEquals("2", pairs.get(1).getValue0());
        assertEquals(6l, pairs.get(1).getValue1());
        assertEquals("3", pairs.get(2).getValue0());
        assertEquals("other", pairs.get(2).getValue1());
        assertEquals("4", pairs.get(3).getValue0());
        assertEquals(1, pairs.get(3).getValue1());
    }

    @Test
    public void shouldGetMap() {
        final Map<String, Object> map = ElementHelper.asMap("1", "this", "2", 6l, "3", "other", "4", 1);
        assertEquals(4, map.size());
        assertEquals("this", map.get("1"));
        assertEquals(6l, map.get("2"));
        assertEquals("other", map.get("3"));
        assertEquals(1, map.get("4"));
    }

    @Test
    public void shouldRemoveAKey() {
        final Optional<Object[]> kvs = ElementHelper.remove("2", "1", "this", "2", 6l, "3", "other", "4", 1);
        assertEquals(6, kvs.get().length);
        assertTrue(Stream.of(kvs.get()).noneMatch(kv -> kv.equals("2") || kv.equals(6l)));
    }

    @Test
    public void shouldRemoveAKeyAndReturnEmpty() {
        final Optional<Object[]> kvs = ElementHelper.remove("1", "1", "this");
        assertEquals(Optional.empty(), kvs);
    }

    @Test
    public void shouldRemoveAccessor() {
        final Optional<Object[]> kvs = ElementHelper.remove(T.id, "1", "this", T.id, 6l, "3", "other", "4", 1);
        assertEquals(6, kvs.get().length);
        assertTrue(Stream.of(kvs.get()).noneMatch(kv -> kv.equals("2") || kv.equals(6l)));
    }

    @Test
    public void shouldRemoveAccessorAndReturnEmpty() {
        final Optional<Object[]> kvs = ElementHelper.remove(T.id, T.id, "this");
        assertEquals(Optional.empty(), kvs);
    }

    @Test
    public void shouldUpsertKeyValueByAddingIt() {
        final Object[] oldKvs = new Object[]{"k", "v"};
        final Object[] newKvs = ElementHelper.upsert(oldKvs, "k1", "v1");
        assertEquals(4, newKvs.length);
        assertEquals("k1", newKvs[2]);
        assertEquals("v1", newKvs[3]);
    }

    @Test
    public void shouldUpsertKeyValueByUpdatingIt() {
        final Object[] oldKvs = new Object[]{"k", "v", "k1", "v0"};
        final Object[] newKvs = ElementHelper.upsert(oldKvs, "k1", "v1");
        assertEquals(4, newKvs.length);
        assertEquals("v1", newKvs[3]);
    }

    @Test
    public void shouldReplaceKey() {
        final Object[] oldKvs = new Object[]{"k", "v", "k1", "v0"};
        final Object[] newKvs = ElementHelper.replaceKey(oldKvs, "k", "k2");
        assertEquals(4, newKvs.length);
        assertEquals("k2", newKvs[0]);
        assertEquals("v", newKvs[1]);
        assertEquals("k1", newKvs[2]);
        assertEquals("v0", newKvs[3]);
    }
}
