// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <set>
#include <string>

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "third_party/protobuf/src/google/protobuf/compiler/code_generator.h"
#include "third_party/protobuf/src/google/protobuf/compiler/cpp/helpers.h"
#include "third_party/protobuf/src/google/protobuf/compiler/cpp/names.h"
#include "third_party/protobuf/src/google/protobuf/compiler/importer.h"
#include "third_party/protobuf/src/google/protobuf/compiler/plugin.h"
#include "third_party/protobuf/src/google/protobuf/descriptor.h"
#include "third_party/protobuf/src/google/protobuf/io/printer.h"

namespace {

using google::protobuf::Descriptor;
using google::protobuf::Edition;
using google::protobuf::FieldDescriptor;
using google::protobuf::FileDescriptor;
using google::protobuf::compiler::GeneratorContext;
using google::protobuf::compiler::cpp::ClassName;
using google::protobuf::compiler::cpp::FieldName;
using google::protobuf::compiler::cpp::Namespace;
using google::protobuf::compiler::cpp::NamespaceOpener;
using google::protobuf::compiler::cpp::QualifiedClassName;
using google::protobuf::io::Printer;
using google::protobuf::io::ZeroCopyOutputStream;

// Returns if the descriptor is for a synthetic 'map entry' message type,
// which is internally created by the protobuf library to support map fields.
// Map fields are instead handled explicitly in the generation via the
// `is_map()` case.
bool IsSyntheticMapEntry(const Descriptor& message) {
  return message.map_key() != nullptr;
}

class ProtoGmockGenerator : public google::protobuf::compiler::CodeGenerator {
 public:
  ProtoGmockGenerator() = default;
  ~ProtoGmockGenerator() override = default;

  uint64_t GetSupportedFeatures() const override {
    return FEATURE_PROTO3_OPTIONAL | FEATURE_SUPPORTS_EDITIONS;
  }

  Edition GetMinimumEdition() const override { return Edition::EDITION_PROTO2; }

  Edition GetMaximumEdition() const override { return Edition::EDITION_2024; }

  bool Generate(const FileDescriptor* file,
                const std::string& options,  // Options from build system
                GeneratorContext* context,
                std::string* error) const override {
    CHECK(file);

    base::FilePath proto_file_path = base::FilePath::FromASCII(file->name());

    base::FilePath h_file_path =
        proto_file_path.ReplaceExtension(FILE_PATH_LITERAL("test.h"));
    const std::unique_ptr<ZeroCopyOutputStream> h_stream(
        context->Open(h_file_path.AsUTF8Unsafe()));
    Printer h_printer(h_stream.get(), Printer::Options{'$', nullptr});

    std::string include_guard =
        base::ToUpperASCII(h_file_path.AsUTF8Unsafe()) + "_";
    CHECK(base::ReplaceChars(include_guard, ".-/\\", "_", &include_guard));

    std::set<std::string> includes = {
        "testing/gmock/include/gmock/gmock.h",
        proto_file_path.ReplaceExtension(FILE_PATH_LITERAL("pb.h"))
            .AsUTF8Unsafe(),
        "components/proto_extras/proto_matchers.h"};
    for (int i = 0; i < file->dependency_count(); i++) {
      base::FilePath dependency_proto_file_path =
          base::FilePath::FromASCII(file->dependency(i)->name());
      includes.insert(dependency_proto_file_path
                          .ReplaceExtension(FILE_PATH_LITERAL("test.h"))
                          .AsUTF8Unsafe());
    }

    h_printer.Emit(
        {{"include_guard", include_guard},
         {"proto_file_path", proto_file_path.AsUTF8Unsafe()},
         {"includes",
          [&] {
            for (const auto& include : includes) {
              h_printer.Print("#include \"$f$\"\n", "f", include);
            }
          }},
         {"equals_proto_declarations",
          [&] {
            NamespaceOpener ns(Namespace(file), &h_printer);
            for (int i = 0; i < file->message_type_count(); i++) {
              EqualsProtoDeclarationsRecursive(*file->message_type(i),
                                               &h_printer);
            }
          }},
         {"matchers",
          [&] {
            NamespaceOpener ns(Namespace(file), &h_printer);
            NamespaceOpener internal_ns("internal", &h_printer);
            for (int i = 0; i < file->message_type_count(); i++) {
              PrintMatchersRecursive(*file->message_type(i), &h_printer);
            }
          }},
         {"print_to_declarations",
          [&] {
            NamespaceOpener ns(Namespace(file), &h_printer);
            for (int i = 0; i < file->message_type_count(); i++) {
              PrintToDeclarationRecursive(*file->message_type(i), &h_printer);
            }
          }}},
        R"(// Generated by the proto_test_extras plugin.  DO NOT EDIT!
// source: $proto_file_path$

#ifndef $include_guard$
#define $include_guard$

#include <iosfwd>

$includes$

$equals_proto_declarations$

$matchers$

$print_to_declarations$

#endif  // $include_guard$
)");

    base::FilePath cc_file_path =
        proto_file_path.ReplaceExtension(FILE_PATH_LITERAL("test.cc"));
    const std::unique_ptr<ZeroCopyOutputStream> cc_stream(
        context->Open(cc_file_path.AsUTF8Unsafe()));
    Printer cc_printer(cc_stream.get(), Printer::Options{'$', nullptr});
    cc_printer.Emit(
        {{"proto_file_path", proto_file_path.AsUTF8Unsafe()},
         {"header",
          proto_file_path.ReplaceExtension(FILE_PATH_LITERAL("test.h"))
              .AsUTF8Unsafe()},
         {"to_value_header",
          proto_file_path.ReplaceExtension(FILE_PATH_LITERAL("to_value.h"))
              .AsUTF8Unsafe()},
         {"equals_proto_definitions",
          [&] {
            NamespaceOpener ns(Namespace(file), &cc_printer);
            for (int i = 0; i < file->message_type_count(); i++) {
              EqualsProtoDefinitionRecursive(*file->message_type(i),
                                             &cc_printer);
            }
          }},
         {"print_to_definitions",
          [&] {
            NamespaceOpener ns(Namespace(file), &cc_printer);
            for (int i = 0; i < file->message_type_count(); i++) {
              PrintToDefinitionRecursive(*file->message_type(i), &cc_printer);
            }
          }}},
        R"(// Generated by the proto_test_extras plugin. DO NOT EDIT!
// source: $proto_file_path$

#include "$header$"

#include <ostream>

#include "base/values.h"
#include "$to_value_header$"

$equals_proto_definitions$

$print_to_definitions$

)");

    return true;
  }

  std::string UniqueMatcherName(const Descriptor& message) const {
    return base::StrCat({"Equals", ClassName(&message)});
  }

  std::string QualifiedMatcher(const Descriptor& message) const {
    return base::StrCat(
        {Namespace(&message), "::", "Equals", ClassName(&message)});
  }

  void PrintFieldMatcher(const FieldDescriptor& field,
                         const std::string& message_class_name,
                         Printer* printer) const {
    std::string field_accessor = FieldName(&field);

    if (field.is_map()) {
      std::string maybe_nested_matcher;
      if (field.message_type()->map_value()->type() ==
              FieldDescriptor::Type::TYPE_MESSAGE ||
          field.message_type()->map_value()->type() ==
              FieldDescriptor::Type::TYPE_GROUP) {
        maybe_nested_matcher =
            ", &" + QualifiedMatcher(
                        *field.message_type()->map_value()->message_type());
      }
      printer->Emit({{"message_class_name", message_class_name},
                     {"field_accessor", field_accessor},
                     {"maybe_nested_matcher", maybe_nested_matcher}},
                    R"(::proto_extras::HasMapField(
    "$field_accessor$",
    &$message_class_name$::$field_accessor$,
    expected$maybe_nested_matcher$))");
      return;
    }
    if (field.is_repeated()) {
      std::string maybe_nested_matcher;
      if (field.type() == FieldDescriptor::Type::TYPE_MESSAGE ||
          field.type() == FieldDescriptor::Type::TYPE_GROUP) {
        maybe_nested_matcher = ", &" + QualifiedMatcher(*field.message_type());
      }
      std::string resolve_field_function;
      if (field.type() == FieldDescriptor::Type::TYPE_MESSAGE ||
          field.type() == FieldDescriptor::Type::TYPE_GROUP ||
          field.type() == FieldDescriptor::Type::TYPE_STRING ||
          field.type() == FieldDescriptor::Type::TYPE_BYTES) {
        resolve_field_function = "::proto_extras::ResolveRepeatedPtrField";
      } else {
        resolve_field_function = "::proto_extras::ResolveRepeatedField";
      }
      printer->Emit(
          {
              {"message_class_name", message_class_name},
              {"field_accessor", field_accessor},
              {"resolve_field_function", resolve_field_function},
              {"maybe_nested_matcher", maybe_nested_matcher},
          },
          R"(::proto_extras::HasRepeatedField(
    "$field_accessor$",
    $resolve_field_function$(&$message_class_name$::$field_accessor$),
    expected$maybe_nested_matcher$))");
    } else if (field.has_presence()) {
      std::string matcher;
      if (field.type() == FieldDescriptor::Type::TYPE_MESSAGE ||
          field.type() == FieldDescriptor::Type::TYPE_GROUP) {
        matcher = QualifiedMatcher(*field.message_type());
      } else {
        matcher = "testing::Eq";
      }
      printer->Emit(
          {
              {"field_accessor", field_accessor},
              {"message_class_name", message_class_name},
              {"matcher", matcher},
          },
          R"(::proto_extras::HasOptionalField(
    "$field_accessor$",
    &$message_class_name$::has_$field_accessor$,
    &$message_class_name$::$field_accessor$,
    expected.has_$field_accessor$(),
    $matcher$(expected.$field_accessor$())))");
    } else {
      printer->Emit(
          {{"message_class_name", message_class_name},
           {"field_accessor", field_accessor},
           {"value_matcher",
            [&] {
              if (field.type() == FieldDescriptor::Type::TYPE_MESSAGE ||
                  field.type() == FieldDescriptor::Type::TYPE_GROUP) {
                printer->Emit({{"nested_matcher",
                                QualifiedMatcher(*field.message_type())},
                               {"field_accessor", field_accessor}},
                              "$nested_matcher$(expected.$field_accessor$())");
              } else {
                printer->Print("testing::Eq(expected.$field_accessor$())",
                               "field_accessor", field_accessor);
              }
            }}},
          R"(testing::Property(
    "$field_accessor$",
    &$message_class_name$::$field_accessor$,
    $value_matcher$))");
    }
  }

  void PrintMatcher(const Descriptor& message, Printer* printer) const {
    std::string message_class_name = ClassName(&message);
    std::string matcher_name = UniqueMatcherName(message);
    if (message.field_count() == 0) {
      printer->Emit({{"matcher_name", matcher_name}},
                    R"(
MATCHER_P($matcher_name$, expected, "") {
  return true;
})");
      return;
    }

    printer->Emit(
        {{"message_type", message_class_name}, {"matcher_name", matcher_name}},
        R"(
MATCHER_P($matcher_name$, expected, "") {
  return testing::ExplainMatchResult(
      testing::AllOf()");
    {
      auto unindent = printer->WithIndent(10);
      if (message.field_count() == 0) {
        printer->Print("testing::IsTrue()");
      } else {
        for (int i = 0; i < message.field_count(); ++i) {
          const FieldDescriptor& field = *message.field(i);
          PrintFieldMatcher(field, message_class_name, printer);
          if (i < message.field_count() - 1) {
            printer->Print(",\n");
          } else {
            printer->Print("\n");
          }
        }
      }
    }

    printer->Print(R"(
      ), arg, result_listener);
}
)");
  }

  void PrintMatchersRecursive(const Descriptor& message,
                              Printer* printer) const {
    if (IsSyntheticMapEntry(message)) {
      return;
    }
    // Nested types need to be printed before the parent message so that the
    // parent can reference them.
    for (int i = 0; i < message.nested_type_count(); i++) {
      PrintMatchersRecursive(*message.nested_type(i), printer);
    }
    PrintMatcher(message, printer);
  }

  void EqualsProtoDeclaration(const Descriptor& message,
                              Printer* printer) const {
    std::string message_class_name = ClassName(&message);
    printer->Emit({{"message_type", message_class_name}},
                  R"(
::testing::Matcher<const $message_type$&> Equals$message_type$(const $message_type$& expected);)");
  }

  void EqualsProtoDefinition(const Descriptor& message,
                             Printer* printer) const {
    std::string message_class_name = ClassName(&message);
    printer->Emit({{"message_type", message_class_name},
                   {"namespace", Namespace(&message)},
                   {"qualified_message_type", QualifiedClassName(&message)}},
                  R"(
::testing::Matcher<const $message_type$&> Equals$message_type$(const $message_type$& expected) {
  return $namespace$::internal::Equals$message_type$<$qualified_message_type$>(expected);
})");
  }

  void EqualsProtoDeclarationsRecursive(const Descriptor& message,
                                        Printer* printer) const {
    if (IsSyntheticMapEntry(message)) {
      return;
    }
    EqualsProtoDeclaration(message, printer);
    for (int i = 0; i < message.nested_type_count(); i++) {
      EqualsProtoDeclarationsRecursive(*message.nested_type(i), printer);
    }
  }

  void EqualsProtoDefinitionRecursive(const Descriptor& message,
                                      Printer* printer) const {
    if (IsSyntheticMapEntry(message)) {
      return;
    }
    EqualsProtoDefinition(message, printer);
    for (int i = 0; i < message.nested_type_count(); i++) {
      EqualsProtoDefinitionRecursive(*message.nested_type(i), printer);
    }
  }

  void PrintToDeclaration(const Descriptor& message, Printer* printer) const {
    std::string message_class_name = ClassName(&message);
    printer->Emit({{"message_type", message_class_name}},
                  R"(
void PrintTo(const $message_type$& msg, std::ostream* os);)");
  }

  void PrintToDefinition(const Descriptor& message, Printer* printer) const {
    std::string message_class_name = ClassName(&message);
    printer->Emit({{"message_type", message_class_name}},
                  R"(
void PrintTo(const $message_type$& msg, std::ostream* os) {
  *os << ToValue(msg).DebugString();
})");
  }

  void PrintToDeclarationRecursive(const Descriptor& message,
                                   Printer* printer) const {
    if (IsSyntheticMapEntry(message)) {
      return;
    }
    PrintToDeclaration(message, printer);
    for (int i = 0; i < message.nested_type_count(); i++) {
      PrintToDeclarationRecursive(*message.nested_type(i), printer);
    }
  }

  void PrintToDefinitionRecursive(const Descriptor& message,
                                  Printer* printer) const {
    if (IsSyntheticMapEntry(message)) {
      return;
    }
    PrintToDefinition(message, printer);
    for (int i = 0; i < message.nested_type_count(); i++) {
      PrintToDefinitionRecursive(*message.nested_type(i), printer);
    }
  }
};
}  // namespace

int main(int argc, char** argv) {
  ProtoGmockGenerator generator;
  return google::protobuf::compiler::PluginMain(argc, argv, &generator);
}
