/*
 * 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.riot.writer;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.fasterxml.jackson.core.JsonParseException;
import com.github.jsonldjava.core.JsonLdError;
import com.github.jsonldjava.core.JsonLdOptions;
import com.github.jsonldjava.utils.JsonUtils;

import org.apache.jena.atlas.json.JsonObject;
import org.apache.jena.atlas.json.JsonString;
import org.apache.jena.query.DatasetFactory;
import org.apache.jena.rdf.model.*;
import org.apache.jena.riot.JsonLDWriteContext;
import org.apache.jena.riot.RDFFormat;
import org.apache.jena.riot.RDFWriter;
import org.apache.jena.riot.system.PrefixMap;
import org.apache.jena.riot.system.RiotLib;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.util.Context;
import org.apache.jena.sparql.vocabulary.FOAF;
import org.apache.jena.vocabulary.RDF;
import org.junit.Test;
import org.slf4j.LoggerFactory;

public class TestJsonLDWriter {

    /**
     * Checks that JSON-LD RDFFormats supposed to be pretty are pretty
     * and that those supposed to be flat are flat
     */
    @Test public final void prettyIsNotFlat() {
        String ns = "http://www.a.com/foo/";
        Model m = simpleModel(ns);
        m.setNsPrefix("ex", ns);
        String s;

        // pretty is pretty

        s = toString(m, RDFFormat.JSONLD_EXPAND_PRETTY, null);
        assertTrue(s.trim().contains("\n"));
        s = toString(m, RDFFormat.JSONLD_COMPACT_PRETTY, null);
        assertTrue(s.trim().contains("\n"));
        s = toString(m, RDFFormat.JSONLD_FLATTEN_PRETTY, null);
        assertTrue(s.trim().contains("\n"));

        // and flat is flat

        s = toString(m, RDFFormat.JSONLD_EXPAND_FLAT, null);
        assertFalse(s.trim().contains("\n"));
        s = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, null);
        assertFalse(s.trim().contains("\n"));
        s = toString(m, RDFFormat.JSONLD_FLATTEN_FLAT, null);
        assertFalse(s.trim().contains("\n"));
        assertFalse(s.trim().contains("\n"));
        // JSON_LD FRAME case not tested here, but in testFrames
    }

    /**
     * Checks that JSON-LD RDFFormats that are supposed to return a "@context" do so.
     */
    @Test public final void contextOrNot() {
        String ns = "http://www.a.com/foo/";
        Model m = simpleModel(ns);
        m.setNsPrefix("ex", ns);
        String s;

        // there's no "@context" in expand

        s = toString(m, RDFFormat.JSONLD_EXPAND_PRETTY, null);
        assertFalse(s.contains("@context"));
        s = toString(m, RDFFormat.JSONLD_EXPAND_FLAT, null);
        assertFalse(s.contains("@context"));

        // there's an "@context" in compact and flatten

        s = toString(m, RDFFormat.JSONLD_COMPACT_PRETTY, null);
        assertTrue(s.contains("@context"));
        s = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, null);
        assertTrue(s.contains("@context"));
        s = toString(m, RDFFormat.JSONLD_FLATTEN_PRETTY, null);
        assertTrue(s.contains("@context"));
        s = toString(m, RDFFormat.JSONLD_FLATTEN_FLAT, null);
        assertTrue(s.contains("@context"));
    }

    private Model simpleModel(String ns) {
        Model m = ModelFactory.createDefaultModel();
        Resource s = m.createResource(ns + "s");
        Property p = m.createProperty(ns + "p");
        Resource o = m.createResource(ns + "o");
        m.add(s,p,o);
        return m;
    }

    /**
     * Write a model and parse it back: you should get the same thing
     * (except with frame)
     */
    @Test public final void roundTrip() {
        String ns = "http://www.a.com/foo/";
        Model m = simpleModel(ns);
        m.setNsPrefix("ex", ns);
        for (RDFFormat f : JSON_LD_FORMATS) {
            if (((RDFFormat.JSONLDVariant) f.getVariant()).isFrame()) continue;
            String s = toString(m, f, null);
            Model m2 = parse(s);
            assertTrue(m2.isIsomorphicWith(m));        
        }
    }

    /**
     * Test that we do not use an "" prefix in @Context.
     */
    @Test public final void noEmptyPrefixInContext() {
        String ns = "http://www.a.com/foo/";
        Model m = simpleModel(ns);
        m.setNsPrefix("", ns);
        String jsonld = toString(m, RDFFormat.JSONLD_COMPACT_PRETTY, null);
        assertFalse(jsonld.contains("\"\""));
        Model m2 = parse(jsonld);
        assertTrue(m2.isIsomorphicWith(m));
    }

    /** verify that one may pass a context as a JSON string, and that it is actually used in the output */
    @Test public void testSettingContextAsJsonString() {
        // 1) get the context generated by default by jena
        // for a simple model with a prefix declaration
        // 2) remove prefix declaration from model,
        // output as jsonld is different
        // 3) output the model as jsonld using the context:
        // we should get the same output as in 1
        String ns = "http://www.a.com/foo/";
        Model m = simpleModel(ns);
        m.setNsPrefix("ex", ns);

        String s1 = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, null);
        // there's a prefix in m, and we find it in the output
        String prefixStringInResult = "\"ex\":\"" + ns + "\"";
        assertTrue(s1.contains(prefixStringInResult));
        Model m1 = parse(s1);

        // this is the json object associated to "@context" in s1
        // it includes the "ex" prefix

        // String js = "{\"p\":{\"@id\":\"http://www.a.com/foo/p\",\"@type\":\"@id\"},\"ex\":\"http://www.a.com/foo/\"}";
        // constructing the js string ny hand:
        JsonObject obj = new JsonObject();  
        obj.put("@id", ns + "p");
        obj.put("@type", "@id");
        JsonObject json = new JsonObject();
        json.put("p", obj);
        json.put("ex", ns);
        String js = json.toString();

        // remove the prefix from m
        m.removeNsPrefix("ex");
        String s2 = toString(m, RDFFormat.JSONLD_COMPACT_PRETTY, null);
        // model wo prefix -> no more prefix string in result:
        assertFalse(s2.contains(prefixStringInResult));

        // the model wo prefix, output as jsonld using a context that defines the prefix    
        JsonLDWriteContext jenaCtx = new JsonLDWriteContext();
        jenaCtx.setJsonLDContext(js);

        String s3 = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, jenaCtx);

        assertTrue(s3.length() == s1.length());
        assertTrue(s3.contains(prefixStringInResult));
        Model m3 = parse(s3);
        assertTrue(m3.isIsomorphicWith(m));
        assertTrue(m3.isIsomorphicWith(m1));

        // to be noted: things also work if passing the "@context"
        js = "{\"@context\":" + js + "}";
        jenaCtx.setJsonLDContext(js);

        String s4 = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, jenaCtx);

        assertTrue(s4.length() == s1.length());
        assertTrue(s4.contains(prefixStringInResult));
        Model m4 = parse(s4);
        assertTrue(m4.isIsomorphicWith(m));
        assertTrue(m4.isIsomorphicWith(m1));
    }

    /**
     * one may pass the object expected by the JSON-LD java AP as context
     * (otherwise, same thing as testSettingContextAsJsonString)
     */
    @Test public void testSettingContextAsObjectExpectedByJsonldAPI() {
        String ns = "http://www.a.com/foo/";
        Model m = simpleModel(ns);
        m.setNsPrefix("ex", ns);

        String s1 = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, null);
        // there's a prefix in m, and we find it in the output
        String prefixStringInResult = "\"ex\":\"" + ns + "\"";
        assertTrue(s1.contains(prefixStringInResult));
        Model m1 = parse(s1);

        // the context used in this case, as it would automatically be created as none is set
        // it includes one prefix
        Object ctx = JsonLDWriter.createJsonldContext(m.getGraph());

        // remove the prefix from m
        m.removeNsPrefix("ex");
        String s2 = toString(m, RDFFormat.JSONLD_COMPACT_PRETTY, null);
        // model wo prefix -> no more prefix string in result:
        assertFalse(s2.contains(prefixStringInResult));

        // the model wo prefix, output as jsonld using a context that defines the prefix
        Context jenaCtx = new Context();
        jenaCtx.set(JsonLDWriter.JSONLD_CONTEXT, ctx);
        String s3 = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, jenaCtx);

        assertTrue(s3.length() == s1.length());
        assertTrue(s3.contains(prefixStringInResult));
        Model m3 = parse(s3);
        assertTrue(m3.isIsomorphicWith(m));
        assertTrue(m3.isIsomorphicWith(m1));
    }

    /**
     * Checks that one can pass a context defined by its URI
     * 
     * -- well NO, this doesn't work in a test setup.
     */
    //@Test 
    public final void testContextByUri() {
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://schema.org/";
        Resource s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Jane Doe");
        m.add(s, m.createProperty(ns + "url"), "http://www.janedoe.com");
        m.add(s, RDF.type, "Person");

        // we can pass an uri in the context, as a quoted string (it is a JSON string)
        JsonLDWriteContext jenaContext = new JsonLDWriteContext();
        try {
            jenaContext.set(JsonLDWriter.JSONLD_CONTEXT, "{\"@context\" : \"http://schema.org/\"}");
            String jsonld = toString(m, RDFFormat.JSONLD, jenaContext);
            // check it parses ok
            Model m2 = parse(jsonld);

            // assertTrue(m2.isIsomorphicWith(m)); // It should be the case, but no.

        } catch (Throwable e) {
            // maybe test run in a setting without external connectivity - not a real problem
            String mess = e.getMessage();
            if ((mess != null) && (mess.contains("loading remote context failed"))) {
                LoggerFactory.getLogger(getClass()).info(mess);
                e.printStackTrace();
            } else {
                throw e;
            }
        }

        // But anyway, that's not what we want to do:
        // there's no point in passing the uri of a context to have it dereferenced by jsonld-java
        // (this is for a situation where one would want to parse a jsonld file containing a context defined by a uri)
        // What we want is to pass a context to jsonld-java (in order for json-ld java to produce the correct jsonld output)
        // and then we want to replace the @context in the output by "@context":"ourUri"

        // How would we do that? see testSubstitutingContext()
    }


    /**
     * Test using a context to compute the output, and replacing the @context with a given value
     */
    @Test public void testSubstitutingContext() {
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://schema.org/";
        Resource person = m.createResource(ns + "Person");
        Resource s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Jane Doe");
        m.add(s, m.createProperty(ns + "url"), "http://www.janedoe.com");
        m.add(s, m.createProperty(ns + "jobTitle"), "Professor");
        m.add(s, RDF.type, person);

        // change @context to a URI

        JsonLDWriteContext jenaCtx = new JsonLDWriteContext();
        jenaCtx.setJsonLDContextSubstitution((new JsonString(ns)).toString());
        String jsonld;
        jsonld = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, jenaCtx);
        String c = "\"@context\":\"http://schema.org/\"";
        assertTrue(jsonld.contains(c));

        // change @context to a given ctx

        String ctx = "{\"jobTitle\":{\"@id\":\"http://ex.com/jobTitle\"},\"url\":{\"@id\":\"http://ex.com/url\"},\"name\":{\"@id\":\"http://ex.com/name\"}}";
        jenaCtx.setJsonLDContextSubstitution(ctx);
        jsonld = toString(m, RDFFormat.JSONLD_COMPACT_FLAT, jenaCtx);
        assertTrue(jsonld.contains("http://ex.com/name"));
    }

    /**
     * Checking frames
     */
    @Test public final void testFrames() throws JsonParseException, IOException {
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://schema.org/";
        Resource person = m.createResource(ns + "Person");
        Resource s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Jane Doe");
        m.add(s, m.createProperty(ns + "url"), "http://www.janedoe.com");
        m.add(s, m.createProperty(ns + "jobTitle"), "Professor");
        m.add(s, RDF.type, person);
        s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Gado Salamatou");
        m.add(s, m.createProperty(ns + "url"), "http://www.salamatou.com");
        m.add(s, RDF.type, person);
        s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Not a person");
        m.add(s, RDF.type, m.createResource(ns + "Event"));

        Context jenaCtx = new Context();
        JsonObject frame = new JsonObject();

        // only output the persons using a frame

        frame.put("@type", ns +"Person");
        jenaCtx.set(JsonLDWriter.JSONLD_FRAME, frame.toString());
        String jsonld = toString(m, RDFFormat.JSONLD_FRAME_PRETTY, jenaCtx);

        Model m2 = parse(jsonld);
        // 2 subjects with a type in m2
        assertTrue(m2.listStatements((Resource) null, RDF.type, (RDFNode) null).toList().size() == 2);
        // 2 persons in m2
        assertTrue(m2.listStatements((Resource) null, RDF.type, person).toList().size() == 2);
        // something we hadn't tested in prettyIsNotFlat
        assertTrue(jsonld.trim().contains("\n"));

        // only output the subjects which have a jobTitle

        frame = new JsonObject();
        frame.put("http://schema.org/jobTitle", new JsonObject());
        jenaCtx.set(JsonLDWriter.JSONLD_FRAME, JsonUtils.fromString(frame.toString()));
        jsonld = toString(m, RDFFormat.JSONLD_FRAME_FLAT, jenaCtx);
        m2 = parse(jsonld);
        // 1 subject with a type in m2
        assertTrue(m2.listStatements((Resource) null, RDF.type, (RDFNode) null).toList().size() == 1);
        // 1 subject with a jobTitle in m2
        assertTrue(m2.listStatements((Resource) null, m.createProperty(ns + "jobTitle"), (RDFNode) null).toList().size() == 1);
        // something we hadn't tested in prettyIsNotFlat
        assertFalse(jsonld.trim().contains("\n"));
    }

    /**
     * There was a problem with props taking a string as value.
     * cf https://mail-archives.apache.org/mod_mbox/jena-users/201604.mbox/%3c218AC4A3-030B-4248-A7DA-2B2597328242@gmail.com%3e
     */
    @Test public final void testStringPropsInContext() {
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://www.a.com/foo/";
        Resource s = m.createResource(ns + "s");
        m.add(s,m.createProperty(ns + "plangstring"),"a langstring","fr");
        m.add(s, m.createProperty(ns + "pint"), m.createTypedLiteral(42));
        m.add(s, m.createProperty(ns + "pfloat"), m.createTypedLiteral((float) 1789.14));
        m.add(s, m.createProperty(ns + "pstring"), m.createTypedLiteral("a TypedLiteral atring"));

        String jsonld = toString(m, RDFFormat.JSONLD_FLAT, null);

        // without following line in JsonLDWriter, the test fails 
        // if (! isLangString(o) && ! isSimpleString(o) )
        String vv = "\"plangstring\":{\"@language\":\"fr\",\"@value\":\"a langstring\"}";
        assertTrue(jsonld.contains(vv));
    }

    /**
     * Check there are no problems when 2 properties have the same localname
     */
    @Test public final void clashOfPropLocalnames() {
        Model m = ModelFactory.createDefaultModel();
        Resource s = m.createResource();
        String ns1 = "http://schema.org/";
        String ns2 = "http://ex.com/";

        m.add(s, m.createProperty(ns1 + "name"), "schema.org name");
        m.add(s, m.createProperty(ns2 + "name"), "ex.com name");

        String jsonld = toString(m, RDFFormat.JSONLD, null);

        // in one case, we have "name" : "xxx", and the other "http://.../name" : "yyy"
        assertTrue(jsonld.contains("\"name\" : \""));
        assertTrue(jsonld.contains("/name\" : \""));

        m.setNsPrefix("ns1", ns1);
        m.setNsPrefix("ns2", "http://ex.com/");
        jsonld = toString(m, RDFFormat.JSONLD, null);
        // we get either:
        /*
      "name" : "ex.com name",
      "ns1:name" : "schema.org name",
         */
        // or
        /*
      "name" : "schema.org name",
      "ns2:name" : "ex.com name",
         */
        assertTrue(jsonld.contains("\"name\" : \""));
        assertTrue((jsonld.contains("\"ns1:name\" : \"")) || (jsonld.contains("\"ns2:name\" : \"")));
    }

    /** Test passing a JsonLdOptions through Context */
    @Test public final void jsonldOptions() {
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://schema.org/";
        Resource s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Jane Doe");
        m.add(s, m.createProperty(ns + "url"), "http://www.janedoe.com");
        m.add(s, m.createProperty(ns + "jobTitle"), "Professor");

        // our default uses true for compactArrays

        String jsonld = toString(m, RDFFormat.JSONLD, null);

        // compactArrays is true -> no "@graph"
        assertFalse(jsonld.contains("@graph"));
        // compactArrays is true -> string, not an array for props with one value
        assertTrue(jsonld.contains("\"jobTitle\" : \"Professor\""));

        // now output using a value for JsonLdOptions in Context that sets compactArrays to false

        JsonLDWriteContext jenaCtx = new JsonLDWriteContext();

        JsonLdOptions opts = new JsonLdOptions(null);
        opts.setCompactArrays(false);       

        jenaCtx.setOptions(opts);

        jsonld = toString(m, RDFFormat.JSONLD, jenaCtx);

        // compactArrays is false -> a "@graph" node
        assertTrue(jsonld.contains("@graph"));
        // compactArrays is false -> an array for all props, even when there's only one value
        assertTrue(jsonld.contains("\"jobTitle\" : [ \"Professor\" ]"));
    }

    //
    // @vocab
    //

    // checks we get @vocab when using an "" ns prefix
    @Test public final void atVocab() throws JsonParseException, JsonLdError, IOException {
        // "Jane knows John" Model
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://schema.org/";
        Resource person = m.createResource(ns + "Person");
        Resource s = m.createResource("http://doe.com/jane");
        m.add(s, m.createProperty(ns + "name"), "Jane Doe");
        m.add(s, RDF.type, person);
        Resource s2 = m.createResource("http://doe.com/joe");
        m.add(s2, m.createProperty(ns + "name"), "John Doe");
        m.add(s2, RDF.type, person);
        m.add(s, m.createProperty(ns + "knows"), s2);

        m.setNsPrefix("", ns);
        String jsonld = toString(m, RDFFormat.JSONLD, null);
        assertTrue(jsonld.contains("@vocab"));
    }

    /**
     * setting @vocab and replacing @context
     * not really a test, sample code for JENA-1292 */
    @SuppressWarnings("unchecked")
    @Test public final void atVocabJENA1292() throws JsonParseException, JsonLdError, IOException {
        Model m = ModelFactory.createDefaultModel();
        String ns = "http://schema.org/";
        Resource person = m.createResource(ns + "Person");
        Resource s = m.createResource();
        m.add(s, m.createProperty(ns + "name"), "Jane Doe");
        m.add(s, m.createProperty(ns + "url"), "http://www.janedoe.com");
        m.add(s, m.createProperty(ns + "jobTitle"), "Professor");
        m.add(s, FOAF.nick, "jd");
        m.add(s, RDF.type, person);

        m.setNsPrefix("", ns);

        DatasetGraph g = DatasetFactory.wrap(m).asDatasetGraph();
        PrefixMap pm = RiotLib.prefixMap(g);
        String base = null;
        Context jenaContext = null;

        // the JSON-LD API object. It's a map
        Map<String, Object> map = (Map<String, Object>) JsonLDWriter.toJsonLDJavaAPI((RDFFormat.JSONLDVariant)RDFFormat.JSONLD.getVariant()
                , g, pm, base, jenaContext);

        // get the @context:
        Map<String, Object> ctx = (Map<String, Object>) map.get("@context");

        // remove from ctx declaration of props in ns
        List<String> remove = new ArrayList<>();
        for (Entry<String, Object> e : ctx.entrySet()) {
            // is it the declaration of a prop in ns?
            Object o = e.getValue();
            if (o instanceof Map) {
                o = ((Map<String, Object>) o).get("@id");
            }           
            if ((o != null) && (o instanceof String)) {
                if (((String) o).equals(ns + e.getKey())) {
                    remove.add(e.getKey());
                }
            }
        }
        for (String key : remove) {
            ctx.remove(key);
        }

        // add to ctx the "@vocab" key
        ctx.put("@vocab", "http://schema.org/");

        // JsonUtils.writePrettyPrint(new PrintWriter(System.out), map) ;
    }


    //
    // some utilities
    //

    private String toString(Model m, RDFFormat f, Context jenaContext) {
        try(ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            RDFWriter.create().source(m).format(f).context(jenaContext).output(out);
            out.flush();
            return out.toString("UTF-8");
        } catch (IOException e) { throw new RuntimeException(e); }
    }

    private Model parse(String jsonld) {
        Model m = ModelFactory.createDefaultModel();
        StringReader reader = new StringReader(jsonld);
        m.read(reader, null, "JSON-LD");
        return m;
    }

    private static RDFFormat[] JSON_LD_FORMATS = {
            RDFFormat.JSONLD_COMPACT_PRETTY,
            RDFFormat.JSONLD_FLATTEN_PRETTY,
            RDFFormat.JSONLD_EXPAND_PRETTY,
            RDFFormat.JSONLD_FRAME_PRETTY,
            RDFFormat.JSONLD_COMPACT_FLAT,
            RDFFormat.JSONLD_FLATTEN_FLAT,
            RDFFormat.JSONLD_EXPAND_FLAT,
            RDFFormat.JSONLD_FRAME_FLAT,
    };
}
