#include <catch2/catch.hpp>
#include <google/protobuf/util/json_util.h>
#include <testpb/messages.pb.h>

TEST_CASE("simple struct", "[protobuf]") {
  auto json = R"(
{
  "int_value": 9223372036854775807,
  "string_value": "foo bar",
  "int_arr": [4, 2],
  "string_arr": ["foo", "bar", "baz"],
  "int_map": {
    "42": 4,
    "72": 2,
  },
  "string_map": {
    "foo": "foo_value",
    "bar": "bar_value"
  },
  "double_value": 42
}
)";

  testproto::SimpleStruct msg;

  google::protobuf::util::JsonParseOptions options;
  auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

  CHECK(status.error_message() == "");
  REQUIRE(status.ok());
  CHECK(msg.string_value() == "foo bar");
  CHECK(msg.int_value() == 9223372036854775807);
  CHECK(msg.int_arr().size() == 2);
  CHECK(msg.int_arr().Get(0) == 4);
  CHECK(msg.int_arr().Get(1) == 2);

  CHECK(msg.string_arr().size() == 3);
  std::vector<std::string> expected{"foo", "bar", "baz"};
  CHECK(
    std::equal(
      msg.string_arr().begin(), msg.string_arr().end(),
      expected.begin(), expected.end()
    )
  );

  CHECK(msg.int_map_size() == 2);
  CHECK(msg.int_map().at(42) == 4);
  CHECK(msg.int_map().at(72) == 2);

  CHECK(msg.string_map_size() == 2);
  CHECK(msg.string_map().at("foo") == "foo_value");
  CHECK(msg.string_map().at("bar") == "bar_value");
  CHECK(msg.double_value() == 42);
}

TEST_CASE("unmarshal bytes", "[protobuf]") {
  auto json = R"(
{
  "bytes_value": "AEJmb29iYXI=",
}
)";

  testproto::BytesType msg;

  google::protobuf::util::JsonParseOptions options;
  auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

  CHECK(status.error_message() == "");
  REQUIRE(status.ok());
  using namespace std::string_literals;
  CHECK(msg.bytes_value() == "\x00\x42"s "foobar");
}

TEST_CASE("two dimensional array", "[protobuf]") {
  auto json = R"(
{
  arr: [
    {
      str_arr: ["foo", "bar"]
    }
  ]
}
)";

  testproto::TwoDimensionalArray msg;

  google::protobuf::util::JsonParseOptions options;
  auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

  CHECK(status.error_message() == "");
  REQUIRE(status.ok());
  REQUIRE(msg.arr().size() == 1);
  REQUIRE(msg.arr().at(0).str_arr().size() == 2);
  REQUIRE(msg.arr().at(0).str_arr().at(0) == "foo");
  REQUIRE(msg.arr().at(0).str_arr().at(1) == "bar");
}

TEST_CASE("invalid json", "[protobuf]") {
  auto json = R"(
{
  "string_value": invalid,
}
)";

  testproto::SimpleStruct msg;

  google::protobuf::util::JsonParseOptions options;
  auto status = JsonStringToMessage(json, &msg, options);

  REQUIRE_FALSE(status.ok());
}

TEST_CASE("null/missing integer value", "[protobuf]") {
  auto json = R"(
{
  "int_value": null,
  "string_value": "foo bar",
}
)";

  google::protobuf::util::JsonParseOptions options;

  testproto::SimpleStruct msg;
  auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

  REQUIRE(status.ok());
  REQUIRE(msg.string_value() == "foo bar");
  REQUIRE(msg.int_value() == 0);

  json = R"(
{
  "string_value": "foo bar",
}
)";

  msg = testproto::SimpleStruct{};
  status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

  REQUIRE(status.ok());
  REQUIRE(msg.string_value() == "foo bar");
  REQUIRE(msg.int_value() == 0);
}

TEST_CASE("json marshal double", "[protobuf]") {
  testproto::SimpleStruct msg;
  msg.set_double_value(42);

  std::string json;
  google::protobuf::util::JsonOptions options;
  options.add_whitespace = true;
  options.preserve_proto_field_names = true;
  options.always_print_enums_as_ints = true;
  auto status = google::protobuf::util::MessageToJsonString(msg, &json, options);
  auto expected = R"({
 "double_value": 42
}
)";

  REQUIRE(json == expected);
}

TEST_CASE("json marshal uint64", "[protobuf]") {
  testproto::SimpleStruct msg;
  msg.set_uint64_value(42);

  std::string json;
  google::protobuf::util::JsonOptions options;
  options.add_whitespace = true;
  options.preserve_proto_field_names = true;
  options.always_print_enums_as_ints = true;
  auto status = google::protobuf::util::MessageToJsonString(msg, &json, options);
  auto expected = R"({
 "uint64_value": "42"
}
)";

  REQUIRE(json == expected);
}

TEST_CASE("json marshal naming", "[protobuf]") {
  testproto::JsonNaming msg;
  msg.set_foo_value(1);
  msg.set_barvalue(2);
  msg.set_bazvalue(3);
  msg.set_qux_value(4);

  std::string json;
  google::protobuf::util::JsonOptions options;
  options.add_whitespace = true;
  options.preserve_proto_field_names = true;
  options.always_print_primitive_fields = true;
  options.always_print_enums_as_ints = true;
  auto status = google::protobuf::util::MessageToJsonString(msg, &json, options);
  auto expected = R"({
 "foo_value": 1,
 "barValue": 2,
 "BazValue": 3,
 "quX_Value": 4
}
)";

  REQUIRE(json == expected);
}

TEST_CASE("json unmarshal naming", "[protobuf]") {
  SECTION("accept snake case") {
    auto json = R"(
{
  "quX_Value": 42,
}
)";

    testproto::JsonNaming msg;
    google::protobuf::util::JsonParseOptions options;
    auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);
    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(msg.qux_value() == 42);
  }

  SECTION("accept camelCase case") {
    auto json = R"(
{
  "barValue": 42,
}
)";

    testproto::JsonNaming msg;
    google::protobuf::util::JsonParseOptions options;
    auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);
    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(msg.barvalue() == 42);
  }

  SECTION("accept CamelCase case") {
    auto json = R"(
{
  "BazValue": 42,
}
)";

    testproto::JsonNaming msg;
    google::protobuf::util::JsonParseOptions options;
    auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);
    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(msg.bazvalue() == 42);
  }
}

TEST_CASE("oneof unmarshal", "[protobuf]") {
  SECTION("accept string value") {
    auto json = R"(
{
  "string_value": "foo bar",
}
  )";

    testproto::OneofValue msg;
    google::protobuf::util::JsonParseOptions options;
    auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(msg.data_case() == testproto::OneofValue::kStringValue);
    CHECK_FALSE(msg.has_struct_value());
    CHECK(msg.string_value() == "foo bar");
  }

  SECTION("accept struct value") {
    auto json = R"(
{
  "struct_value": {
    "int_value": 42
  }
}
  )";

    testproto::OneofValue msg;
    google::protobuf::util::JsonParseOptions options;
    auto status = google::protobuf::util::JsonStringToMessage(json, &msg, options);

    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(msg.data_case() == testproto::OneofValue::kStructValue);
    CHECK(msg.has_struct_value());
    CHECK(msg.struct_value().int_value() == 42);
  }
}

TEST_CASE("oneof marshal", "[protobuf]") {
  SECTION("marshal string value") {
    testproto::OneofValue msg;
    msg.set_string_value("foo bar");
    google::protobuf::util::JsonPrintOptions options;
    options.preserve_proto_field_names = true;
    std::string json;
    auto status = google::protobuf::util::MessageToJsonString(msg, &json, options);

    auto expected = R"({"string_value":"foo bar"})";

    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(json == expected);
  }

  SECTION("marshal struct value") {
    testproto::OneofValue msg;
    msg.mutable_struct_value()->set_int_value(42);
    google::protobuf::util::JsonPrintOptions options;
    options.preserve_proto_field_names = true;
    std::string json;
    auto status = google::protobuf::util::MessageToJsonString(msg, &json, options);

    auto expected = R"({"struct_value":{"int_value":"42"}})";

    CHECK(status.error_message() == "");
    REQUIRE(status.ok());
    CHECK(json == expected);
  }

}

TEST_CASE("empty messages with OmitEmpty", "[protobuf]") {
  testproto::MessageType msg;
  msg.add_foo_arr();

  google::protobuf::util::JsonPrintOptions options;
  options.preserve_proto_field_names = true;
  options.always_print_primitive_fields = false;
  std::string json;
  auto status = google::protobuf::util::MessageToJsonString(msg, &json, options);

  auto expected = R"({"foo_arr":[{}]})";

  CHECK(json == expected);
}