// Protocol Buffers - Google's data interchange format
// Copyright 2023 Google Inc.  All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

#include "google/protobuf/compiler/retention.h"

#include <memory>
#include <string>

#include "google/protobuf/descriptor.pb.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "absl/log/absl_check.h"
#include "absl/log/die_if_null.h"
#include "absl/strings/substitute.h"
#include "google/protobuf/compiler/parser.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/dynamic_message.h"
#include "google/protobuf/io/tokenizer.h"
#include "google/protobuf/io/zero_copy_stream_impl_lite.h"
#include "google/protobuf/text_format.h"


namespace google {
namespace protobuf {
namespace compiler {
namespace {

MATCHER_P(EqualsProto, msg, "") {
  return msg.DebugString() == arg.DebugString();
}

class FakeErrorCollector : public io::ErrorCollector {
 public:
  FakeErrorCollector() = default;
  ~FakeErrorCollector() override = default;

  void RecordError(int line, io::ColumnNumber column,
                   absl::string_view message) override {
    ABSL_CHECK(false) << line << ":" << column << ": " << message;
  }

  void RecordWarning(int line, io::ColumnNumber column,
                     absl::string_view message) override {
    ABSL_CHECK(false) << line << ":" << column << ": " << message;
  }
};
class RetentionStripTest : public testing::Test {
 protected:
  void SetUp() override {
    FileDescriptorProto descriptor_proto_descriptor;
    FileDescriptorSet::descriptor()->file()->CopyTo(
        &descriptor_proto_descriptor);
    pool_.BuildFile(descriptor_proto_descriptor);
  }

  const FileDescriptor* ParseSchema(absl::string_view contents,
                                    absl::string_view file_name = "foo.proto") {
    std::string proto_file = absl::Substitute(
        R"schema(
          syntax = "proto2";

          package google.protobuf.internal;

          import "$0";

          $1
        )schema",
        FileDescriptorSet::descriptor()->file()->name(), contents);
    io::ArrayInputStream input_stream(proto_file.data(),
                                      static_cast<int>(proto_file.size()));
    FakeErrorCollector error_collector;
    io::Tokenizer tokenizer(&input_stream, &error_collector);
    Parser parser;
    parser.RecordErrorsTo(&error_collector);
    FileDescriptorProto file_descriptor;
    ABSL_CHECK(parser.Parse(&tokenizer, &file_descriptor));
    file_descriptor.set_name(file_name);

    return pool_.BuildFile(file_descriptor);
  }

  template <typename ProtoType>
  ProtoType BuildDynamicProto(absl::string_view data) {
    // We use a dynamic message to generate the expected options proto. This
    // lets us parse the custom options in text format.
    const Descriptor* file_options_descriptor =
        pool_.FindMessageTypeByName(ProtoType().GetTypeName());
    DynamicMessageFactory factory;
    std::unique_ptr<Message> dynamic_message(
        factory.GetPrototype(file_options_descriptor)->New());
    ABSL_CHECK(TextFormat::ParseFromString(data, dynamic_message.get()));
    ProtoType ret;
    ABSL_CHECK(ret.ParseFromString(dynamic_message->SerializeAsString()));
    return ret;
  }

  DescriptorPool pool_;
};

TEST_F(RetentionStripTest, StripSourceRetentionFileOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      option (source_retention_option) = 123;
      option (options) = {
        i1: 123
        i2: 456
        c { s: "abc" }
        rc { s: "abc" }
      };
      option (repeated_options) = {
        i1: 111 i2: 222
      };

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.FileOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  FileOptions expected_options = BuildDynamicProto<FileOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");

  EXPECT_THAT(StripSourceRetentionOptions(*file).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*file),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionProtoFileOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      option (source_retention_option) = 123;
      option (options) = {
        i1: 123
        i2: 456
        c { s: "abc" }
        rc { s: "abc" }
      };
      option (repeated_options) = {
        i1: 111 i2: 222
      };

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.FileOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      }
  )schema");

  FileDescriptorProto proto;
  file->CopyTo(&proto);

  ASSERT_THAT(proto.options(), EqualsProto(BuildDynamicProto<FileOptions>(R"pb(
                [google.protobuf.internal.source_retention_option]: 123
                [google.protobuf.internal.options] {
                  i1: 123
                  i2: 456
                  c { s: "abc" }
                  rc { s: "abc" }
                }
                [google.protobuf.internal.repeated_options] { i1: 111 i2: 222 })pb")));

  StripSourceRetentionOptions(*file->pool(), proto);

  EXPECT_THAT(proto.options(), EqualsProto(BuildDynamicProto<FileOptions>(R"pb(
                [google.protobuf.internal.options] {
                  i2: 456
                  c {}
                  rc {}
                }
                [google.protobuf.internal.repeated_options] { i2: 222 })pb")));
}

TEST_F(RetentionStripTest, StripSourceRetentionMessageOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      message TestMessage {
        option (source_retention_option) = 123;
        option (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        };
        option (repeated_options) = {
          i1: 111 i2: 222
        };
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.MessageOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  MessageOptions expected_options = BuildDynamicProto<MessageOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const Descriptor* message =
      ABSL_DIE_IF_NULL(file->FindMessageTypeByName("TestMessage"));

  EXPECT_THAT(StripSourceRetentionOptions(*file).message_type(0).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*message).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*message),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionEnumOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      enum TestEnum {
        option (source_retention_option) = 123;
        option (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        };
        option (repeated_options) = {
          i1: 111 i2: 222
        };
        VALUE1 = 0;
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.EnumOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  EnumOptions expected_options = BuildDynamicProto<EnumOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const EnumDescriptor* enm =
      ABSL_DIE_IF_NULL(file->FindEnumTypeByName("TestEnum"));

  EXPECT_THAT(StripSourceRetentionOptions(*file).enum_type(0).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*enm).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*enm),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionEnumValueOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      enum TestEnum {
        VALUE1 = 0 [(source_retention_option) = 123, (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        }, (repeated_options) = {
          i1: 111 i2: 222
        }];
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.EnumValueOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  EnumValueOptions expected_options = BuildDynamicProto<EnumValueOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const EnumDescriptor* enm =
      ABSL_DIE_IF_NULL(file->FindEnumTypeByName("TestEnum"));
  const EnumValueDescriptor* value = ABSL_DIE_IF_NULL(enm->value(0));

  EXPECT_THAT(
      StripSourceRetentionOptions(*file).enum_type(0).value(0).options(),
      EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*enm).value(0).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*value),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionFieldOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      message TestMessage {
        optional string test_field = 1 [(source_retention_option) = 123, (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        }, (repeated_options) = {
          i1: 111 i2: 222
        }];
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.FieldOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  FieldOptions expected_options = BuildDynamicProto<FieldOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const Descriptor* message =
      ABSL_DIE_IF_NULL(file->FindMessageTypeByName("TestMessage"));
  const FieldDescriptor* field =
      ABSL_DIE_IF_NULL(message->FindFieldByName("test_field"));

  EXPECT_THAT(
      StripSourceRetentionOptions(*file).message_type(0).field(0).options(),
      EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*message).field(0).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*field).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*field),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionExtensionOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      message TestMessage {
        extensions 1;
      }

      extend TestMessage {
        optional string test_field = 1 [(source_retention_option) = 123, (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        }, (repeated_options) = {
          i1: 111 i2: 222
        }];
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.FieldOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  FieldOptions expected_options = BuildDynamicProto<FieldOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const FieldDescriptor* field =
      ABSL_DIE_IF_NULL(file->FindExtensionByName("test_field"));

  EXPECT_THAT(StripSourceRetentionOptions(*file).extension(0).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*field).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*field),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionOneofOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      message TestMessage {
        oneof test_oneof {
        option (source_retention_option) = 123;
        option (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        };
        option (repeated_options) = {
          i1: 111 i2: 222
        };
          string field1 = 1;
          string field2 = 2;
        };
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.OneofOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  OneofOptions expected_options = BuildDynamicProto<OneofOptions>(
      R"pb(
        [google.protobuf.internal.options] {
          i2: 456
          c {}
          rc {}
        }
        [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const Descriptor* message =
      ABSL_DIE_IF_NULL(file->FindMessageTypeByName("TestMessage"));
  const OneofDescriptor* oneof =
      ABSL_DIE_IF_NULL(message->FindOneofByName("test_oneof"));

  EXPECT_THAT(StripSourceRetentionOptions(*file)
                  .message_type(0)
                  .oneof_decl(0)
                  .options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*message).oneof_decl(0).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*oneof).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*oneof),
              EqualsProto(expected_options));
}

TEST_F(RetentionStripTest, StripSourceRetentionExtensionRangeOptions) {
  const FileDescriptor* file = ParseSchema(R"schema(
      message TestMessage {
        extensions 1 to max [(source_retention_option) = 123, (options) = {
          i1: 123
          i2: 456
          c { s: "abc" }
          rc { s: "abc" }
        }, (repeated_options) = {
          i1: 111 i2: 222
        }];
      }

      message Options {
        optional int32 i1 = 1 [retention = RETENTION_SOURCE];
        optional int32 i2 = 2;
        message ChildMessage {
          optional string s = 1 [retention = RETENTION_SOURCE];
        }
        optional ChildMessage c = 3;
        repeated ChildMessage rc = 4;
      }

      extend google.protobuf.ExtensionRangeOptions {
        optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
        optional Options options = 50001;
        repeated Options repeated_options = 50002;
      })schema");

  ExtensionRangeOptions expected_options =
      BuildDynamicProto<ExtensionRangeOptions>(
          R"pb(
            [google.protobuf.internal.options] {
              i2: 456
              c {}
              rc {}
            }
            [google.protobuf.internal.repeated_options] { i2: 222 })pb");
  const Descriptor* message =
      ABSL_DIE_IF_NULL(file->FindMessageTypeByName("TestMessage"));
  const Descriptor::ExtensionRange* range =
      ABSL_DIE_IF_NULL(message->FindExtensionRangeContainingNumber(2));

  EXPECT_THAT(StripSourceRetentionOptions(*file)
                  .message_type(0)
                  .extension_range(0)
                  .options(),
              EqualsProto(expected_options));
  EXPECT_THAT(
      StripSourceRetentionOptions(*message).extension_range(0).options(),
      EqualsProto(expected_options));
  EXPECT_THAT(StripSourceRetentionOptions(*message, *range).options(),
              EqualsProto(expected_options));
  EXPECT_THAT(StripLocalSourceRetentionOptions(*message, *range),
              EqualsProto(expected_options));
}


}  // namespace
}  // namespace compiler
}  // namespace protobuf
}  // namespace google
