/*
 * 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.jena.sparql.negation;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.StringReader;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import org.apache.jena.atlas.lib.StrUtils;
import org.apache.jena.query.*;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;

/**
 * Tests for calculating graph deltas using SPARQL
 */
@ParameterizedClass(name="{index}: {0}")
@MethodSource("provideArgs")
public class TestGraphDeltas {

    private static final String testData = StrUtils.strjoinNL("<http://r1> <http://r1> <http://r1> .",
            "<http://r2> <http://r2> <http://r2> .");

    private static final String testData2 = StrUtils.strjoinNL("<http://r1> <http://r1> <http://r1> , <http://r2> .",
            "<http://r2> <http://r2> <http://r2> .");

    private static final String testData3 = StrUtils.strjoinNL("<http://r1> <http://r1> <http://r1> , \"value\" .",
            "<http://r2> <http://r2> <http://r2> , 1234 .");

    private static final String testData4 = StrUtils.strjoinNL(
            "<http://r1> <http://r1> <http://r1> , \"value\" , 1234, 123e4, 123.4, true, false .",
            "<http://r2> <http://r2> <http://r2> .");

    private static final String MinusQuery = StrUtils.strjoinNL
        ("SELECT *",
         "{",
         "  GRAPH <http://a>",
         "  {",
         "     ?s ?p ?o .",
         "  }",
         "  MINUS",
         "  {",
         "    GRAPH <http://b> { ?s ?p ?o }",
         "  }",
         "}");

    private static final String OptionalSameTermQuery1 = StrUtils.strjoinNL
        ("SELECT *",
         "{",
         "  GRAPH <http://a>",
         "  {",
         "     ?s ?p ?o .",
         "  }",
         "  OPTIONAL",
         "  {",
         "     GRAPH <http://b> { ?s0 ?p0 ?o0 . }",
         "     FILTER (SAMETERM(?s, ?s0) && SAMETERM(?p, ?p0) && SAMETERM(?o, ?o0))",
         "  }",
         "  FILTER(!BOUND(?s0))",
         "}");

    private static final String OptionalSameTermQuery2 = StrUtils.strjoinNL
        ("SELECT *",
         "{",
         "  GRAPH <http://a>",
         "  {",
         "    ?s ?p ?o .",
         "  }",
         "  OPTIONAL",
         "  {",
         "    GRAPH <http://b> { ?s ?p ?o0 . }",
         "    FILTER (SAMETERM(?o, ?o0))",
         "  }",
         "  FILTER(!BOUND(?o0))",
         "}");

    private static final String NotExistsQuery = StrUtils.strjoinNL
        ("SELECT *",
         "{",
         "  GRAPH <http://a>",
         "  {",
         "     ?s ?p ?o .",
         "  }",
         "  FILTER NOT EXISTS { GRAPH <http://b> { ?s ?p ?o } }",
         "}");


    @AfterAll
    public static void afterTests() {
        ARQ.getContext().set(ARQ.optimization, true);
    }

    enum Optimizer { ENABLED , DISABLED }

    /**
     * Data for parameters
     */

    private static Stream<Arguments> provideArgs() {
         List<Arguments> x = List.of(
                Arguments.of( Optimizer.ENABLED ),
                Arguments.of( Optimizer.DISABLED )
                    );
        return x.stream();
        }

    /**
     * Creates new tests
     *
     * @param optimized
     *            Whether to enable the ARQ optimizer
     */
    public TestGraphDeltas(Optimizer optimized) {
        ARQ.getContext().set(ARQ.optimization, optimized==Optimizer.ENABLED);
    }

    private void testQuery(Dataset ds, String query, String queryName, int differences) {
        try(QueryExecution qe = QueryExecutionFactory.create(query, ds)) {
            ResultSetRewindable results = ResultSetFactory.makeRewindable(qe.execSelect());
            assertEquals(differences, results.size(), ()->queryName + " gave incorrect results");
        }
    }

    /**
     * Tests the delta queries which calculate deltas correctly i.e. finds
     * triples in {@code a} which are not present in {@code b}
     *
     * @param a
     *            Model A
     * @param b
     *            Model B
     * @param differences
     *            Expected number of differences
     */
    private void testDeltas(Model a, Model b, int differences) {
        Dataset ds = DatasetFactory.createGeneral();
        ds.addNamedModel("http://a", a);
        ds.addNamedModel("http://b", b);

        this.testQuery(ds, MinusQuery, "Minus", differences);
        this.testQuery(ds, OptionalSameTermQuery1, "OptionalSameTerm1", differences);
        this.testQuery(ds, OptionalSameTermQuery2, "OptionalSameTerm2", differences);
        this.testQuery(ds, NotExistsQuery, "NotExists", differences);
    }

    /**
     * Tests graph deltas calculation with SPARQL
     */
    @Test
    public void sparql_graph_delta_01() {
        Model a = ModelFactory.createDefaultModel();
        Model b = ModelFactory.createDefaultModel();

        a.read(new StringReader(testData), null, "TTL");
        b.read(new StringReader(testData), null, "TTL");

        this.testDeltas(a, b, 0);
        this.testDeltas(b, a, 0);
    }

    /**
     * Tests graph deltas calculate with SPARQL
     */
    @Test
    public void sparql_graph_delta_02() {
        Model a = ModelFactory.createDefaultModel();
        Model b = ModelFactory.createDefaultModel();

        a.read(new StringReader(testData), null, "TTL");
        b.read(new StringReader(testData2), null, "TTL");

        this.testDeltas(a, b, 0);
        this.testDeltas(b, a, 1);
    }

    /**
     * Tests graph deltas calculate with SPARQL
     */
    @Test
    public void sparql_graph_delta_03() {
        Model a = ModelFactory.createDefaultModel();
        Model b = ModelFactory.createDefaultModel();

        a.read(new StringReader(testData), null, "TTL");
        b.read(new StringReader(testData2), null, "TTL");
        b.removeAll(b.createResource("http://r1"), null, null);
        assertEquals(1, b.size());

        this.testDeltas(a, b, 1);
        this.testDeltas(b, a, 0);
    }

    /**
     * Tests graph deltas calculate with SPARQL
     */
    @Test
    public void sparql_graph_delta_04() {
        Model a = ModelFactory.createDefaultModel();
        Model b = ModelFactory.createDefaultModel();

        a.read(new StringReader(testData), null, "TTL");

        this.testDeltas(a, b, 2);
        this.testDeltas(b, a, 0);
    }

    /**
     * Tests graph deltas calculate with SPARQL
     */
    @Test
    public void sparql_graph_delta_05() {
        Model a = ModelFactory.createDefaultModel();
        Model b = ModelFactory.createDefaultModel();

        a.read(new StringReader(testData3), null, "TTL");
        b.read(new StringReader(testData), null, "TTL");

        this.testDeltas(a, b, 2);
        this.testDeltas(b, a, 0);
    }

    /**
     * Tests graph deltas calculate with SPARQL
     */
    @Test
    public void sparql_graph_delta_06() {
        Model a = ModelFactory.createDefaultModel();
        Model b = ModelFactory.createDefaultModel();

        a.read(new StringReader(testData4), null, "TTL");
        b.read(new StringReader(testData), null, "TTL");

        this.testDeltas(a, b, 6);
        this.testDeltas(b, a, 0);
    }
}
