/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/Assertions.h"
#include "mozilla/JSONWriter.h"
#include "mozilla/UniquePtr.h"
#include <stdio.h>
#include <string>
#include <string.h>

using mozilla::JSONWriteFunc;
using mozilla::JSONWriter;
using mozilla::MakeStringSpan;
using mozilla::MakeUnique;
using mozilla::Span;

// This writes all the output into a big buffer.
struct StringWriteFunc final : public JSONWriteFunc {
  std::string mString;

  void Write(const mozilla::Span<const char>& aStr) final {
    mString.append(aStr.data(), aStr.size());
  }
};

void Check(JSONWriter& aWriter, const char* aExpected) {
  JSONWriteFunc& func = aWriter.WriteFunc();
  const std::string& actual = static_cast<StringWriteFunc&>(func).mString;
  if (strcmp(aExpected, actual.c_str()) != 0) {
    fprintf(stderr,
            "---- EXPECTED ----\n<<<%s>>>\n"
            "---- ACTUAL ----\n<<<%s>>>\n",
            aExpected, actual.c_str());
    MOZ_RELEASE_ASSERT(false, "expected and actual output don't match");
  }
}

// Note: to convert actual output into |expected| strings that C++ can handle,
// apply the following substitutions, in order, to each line.
// - s/\\/\\\\/g    # escapes backslashes
// - s/"/\\"/g      # escapes quotes
// - s/$/\\n\\/     # adds a newline and string continuation char to each line

void TestBasicProperties() {
  const char* expected =
      "\
{\n\
 \"null\": null,\n\
 \"bool1\": true,\n\
 \"bool2\": false,\n\
 \"int1\": 123,\n\
 \"int2\": -123,\n\
 \"int3\": -123456789000,\n\
 \"double1\": 1.2345,\n\
 \"double2\": -3,\n\
 \"double3\": 1e-7,\n\
 \"double4\": 1.1111111111111111e+21,\n\
 \"string1\": \"\",\n\
 \"string2\": \"1234\",\n\
 \"string3\": \"hello\",\n\
 \"string4\": \"\\\" \\\\ \\u0007 \\b \\t \\n \\u000b \\f \\r\",\n\
 \"string5\": \"hello\",\n\
 \"string6\": \"\\\" \\\\ \\u0007 \\b \\t \",\n\
 \"span1\": \"buf1\",\n\
 \"span2\": \"buf2\",\n\
 \"span3\": \"buf3\",\n\
 \"span4\": \"buf\\n4\",\n\
 \"span5\": \"MakeStringSpan\",\n\
 \"len 0 array, multi-line\": [\n\
 ],\n\
 \"len 0 array, single-line\": [],\n\
 \"len 1 array\": [\n\
  1\n\
 ],\n\
 \"len 5 array, multi-line\": [\n\
  1,\n\
  2,\n\
  3,\n\
  4,\n\
  5\n\
 ],\n\
 \"len 3 array, single-line\": [1, [{}, 2, []], 3],\n\
 \"len 0 object, multi-line\": {\n\
 },\n\
 \"len 0 object, single-line\": {},\n\
 \"len 1 object\": {\n\
  \"one\": 1\n\
 },\n\
 \"len 5 object\": {\n\
  \"one\": 1,\n\
  \"two\": 2,\n\
  \"three\": 3,\n\
  \"four\": 4,\n\
  \"five\": 5\n\
 },\n\
 \"len 3 object, single-line\": {\"a\": 1, \"b\": [{}, 2, []], \"c\": 3}\n\
}\n\
";

  JSONWriter w(MakeUnique<StringWriteFunc>());

  w.Start();
  {
    w.NullProperty("null");

    w.BoolProperty("bool1", true);
    w.BoolProperty("bool2", false);

    w.IntProperty("int1", 123);
    w.IntProperty("int2", -0x7b);
    w.IntProperty("int3", -123456789000ll);

    w.DoubleProperty("double1", 1.2345);
    w.DoubleProperty("double2", -3);
    w.DoubleProperty("double3", 1e-7);
    w.DoubleProperty("double4", 1.1111111111111111e+21);

    w.StringProperty("string1", "");
    w.StringProperty("string2", "1234");
    w.StringProperty("string3", "hello");
    w.StringProperty("string4", "\" \\ \a \b \t \n \v \f \r");
    w.StringProperty("string5", "hello\0cut");  // '\0' marks the end.
    w.StringProperty("string6", "\" \\ \a \b \t \0 \n \v \f \r");

    const char buf1[] = {'b', 'u', 'f', '1'};
    w.StringProperty("span1", buf1);
    const char buf2[] = {'b', 'u', 'f', '2', '\0'};
    w.StringProperty("span2", buf2);
    const char buf3[] = {'b', 'u', 'f', '3', '\0', '?'};
    w.StringProperty("span3", buf3);
    const char buf4[] = {'b', 'u', 'f', '\n', '4', '\0', '?'};
    w.StringProperty("span4", buf4);
    w.StringProperty("span5", MakeStringSpan("MakeStringSpan"));

    w.StartArrayProperty("len 0 array, multi-line", w.MultiLineStyle);
    w.EndArray();

    w.StartArrayProperty("len 0 array, single-line", w.SingleLineStyle);
    w.EndArray();

    w.StartArrayProperty("len 1 array");
    { w.IntElement(1); }
    w.EndArray();

    w.StartArrayProperty("len 5 array, multi-line", w.MultiLineStyle);
    {
      w.IntElement(1);
      w.IntElement(2);
      w.IntElement(3);
      w.IntElement(4);
      w.IntElement(5);
    }
    w.EndArray();

    w.StartArrayProperty("len 3 array, single-line", w.SingleLineStyle);
    {
      w.IntElement(1);
      w.StartArrayElement();
      {
        w.StartObjectElement(w.SingleLineStyle);
        w.EndObject();

        w.IntElement(2);

        w.StartArrayElement(w.MultiLineStyle);  // style overridden from above
        w.EndArray();
      }
      w.EndArray();
      w.IntElement(3);
    }
    w.EndArray();

    w.StartObjectProperty("len 0 object, multi-line");
    w.EndObject();

    w.StartObjectProperty("len 0 object, single-line", w.SingleLineStyle);
    w.EndObject();

    w.StartObjectProperty("len 1 object");
    { w.IntProperty("one", 1); }
    w.EndObject();

    w.StartObjectProperty("len 5 object");
    {
      w.IntProperty("one", 1);
      w.IntProperty("two", 2);
      w.IntProperty("three", 3);
      w.IntProperty("four", 4);
      w.IntProperty("five", 5);
    }
    w.EndObject();

    w.StartObjectProperty("len 3 object, single-line", w.SingleLineStyle);
    {
      w.IntProperty("a", 1);
      w.StartArrayProperty("b");
      {
        w.StartObjectElement();
        w.EndObject();

        w.IntElement(2);

        w.StartArrayElement(w.SingleLineStyle);
        w.EndArray();
      }
      w.EndArray();
      w.IntProperty("c", 3);
    }
    w.EndObject();
  }
  w.End();

  Check(w, expected);
}

void TestBasicElements() {
  const char* expected =
      "\
{\n\
 \"array\": [\n\
  null,\n\
  true,\n\
  false,\n\
  123,\n\
  -123,\n\
  -123456789000,\n\
  1.2345,\n\
  -3,\n\
  1e-7,\n\
  1.1111111111111111e+21,\n\
  \"\",\n\
  \"1234\",\n\
  \"hello\",\n\
  \"\\\" \\\\ \\u0007 \\b \\t \\n \\u000b \\f \\r\",\n\
  \"hello\",\n\
  \"\\\" \\\\ \\u0007 \\b \\t \",\n\
  \"buf1\",\n\
  \"buf2\",\n\
  \"buf3\",\n\
  \"buf\\n4\",\n\
  \"MakeStringSpan\",\n\
  [\n\
  ],\n\
  [],\n\
  [\n\
   1\n\
  ],\n\
  [\n\
   1,\n\
   2,\n\
   3,\n\
   4,\n\
   5\n\
  ],\n\
  [1, [{}, 2, []], 3],\n\
  {\n\
  },\n\
  {},\n\
  {\n\
   \"one\": 1\n\
  },\n\
  {\n\
   \"one\": 1,\n\
   \"two\": 2,\n\
   \"three\": 3,\n\
   \"four\": 4,\n\
   \"five\": 5\n\
  },\n\
  {\"a\": 1, \"b\": [{}, 2, []], \"c\": 3}\n\
 ]\n\
}\n\
";

  JSONWriter w(MakeUnique<StringWriteFunc>());

  w.Start();
  w.StartArrayProperty("array");
  {
    w.NullElement();

    w.BoolElement(true);
    w.BoolElement(false);

    w.IntElement(123);
    w.IntElement(-0x7b);
    w.IntElement(-123456789000ll);

    w.DoubleElement(1.2345);
    w.DoubleElement(-3);
    w.DoubleElement(1e-7);
    w.DoubleElement(1.1111111111111111e+21);

    w.StringElement("");
    w.StringElement("1234");
    w.StringElement("hello");
    w.StringElement("\" \\ \a \b \t \n \v \f \r");
    w.StringElement("hello\0cut");  // '\0' marks the end.
    w.StringElement("\" \\ \a \b \t \0 \n \v \f \r");

    const char buf1[] = {'b', 'u', 'f', '1'};
    w.StringElement(buf1);
    const char buf2[] = {'b', 'u', 'f', '2', '\0'};
    w.StringElement(buf2);
    const char buf3[] = {'b', 'u', 'f', '3', '\0', '?'};
    w.StringElement(buf3);
    const char buf4[] = {'b', 'u', 'f', '\n', '4', '\0', '?'};
    w.StringElement(buf4);
    w.StringElement(MakeStringSpan("MakeStringSpan"));

    w.StartArrayElement();
    w.EndArray();

    w.StartArrayElement(w.SingleLineStyle);
    w.EndArray();

    w.StartArrayElement();
    { w.IntElement(1); }
    w.EndArray();

    w.StartArrayElement();
    {
      w.IntElement(1);
      w.IntElement(2);
      w.IntElement(3);
      w.IntElement(4);
      w.IntElement(5);
    }
    w.EndArray();

    w.StartArrayElement(w.SingleLineStyle);
    {
      w.IntElement(1);
      w.StartArrayElement();
      {
        w.StartObjectElement(w.SingleLineStyle);
        w.EndObject();

        w.IntElement(2);

        w.StartArrayElement(w.MultiLineStyle);  // style overridden from above
        w.EndArray();
      }
      w.EndArray();
      w.IntElement(3);
    }
    w.EndArray();

    w.StartObjectElement();
    w.EndObject();

    w.StartObjectElement(w.SingleLineStyle);
    w.EndObject();

    w.StartObjectElement();
    { w.IntProperty("one", 1); }
    w.EndObject();

    w.StartObjectElement();
    {
      w.IntProperty("one", 1);
      w.IntProperty("two", 2);
      w.IntProperty("three", 3);
      w.IntProperty("four", 4);
      w.IntProperty("five", 5);
    }
    w.EndObject();

    w.StartObjectElement(w.SingleLineStyle);
    {
      w.IntProperty("a", 1);
      w.StartArrayProperty("b");
      {
        w.StartObjectElement();
        w.EndObject();

        w.IntElement(2);

        w.StartArrayElement(w.SingleLineStyle);
        w.EndArray();
      }
      w.EndArray();
      w.IntProperty("c", 3);
    }
    w.EndObject();
  }
  w.EndArray();
  w.End();

  Check(w, expected);
}

void TestOneLineObject() {
  const char* expected =
      "\
{\"i\": 1, \"array\": [null, [{}], {\"o\": {}}, \"s\"], \"d\": 3.33}\n\
";

  JSONWriter w(MakeUnique<StringWriteFunc>());

  w.Start(w.SingleLineStyle);

  w.IntProperty("i", 1);

  w.StartArrayProperty("array");
  {
    w.NullElement();

    w.StartArrayElement(w.MultiLineStyle);  // style overridden from above
    {
      w.StartObjectElement();
      w.EndObject();
    }
    w.EndArray();

    w.StartObjectElement();
    {
      w.StartObjectProperty("o");
      w.EndObject();
    }
    w.EndObject();

    w.StringElement("s");
  }
  w.EndArray();

  w.DoubleProperty("d", 3.33);

  w.End();

  Check(w, expected);
}

void TestOneLineJson() {
  const char* expected =
      "\
{\"i\":1,\"array\":[null,[{}],{\"o\":{}},\"s\"],\"d\":3.33}\
";

  StringWriteFunc func;
  JSONWriter w(func, JSONWriter::SingleLineStyle);

  w.Start(w.MultiLineStyle);  // style overridden from above

  w.IntProperty("i", 1);

  w.StartArrayProperty("array");
  {
    w.NullElement();

    w.StartArrayElement(w.MultiLineStyle);  // style overridden from above
    {
      w.StartObjectElement();
      w.EndObject();
    }
    w.EndArray();

    w.StartObjectElement();
    {
      w.StartObjectProperty("o");
      w.EndObject();
    }
    w.EndObject();

    w.StringElement("s");
  }
  w.EndArray();

  w.DoubleProperty("d", 3.33);

  w.End();  // No newline in this case.

  Check(w, expected);
}

void TestStringEscaping() {
  // This test uses hexadecimal character escapes because UTF8 literals cause
  // problems for some compilers (see bug 1069726).
  const char* expected =
      "\
{\n\
 \"ascii\": \"\x7F~}|{zyxwvutsrqponmlkjihgfedcba`_^]\\\\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\\\"! \\u001f\\u001e\\u001d\\u001c\\u001b\\u001a\\u0019\\u0018\\u0017\\u0016\\u0015\\u0014\\u0013\\u0012\\u0011\\u0010\\u000f\\u000e\\r\\f\\u000b\\n\\t\\b\\u0007\\u0006\\u0005\\u0004\\u0003\\u0002\\u0001\",\n\
 \"\xD9\x85\xD8\xB1\xD8\xAD\xD8\xA8\xD8\xA7 \xD9\x87\xD9\x86\xD8\xA7\xD9\x83\": true,\n\
 \"\xD5\xA2\xD5\xA1\xD6\x80\xD5\xA5\xD6\x82 \xD5\xB9\xD5\xAF\xD5\xA1\": -123,\n\
 \"\xE4\xBD\xA0\xE5\xA5\xBD\": 1.234,\n\
 \"\xCE\xB3\xCE\xB5\xCE\xB9\xCE\xB1 \xCE\xB5\xCE\xBA\xCE\xB5\xCE\xAF\": \"\xD8\xB3\xD9\x84\xD8\xA7\xD9\x85\",\n\
 \"hall\xC3\xB3 \xC3\xBE"
      "arna\": 4660,\n\
 \"\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF\": {\n\
  \"\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82\": [\n\
  ]\n\
 }\n\
}\n\
";

  JSONWriter w(MakeUnique<StringWriteFunc>());

  // Test the string escaping behaviour.
  w.Start();
  {
    // Test all 127 ascii values. Do it in reverse order so that the 0
    // at the end serves as the null char.
    char buf[128];
    for (int i = 0; i < 128; i++) {
      buf[i] = 127 - i;
    }
    w.StringProperty("ascii", buf);

    // Test lots of unicode stuff. Note that this file is encoded as UTF-8.
    w.BoolProperty(
        "\xD9\x85\xD8\xB1\xD8\xAD\xD8\xA8\xD8\xA7 "
        "\xD9\x87\xD9\x86\xD8\xA7\xD9\x83",
        true);
    w.IntProperty(
        "\xD5\xA2\xD5\xA1\xD6\x80\xD5\xA5\xD6\x82 \xD5\xB9\xD5\xAF\xD5\xA1",
        -123);
    w.DoubleProperty("\xE4\xBD\xA0\xE5\xA5\xBD", 1.234);
    w.StringProperty(
        "\xCE\xB3\xCE\xB5\xCE\xB9\xCE\xB1 \xCE\xB5\xCE\xBA\xCE\xB5\xCE\xAF",
        "\xD8\xB3\xD9\x84\xD8\xA7\xD9\x85");
    w.IntProperty(
        "hall\xC3\xB3 \xC3\xBE"
        "arna",
        0x1234);
    w.StartObjectProperty(
        "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF");
    {
      w.StartArrayProperty("\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82");
      w.EndArray();
    }
    w.EndObject();
  }
  w.End();

  Check(w, expected);
}

void TestDeepNesting() {
  const char* expected =
      "\
{\n\
 \"a\": [\n\
  {\n\
   \"a\": [\n\
    {\n\
     \"a\": [\n\
      {\n\
       \"a\": [\n\
        {\n\
         \"a\": [\n\
          {\n\
           \"a\": [\n\
            {\n\
             \"a\": [\n\
              {\n\
               \"a\": [\n\
                {\n\
                 \"a\": [\n\
                  {\n\
                   \"a\": [\n\
                    {\n\
                    }\n\
                   ]\n\
                  }\n\
                 ]\n\
                }\n\
               ]\n\
              }\n\
             ]\n\
            }\n\
           ]\n\
          }\n\
         ]\n\
        }\n\
       ]\n\
      }\n\
     ]\n\
    }\n\
   ]\n\
  }\n\
 ]\n\
}\n\
";

  JSONWriter w(MakeUnique<StringWriteFunc>());

  w.Start();
  {
    static const int n = 10;
    for (int i = 0; i < n; i++) {
      w.StartArrayProperty("a");
      w.StartObjectElement();
    }
    for (int i = 0; i < n; i++) {
      w.EndObject();
      w.EndArray();
    }
  }
  w.End();

  Check(w, expected);
}

void TestEscapedPropertyNames() {
  const char* expected =
      "\
{\"i\\t\": 1, \"array\\t\": [null, [{}], {\"o\\t\": {}}, \"s\"], \"d\": 3.33}\n\
";

  JSONWriter w(MakeUnique<StringWriteFunc>());

  w.Start(w.SingleLineStyle);

  w.IntProperty("i\t\0cut", 1);  // '\0' marks the end.

  w.StartArrayProperty("array\t");
  {
    w.NullElement();

    w.StartArrayElement(w.MultiLineStyle);  // style overridden from above
    {
      w.StartObjectElement();
      w.EndObject();
    }
    w.EndArray();

    w.StartObjectElement();
    {
      w.StartObjectProperty("o\t");
      w.EndObject();
    }
    w.EndObject();

    w.StringElement("s");
  }
  w.EndArray();

  w.DoubleProperty("d\0\t", 3.33);

  w.End();

  Check(w, expected);
}

int main(void) {
  TestBasicProperties();
  TestBasicElements();
  TestOneLineObject();
  TestOneLineJson();
  TestStringEscaping();
  TestDeepNesting();
  TestEscapedPropertyNames();

  return 0;
}
