// Protocol Buffers - Google's data interchange format
// Copyright 2008 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

// Author: kenton@google.com (Kenton Varda)

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

#include <stdlib.h>

#include <cstdint>
#include <iostream>
#include <memory>
#include <ostream>
#include <string>
#include <utility>
#include <vector>

#include "google/protobuf/testing/file.h"
#include "google/protobuf/testing/file.h"
#include "google/protobuf/descriptor.pb.h"
#include <gtest/gtest.h>
#include "absl/log/absl_check.h"
#include "absl/log/absl_log.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "absl/strings/strip.h"
#include "absl/strings/substitute.h"
#include "google/protobuf/compiler/plugin.pb.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor_visitor.h"
#include "google/protobuf/io/printer.h"
#include "google/protobuf/io/zero_copy_stream.h"
#include "google/protobuf/text_format.h"
#include "google/protobuf/unittest_features.pb.h"

#ifdef major
#undef major
#endif
#ifdef minor
#undef minor
#endif

namespace google {
namespace protobuf {
namespace compiler {
namespace {

// Returns the list of the names of files in all_files in the form of a
// comma-separated string.
std::string CommaSeparatedList(
    const std::vector<const FileDescriptor*>& all_files) {
  std::vector<absl::string_view> names;
  names.reserve(all_files.size());
  for (size_t i = 0; i < all_files.size(); i++) {
    names.push_back(all_files[i]->name());
  }
  return absl::StrJoin(names, ",");
}

static constexpr absl::string_view kFirstInsertionPointName =
    "first_mock_insertion_point";
static constexpr absl::string_view kSecondInsertionPointName =
    "second_mock_insertion_point";
static constexpr absl::string_view kFirstInsertionPoint =
    "# @@protoc_insertion_point(first_mock_insertion_point) is here\n";
static constexpr absl::string_view kSecondInsertionPoint =
    "  # @@protoc_insertion_point(second_mock_insertion_point) is here\n";

absl::string_view GetTestCase() {
  const char* c_key = getenv("TEST_CASE");
  if (c_key == nullptr) {
    // In Windows, setting 'TEST_CASE=' is equivalent to unsetting
    // and therefore c_key can be nullptr
    return "";
  }
  return c_key;
}

}  // namespace

MockCodeGenerator::MockCodeGenerator(absl::string_view name) : name_(name) {
  absl::string_view key = GetTestCase();
  if (key == "no_editions") {
    suppressed_features_ |= CodeGenerator::FEATURE_SUPPORTS_EDITIONS;
  } else if (key == "invalid_features") {
    feature_extensions_ = {nullptr};
  } else if (key == "no_feature_defaults") {
    feature_extensions_ = {};
  } else if (key == "high_maximum") {
    maximum_edition_ = Edition::EDITION_99997_TEST_ONLY;
  } else if (key == "low_minimum") {
    maximum_edition_ = Edition::EDITION_1_TEST_ONLY;
  }
}

MockCodeGenerator::~MockCodeGenerator() = default;

uint64_t MockCodeGenerator::GetSupportedFeatures() const {
  uint64_t all_features = CodeGenerator::FEATURE_PROTO3_OPTIONAL |
                          CodeGenerator::FEATURE_SUPPORTS_EDITIONS;
  return all_features & ~suppressed_features_;
}

void MockCodeGenerator::SuppressFeatures(uint64_t features) {
  suppressed_features_ = features;
}

void MockCodeGenerator::ExpectGenerated(
    absl::string_view name, absl::string_view parameter,
    absl::string_view insertions, absl::string_view file,
    absl::string_view first_message_name,
    absl::string_view first_parsed_file_name,
    absl::string_view output_directory) {
  std::string content;
  ABSL_CHECK_OK(File::GetContents(
      absl::StrCat(output_directory, "/", GetOutputFileName(name, file)),
      &content, true));

  std::vector<std::string> lines =
      absl::StrSplit(content, '\n', absl::SkipEmpty());

  while (!lines.empty() && lines.back().empty()) {
    lines.pop_back();
  }
  for (size_t i = 0; i < lines.size(); i++) {
    absl::StrAppend(&lines[i], "\n");
  }

  std::vector<std::string> insertion_list;
  if (!insertions.empty()) {
    insertion_list = absl::StrSplit(insertions, ',', absl::SkipEmpty());
  }

  EXPECT_EQ(lines.size(), 3 + insertion_list.size() * 2);
  EXPECT_EQ(GetOutputFileContent(name, parameter, file, first_parsed_file_name,
                                 first_message_name),
            lines[0]);

  EXPECT_EQ(kFirstInsertionPoint, lines[1 + insertion_list.size()]);
  EXPECT_EQ(kSecondInsertionPoint, lines[2 + insertion_list.size() * 2]);

  for (size_t i = 0; i < insertion_list.size(); i++) {
    EXPECT_EQ(GetOutputFileContent(insertion_list[i], "first_insert", file,
                                   file, first_message_name),
              lines[1 + i]);
    // Second insertion point is indented, so the inserted text should
    // automatically be indented too.
    EXPECT_EQ(absl::StrCat(
                  "  ", GetOutputFileContent(insertion_list[i], "second_insert",
                                             file, file, first_message_name)),
              lines[2 + insertion_list.size() + i]);
  }
}

namespace {
void CheckSingleAnnotation(absl::string_view expected_file,
                           absl::string_view expected_text,
                           absl::string_view file_content,
                           const GeneratedCodeInfo::Annotation& annotation) {
  EXPECT_EQ(expected_file, annotation.source_file());
  ASSERT_GE(file_content.size(), annotation.begin());
  ASSERT_GE(file_content.size(), annotation.end());
  ASSERT_LE(annotation.begin(), annotation.end());
  EXPECT_EQ(expected_text.size(), annotation.end() - annotation.begin());
  EXPECT_EQ(expected_text,
            file_content.substr(annotation.begin(), expected_text.size()));
}
}  // anonymous namespace

void MockCodeGenerator::CheckGeneratedAnnotations(
    absl::string_view name, absl::string_view file,
    absl::string_view output_directory) {
  std::string file_content;
  ABSL_CHECK_OK(File::GetContents(
      absl::StrCat(output_directory, "/", GetOutputFileName(name, file)),
      &file_content, true));
  std::string meta_content;
  ABSL_CHECK_OK(
      File::GetContents(absl::StrCat(output_directory, "/",
                                     GetOutputFileName(name, file), ".pb.meta"),
                        &meta_content, true));
  GeneratedCodeInfo annotations;
  ABSL_CHECK(TextFormat::ParseFromString(meta_content, &annotations));
  ASSERT_EQ(7, annotations.annotation_size());

  CheckSingleAnnotation("first_annotation", "first", file_content,
                        annotations.annotation(0));
  CheckSingleAnnotation("first_path",
                        "test_generator: first_insert,\n foo.proto,\n "
                        "MockCodeGenerator_Annotate,\n foo.proto\n",
                        file_content, annotations.annotation(1));
  CheckSingleAnnotation("first_path",
                        "test_plugin: first_insert,\n foo.proto,\n "
                        "MockCodeGenerator_Annotate,\n foo.proto\n",
                        file_content, annotations.annotation(2));
  CheckSingleAnnotation("second_annotation", "second", file_content,
                        annotations.annotation(3));
  // This annotated text has changed because it was inserted at an indented
  // insertion point.
  CheckSingleAnnotation("second_path",
                        "test_generator: second_insert,\n   foo.proto,\n   "
                        "MockCodeGenerator_Annotate,\n   foo.proto\n",
                        file_content, annotations.annotation(4));
  CheckSingleAnnotation("second_path",
                        "test_plugin: second_insert,\n   foo.proto,\n   "
                        "MockCodeGenerator_Annotate,\n   foo.proto\n",
                        file_content, annotations.annotation(5));
  CheckSingleAnnotation("third_annotation", "third", file_content,
                        annotations.annotation(6));
}

bool MockCodeGenerator::Generate(const FileDescriptor* file,
                                 const std::string& parameter,
                                 GeneratorContext* context,
                                 std::string* error) const {
  // Override minimum/maximum after generating the pool to simulate a plugin
  // that "works" but doesn't advertise support of the current edition.
  absl::string_view test_case = GetTestCase();
  if (test_case == "high_minimum") {
    minimum_edition_ = Edition::EDITION_99997_TEST_ONLY;
  } else if (test_case == "low_maximum") {
    maximum_edition_ = Edition::EDITION_PROTO2;
  }

  if (GetEdition(*file) >= Edition::EDITION_2023 &&
      (suppressed_features_ & CodeGenerator::FEATURE_SUPPORTS_EDITIONS) == 0) {
    internal::VisitDescriptors(*file, [&](const auto& descriptor) {
      const FeatureSet& features = GetResolvedSourceFeatures(descriptor);
      ABSL_CHECK(features.HasExtension(pb::test))
          << "Test features were not resolved properly";
      ABSL_CHECK(features.GetExtension(pb::test).has_file_feature())
          << "Test features were not resolved properly";
      ABSL_CHECK(features.GetExtension(pb::test).has_source_feature())
          << "Test features were not resolved properly";
    });
  }

  bool annotate = false;
  for (int i = 0; i < file->message_type_count(); i++) {
    if (absl::StartsWith(file->message_type(i)->name(), "MockCodeGenerator_")) {
      absl::string_view command = absl::StripPrefix(
          file->message_type(i)->name(), "MockCodeGenerator_");
      if (command == "Error") {
        *error = "Saw message type MockCodeGenerator_Error.";
        return false;
      }
      if (command == "Exit") {
        std::cerr << "Saw message type MockCodeGenerator_Exit." << std::endl;
        exit(123);
      }
      ABSL_CHECK(command != "Abort")
          << "Saw message type MockCodeGenerator_Abort.";
      if (command == "HasSourceCodeInfo") {
        FileDescriptorProto file_descriptor_proto;
        file->CopySourceCodeInfoTo(&file_descriptor_proto);
        bool has_source_code_info =
            file_descriptor_proto.has_source_code_info() &&
            file_descriptor_proto.source_code_info().location_size() > 0;
        ABSL_LOG(FATAL)
            << "Saw message type MockCodeGenerator_HasSourceCodeInfo: "
            << has_source_code_info << ".";
      } else if (command == "HasJsonName") {
        FieldDescriptorProto field_descriptor_proto;
        file->message_type(i)->field(0)->CopyTo(&field_descriptor_proto);
        ABSL_LOG(FATAL) << "Saw json_name: "
                        << field_descriptor_proto.has_json_name();
      } else if (command == "Annotate") {
        annotate = true;
      } else if (command == "ShowVersionNumber") {
        Version compiler_version;
        context->GetCompilerVersion(&compiler_version);
        ABSL_LOG(FATAL) << "Saw compiler_version: "
                        << compiler_version.major() * 1000000 +
                               compiler_version.minor() * 1000 +
                               compiler_version.patch()
                        << " " << compiler_version.suffix();
      } else {
        ABSL_LOG(FATAL) << "Unknown MockCodeGenerator command: " << command;
      }
    }
  }

  bool insert_endlines = absl::StartsWith(parameter, "insert_endlines=");
  if (insert_endlines || absl::StartsWith(parameter, "insert=")) {
    std::vector<std::string> insert_into = absl::StrSplit(
        absl::StripPrefix(parameter,
                          insert_endlines ? "insert_endlines=" : "insert="),
        ',', absl::SkipEmpty());

    for (size_t i = 0; i < insert_into.size(); i++) {
      {
        google::protobuf::GeneratedCodeInfo info;
        std::string content =
            GetOutputFileContent(name_, "first_insert", file, context);
        if (insert_endlines) {
          absl::StrReplaceAll({{",", ",\n"}}, &content);
        }
        if (annotate) {
          auto* annotation = info.add_annotation();
          annotation->set_begin(0);
          annotation->set_end(content.size());
          annotation->set_source_file("first_path");
        }
        std::unique_ptr<io::ZeroCopyOutputStream> output(
            context->OpenForInsertWithGeneratedCodeInfo(
                GetOutputFileName(insert_into[i], file),
                std::string(kFirstInsertionPointName), info));
        io::Printer printer(output.get(), '$');
        printer.PrintRaw(content);
        if (printer.failed()) {
          *error = "MockCodeGenerator detected write error.";
          return false;
        }
      }

      {
        google::protobuf::GeneratedCodeInfo info;
        std::string content =
            GetOutputFileContent(name_, "second_insert", file, context);
        if (insert_endlines) {
          absl::StrReplaceAll({{",", ",\n"}}, &content);
        }
        if (annotate) {
          auto* annotation = info.add_annotation();
          annotation->set_begin(0);
          annotation->set_end(content.size());
          annotation->set_source_file("second_path");
        }
        std::unique_ptr<io::ZeroCopyOutputStream> output(
            context->OpenForInsertWithGeneratedCodeInfo(
                GetOutputFileName(insert_into[i], file),
                std::string(kSecondInsertionPointName), info));
        io::Printer printer(output.get(), '$');
        printer.PrintRaw(content);
        if (printer.failed()) {
          *error = "MockCodeGenerator detected write error.";
          return false;
        }
      }
    }
  } else {
    std::unique_ptr<io::ZeroCopyOutputStream> output(
        context->Open(GetOutputFileName(name_, file)));

    GeneratedCodeInfo annotations;
    io::AnnotationProtoCollector<GeneratedCodeInfo> annotation_collector(
        &annotations);
    io::Printer printer(output.get(), '$',
                        annotate ? &annotation_collector : nullptr);
    printer.PrintRaw(GetOutputFileContent(name_, parameter, file, context));
    std::string annotate_suffix = "_annotation";
    if (annotate) {
      printer.Print("$p$\n", "p", "first");
      printer.Annotate("p", absl::StrCat("first", annotate_suffix));
    }
    printer.PrintRaw(kFirstInsertionPoint);
    if (annotate) {
      printer.Print("$p$\n", "p", "second");
      printer.Annotate("p", absl::StrCat("second", annotate_suffix));
    }
    printer.PrintRaw(kSecondInsertionPoint);
    if (annotate) {
      printer.Print("$p$\n", "p", "third");
      printer.Annotate("p", absl::StrCat("third", annotate_suffix));
    }

    if (printer.failed()) {
      *error = "MockCodeGenerator detected write error.";
      return false;
    }
    if (annotate) {
      std::unique_ptr<io::ZeroCopyOutputStream> meta_output(context->Open(
          absl::StrCat(GetOutputFileName(name_, file), ".pb.meta")));
      if (!TextFormat::Print(annotations, meta_output.get())) {
        *error = "MockCodeGenerator couldn't write .pb.meta";
        return false;
      }
    }
  }

  return true;
}

std::string MockCodeGenerator::GetOutputFileName(
    absl::string_view generator_name, const FileDescriptor* file) {
  return GetOutputFileName(generator_name, file->name());
}

std::string MockCodeGenerator::GetOutputFileName(
    absl::string_view generator_name, absl::string_view file) {
  return absl::StrCat(file, ".MockCodeGenerator.", generator_name);
}

std::string MockCodeGenerator::GetOutputFileContent(
    absl::string_view generator_name, absl::string_view parameter,
    const FileDescriptor* file, GeneratorContext* context) {
  std::vector<const FileDescriptor*> all_files;
  context->ListParsedFiles(&all_files);
  return GetOutputFileContent(
      generator_name, parameter, file->name(), CommaSeparatedList(all_files),
      file->message_type_count() > 0 ? file->message_type(0)->name()
                                     : "(none)");
}

std::string MockCodeGenerator::GetOutputFileContent(
    absl::string_view generator_name, absl::string_view parameter,
    absl::string_view file, absl::string_view parsed_file_list,
    absl::string_view first_message_name) {
  return absl::Substitute("$0: $1, $2, $3, $4\n", generator_name, parameter,
                          file, first_message_name, parsed_file_list);
}

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