/*
 *
 * Copyright 2015 Wei-Ming Wu
 *
 * Licensed 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 com.github.wnameless.json.flattener;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.wnameless.json.base.JacksonJsonValue;
import com.github.wnameless.json.unflattener.JsonUnflattener;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;

public class JsonFlattenerTest {

  ObjectMapper mapper = new ObjectMapper();

  @Test
  public void testFlattenAsMap() throws IOException {
    URL url = Resources.getResource("test2.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    assertEquals(
        "{\"a.b\":1,\"a.c\":null,\"a.d[0]\":false,\"a.d[1]\":true,\"e\":\"f\",\"g\":2.3}",
        JsonFlattener.flattenAsMap(json).toString());
  }

  @Test
  public void testFlatten() throws IOException {
    URL url = Resources.getResource("test2.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    assertEquals(
        "{\"a.b\":1,\"a.c\":null,\"a.d[0]\":false,\"a.d[1]\":true,\"e\":\"f\",\"g\":2.3}",
        JsonFlattener.flatten(json));

    assertEquals("{\"[0].a\":1,\"[1]\":2,\"[2].c[0]\":3,\"[2].c[1]\":4}",
        JsonFlattener.flatten("[{\"a\":1},2,{\"c\":[3,4]}]"));
  }

  @Test
  public void testFlattenWithJsonValueBase() throws IOException {
    URL url = Resources.getResource("test2.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    JsonNode jsonVal = new ObjectMapper().readTree(json);
    assertEquals(
        "{\"a.b\":1,\"a.c\":null,\"a.d[0]\":false,\"a.d[1]\":true,\"e\":\"f\",\"g\":2.3}",
        JsonFlattener.flatten(new JacksonJsonValue(jsonVal)));

    assertEquals("{\"[0].a\":1,\"[1]\":2,\"[2].c[0]\":3,\"[2].c[1]\":4}",
        JsonFlattener.flatten("[{\"a\":1},2,{\"c\":[3,4]}]"));
  }

  @Test
  public void testFlattenAsMapWithJsonValueBase() throws IOException {
    URL url = Resources.getResource("test2.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    JsonNode jsonVal = new ObjectMapper().readTree(json);
    assertEquals(
        "{\"a.b\":1,\"a.c\":null,\"a.d[0]\":false,\"a.d[1]\":true,\"e\":\"f\",\"g\":2.3}",
        JsonFlattener.flattenAsMap(new JacksonJsonValue(jsonVal)).toString());

    assertEquals("{\"[0].a\":1,\"[1]\":2,\"[2].c[0]\":3,\"[2].c[1]\":4}",
        JsonFlattener.flattenAsMap("[{\"a\":1},2,{\"c\":[3,4]}]").toString());
  }

  @Test
  public void testFlattenWithKeyContainsDotAndSquareBracket()
      throws IOException {
    assertEquals(
        "{\"[0][\\\"a.a.[\\\"]\":1,\"[1]\":2,\"[2].c[0]\":3,\"[2].c[1]\":4}",
        JsonFlattener.flatten("[{\"a.a.[\":1},2,{\"c\":[3,4]}]"));
  }

  @Test
  public void testHashCode() throws IOException {
    URL url1 = Resources.getResource("test.json");
    String json1 = Resources.toString(url1, Charsets.UTF_8);
    URL url2 = Resources.getResource("test2.json");
    String json2 = Resources.toString(url2, Charsets.UTF_8);

    JsonFlattener flattener = new JsonFlattener(json1);
    assertEquals(flattener.hashCode(), flattener.hashCode());
    assertEquals(flattener.hashCode(), new JsonFlattener(json1).hashCode());
    assertNotEquals(flattener.hashCode(), new JsonFlattener(json2).hashCode());
  }

  @SuppressWarnings("unlikely-arg-type")
  @Test
  public void testEquals() throws IOException {
    URL url1 = Resources.getResource("test.json");
    String json1 = Resources.toString(url1, Charsets.UTF_8);
    URL url2 = Resources.getResource("test2.json");
    String json2 = Resources.toString(url2, Charsets.UTF_8);

    JsonFlattener flattener = new JsonFlattener(json1);
    assertTrue(flattener.equals(flattener));
    assertTrue(flattener.equals(new JsonFlattener(json1)));
    assertFalse(flattener.equals(new JsonFlattener(json2)));
    assertFalse(flattener.equals(123L));
  }

  @Test
  public void testToString() throws IOException {
    URL url = Resources.getResource("test2.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    assertEquals(
        "JsonFlattener{source={\"a\":{\"b\":1,\"c\":null,\"d\":[false,true]},\"e\":\"f\",\"g\":2.3}}",
        new JsonFlattener(json).toString());
  }

  @Test
  public void testWithNoPrecisionDouble() throws IOException {
    String json = "{\"39473331\":{\"mega\":6.0,\"goals\":1.0}}";
    assertEquals("{\"39473331.mega\":6.0,\"39473331.goals\":1.0}",
        new JsonFlattener(json).flatten());
  }

  @Test
  public void testWithEmptyJsonObject() throws IOException {
    String json = "{}";
    assertEquals("{}", new JsonFlattener(json).flatten());
    assertEquals(json,
        JsonUnflattener.unflatten(new JsonFlattener(json).flatten()));
    assertEquals(newHashMap(), new JsonFlattener(json).flattenAsMap());
  }

  @Test
  public void testWithEmptyJsonArray() throws IOException {
    String json = "[]";
    assertEquals("[]", new JsonFlattener(json).flatten());
    assertEquals(ImmutableMap.of("root", newArrayList()),
        new JsonFlattener(json).flattenAsMap());
    assertEquals(json,
        JsonUnflattener.unflatten(new JsonFlattener(json).flatten()));
    assertEquals("[]", new JsonFlattener(json)
        .withFlattenMode(FlattenMode.KEEP_ARRAYS).flatten());
    assertEquals(ImmutableMap.of("root", newArrayList()),
        new JsonFlattener(json).withFlattenMode(FlattenMode.KEEP_ARRAYS)
            .flattenAsMap());
    assertEquals(json, JsonUnflattener.unflatten(new JsonFlattener(json)
        .withFlattenMode(FlattenMode.KEEP_ARRAYS).flatten()));
  }

  @Test
  public void testWithEmptyArray() {
    String json = "{\"no\":\"1\",\"name\":\"riya\",\"marks\":[]}";
    assertEquals("{\"no\":\"1\",\"name\":\"riya\",\"marks\":[]}",
        new JsonFlattener(json).flatten());
    assertEquals(json,
        JsonUnflattener.unflatten(new JsonFlattener(json).flatten()));
  }

  @Test
  public void testWithEmptyObject() {
    String json = "{\"no\":\"1\",\"name\":\"riya\",\"marks\":[{}]}";
    assertEquals("{\"no\":\"1\",\"name\":\"riya\",\"marks[0]\":{}}",
        new JsonFlattener(json).flatten());
    assertEquals(json,
        JsonUnflattener.unflatten(new JsonFlattener(json).flatten()));
  }

  @Test
  public void testWithArray() {
    String json = "[{\"abc\":123},456,[null]]";
    assertEquals("{\"[0].abc\":123,\"[1]\":456,\"[2][0]\":null}",
        new JsonFlattener(json).flatten());
    assertEquals(json,
        JsonUnflattener.unflatten(new JsonFlattener(json).flatten()));
  }

  @Test
  public void testWithSpecialCharacters() {
    String json = "[{\"abc\\t\":\" \\\" \\r \\t \1234 \"}]";
    assertEquals("{\"[0].abc\\t\":\" \\\" \\r \\t \1234 \"}",
        new JsonFlattener(json).flatten());
    json = "{\" \":[123,\"abc\"]}";
    assertEquals("{\" [0]\":123,\" [1]\":\"abc\"}",
        new JsonFlattener(json).flatten());
  }

  @Test
  public void testWithUnicodeCharacters() {
    String json = "[{\"姓名\":123}]";
    assertEquals("{\"[0].姓名\":123}", new JsonFlattener(json).flatten());
  }

  @Test
  public void testWithFlattenMode() throws IOException {
    URL url = Resources.getResource("test4.json");
    String json = Resources.toString(url, Charsets.UTF_8);
    assertEquals(
        "{\"a.b\":1,\"a.c\":null,\"a.d\":[false,{\"i.j\":[false,true,\"xy\"]}],\"e\":\"f\",\"g\":2.3,\"z\":[]}",
        new JsonFlattener(json).withFlattenMode(FlattenMode.KEEP_ARRAYS)
            .flatten());
  }

  @SuppressWarnings("deprecation")
  @Test
  public void testWithStringEscapePolicyALL_UNICODES() {
    String json = "{\"abc\":{\"def\":\"太極\"}}";
    assertEquals("{\"abc.def\":\"\\u592A\\u6975\"}", new JsonFlattener(json)
        .withStringEscapePolicy(StringEscapePolicy.ALL_UNICODES).flatten());
  }

  @Test
  public void testWithStringEscapePolicyALL() {
    String json = "{\"abc\":{\"def\":\"太極/兩儀\"}}";
    assertEquals("{\"abc.def\":\"\\u592A\\u6975\\/\\u5169\\u5100\"}",
        new JsonFlattener(json).withStringEscapePolicy(StringEscapePolicy.ALL)
            .flatten());
  }

  @Test
  public void testWithStringEscapePolicyALL_BUT_SLASH() {
    String json = "{\"abc\":{\"def\":\"太極/兩儀\"}}";
    assertEquals("{\"abc.def\":\"\\u592A\\u6975/\\u5169\\u5100\"}",
        new JsonFlattener(json)
            .withStringEscapePolicy(StringEscapePolicy.ALL_BUT_SLASH)
            .flatten());
  }

  @Test
  public void testWithStringEscapePolicyALL_BUT_UNICODE() {
    String json = "{\"abc\":{\"def\":\"太極/兩儀\"}}";
    assertEquals("{\"abc.def\":\"太極\\/兩儀\"}", new JsonFlattener(json)
        .withStringEscapePolicy(StringEscapePolicy.ALL_BUT_UNICODE).flatten());
  }

  @Test
  public void testWithStringEscapePolicyALL_BUT_SLASH_AND_UNICODE() {
    String json = "{\"abc\":{\"def\":\"太極/兩儀\"}}";
    assertEquals("{\"abc.def\":\"太極/兩儀\"}", new JsonFlattener(json)
        .withStringEscapePolicy(StringEscapePolicy.ALL_BUT_SLASH_AND_UNICODE)
        .flatten());
  }

  @Test
  public void testWithSeparator() {
    String json = "{\"abc\":{\"def\":123}}";
    assertEquals("{\"abc*def\":123}",
        new JsonFlattener(json).withSeparator('*').flatten());
  }

  @Test
  public void testWithSeparatorExceptions() {
    String json = "{\"abc\":{\"def\":123}}";
    try {
      new JsonFlattener(json).withSeparator('"');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Separator contains illegal chracter(\")", e.getMessage());
    }
    try {
      new JsonFlattener(json).withSeparator(' ');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Separator contains illegal chracter( )", e.getMessage());
    }
    try {
      new JsonFlattener(json).withSeparator('[');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Separator([) is already used in brackets", e.getMessage());
    }
    try {
      new JsonFlattener(json).withSeparator(']');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Separator(]) is already used in brackets", e.getMessage());
    }
  }

  @Test
  public void testWithLeftAndRightBracket() {
    String json = "{\"abc\":{\"A.\":[123,\"def\"]}}";
    assertEquals("{\"abc{\\\"A.\\\"}{0}\":123,\"abc{\\\"A.\\\"}{1}\":\"def\"}",
        new JsonFlattener(json).withLeftAndRightBrackets('{', '}').flatten());
  }

  @Test
  public void testWithLeftAndRightBracketsExceptions() {
    String json = "{\"abc\":{\"def\":123}}";
    try {
      new JsonFlattener(json).withLeftAndRightBrackets('#', '#');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Both brackets cannot be the same", e.getMessage());
    }
    try {
      new JsonFlattener(json).withLeftAndRightBrackets('"', ']');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Left bracket contains illegal chracter(\")",
          e.getMessage());
    }
    try {
      new JsonFlattener(json).withLeftAndRightBrackets(' ', ']');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Left bracket contains illegal chracter( )", e.getMessage());
    }
    try {
      new JsonFlattener(json).withLeftAndRightBrackets('.', ']');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Left bracket contains illegal chracter(.)", e.getMessage());
    }
    try {
      new JsonFlattener(json).withLeftAndRightBrackets('[', '"');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Right bracket contains illegal chracter(\")",
          e.getMessage());
    }
    try {
      new JsonFlattener(json).withLeftAndRightBrackets('[', ' ');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Right bracket contains illegal chracter( )",
          e.getMessage());
    }
    try {
      new JsonFlattener(json).withLeftAndRightBrackets('[', '.');
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Right bracket contains illegal chracter(.)",
          e.getMessage());
    }
  }

  @SuppressWarnings("unchecked")
  @Test
  public void testRootInMap() {
    assertEquals("null", JsonFlattener.flatten("null"));
    assertEquals(null, JsonFlattener.flattenAsMap("null").get("root"));
    assertEquals("123", JsonFlattener.flatten("123"));
    assertEquals(new BigDecimal("123"),
        JsonFlattener.flattenAsMap("123").get("root"));
    assertEquals("\"abc\"", JsonFlattener.flatten("\"abc\""));
    assertEquals("abc", JsonFlattener.flattenAsMap("\"abc\"").get("root"));
    assertEquals("true", JsonFlattener.flatten("true"));
    assertEquals(true, JsonFlattener.flattenAsMap("true").get("root"));
    assertEquals("[]", JsonFlattener.flatten("[]"));
    assertEquals(Collections.emptyList(),
        JsonFlattener.flattenAsMap("[]").get("root"));
    assertEquals("[[{\"abc.def\":123}]]",
        new JsonFlattener("[[{\"abc\":{\"def\":123}}]]")
            .withFlattenMode(FlattenMode.KEEP_ARRAYS).flatten());
    List<List<Map<String, Object>>> root =
        (List<List<Map<String, Object>>>) new JsonFlattener(
            "[[{\"abc\":{\"def\":123}}]]")
                .withFlattenMode(FlattenMode.KEEP_ARRAYS).flattenAsMap()
                .get("root");
    assertEquals(ImmutableMap.of("abc.def", new BigDecimal(123)),
        root.get(0).get(0));
  }

  @SuppressWarnings("deprecation")
  @Test
  public void testPrintMode() throws IOException {
    URL url = Resources.getResource("test.json");
    String src = Resources.toString(url, Charsets.UTF_8);

    String json =
        new JsonFlattener(src).withPrintMode(PrintMode.MINIMAL).flatten();
    assertEquals(mapper.readTree(json).toString(), json);

    json = new JsonFlattener(src).withPrintMode(PrintMode.REGULAR).flatten();
    assertEquals(mapper.readTree(json).toString(), json);

    json = new JsonFlattener(src).withPrintMode(PrintMode.PRETTY).flatten();
    assertEquals(mapper.readTree(json).toPrettyString(), json);

    src = "[[123]]";
    json = new JsonFlattener(src).withFlattenMode(FlattenMode.KEEP_ARRAYS)
        .withPrintMode(PrintMode.MINIMAL).flatten();
    assertEquals(mapper.readTree(json).toString(), json);

    json = new JsonFlattener(src).withFlattenMode(FlattenMode.KEEP_ARRAYS)
        .withPrintMode(PrintMode.REGULAR).flatten();
    assertEquals(mapper.readTree(json).toString(), json);

    json = new JsonFlattener(src).withFlattenMode(FlattenMode.KEEP_ARRAYS)
        .withPrintMode(PrintMode.PRETTY).flatten();
    assertEquals(mapper.readTree(json).toPrettyString(), json);
  }

  @SuppressWarnings("deprecation")
  @Test
  public void testNoCache() {
    JsonFlattener jf = new JsonFlattener("{\"abc\":{\"def\":123}}");
    assertSame(jf.flattenAsMap(), jf.flattenAsMap());
    assertNotSame(jf.flatten(), jf.flatten());
    assertEquals("{\"abc*def\":123}", jf.withSeparator('*').flatten());
    assertEquals(jf.flatten(), jf.withPrintMode(PrintMode.REGULAR).flatten());
  }

  @Test
  public void testNullPointerException() {
    try {
      new JsonFlattener("{\"abc\":{\"def\":123}}").withFlattenMode(null);
      fail();
    } catch (NullPointerException e) {}
    try {
      new JsonFlattener("{\"abc\":{\"def\":123}}").withStringEscapePolicy(null);
      fail();
    } catch (NullPointerException e) {}
    try {
      new JsonFlattener("{\"abc\":{\"def\":123}}").withPrintMode(null);
      fail();
    } catch (NullPointerException e) {}
  }

  @Test
  public void testFlattenWithNestedEmptyJsonObjectAndKeepArraysMode()
      throws IOException {
    URL url = Resources.getResource("test5.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    assertEquals(
        "{\"a.b\":1,\"a.c\":null,\"a.d\":[false,{\"i.j\":[false,true]}],\"e\":\"f\",\"g\":2.3,\"z\":{}}",
        new JsonFlattener(json).withFlattenMode(FlattenMode.KEEP_ARRAYS)
            .flatten());
  }

  @Test
  public void testWithSeparatorAndNestedObject() throws IOException {
    URL url = Resources.getResource("test5.json");
    String json = Resources.toString(url, Charsets.UTF_8);
    assertEquals(
        "{\"a_b\":1,\"a_c\":null,\"a_d\":[false,{\"i_j\":[false,true]}],\"e\":\"f\",\"g\":2.3,\"z\":{}}",
        new JsonFlattener(json).withFlattenMode(FlattenMode.KEEP_ARRAYS)
            .withSeparator('_').flatten());
  }

  @Test
  public void testWithRootKeyInSourceObject() {
    String json = "{\"" + JsonFlattener.ROOT + "\":null, \"ss\":[123]}";
    assertEquals("{\"" + JsonFlattener.ROOT + "\":null,\"ss[0]\":123}",
        JsonFlattener.flatten(json));
  }

  @Test
  public void testInitByReader() throws IOException {
    URL url = Resources.getResource("test.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    JsonFlattener jf =
        new JsonFlattener(new InputStreamReader(url.openStream()));
    assertEquals(jf, new JsonFlattener(json));
  }

  @Test
  public void testFlattenModeMongodb() throws IOException {
    URL url = Resources.getResource("test_mongo.json");
    String src = Resources.toString(url, Charsets.UTF_8);

    URL urlMongo = Resources.getResource("test_mongo_flattened.json");
    String expectedJson = Resources.toString(urlMongo, Charsets.UTF_8);

    String flattened =
        new JsonFlattener(src).withFlattenMode(FlattenMode.MONGODB)
            .withPrintMode(PrintMode.PRETTY).flatten();

    assertEquals(expectedJson, flattened);
  }

  @Test
  public void testFlattenModeMongodbException() {
    String json = "{\"abc\":{\"de.f\":123}}";
    JsonFlattener jf =
        new JsonFlattener(json).withFlattenMode(FlattenMode.MONGODB);
    try {
      jf.flatten();
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("Key cannot contain separator(.) in FlattenMode.MONGODB",
          e.getMessage());
    }
  }

  @Test
  public void testWithKeyTransformer() {
    String json = "{\"abc\":{\"de.f\":123}}";
    JsonFlattener jf =
        new JsonFlattener(json).withFlattenMode(FlattenMode.MONGODB)
            .withKeyTransformer(new KeyTransformer() {

              @Override
              public String transform(String key) {
                return key.replace('.', '_');
              }

            });
    assertEquals("{\"abc.de_f\":123}", jf.flatten());
  }

  @Test
  public void testWithFlattenModeKeepBottomArrays() throws IOException {
    URL url = Resources.getResource("test_keep_primitive_arrays.json");
    String json = Resources.toString(url, Charsets.UTF_8);

    URL urlKBA =
        Resources.getResource("test_keep_primitive_arrays_flattened.json");
    String expectedJson = Resources.toString(urlKBA, Charsets.UTF_8);

    JsonFlattener jf = new JsonFlattener(json)
        .withFlattenMode(FlattenMode.KEEP_PRIMITIVE_ARRAYS)
        .withPrintMode(PrintMode.PRETTY);
    String flattened = jf.flatten();

    assertEquals(expectedJson, flattened);
  }

}