package org.apache.rya.accumulo.mr;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;

import org.apache.rya.accumulo.AccumuloRdfConfiguration;
import org.apache.rya.api.domain.RyaStatement;
import org.apache.rya.api.domain.RyaType;
import org.apache.rya.api.domain.RyaIRI;
import org.apache.rya.api.resolver.RyaTripleContext;
import org.eclipse.rdf4j.model.vocabulary.XMLSchema;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

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

public class RyaStatementWritableTest {
    private static final RyaIRI s1 = new RyaIRI(":s");
    private static final RyaIRI p1 = new RyaIRI(":p");
    private static final RyaType o1 = new RyaType(XMLSchema.INTEGER, "123");
    private static final RyaIRI s2 = new RyaIRI(":s2");
    private static final RyaIRI p2 = new RyaIRI(":p2");
    private static final RyaType o2 = new RyaType(XMLSchema.STRING, "123");
    private static final RyaIRI graph1 = new RyaIRI("http://example.org/graph1");
    private static final RyaIRI graph2 = new RyaIRI("http://example.org/graph2");
    private static final byte[] cv1 = "test_visibility".getBytes();
    private static final long t1 = 1000;
    private static final long t2 = 1001;
    private static final RyaStatement rs1 = RyaStatement.builder().setSubject(s1).setPredicate(p1).setObject(o1)
            .setContext(graph1).setColumnVisibility(cv1).setQualifier("q1").setTimestamp(t1).build();
    // Equivalent:
    private static final RyaStatement rs1b = RyaStatement.builder().setSubject(s1).setPredicate(p1).setObject(o1)
            .setContext(graph1).setColumnVisibility(cv1).setQualifier("q1").setTimestamp(t1).build();
    // Differ in one way each:
    private static final RyaStatement rsGraph = RyaStatement.builder().setSubject(s1).setPredicate(p1).setObject(o1)
            .setContext(graph2).setColumnVisibility(cv1).setQualifier("q1").setTimestamp(t1).build();
    private static final RyaStatement rsCv = RyaStatement.builder().setSubject(s1).setPredicate(p1).setObject(o1)
            .setContext(graph1).setColumnVisibility(null).setQualifier("q1").setTimestamp(t1).build();
    private static final RyaStatement rsQualifier = RyaStatement.builder().setSubject(s1).setPredicate(p1).setObject(o1)
            .setContext(graph1).setColumnVisibility(cv1).setQualifier("q2").setTimestamp(t1).build();
    private static final RyaStatement rsTimestamp = RyaStatement.builder().setSubject(s1).setPredicate(p1).setObject(o1)
            .setContext(graph1).setColumnVisibility(cv1).setQualifier("q1").setTimestamp(t2).build();
    // Different triple:
    private static final RyaStatement rs2 = RyaStatement.builder().setSubject(s2).setPredicate(p2).setObject(o2)
            .setContext(graph1).setColumnVisibility(null).setQualifier("q1").setTimestamp(t1).build();

    private static final RyaTripleContext ryaContext = RyaTripleContext.getInstance(new AccumuloRdfConfiguration());

    @Rule
    public ExpectedException expected = ExpectedException.none();

    @Test
    public void testEquals() throws Exception {
        RyaStatementWritable rsw1 = new RyaStatementWritable(rs1, ryaContext);
        RyaStatementWritable rsw1b = new RyaStatementWritable(rs1b, null);
        RyaStatementWritable rsw2 = new RyaStatementWritable(rs2, ryaContext);
        RyaStatementWritable rswNull = new RyaStatementWritable(null, ryaContext);
        Assert.assertEquals("Equivalent statements should be equal", rsw1, rsw1b);
        Assert.assertFalse("equals(null) should be false", rsw1.equals(null));
        Assert.assertNotEquals("Statements representing different triples are not equal", rsw1, rsw2);
        Assert.assertNotEquals("Statements representing different triples are not equal", rsw1, rswNull);
        Assert.assertNotEquals("Statements with different named graphs are not equal", rsw1,
                new RyaStatementWritable(rsGraph, ryaContext));
        Assert.assertNotEquals("Statements with different column visibilities are not equal", rsw1,
                new RyaStatementWritable(rsCv, ryaContext));
        Assert.assertNotEquals("Statements with different column qualifiers are not equal", rsw1,
                new RyaStatementWritable(rsQualifier, ryaContext));
        Assert.assertNotEquals("Statements with different timestamps are not equal", rsw1,
                new RyaStatementWritable(rsTimestamp, ryaContext));
    }

    @Test
    public void testCompareTo() throws Exception {
        RyaStatementWritable rsw1 = new RyaStatementWritable(rs1, ryaContext);
        RyaStatementWritable rsw1b = new RyaStatementWritable(rs1b, null);
        RyaStatementWritable rsw2 = new RyaStatementWritable(rs2, null);
        RyaStatementWritable rswGraph = new RyaStatementWritable(rsCv, ryaContext);
        RyaStatementWritable rswCv = new RyaStatementWritable(rsCv, ryaContext);
        RyaStatementWritable rswQualifier = new RyaStatementWritable(rsQualifier, ryaContext);
        RyaStatementWritable rswTimestamp = new RyaStatementWritable(rsTimestamp, ryaContext);
        Assert.assertEquals("x.compareTo(x) should always return 0", 0, rsw1.compareTo(rsw1));
        Assert.assertEquals("x.compareTo(x') where x and x' are equal should return 0", 0, rsw1.compareTo(rsw1b));
        Assert.assertEquals("x.compareTo(x') where x and x' are equal should return 0", 0, rsw1b.compareTo(rsw1));
        Assert.assertNotEquals("Statements with different named graphs are not equal", 0, rsw1.compareTo(rswGraph));
        Assert.assertNotEquals("Statements with different column visibilities are not equal", 0, rsw1.compareTo(rswCv));
        Assert.assertNotEquals("Statements with different column qualifiers are not equal", 0, rsw1.compareTo(rswQualifier));
        Assert.assertNotEquals("Statements with different timestamps are not equal", 0, rsw1.compareTo(rswTimestamp));
        Assert.assertEquals("compareTo in opposite directions should yield opposite signs",
                Integer.signum(rsw1.compareTo(rsw2))*-1, Integer.signum(rsw2.compareTo(rsw1)));
        // cycles shouldn't be possible; these comparisons can't all be negative or all be positive:
        int x = Integer.signum(rsw1.compareTo(rsw2))
                + Integer.signum(rsw2.compareTo(rsw1b))
                + Integer.signum(rsw1b.compareTo(rsw1));
        Assert.assertNotEquals("compareTo cycle detected", 3, Math.abs(x));
        // compareTo(null) should always throw an exception:
        expected.expect(NullPointerException.class);
        rsw1.compareTo(null);
    }

    @Test
    public void testSerializeDeserialize() throws Exception {
        RyaStatementWritable rsw1 = new RyaStatementWritable(rs1, ryaContext);
        RyaStatementWritable rsw2 = new RyaStatementWritable(rs2, ryaContext);
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        DataOutputStream bytesOut = new DataOutputStream(bytes);
        rsw1.write(bytesOut);
        rsw2.write(bytesOut);
        DataInputStream bytesIn = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()));
        RyaStatementWritable deserialized = new RyaStatementWritable();
        // Verify initial deserialization:
        deserialized.readFields(bytesIn);
        Assert.assertEquals("Deserialized statement not equal to original", rsw1, deserialized);
        Assert.assertEquals("Deserialized statement has different hash code", rsw1.hashCode(), deserialized.hashCode());
        Assert.assertEquals("original.compareTo(deserialized) should equal 0", 0, rsw1.compareTo(deserialized));
        // Verify that a second read mutates the Writable object into the correct second record:
        RyaStatement deserializedStatement = deserialized.getRyaStatement();
        deserialized.readFields(bytesIn);
        Assert.assertEquals("Deserialized statement not overwritten on second read", rsw2, deserialized);
        // Verify that the internal RyaStatement object is recreated, not overwritten:
        RyaStatement deserializedStatement2 = deserialized.getRyaStatement();
        Assert.assertNotSame("Reading a second record should create a new internal RyaStatement",
                deserializedStatement, deserializedStatement2);
    }
}