// Copyright 2017 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#ifndef GRPC_TEST_CPP_END2END_XDS_XDS_UTILS_H
#define GRPC_TEST_CPP_END2END_XDS_XDS_UTILS_H

#include <string>
#include <vector>

#include "absl/strings/string_view.h"

#include "src/proto/grpc/testing/xds/v3/cluster.pb.h"
#include "src/proto/grpc/testing/xds/v3/endpoint.pb.h"
#include "src/proto/grpc/testing/xds/v3/http_connection_manager.pb.h"
#include "src/proto/grpc/testing/xds/v3/listener.pb.h"
#include "src/proto/grpc/testing/xds/v3/route.pb.h"
#include "test/cpp/end2end/xds/xds_server.h"

namespace grpc {
namespace testing {

// A builder for the xDS bootstrap config.
class XdsBootstrapBuilder {
 public:
  XdsBootstrapBuilder() {}
  XdsBootstrapBuilder& SetIgnoreResourceDeletion() {
    ignore_resource_deletion_ = true;
    return *this;
  }
  // If ignore_if_set is true, sets the default server only if it has
  // not already been set.
  XdsBootstrapBuilder& SetDefaultServer(const std::string& server,
                                        bool ignore_if_set = false) {
    if (!ignore_if_set || top_server_.empty()) top_server_ = server;
    return *this;
  }
  XdsBootstrapBuilder& SetXdsChannelCredentials(const std::string& type) {
    xds_channel_creds_type_ = type;
    return *this;
  }
  XdsBootstrapBuilder& SetClientDefaultListenerResourceNameTemplate(
      const std::string& client_default_listener_resource_name_template) {
    client_default_listener_resource_name_template_ =
        client_default_listener_resource_name_template;
    return *this;
  }
  XdsBootstrapBuilder& AddCertificateProviderPlugin(
      const std::string& key, const std::string& name,
      const std::string& plugin_config = "") {
    plugins_[key] = {name, plugin_config};
    return *this;
  }
  XdsBootstrapBuilder& AddAuthority(
      const std::string& authority, const std::string& server = "",
      const std::string& client_listener_resource_name_template = "") {
    authorities_[authority] = {server, client_listener_resource_name_template};
    return *this;
  }
  XdsBootstrapBuilder& SetServerListenerResourceNameTemplate(
      const std::string& server_listener_resource_name_template = "") {
    server_listener_resource_name_template_ =
        server_listener_resource_name_template;
    return *this;
  }

  std::string Build();

 private:
  struct PluginInfo {
    std::string name;
    std::string plugin_config;
  };
  struct AuthorityInfo {
    std::string server;
    std::string client_listener_resource_name_template;
  };

  std::string MakeXdsServersText(absl::string_view server_uri);
  std::string MakeNodeText();
  std::string MakeCertificateProviderText();
  std::string MakeAuthorityText();

  bool ignore_resource_deletion_ = false;
  std::string top_server_;
  std::string xds_channel_creds_type_ = "fake";
  std::string client_default_listener_resource_name_template_;
  std::map<std::string /*key*/, PluginInfo> plugins_;
  std::map<std::string /*authority_name*/, AuthorityInfo> authorities_;
  std::string server_listener_resource_name_template_ =
      "grpc/server?xds.resource.listening_address=%s";
};

// Utilities for constructing xDS resources.
class XdsResourceUtils {
 public:
  using HttpConnectionManager = envoy::extensions::filters::network::
      http_connection_manager::v3::HttpConnectionManager;
  using Listener = envoy::config::listener::v3::Listener;
  using RouteConfiguration = envoy::config::route::v3::RouteConfiguration;
  using Cluster = envoy::config::cluster::v3::Cluster;
  using ClusterLoadAssignment =
      envoy::config::endpoint::v3::ClusterLoadAssignment;

  // Interface for accessing HttpConnectionManager config in Listener.
  class HcmAccessor {
   public:
    virtual ~HcmAccessor() = default;
    virtual HttpConnectionManager Unpack(const Listener& listener) const = 0;
    virtual void Pack(const HttpConnectionManager& hcm,
                      Listener* listener) const = 0;
  };

  // Client-side impl.
  class ClientHcmAccessor : public HcmAccessor {
   public:
    HttpConnectionManager Unpack(const Listener& listener) const override;
    void Pack(const HttpConnectionManager& hcm,
              Listener* listener) const override;
  };

  // Server-side impl.
  class ServerHcmAccessor : public HcmAccessor {
   public:
    HttpConnectionManager Unpack(const Listener& listener) const override;
    void Pack(const HttpConnectionManager& hcm,
              Listener* listener) const override;
  };

  // Default values for locality fields.
  static const char kDefaultLocalityRegion[];
  static const char kDefaultLocalityZone[];
  static const uint32_t kDefaultLocalityWeight = 3;
  static const int kDefaultLocalityPriority = 0;

  // Default resource names.
  static const char kServerName[];
  static const char kDefaultRouteConfigurationName[];
  static const char kDefaultClusterName[];
  static const char kDefaultEdsServiceName[];
  static const char kDefaultServerRouteConfigurationName[];

  // Returns default xDS resources.
  static Listener DefaultListener();
  static RouteConfiguration DefaultRouteConfig();
  static Cluster DefaultCluster();
  static Listener DefaultServerListener();
  static RouteConfiguration DefaultServerRouteConfig();
  static HttpConnectionManager DefaultHcm();

  // Returns the name of the server-side xDS Listener resource for a
  // backend on the specified port.
  static std::string GetServerListenerName(int port);

  // Returns a copy of listener_template with the server-side resource
  // name and the port in the socket address populated.
  static Listener PopulateServerListenerNameAndPort(
      const Listener& listener_template, int port);

  // Sets the Listener and RouteConfiguration resource on the specified
  // balancer.  If RDS is in use, they will be set as separate resources;
  // otherwise, the RouteConfig will be inlined into the Listener.
  static void SetListenerAndRouteConfiguration(
      AdsServiceImpl* ads_service, Listener listener,
      const RouteConfiguration& route_config, bool use_rds = false,
      const HcmAccessor& hcm_accessor = ClientHcmAccessor());

  // A convenient wrapper for setting the Listener and
  // RouteConfiguration resources on the server side.
  static void SetServerListenerNameAndRouteConfiguration(
      AdsServiceImpl* ads_service, Listener listener, int port,
      const RouteConfiguration& route_config, bool use_rds = false) {
    SetListenerAndRouteConfiguration(
        ads_service, PopulateServerListenerNameAndPort(listener, port),
        route_config, use_rds, ServerHcmAccessor());
  }

  // Sets the RouteConfiguration resource on the specified balancer.
  // If RDS is in use, it will be set directly as an independent
  // resource; otherwise, it will be inlined into a Listener resource
  // (either listener_to_copy, or if that is null, default_listener_).
  static void SetRouteConfiguration(AdsServiceImpl* ads_service,
                                    const RouteConfiguration& route_config,
                                    bool use_rds = false,
                                    const Listener* listener_to_copy = nullptr);

  // Arguments for constructing an EDS resource.
  struct EdsResourceArgs {
    // An individual endpoint for a backend running on a specified port.
    struct Endpoint {
      explicit Endpoint(int port,
                        ::envoy::config::core::v3::HealthStatus health_status =
                            ::envoy::config::core::v3::HealthStatus::UNKNOWN,
                        int lb_weight = 1,
                        std::vector<int> additional_ports = {})
          : port(port),
            health_status(health_status),
            lb_weight(lb_weight),
            additional_ports(std::move(additional_ports)) {}

      int port;
      ::envoy::config::core::v3::HealthStatus health_status;
      int lb_weight;
      std::vector<int> additional_ports;
    };

    // A locality.
    struct Locality {
      Locality(std::string sub_zone, std::vector<Endpoint> endpoints,
               uint32_t lb_weight = kDefaultLocalityWeight,
               int priority = kDefaultLocalityPriority)
          : sub_zone(std::move(sub_zone)),
            endpoints(std::move(endpoints)),
            lb_weight(lb_weight),
            priority(priority) {}

      const std::string sub_zone;
      std::vector<Endpoint> endpoints;
      uint32_t lb_weight;
      int priority;
    };

    EdsResourceArgs() = default;
    explicit EdsResourceArgs(std::vector<Locality> locality_list)
        : locality_list(std::move(locality_list)) {}

    std::vector<Locality> locality_list;
    std::map<std::string, uint32_t> drop_categories;
    ::envoy::type::v3::FractionalPercent::DenominatorType drop_denominator =
        ::envoy::type::v3::FractionalPercent::MILLION;
  };

  // Constructs an EDS resource.
  static ClusterLoadAssignment BuildEdsResource(
      const EdsResourceArgs& args,
      absl::string_view eds_service_name = kDefaultEdsServiceName);
};

}  // namespace testing
}  // namespace grpc

#endif  // GRPC_TEST_CPP_END2END_XDS_XDS_UTILS_H
