/*
 *
 * Copyright 2016 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.
 *
 */

#include <thread>

#include <grpc++/channel.h>
#include <grpc++/client_context.h>
#include <grpc++/create_channel.h>
#include <grpc++/impl/server_builder_option.h>
#include <grpc++/impl/server_builder_plugin.h>
#include <grpc++/impl/server_initializer.h>
#include <grpc++/security/credentials.h>
#include <grpc++/security/server_credentials.h>
#include <grpc++/server.h>
#include <grpc++/server_builder.h>
#include <grpc++/server_context.h>
#include <grpc/grpc.h>

#include "src/proto/grpc/testing/echo.grpc.pb.h"
#include "test/core/util/port.h"
#include "test/core/util/test_config.h"
#include "test/cpp/end2end/test_service_impl.h"

#include <gtest/gtest.h>

#define PLUGIN_NAME "TestServerBuilderPlugin"

namespace grpc {
namespace testing {

class TestServerBuilderPlugin : public ServerBuilderPlugin {
 public:
  TestServerBuilderPlugin() : service_(new TestServiceImpl()) {
    init_server_is_called_ = false;
    finish_is_called_ = false;
    change_arguments_is_called_ = false;
    register_service_ = false;
  }

  grpc::string name() override { return PLUGIN_NAME; }

  void InitServer(ServerInitializer* si) override {
    init_server_is_called_ = true;
    if (register_service_) {
      si->RegisterService(service_);
    }
  }

  void Finish(ServerInitializer* si) override { finish_is_called_ = true; }

  void ChangeArguments(const grpc::string& name, void* value) override {
    change_arguments_is_called_ = true;
  }

  bool has_async_methods() const override {
    if (register_service_) {
      return service_->has_async_methods();
    }
    return false;
  }

  bool has_sync_methods() const override {
    if (register_service_) {
      return service_->has_synchronous_methods();
    }
    return false;
  }

  void SetRegisterService() { register_service_ = true; }

  bool init_server_is_called() { return init_server_is_called_; }
  bool finish_is_called() { return finish_is_called_; }
  bool change_arguments_is_called() { return change_arguments_is_called_; }

 private:
  bool init_server_is_called_;
  bool finish_is_called_;
  bool change_arguments_is_called_;
  bool register_service_;
  std::shared_ptr<TestServiceImpl> service_;
};

class InsertPluginServerBuilderOption : public ServerBuilderOption {
 public:
  InsertPluginServerBuilderOption() { register_service_ = false; }

  void UpdateArguments(ChannelArguments* arg) override {}

  void UpdatePlugins(
      std::vector<std::unique_ptr<ServerBuilderPlugin>>* plugins) override {
    plugins->clear();

    std::unique_ptr<TestServerBuilderPlugin> plugin(
        new TestServerBuilderPlugin());
    if (register_service_) plugin->SetRegisterService();
    plugins->emplace_back(std::move(plugin));
  }

  void SetRegisterService() { register_service_ = true; }

 private:
  bool register_service_;
};

std::unique_ptr<ServerBuilderPlugin> CreateTestServerBuilderPlugin() {
  return std::unique_ptr<ServerBuilderPlugin>(new TestServerBuilderPlugin());
}

void AddTestServerBuilderPlugin() {
  static bool already_here = false;
  if (already_here) return;
  already_here = true;
  ::grpc::ServerBuilder::InternalAddPluginFactory(
      &CreateTestServerBuilderPlugin);
}

// Force AddServerBuilderPlugin() to be called at static initialization time.
struct StaticTestPluginInitializer {
  StaticTestPluginInitializer() { AddTestServerBuilderPlugin(); }
} static_plugin_initializer_test_;

// When the param boolean is true, the ServerBuilder plugin will be added at the
// time of static initialization. When it's false, the ServerBuilder plugin will
// be added using ServerBuilder::SetOption().
class ServerBuilderPluginTest : public ::testing::TestWithParam<bool> {
 public:
  ServerBuilderPluginTest() {}

  void SetUp() override {
    port_ = grpc_pick_unused_port_or_die();
    builder_.reset(new ServerBuilder());
  }

  void InsertPlugin() {
    if (GetParam()) {
      // Add ServerBuilder plugin in static initialization
      CheckPresent();
    } else {
      // Add ServerBuilder plugin using ServerBuilder::SetOption()
      builder_->SetOption(std::unique_ptr<ServerBuilderOption>(
          new InsertPluginServerBuilderOption()));
    }
  }

  void InsertPluginWithTestService() {
    if (GetParam()) {
      // Add ServerBuilder plugin in static initialization
      auto plugin = CheckPresent();
      EXPECT_TRUE(plugin);
      plugin->SetRegisterService();
    } else {
      // Add ServerBuilder plugin using ServerBuilder::SetOption()
      std::unique_ptr<InsertPluginServerBuilderOption> option(
          new InsertPluginServerBuilderOption());
      option->SetRegisterService();
      builder_->SetOption(std::move(option));
    }
  }

  void StartServer() {
    grpc::string server_address = "localhost:" + to_string(port_);
    builder_->AddListeningPort(server_address, InsecureServerCredentials());
    // we run some tests without a service, and for those we need to supply a
    // frequently polled completion queue
    cq_ = builder_->AddCompletionQueue();
    cq_thread_ = new std::thread(&ServerBuilderPluginTest::RunCQ, this);
    server_ = builder_->BuildAndStart();
    EXPECT_TRUE(CheckPresent());
  }

  void ResetStub() {
    string target = "dns:localhost:" + to_string(port_);
    channel_ = CreateChannel(target, InsecureChannelCredentials());
    stub_ = grpc::testing::EchoTestService::NewStub(channel_);
  }

  void TearDown() override {
    auto plugin = CheckPresent();
    EXPECT_TRUE(plugin);
    EXPECT_TRUE(plugin->init_server_is_called());
    EXPECT_TRUE(plugin->finish_is_called());
    server_->Shutdown();
    cq_->Shutdown();
    cq_thread_->join();
    delete cq_thread_;
  }

  string to_string(const int number) {
    std::stringstream strs;
    strs << number;
    return strs.str();
  }

 protected:
  std::shared_ptr<Channel> channel_;
  std::unique_ptr<ServerBuilder> builder_;
  std::unique_ptr<grpc::testing::EchoTestService::Stub> stub_;
  std::unique_ptr<ServerCompletionQueue> cq_;
  std::unique_ptr<Server> server_;
  std::thread* cq_thread_;
  TestServiceImpl service_;
  int port_;

 private:
  TestServerBuilderPlugin* CheckPresent() {
    auto it = builder_->plugins_.begin();
    for (; it != builder_->plugins_.end(); it++) {
      if ((*it)->name() == PLUGIN_NAME) break;
    }
    if (it != builder_->plugins_.end()) {
      return static_cast<TestServerBuilderPlugin*>(it->get());
    } else {
      return nullptr;
    }
  }

  void RunCQ() {
    void* tag;
    bool ok;
    while (cq_->Next(&tag, &ok))
      ;
  }
};

TEST_P(ServerBuilderPluginTest, PluginWithoutServiceTest) {
  InsertPlugin();
  StartServer();
}

TEST_P(ServerBuilderPluginTest, PluginWithServiceTest) {
  InsertPluginWithTestService();
  StartServer();
  ResetStub();

  EchoRequest request;
  EchoResponse response;
  request.set_message("Hello hello hello hello");
  ClientContext context;
  context.set_compression_algorithm(GRPC_COMPRESS_GZIP);
  Status s = stub_->Echo(&context, request, &response);
  EXPECT_EQ(response.message(), request.message());
  EXPECT_TRUE(s.ok());
}

INSTANTIATE_TEST_CASE_P(ServerBuilderPluginTest, ServerBuilderPluginTest,
                        ::testing::Values(false, true));

}  // namespace testing
}  // namespace grpc

int main(int argc, char** argv) {
  grpc_test_init(argc, argv);
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
