/*
 * 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.solr.response;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BinaryNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.Utils;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.ReturnFields;
import org.apache.solr.search.SolrReturnFields;
import org.junit.BeforeClass;
import org.junit.Test;
import org.noggit.CharArr;
import org.noggit.JSONParser;
import org.noggit.JSONWriter;
import org.noggit.ObjectBuilder;

public class SmileWriterTest extends SolrTestCaseJ4 {
  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig.xml", "schema.xml");
  }

  @Test
  public void testTypes() throws IOException {
    SolrQueryRequest req = req("dummy");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.add("data1", Float.NaN);
    rsp.add("data2", Double.NEGATIVE_INFINITY);
    rsp.add("data3", Float.POSITIVE_INFINITY);
    SmileResponseWriter smileResponseWriter = new SmileResponseWriter();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    smileResponseWriter.write(baos, req, rsp);
    @SuppressWarnings({"rawtypes"})
    Map m = (Map) decodeSmile(new ByteArrayInputStream(baos.toByteArray()));
    CharArr out = new CharArr();
    JSONWriter jsonWriter = new JSONWriter(out, 2);
    jsonWriter.setIndentSize(-1); // indentation by default
    jsonWriter.write(m);
    String s = new String(Utils.toUTF8(out), StandardCharsets.UTF_8);
    assertEquals(s, "{\"data1\":NaN,\"data2\":-Infinity,\"data3\":Infinity}");

    req.close();
  }

  @Test
  @SuppressWarnings({"unchecked"})
  public void testJSON() throws IOException {
    SolrQueryRequest req = req("wt", "json", "json.nl", "arrarr");
    SolrQueryResponse rsp = new SolrQueryResponse();
    SmileResponseWriter w = new SmileResponseWriter();

    ByteArrayOutputStream buf = new ByteArrayOutputStream();
    NamedList<Object> nl = new NamedList<>();
    // make sure that 2028 and 2029 are both escaped (they are illegal in javascript)
    nl.add("data1", "he\u2028llo\u2029!");
    nl.add(null, 42);
    rsp.add("nl", nl);

    rsp.add("byte", (byte) -3);
    rsp.add("short", (short) -4);
    String expected = "{\"nl\":[[\"data1\",\"he\\u2028llo\\u2029!\"],[null,42]],byte:-3,short:-4}";
    w.write(buf, req, rsp);
    @SuppressWarnings({"rawtypes"})
    Map m = (Map) decodeSmile(new ByteArrayInputStream(buf.toByteArray()));
    @SuppressWarnings({"rawtypes"})
    Map o2 = (Map) new ObjectBuilder(new JSONParser(new StringReader(expected))).getObject();
    assertEquals(Utils.toJSONString(m), Utils.toJSONString(o2));
    req.close();
  }

  @Test
  public void testJSONSolrDocument() throws IOException {
    SolrQueryRequest req = req(CommonParams.WT, "json", CommonParams.FL, "id,score");
    SolrQueryResponse rsp = new SolrQueryResponse();
    SmileResponseWriter w = new SmileResponseWriter();

    ReturnFields returnFields = new SolrReturnFields(req);
    rsp.setReturnFields(returnFields);

    ByteArrayOutputStream buf = new ByteArrayOutputStream();

    SolrDocument solrDoc = new SolrDocument();
    solrDoc.addField("id", "1");
    solrDoc.addField("subject", "hello2");
    solrDoc.addField("title", "hello3");
    solrDoc.addField("score", "0.7");

    SolrDocumentList list = new SolrDocumentList();
    list.setNumFound(1);
    list.setStart(0);
    list.setMaxScore(0.7f);
    list.add(solrDoc);

    rsp.addResponse(list);

    w.write(buf, req, rsp);

    byte[] bytes = buf.toByteArray();
    @SuppressWarnings({"rawtypes"})
    Map m = (Map) decodeSmile(new ByteArrayInputStream(bytes));
    m = (Map) m.get("response");
    @SuppressWarnings({"rawtypes"})
    List l = (List) m.get("docs");
    @SuppressWarnings({"rawtypes"})
    Map doc = (Map) l.get(0);
    assertFalse(doc.containsKey("subject"));
    assertFalse(doc.containsKey("title"));
    assertTrue(doc.containsKey("id"));
    assertTrue(doc.containsKey("score"));
    req.close();
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void test10Docs() throws IOException {
    SolrQueryResponse response = new SolrQueryResponse();
    SolrDocumentList l = constructSolrDocList(response);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    new SmileResponseWriter()
        .write(baos, new LocalSolrQueryRequest(null, new ModifiableSolrParams()), response);

    byte[] bytes = baos.toByteArray();
    Map m = (Map) decodeSmile(new ByteArrayInputStream(bytes, 0, bytes.length));
    m = (Map) m.get("results");
    List lst = (List) m.get("docs");
    assertEquals(lst.size(), 10);
    for (int i = 0; i < lst.size(); i++) {
      m = (Map) lst.get(i);
      SolrDocument d = new SolrDocument();
      d.putAll(m);
      compareSolrDocument(l.get(i), d);
    }
  }

  @SuppressWarnings({"unchecked"})
  public static SolrDocumentList constructSolrDocList(SolrQueryResponse response) {
    SolrDocumentList l = new SolrDocumentList();
    for (int i = 0; i < 10; i++) {
      l.add(sampleDoc(random(), i));
    }

    response.getValues().add("results", l);
    return l;
  }

  public static SolrDocument sampleDoc(Random r, int bufnum) {
    SolrDocument sdoc = new SolrDocument();
    sdoc.put("id", "my_id_" + bufnum);
    sdoc.put("author", str(r, 10 + r.nextInt(10)));
    sdoc.put("address", str(r, 20 + r.nextInt(20)));
    sdoc.put("license", str(r, 10));
    sdoc.put("title", str(r, 5 + r.nextInt(10)));
    sdoc.put("title_bin", str(r, 5 + r.nextInt(10)).getBytes(StandardCharsets.UTF_8));
    sdoc.put("modified_dt", r.nextInt(1000000));
    sdoc.put("creation_dt", r.nextInt(1000000));
    sdoc.put("birthdate_dt", r.nextInt(1000000));
    sdoc.put("clean", r.nextBoolean());
    sdoc.put("dirty", r.nextBoolean());
    sdoc.put("employed", r.nextBoolean());
    sdoc.put("priority", r.nextInt(100));
    sdoc.put("dependents", r.nextInt(6));
    sdoc.put("level", r.nextInt(101));
    sdoc.put("education_level", r.nextInt(10));
    // higher level of reuse for string values
    sdoc.put("state", "S" + r.nextInt(50));
    sdoc.put("country", "Country" + r.nextInt(20));
    sdoc.put("some_boolean", "" + r.nextBoolean());
    sdoc.put("another_boolean", "" + r.nextBoolean());
    return sdoc;
  }
  // common-case ascii
  static String str(Random r, int sz) {
    StringBuilder sb = new StringBuilder(sz);
    for (int i = 0; i < sz; i++) {
      sb.append('\n' + r.nextInt(128 - '\n'));
    }
    return sb.toString();
  }

  public static Object decodeSmile(InputStream is) throws IOException {
    final SmileFactory smileFactory = new SmileFactory();
    com.fasterxml.jackson.databind.ObjectMapper mapper =
        new com.fasterxml.jackson.databind.ObjectMapper(smileFactory);
    JsonNode jsonNode = mapper.readTree(is);
    return getVal(jsonNode);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  public static Object getVal(JsonNode value) {
    if (value instanceof NullNode) {
      return null;
    }
    if (value instanceof NumericNode) {
      return ((NumericNode) value).numberValue();
    }
    if (value instanceof BooleanNode) {
      ((BooleanNode) value).booleanValue();
    }
    if (value instanceof ObjectNode) {
      Iterator<Map.Entry<String, JsonNode>> it = ((ObjectNode) value).fields();
      Map result = new LinkedHashMap<>();
      while (it.hasNext()) {
        Map.Entry<String, JsonNode> e = it.next();
        result.put(e.getKey(), getVal(e.getValue()));
      }
      return result;
    }
    if (value instanceof ArrayNode) {
      ArrayList result = new ArrayList();
      Iterator<JsonNode> it = ((ArrayNode) value).elements();
      while (it.hasNext()) {
        result.add(getVal(it.next()));
      }
      return result;
    }
    if (value instanceof BinaryNode) {
      return ((BinaryNode) value).binaryValue();
    }

    return value.textValue();
  }
}
