/**
 * 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.commons.rdf.api;

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;

/**
 * Test Dataset implementation
 * <p>
 * To add to your implementation's tests, create a subclass with a name ending
 * in <code>Test</code> and provide {@link #createFactory()} which minimally
 * must support {@link RDF#createDataset()} and {@link RDF#createIRI(String)}, but
 * ideally support all operations.
 * <p>
 * This test uses try-with-resources blocks for calls to {@link Dataset#stream()}
 * and {@link Dataset#iterate()}.
 *
 * @see Dataset
 * @see RDF
 */
public abstract class AbstractDatasetTest {

    protected RDF factory;
    protected Dataset dataset;
    protected IRI alice;
    protected IRI bob;
    protected IRI name;
    protected IRI knows;
    protected IRI member;
    protected BlankNode bnode1;
    protected BlankNode bnode2;
    protected Literal aliceName;
    protected Literal bobName;
    protected Literal secretClubName;
    protected Literal companyName;
    protected Quad bobNameQuad;
    private IRI isPrimaryTopicOf;
    private IRI graph1;
    private BlankNode graph2;

    /**
     *
     * This method must be overridden by the implementing test to provide a
     * factory for the test to create {@link Dataset}, {@link IRI} etc.
     *
     * @return {@link RDF} instance to be tested.
     */
    protected abstract RDF createFactory();

    @Before
    public void createDatasetAndAdd() {
        factory = createFactory();
        dataset = factory.createDataset();
        assertEquals(0, dataset.size());

        graph1 = factory.createIRI("http://example.com/graph1");
        graph2 = factory.createBlankNode();

        alice = factory.createIRI("http://example.com/alice");
        bob = factory.createIRI("http://example.com/bob");
        name = factory.createIRI("http://xmlns.com/foaf/0.1/name");
        knows = factory.createIRI("http://xmlns.com/foaf/0.1/knows");
        member = factory.createIRI("http://xmlns.com/foaf/0.1/member");
        bnode1 = factory.createBlankNode("org1");
        bnode2 = factory.createBlankNode("org2");

        secretClubName = factory.createLiteral("The Secret Club");
        companyName = factory.createLiteral("A company");
        aliceName = factory.createLiteral("Alice");
        bobName = factory.createLiteral("Bob", "en-US");

        dataset.add(graph1, alice, name, aliceName);
        dataset.add(graph1, alice, knows, bob);

        dataset.add(graph1, alice, member, bnode1);

        bobNameQuad = factory.createQuad(graph2, bob, name, bobName);
        dataset.add(bobNameQuad);

        dataset.add(factory.createQuad(graph2, bob, member, bnode1));
        dataset.add(factory.createQuad(graph2, bob, member, bnode2));
        // NOTE: bnode1 used in both graph1 and graph2
        dataset.add(graph1, bnode1, name, secretClubName);
        dataset.add(graph2, bnode2, name, companyName);

        // default graph describes graph1 and graph2
        isPrimaryTopicOf = factory.createIRI("http://xmlns.com/foaf/0.1/isPrimaryTopicOf");
        dataset.add(null, alice, isPrimaryTopicOf, graph1);
        dataset.add(null, bob, isPrimaryTopicOf, graph2);


    }

    @Test
    public void size() throws Exception {
        assertEquals(10, dataset.size());
    }

    @Test
    public void iterate() throws Exception {
        Assume.assumeTrue(dataset.size() > 0);
        final List<Quad> quads = new ArrayList<>();
        for (final Quad t : dataset.iterate()) {
            quads.add(t);
        }
        assertEquals(dataset.size(), quads.size());

        //assertTrue(quads.contains(bobNameQuad));
        // java.util.List won't do any BlankNode mapping, so
        // instead bobNameQuad of let's check for an IRI-centric quad
        final Quad q = factory.createQuad(graph1, alice, name, aliceName);
        quads.contains(q);

        // aborted iteration
        final Iterable<Quad> iterate = dataset.iterate();
        final Iterator<Quad> it = iterate.iterator();

        assertTrue(it.hasNext());
        it.next();
        closeIterable(iterate);

        // second iteration - should start from fresh and
        // get the same count
        long count = 0;
        final Iterable<Quad> iterable = dataset.iterate();
        for (@SuppressWarnings("unused") final
        Quad t : iterable) {
            count++;
        }
        assertEquals(dataset.size(), count);

        // Pattern iteration which should cover multiple graphs.

        final Set<Quad> aliceQuads = new HashSet<>();
        for (final Quad aliceQ : dataset.iterate(null, alice, null, null)) {
            aliceQuads.add(aliceQ);
        }
        assertTrue(aliceQuads.contains(factory.createQuad(graph1, alice, name, aliceName)));
        assertTrue(aliceQuads.contains(factory.createQuad(graph1, alice, knows, bob)));
        // We can't test this by Quad equality, as bnode1 might become mapped by the
        // dataset
        //assertTrue(aliceQuads.contains(factory.createQuad(graph1, alice, member, bnode1)));
        assertTrue(aliceQuads.contains(factory.createQuad(null, alice, isPrimaryTopicOf, graph1)));
        assertEquals(4, aliceQuads.size());

        // Check the isPrimaryTopicOf statements in the default graph
        int topics = 0;
        for (final Quad topic : dataset.iterate(null, null, isPrimaryTopicOf, null)) {
            topics++;
            // COMMONSRDF-55: should not be <urn:x-arq:defaultgraph> or similar
            assertFalse(topic.getGraphName().isPresent());
        }
        assertEquals(2, topics);
    }

    @Test
    public void streamDefaultGraphNameAlice() throws Exception {
        // null below would match in ANY graph (including default graph)
        final Optional<? extends Quad> aliceTopic = dataset.stream(null, alice, isPrimaryTopicOf, null).findAny();
        assertTrue(aliceTopic.isPresent());
        // COMMONSRDF-55: should not be <urn:x-arq:defaultgraph> or similar
        assertNull(aliceTopic.get().getGraphName().orElse(null));
        assertFalse(aliceTopic.get().getGraphName().isPresent());
    }


    @Test
    public void streamDefaultGraphNameByPattern() throws Exception {
        // Explicitly select in only the default graph Optional.empty()
        final Optional<? extends Quad> aliceTopic = dataset.stream(Optional.empty(), null, null, null).findAny();
        assertTrue(aliceTopic.isPresent());
        // COMMONSRDF-55: should not be <urn:x-arq:defaultgraph> or similar
        assertNull(aliceTopic.get().getGraphName().orElse(null));
        assertFalse(aliceTopic.get().getGraphName().isPresent());
    }


    /**
     * Special quad closing for RDF4J.
     */
    private void closeIterable(final Iterable<Quad> iterate) throws Exception {
        if (iterate instanceof AutoCloseable) {
            ((AutoCloseable) iterate).close();
        }
    }

    @Test
    public void iterateFilter() throws Exception {
        final List<RDFTerm> friends = new ArrayList<>();
        final IRI alice = factory.createIRI("http://example.com/alice");
        final IRI knows = factory.createIRI("http://xmlns.com/foaf/0.1/knows");
        for (final Quad t : dataset.iterate(null, alice, knows, null)) {
            friends.add(t.getObject());
        }
        assertEquals(1, friends.size());
        assertEquals(bob, friends.get(0));

        // .. can we iterate over zero hits?
        final Iterable<Quad> iterate = dataset.iterate(Optional.of(graph2), bob, knows, alice);
        for (final Quad unexpected : iterate) {
            fail("Unexpected quad " + unexpected);
        }
        // closeIterable(iterate);
    }

    @Test
    public void contains() throws Exception {
        assertFalse(dataset.contains(null, bob, knows, alice)); // or so he claims..

        assertTrue(dataset.contains(Optional.of(graph1), alice, knows, bob));

        try (Stream<? extends Quad> stream = dataset.stream()) {
            final Optional<? extends Quad> first = stream.skip(4).findFirst();
            Assume.assumeTrue(first.isPresent());
            final Quad existingQuad = first.get();
            assertTrue(dataset.contains(existingQuad));
        }

        final Quad nonExistingQuad = factory.createQuad(graph2, bob, knows, alice);
        assertFalse(dataset.contains(nonExistingQuad));

        // An existing quad
        final Quad quad = factory.createQuad(graph1, alice, knows, bob);
        // FIXME: Should not this always be true?
         assertTrue(dataset.contains(quad));
    }

    @Test
    public void remove() throws Exception {
        final long fullSize = dataset.size();
        dataset.remove(Optional.of(graph1), alice, knows, bob);
        final long shrunkSize = dataset.size();
        assertEquals(1, fullSize - shrunkSize);

        dataset.remove(Optional.of(graph1), alice, knows, bob);
        assertEquals(shrunkSize, dataset.size()); // unchanged

        dataset.add(graph1, alice, knows, bob);
        dataset.add(graph2, alice, knows, bob);
        dataset.add(graph2, alice, knows, bob);
        // Undetermined size at this point -- but at least it
        // should be bigger
        assertTrue(dataset.size() > shrunkSize);

        // and after a single remove they should all be gone
        dataset.remove(null, alice, knows, bob);
        assertEquals(shrunkSize, dataset.size());

        Quad otherQuad;
        try (Stream<? extends Quad> stream = dataset.stream()) {
            final Optional<? extends Quad> anyQuad = stream.findAny();
            Assume.assumeTrue(anyQuad.isPresent());
            otherQuad = anyQuad.get();
        }

        dataset.remove(otherQuad);
        assertEquals(shrunkSize - 1, dataset.size());
        dataset.remove(otherQuad);
        assertEquals(shrunkSize - 1, dataset.size()); // no change

        // for some reason in rdf4j this causes duplicates!
        dataset.add(otherQuad);
        // dataset.stream().forEach(System.out::println);
        // should have increased
        assertTrue(dataset.size() >= shrunkSize);
    }

    @Test
    public void clear() throws Exception {
        dataset.clear();
        assertFalse(dataset.contains(null, alice, knows, bob));
        assertEquals(0, dataset.size());
        dataset.clear(); // no-op
        assertEquals(0, dataset.size());
        assertFalse(dataset.contains(null, null, null, null)); // nothing here
    }

    @Test
    public void getQuads() throws Exception {
        long quadCount;
        try (Stream<? extends Quad> stream = dataset.stream()) {
            quadCount = stream.count();
        }
        assertTrue(quadCount > 0);

        try (Stream<? extends Quad> stream = dataset.stream()) {
            assertTrue(stream.allMatch(t -> dataset.contains(t)));
        }

        // Check exact count
        Assume.assumeNotNull(bnode1, bnode2, aliceName, bobName, secretClubName, companyName, bobNameQuad);
        assertEquals(10, quadCount);
    }

    @Test
    public void getQuadsQuery() throws Exception {

        try (Stream<? extends Quad> stream = dataset.stream(Optional.of(graph1), alice, null, null)) {
            final long aliceCount = stream.count();
            assertTrue(aliceCount > 0);
            Assume.assumeNotNull(aliceName);
            assertEquals(3, aliceCount);
        }

        Assume.assumeNotNull(bnode1, bnode2, bobName, companyName, secretClubName);
        try (Stream<? extends Quad> stream = dataset.stream(null, null, name, null)) {
            assertEquals(4, stream.count());
        }
        Assume.assumeNotNull(bnode1);
        try (Stream<? extends Quad> stream = dataset.stream(null, null, member, null)) {
            assertEquals(3, stream.count());
        }
    }

    @Test
    public void addBlankNodesFromMultipleDatasets() throws Exception {
        // Create two separate Dataset instances
        try (final Dataset g1 = createDataset1();
                final Dataset g2 = createDataset2();
                final Dataset g3 = factory.createDataset()) {

            addAllQuads(g1, g3);
            addAllQuads(g2, g3);

            // Let's make a map to find all those blank nodes after insertion
            // (The Dataset implementation is not currently required to
            // keep supporting those BlankNodes with contains() - see
            // COMMONSRDF-15)

            final Map<String, BlankNodeOrIRI> whoIsWho = new ConcurrentHashMap<>();
            // ConcurrentHashMap as we will try parallel forEach below,
            // which should not give inconsistent results (it does with a
            // HashMap!)

            // look up BlankNodes by name
            final IRI name = factory.createIRI("http://xmlns.com/foaf/0.1/name");
            try (Stream<? extends Quad> stream = g3.stream(null, null, name, null)) {
                stream.parallel().forEach(t -> whoIsWho.put(t.getObject().ntriplesString(), t.getSubject()));
            }

            assertEquals(4, whoIsWho.size());
            // and contains 4 unique values
            assertEquals(4, new HashSet<>(whoIsWho.values()).size());

            final BlankNodeOrIRI b1Alice = whoIsWho.get("\"Alice\"");
            assertNotNull(b1Alice);
            final BlankNodeOrIRI b2Bob = whoIsWho.get("\"Bob\"");
            assertNotNull(b2Bob);
            final BlankNodeOrIRI b1Charlie = whoIsWho.get("\"Charlie\"");
            assertNotNull(b1Charlie);
            final BlankNodeOrIRI b2Dave = whoIsWho.get("\"Dave\"");
            assertNotNull(b2Dave);

            // All blank nodes should differ
            notEquals(b1Alice, b2Bob);
            notEquals(b1Alice, b1Charlie);
            notEquals(b1Alice, b2Dave);
            notEquals(b2Bob, b1Charlie);
            notEquals(b2Bob, b2Dave);
            notEquals(b1Charlie, b2Dave);

            // And we should be able to query with them again
            // as we got them back from g3
            final IRI hasChild = factory.createIRI("http://example.com/hasChild");
            // FIXME: Check graph2 BlankNode in these ..?
            assertTrue(g3.contains(null, b1Alice, hasChild, b2Bob));
            assertTrue(g3.contains(null, b2Dave, hasChild, b1Charlie));
            // But not
            assertFalse(g3.contains(null, b1Alice, hasChild, b1Alice));
            assertFalse(g3.contains(null, b1Alice, hasChild, b1Charlie));
            assertFalse(g3.contains(null, b1Alice, hasChild, b2Dave));
            // nor
            assertFalse(g3.contains(null, b2Dave, hasChild, b1Alice));
            assertFalse(g3.contains(null, b2Dave, hasChild, b1Alice));

            // and these don't have any children (as far as we know)
            assertFalse(g3.contains(null, b2Bob, hasChild, null));
            assertFalse(g3.contains(null, b1Charlie, hasChild, null));
        }
    }

    private void notEquals(final BlankNodeOrIRI node1, final BlankNodeOrIRI node2) {
        assertFalse(node1.equals(node2));
        // in which case we should be able to assume
        // (as they are in the same dataset)
        assertFalse(node1.ntriplesString().equals(node2.ntriplesString()));
    }

    /**
     * Add all quads from the source to the target.
     * <p>
     * The quads may be copied in any order. No special conversion or
     * adaptation of {@link BlankNode}s are performed.
     *
     * @param source
     *            Source Dataset to copy quads from
     * @param target
     *            Target Dataset where quads will be added
     */
    private void addAllQuads(final Dataset source, final Dataset target) {

        // unordered() as we don't need to preserve quad order
        // sequential() as we don't (currently) require target Dataset to be
        // thread-safe

        try (Stream<? extends Quad> stream = source.stream()) {
            stream.unordered().sequential().forEach(t -> target.add(t));
        }
    }

    /**
     * Make a new dataset with two BlankNodes - each with a different
     * uniqueReference
     */
    private Dataset createDataset1() {
        final RDF factory1 = createFactory();

        final IRI name = factory1.createIRI("http://xmlns.com/foaf/0.1/name");
        final Dataset g1 = factory1.createDataset();
        final BlankNode b1 = createOwnBlankNode("b1", "0240eaaa-d33e-4fc0-a4f1-169d6ced3680");
        g1.add(b1, b1, name, factory1.createLiteral("Alice"));

        final BlankNode b2 = createOwnBlankNode("b2", "9de7db45-0ce7-4b0f-a1ce-c9680ffcfd9f");
        g1.add(b2, b2, name, factory1.createLiteral("Bob"));

        final IRI hasChild = factory1.createIRI("http://example.com/hasChild");
        g1.add(null, b1, hasChild, b2);

        return g1;
    }

    /**
     * Create a different implementation of BlankNode to be tested with
     * dataset.add(a,b,c); (the implementation may or may not then choose to
     * translate such to its own instances)
     *
     * @param name
     * @return
     */
    private BlankNode createOwnBlankNode(final String name, final String uuid) {
        return new BlankNode() {
            @Override
            public String ntriplesString() {
                return "_: " + name;
            }

            @Override
            public String uniqueReference() {
                return uuid;
            }

            @Override
            public int hashCode() {
                return uuid.hashCode();
            }

            @Override
            public boolean equals(final Object obj) {
                if (!(obj instanceof BlankNode)) {
                    return false;
                }
                final BlankNode other = (BlankNode) obj;
                return uuid.equals(other.uniqueReference());
            }
        };
    }

    private Dataset createDataset2() {
        final RDF factory2 = createFactory();
        final IRI name = factory2.createIRI("http://xmlns.com/foaf/0.1/name");

        final Dataset g2 = factory2.createDataset();

        final BlankNode b1 = createOwnBlankNode("b1", "bc8d3e45-a08f-421d-85b3-c25b373abf87");
        g2.add(b1, b1, name, factory2.createLiteral("Charlie"));

        final BlankNode b2 = createOwnBlankNode("b2", "2209097a-5078-4b03-801a-6a2d2f50d739");
        g2.add(b2, b2, name, factory2.createLiteral("Dave"));

        final IRI hasChild = factory2.createIRI("http://example.com/hasChild");
        // NOTE: Opposite direction of loadDataset1
        g2.add(b2, b2, hasChild, b1);
        return g2;
    }

    /**
     * Ensure {@link Dataset#getGraphNames()} contains our two graphs.
     *
     * @throws Exception
     *             If test fails
     */
    @Test
    public void getGraphNames() throws Exception {
        final Set<BlankNodeOrIRI> names = dataset.getGraphNames().collect(Collectors.toSet());
        assertTrue("Can't find graph name " + graph1, names.contains(graph1));
        assertTrue("Found no quads in graph1", dataset.contains(Optional.of(graph1), null, null, null));

        final Optional<BlankNodeOrIRI> graphName2 = dataset.getGraphNames().filter(BlankNode.class::isInstance).findAny();
        assertTrue("Could not find graph2-like BlankNode", graphName2.isPresent());
        assertTrue("Found no quads in graph2", dataset.contains(graphName2, null, null, null));

        // Some implementations like Virtuoso might have additional internal graphs,
        // so we can't assume this:
        //assertEquals(2, names.size());
    }

    @Test
    public void getGraph() throws Exception {
        try (final Graph defaultGraph = dataset.getGraph()) {
            // TODO: Can we assume the default graph was empty before our new triples?
            assertEquals(2, defaultGraph.size());
            assertTrue(defaultGraph.contains(alice, isPrimaryTopicOf, graph1));
            // NOTE: graph2 is a BlankNode
            assertTrue(defaultGraph.contains(bob, isPrimaryTopicOf, null));
        }
    }


    @Test
    public void getGraphNull() throws Exception {
        // Default graph should be present
        try (final Graph defaultGraph = dataset.getGraph(null).get()) {
            // TODO: Can we assume the default graph was empty before our new triples?
            assertEquals(2, defaultGraph.size());
            assertTrue(defaultGraph.contains(alice, isPrimaryTopicOf, graph1));
            // NOTE: wildcard as graph2 is a (potentially mapped) BlankNode
            assertTrue(defaultGraph.contains(bob, isPrimaryTopicOf, null));
        }
    }


    @Test
    public void getGraph1() throws Exception {
        // graph1 should be present
        try (final Graph g1 = dataset.getGraph(graph1).get()) {
            assertEquals(4, g1.size());

            assertTrue(g1.contains(alice, name, aliceName));
            assertTrue(g1.contains(alice, knows, bob));
            assertTrue(g1.contains(alice, member, null));
            assertTrue(g1.contains(null, name, secretClubName));
        }
    }

    @Test
    public void getGraph2() throws Exception {
        // graph2 should be present, even if is named by a BlankNode
        // We'll look up the potentially mapped graph2 blanknode
        final BlankNodeOrIRI graph2Name = (BlankNodeOrIRI) dataset.stream(Optional.empty(), bob, isPrimaryTopicOf, null)
                .map(Quad::getObject).findAny().get();

        try (final Graph g2 = dataset.getGraph(graph2Name).get()) {
            assertEquals(4, g2.size());
            final Triple bobNameTriple = bobNameQuad.asTriple();
            assertTrue(g2.contains(bobNameTriple));
            assertTrue(g2.contains(bob, member, bnode1));
            assertTrue(g2.contains(bob, member, bnode2));
            assertFalse(g2.contains(bnode1, name, secretClubName));
            assertTrue(g2.contains(bnode2, name, companyName));
        }
    }

    @Test
    public void containsLanguageTagsCaseInsensitive() {
        // COMMONSRDF-51: Ensure we can add/contains/remove with any casing
        // of literal language tag
        final Literal lower = factory.createLiteral("Hello there", "en-gb");
        final Literal upper = factory.createLiteral("Hello there", "EN-GB");
        final Literal mixed = factory.createLiteral("Hello there", "en-GB");

        final IRI example1 = factory.createIRI("http://example.com/s1");
        final IRI greeting = factory.createIRI("http://example.com/greeting");


        dataset.add(null, example1, greeting, upper);

        // any kind of Triple should match
        assertTrue(dataset.contains(factory.createQuad(null, example1, greeting, upper)));
        assertTrue(dataset.contains(factory.createQuad(null, example1, greeting, lower)));
        assertTrue(dataset.contains(factory.createQuad(null, example1, greeting, mixed)));

        // or as patterns
        assertTrue(dataset.contains(null, null, null, upper));
        assertTrue(dataset.contains(null, null, null, lower));
        assertTrue(dataset.contains(null, null, null, mixed));
    }

    @Test
    public void containsLanguageTagsCaseInsensitiveTurkish() {
        // COMMONSRDF-51: Special test for Turkish issue where
        // "i".toLowerCase() != "i"
        // See also:
        // https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/

        // This is similar to the test in AbstractRDFTest, but on a graph
        final Locale defaultLocale = Locale.getDefault();
        try {
            Locale.setDefault(Locale.ROOT);
            final Literal lowerROOT = factory.createLiteral("moi", "fi");
            final Literal upperROOT = factory.createLiteral("moi", "FI");
            final Literal mixedROOT = factory.createLiteral("moi", "fI");
            final IRI exampleROOT = factory.createIRI("http://example.com/s1");
            final IRI greeting = factory.createIRI("http://example.com/greeting");
            dataset.add(null, exampleROOT, greeting, mixedROOT);

            final Locale turkish = Locale.forLanguageTag("TR");
            Locale.setDefault(turkish);
            // If the below assertion fails, then the Turkish
            // locale no longer have this peculiarity that
            // we want to test.
            Assume.assumeFalse("FI".toLowerCase().equals("fi"));

            // Below is pretty much the same as in
            // containsLanguageTagsCaseInsensitive()
            final Literal lower = factory.createLiteral("moi", "fi");
            final Literal upper = factory.createLiteral("moi", "FI");
            final Literal mixed = factory.createLiteral("moi", "fI");

            final IRI exampleTR = factory.createIRI("http://example.com/s2");
            dataset.add(null, exampleTR, greeting, upper);
            assertTrue(dataset.contains(factory.createQuad(null, exampleTR, greeting, upper)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleTR, greeting, upperROOT)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleTR, greeting, lower)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleTR, greeting, lowerROOT)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleTR, greeting, mixed)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleTR, greeting, mixedROOT)));
            assertTrue(dataset.contains(null, exampleTR, null, upper));
            assertTrue(dataset.contains(null, exampleTR, null, upperROOT));
            assertTrue(dataset.contains(null, exampleTR, null, lower));
            assertTrue(dataset.contains(null, exampleTR, null, lowerROOT));
            assertTrue(dataset.contains(null, exampleTR, null, mixed));
            assertTrue(dataset.contains(null, exampleTR, null, mixedROOT));

            // What about the triple we added while in ROOT locale?
            assertTrue(dataset.contains(factory.createQuad(null, exampleROOT, greeting, upper)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleROOT, greeting, lower)));
            assertTrue(dataset.contains(factory.createQuad(null, exampleROOT, greeting, mixed)));
            assertTrue(dataset.contains(null, exampleROOT, null, upper));
            assertTrue(dataset.contains(null, exampleROOT, null, lower));
            assertTrue(dataset.contains(null, exampleROOT, null, mixed));
        } finally {
            Locale.setDefault(defaultLocale);
        }
    }


    @Test
    public void removeLanguageTagsCaseInsensitive() {
        // COMMONSRDF-51: Ensure we can remove with any casing
        // of literal language tag
        final Literal lower = factory.createLiteral("Howdy", "en-us");
        final Literal upper = factory.createLiteral("Howdy", "EN-US");
        final Literal mixed = factory.createLiteral("Howdy", "en-US");

        final IRI example1 = factory.createIRI("http://example.com/s1");
        final IRI greeting = factory.createIRI("http://example.com/greeting");

        dataset.add(null, example1, greeting, upper);

        // Remove should also honour any case
        dataset.remove(null, example1, null, mixed);
        assertFalse(dataset.contains(null, null, greeting, null));

        dataset.add(null, example1, greeting, lower);
        dataset.remove(null, example1, null, upper);

        // Check with Triple
        dataset.add(factory.createQuad(null, example1, greeting, mixed));
        dataset.remove(factory.createQuad(null, example1, greeting, upper));
        assertFalse(dataset.contains(null, null, greeting, null));
    }

    private static Optional<? extends Quad> closableFindAny(final Stream<? extends Quad> stream) {
        try (Stream<? extends Quad> s = stream) {
            return s.findAny();
        }
    }

    @Test
    public void streamLanguageTagsCaseInsensitive() {
        // COMMONSRDF-51: Ensure we can add/contains/remove with any casing
        // of literal language tag
        final Literal lower = factory.createLiteral("Good afternoon", "en-gb");
        final Literal upper = factory.createLiteral("Good afternoon", "EN-GB");
        final Literal mixed = factory.createLiteral("Good afternoon", "en-GB");

        final IRI example1 = factory.createIRI("http://example.com/s1");
        final IRI greeting = factory.createIRI("http://example.com/greeting");

        dataset.add(null, example1, greeting, upper);

        // or as patterns
        assertTrue(closableFindAny(dataset.stream(null, null, null, upper)).isPresent());
        assertTrue(closableFindAny(dataset.stream(null, null, null, lower)).isPresent());
        assertTrue(closableFindAny(dataset.stream(null, null, null, mixed)).isPresent());

        // Check the quad returned equal a new quad
        final Quad q = closableFindAny(dataset.stream(null, null, null, lower)).get();
        assertEquals(q, factory.createQuad(null, example1, greeting, mixed));
    }

    /**
     * An attempt to use the Java 8 streams to look up a more complicated query.
     * <p>
     * FYI, the equivalent SPARQL version (untested):
     *
     * <pre>
     *     SELECT ?orgName WHERE {
     *             ?org foaf:name ?orgName .
     *             ?alice foaf:member ?org .
     *             ?bob foaf:member ?org .
     *             ?alice foaf:knows ?bob .
     *           FILTER NOT EXIST { ?bob foaf:knows ?alice }
     *    }
     * </pre>
     *
     * @throws Exception If test fails
     */
    @Test
    public void whyJavaStreamsMightNotTakeOverFromSparql() throws Exception {
        Assume.assumeNotNull(bnode1, bnode2, secretClubName);
        // Find a secret organizations
        try (Stream<? extends Quad> stream = dataset.stream(null, null, knows, null)) {
            assertEquals("\"The Secret Club\"",
                    // Find One-way "knows"
                    stream.filter(t -> !dataset.contains(null, (BlankNodeOrIRI) t.getObject(), knows, t.getSubject()))
                            .map(knowsQuad -> {
                                try (Stream<? extends Quad> memberOf = dataset
                                        // and those they know, what are they
                                        // member of?
                                        .stream(null, (BlankNodeOrIRI) knowsQuad.getObject(), member, null)) {
                                    return memberOf
                                            // keep those which first-guy is a
                                            // member of
                                            .filter(memberQuad -> dataset.contains(null, knowsQuad.getSubject(), member,
                                                    // First hit is good enough
                                                    memberQuad.getObject()))
                                            .findFirst().get().getObject();
                                }
                            })
                            // then look up the name of that org
                            .map(org -> {
                                try (Stream<? extends Quad> orgName = dataset.stream(null, (BlankNodeOrIRI) org, name,
                                        null)) {
                                    return orgName.findFirst().get().getObject().ntriplesString();
                                }
                            }).findFirst().get());
        }
    }
}
