// Protocol Buffers - Google's data interchange format
// Copyright 2025 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/symbol_checker.h"

#include <utility>

#include "google/protobuf/descriptor.pb.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor_test_utils.h"
#include "google/protobuf/feature_resolver.h"

// Must be included last.
#include "google/protobuf/port_def.inc"

using ::testing::NotNull;

namespace {
absl::Status GetStatus(const absl::Status& s) { return s; }
template <typename T>
absl::Status GetStatus(const absl::StatusOr<T>& s) {
  return s.status();
}
MATCHER_P2(StatusIs, status, message,
           absl::StrCat(".status() is ", testing::PrintToString(status))) {
  return GetStatus(arg).code() == status &&
         testing::ExplainMatchResult(message, GetStatus(arg).message(),
                                     result_listener);
}
#define EXPECT_OK(x) EXPECT_THAT(x, StatusIs(absl::StatusCode::kOk, testing::_))
#define ASSERT_OK(x) ASSERT_THAT(x, StatusIs(absl::StatusCode::kOk, testing::_))
}  // namespace

namespace google {
namespace protobuf {
namespace descriptor_unittest {

class SymbolCheckerTest : public ValidationErrorTest {
 protected:
  void SetUp() override {
    ValidationErrorTest::SetUp();

    auto default_spec = FeatureResolver::CompileDefaults(
        FeatureSet::descriptor(), {}, EDITION_PROTO2, EDITION_99999_TEST_ONLY);
    ASSERT_OK(default_spec);
    ASSERT_OK(pool_.SetFeatureSetDefaults(std::move(default_spec).value()));
  }
};

TEST_F(SymbolCheckerTest, VisibilityFromSame) {
  pool_.EnforceSymbolVisibility(true);
  EXPECT_THAT(ParseAndBuildFile("vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        local message LocalMessage {
        }
        export message ExportMessage {
          LocalMessage foo = 1;
        }
        )schema"),
              NotNull());
}

TEST_F(SymbolCheckerTest, ExplicitVisibilityFromOther) {
  pool_.EnforceSymbolVisibility(true);
  ASSERT_THAT(ParseAndBuildFile("vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        local message LocalMessage {
        }
        export message ExportMessage {
        }
        )schema"),
              NotNull());

  ParseAndBuildFileWithErrorSubstr(
      "importer.proto",
      R"schema(
        edition = "2024";
        import "vis.proto";

        message BadImport {
          vis.test.LocalMessage foo = 1;
        }
      )schema",
      "importer.proto: BadImport.foo: TYPE: Symbol \"vis.test.LocalMessage\", "
      "defined in \"vis.proto\"  is not visible from \"importer.proto\". It is "
      "explicitly marked 'local' and cannot be accessed outside its own "
      "file\n");
}

TEST_F(SymbolCheckerTest, ExplicitVisibilityFromOtherDisabled) {
  pool_.EnforceSymbolVisibility(false);
  ASSERT_THAT(ParseAndBuildFile("vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        local message LocalMessage {
        }
        export message ExportMessage {
        }
        )schema"),
              NotNull());

  EXPECT_THAT(ParseAndBuildFile("importer.proto",
                                R"schema(
        edition = "2024";
        import "vis.proto";

        message BadImport {
          vis.test.LocalMessage foo = 1;
        }
      )schema"),
              NotNull());
}

TEST_F(SymbolCheckerTest, Edition2024DefaultVisibilityFromOther) {
  pool_.EnforceSymbolVisibility(true);
  ASSERT_THAT(ParseAndBuildFile("vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        message TopLevelMessage {
          message NestedMessage {
          }
        }
        )schema"),
              NotNull());

  ASSERT_THAT(ParseAndBuildFile("good_importer.proto", R"schema(
        edition = "2024";
        import "vis.proto";

        message GoodImport {
          vis.test.TopLevelMessage foo = 1;
        }
        )schema"),
              NotNull());

  ParseAndBuildFileWithErrorSubstr(
      "bad_importer.proto", R"schema(
        edition = "2024";
        import "vis.proto";

        message BadImport {
          vis.test.TopLevelMessage.NestedMessage foo = 1;
        }
        )schema",

      "bad_importer.proto: BadImport.foo: TYPE: Symbol "
      "\"vis.test.TopLevelMessage.NestedMessage\", defined in \"vis.proto\"  "
      "is not visible from \"bad_importer.proto\". It defaulted to local from "
      "file-level 'option features.default_symbol_visibility = "
      "'EXPORT_TOP_LEVEL'; and cannot be accessed outside its own file\n");
}

TEST_F(SymbolCheckerTest, VisibilityFromLocalExtender) {
  pool_.EnforceSymbolVisibility(true);
  ASSERT_THAT(ParseAndBuildFile("vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        local message LocalExtendee {
          extensions 1 to 100;
        }
        )schema"),
              NotNull());

  ParseAndBuildFileWithErrorSubstr(
      "bad_importer.proto", R"schema(
        edition = "2024";
        import "vis.proto";

        extend vis.test.LocalExtendee {
          string bar = 1;
        }
      )schema",
      "bad_importer.proto: bar: EXTENDEE: Symbol \"vis.test.LocalExtendee\", "
      "defined in \"vis.proto\" target of extend is not visible from "
      "\"bad_importer.proto\". It is explicitly marked 'local' and cannot be "
      "accessed outside its own file\n");
}

TEST_F(SymbolCheckerTest, IsEnumNamespaceMessage) {
  pool_.EnforceSymbolVisibility(true);
  const FileDescriptor* file = ParseAndBuildFile("vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        option features.default_symbol_visibility = STRICT;

        local message EnumNamespaceMessage {
          export enum Enum {
            FOO = 0;
          }
          reserved 1 to max;
        }

        enum TopLevelEnum {
          BAR = 0;
        }
        )schema");

  ASSERT_THAT(file, NotNull());

  const Descriptor* namespace_message =
      file->FindMessageTypeByName("EnumNamespaceMessage");
  ASSERT_THAT(namespace_message, NotNull());

  ASSERT_TRUE(SymbolChecker::IsEnumNamespaceMessage(*namespace_message));

  const EnumDescriptor* enum_descriptor =
      namespace_message->FindEnumTypeByName("Enum");
  ASSERT_THAT(enum_descriptor, NotNull());

  ASSERT_TRUE(SymbolChecker::IsNamespacedEnum(*enum_descriptor));

  // Make sure it can be imported.
  ASSERT_THAT(ParseAndBuildFile("other.proto", R"schema(
        edition = "2024";
        package vis.test;

        import "vis.proto";

        message OtherMessage {
          vis.test.EnumNamespaceMessage.Enum foo = 1;
        }
        )schema"),
              NotNull());

  const EnumDescriptor* top_enum_descriptor =
      file->FindEnumTypeByName("TopLevelEnum");
  ASSERT_THAT(top_enum_descriptor, NotNull());
  ASSERT_FALSE(SymbolChecker::IsNamespacedEnum(*top_enum_descriptor));
}

TEST_F(SymbolCheckerTest, IsNotEnumNamespaceMessage) {
  pool_.EnforceSymbolVisibility(true);

  // message is not local
  ParseAndBuildFileWithErrorSubstr(
      "vis.proto", R"schema(
        edition = "2024";
        package vis.test;

        option features.default_symbol_visibility = STRICT;

        export message EnumNamespaceMessage {
          export enum Enum {
            FOO = 0;
          }
          reserved 1 to max;
        }
        )schema",
      "is a nested enum and cannot be marked `export` with STRICT");

  // message doesn't have reserved range.
  ParseAndBuildFileWithErrorSubstr(
      "vis2.proto", R"schema(
        edition = "2024";
        package vis.test;

        option features.default_symbol_visibility = STRICT;

        local message EnumNamespaceMessage {
          export enum Enum {
            FOO = 0;
          }
        }
        )schema",
      "is a nested enum and cannot be marked `export` with STRICT");

  // message doesn't have full reserved range.
  ParseAndBuildFileWithErrorSubstr(
      "vis2.proto", R"schema(
        edition = "2024";
        package vis.test;

        option features.default_symbol_visibility = STRICT;

        local message EnumNamespaceMessage {
          export enum Enum {
            FOO = 0;
          }
          reserved 1 to 10;
        }
        )schema",
      "is a nested enum and cannot be marked `export` with STRICT");
}

}  // namespace descriptor_unittest
}  // namespace protobuf
}  // namespace google

#include "google/protobuf/port_undef.inc"
