/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "RpcHeader.pb.h"

#include "hdfspp/ioservice.h"
#include "mock_connection.h"
#include "test.pb.h"
#include "rpc/rpc_connection_impl.h"
#include "common/namenode_info.h"

#include <memory>
#include <string>

#include <google/protobuf/io/coded_stream.h>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <gmock/gmock-spec-builders.h>
#include <gmock/gmock-generated-actions.h>
#include <boost/system/error_code.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>

using ::hadoop::common::RpcResponseHeaderProto;
using ::hadoop::common::EmptyRequestProto;
using ::hadoop::common::EmptyResponseProto;
using ::hadoop::common::EchoRequestProto;
using ::hadoop::common::EchoResponseProto;

using boost::system::error_code;

using ::testing::Return;

using ::std::make_pair;
using ::std::string;

namespace pb = ::google::protobuf;
namespace pbio = ::google::protobuf::io;

namespace hdfs {

std::vector<ResolvedNamenodeInfo> make_endpoint() {
  ResolvedNamenodeInfo result;
  result.endpoints.push_back(boost::asio::ip::basic_endpoint<boost::asio::ip::tcp>());
  return std::vector<ResolvedNamenodeInfo>({result});
}

class MockRPCConnection : public MockConnectionBase {
 public:
  MockRPCConnection(boost::asio::io_service &io_service)
      : MockConnectionBase(&io_service) {}
  MOCK_METHOD0(Produce, ProducerResult());
};

class SharedMockRPCConnection : public SharedMockConnection {
 public:
  SharedMockRPCConnection(boost::asio::io_service &io_service)
      : SharedMockConnection(&io_service) {}
};

class SharedConnectionEngine : public RpcEngine {
  using RpcEngine::RpcEngine;

protected:
  std::shared_ptr<RpcConnection> NewConnection() override {
    // Stuff in some dummy endpoints so we don't error out
    last_endpoints_ = make_endpoint()[0].endpoints;

    return std::make_shared<RpcConnectionImpl<SharedMockRPCConnection>>(shared_from_this());
  }

};

}

static inline std::pair<boost::system::error_code, string> RpcResponse(
    const RpcResponseHeaderProto &h, const std::string &data,
    const boost::system::error_code &ec = boost::system::error_code()) {
  const auto payload_length =
      pbio::CodedOutputStream::VarintSize64(h.ByteSizeLong()) +
      pbio::CodedOutputStream::VarintSize64(data.size()) + h.ByteSizeLong() +
      data.size();

  std::string res;
  res.resize(sizeof(uint32_t) + payload_length);
  uint8_t *buf = reinterpret_cast<uint8_t *>(const_cast<char *>(res.c_str()));

  buf = pbio::CodedOutputStream::WriteLittleEndian32ToArray(
      htonl(payload_length), buf);
  buf = pbio::CodedOutputStream::WriteVarint64ToArray(h.ByteSizeLong(), buf);
  buf = h.SerializeWithCachedSizesToArray(buf);
  buf = pbio::CodedOutputStream::WriteVarint64ToArray(data.size(), buf);
  buf = pbio::CodedOutputStream::WriteStringToArray(data, buf);

  return std::make_pair(ec, std::move(res));
}


using namespace hdfs;

TEST(RpcEngineTest, TestRoundTrip) {

  std::shared_ptr<IoService> io_service = IoService::MakeShared();
  Options options;
  auto engine = std::make_shared<RpcEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);
  auto conn =
      std::make_shared<RpcConnectionImpl<MockRPCConnection> >(engine);
  conn->TEST_set_connected(true);
  conn->StartReading();

  EchoResponseProto server_resp;
  server_resp.set_message("foo");

  RpcResponseHeaderProto h;
  h.set_callid(1);
  h.set_status(RpcResponseHeaderProto::SUCCESS);
  EXPECT_CALL(conn->TEST_get_mutable_socket(), Produce())
      .WillOnce(Return(RpcResponse(h, server_resp.SerializeAsString())));

  std::shared_ptr<RpcConnection> conn_ptr(conn);
  engine->TEST_SetRpcConnection(conn_ptr);

  bool complete = false;

  EchoRequestProto req;
  req.set_message("foo");
  std::shared_ptr<EchoResponseProto> resp(new EchoResponseProto());
  engine->AsyncRpc("test", &req, resp, [resp, &complete,io_service](const Status &stat) {
    ASSERT_TRUE(stat.ok());
    ASSERT_EQ("foo", resp->message());
    complete = true;
    io_service->Stop();
  });
  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestConnectionResetAndFail) {
  std::shared_ptr<IoService> io_service = IoService::MakeShared();
  Options options;
  auto engine = std::make_shared<RpcEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);
  auto conn =
      std::make_shared<RpcConnectionImpl<MockRPCConnection> >(engine);
  conn->TEST_set_connected(true);
  conn->StartReading();

  bool complete = false;

  RpcResponseHeaderProto h;
  h.set_callid(1);
  h.set_status(RpcResponseHeaderProto::SUCCESS);
  EXPECT_CALL(conn->TEST_get_mutable_socket(), Produce())
      .WillOnce(Return(RpcResponse(
          h, "", make_error_code(boost::asio::error::connection_reset))));

  std::shared_ptr<RpcConnection> conn_ptr(conn);
  engine->TEST_SetRpcConnection(conn_ptr);

  EchoRequestProto req;
  req.set_message("foo");
  std::shared_ptr<EchoResponseProto> resp(new EchoResponseProto());

  engine->AsyncRpc("test", &req, resp, [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_FALSE(stat.ok());
  });
  io_service->Run();
  ASSERT_TRUE(complete);
}


TEST(RpcEngineTest, TestConnectionResetAndRecover) {
  std::shared_ptr<IoService> io_service = IoService::MakeShared();
  Options options;
  options.max_rpc_retries = 1;
  options.rpc_retry_delay_ms = 0;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);

  // Normally determined during RpcEngine::Connect, but in this case options
  // provides enough info to determine policy here.
  engine->TEST_SetRetryPolicy(engine->TEST_GenerateRetryPolicyUsingOptions());


  EchoResponseProto server_resp;
  server_resp.set_message("foo");

  bool complete = false;

  auto producer = std::make_shared<SharedConnectionData>();
  RpcResponseHeaderProto h;
  h.set_callid(1);
  h.set_status(RpcResponseHeaderProto::SUCCESS);
  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(RpcResponse(
          h, "", make_error_code(boost::asio::error::connection_reset))))
      .WillOnce(Return(RpcResponse(h, server_resp.SerializeAsString())));
  SharedMockConnection::SetSharedConnectionData(producer);

  EchoRequestProto req;
  req.set_message("foo");
  std::shared_ptr<EchoResponseProto> resp(new EchoResponseProto());

  engine->AsyncRpc("test", &req, resp, [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_TRUE(stat.ok());
  });
  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestConnectionResetAndRecoverWithDelay) {
  std::shared_ptr<IoService> io_service = IoService::MakeShared();
  Options options;
  options.max_rpc_retries = 1;
  options.rpc_retry_delay_ms = 1;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);

  // Normally determined during RpcEngine::Connect, but in this case options
  // provides enough info to determine policy here.
  engine->TEST_SetRetryPolicy(engine->TEST_GenerateRetryPolicyUsingOptions());

  EchoResponseProto server_resp;
  server_resp.set_message("foo");

  bool complete = false;

  auto producer = std::make_shared<SharedConnectionData>();
  RpcResponseHeaderProto h;
  h.set_callid(1);
  h.set_status(RpcResponseHeaderProto::SUCCESS);
  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(RpcResponse(
          h, "", make_error_code(boost::asio::error::connection_reset))))
      .WillOnce(Return(RpcResponse(h, server_resp.SerializeAsString())));
  SharedMockConnection::SetSharedConnectionData(producer);

  EchoRequestProto req;
  req.set_message("foo");
  std::shared_ptr<EchoResponseProto> resp(new EchoResponseProto());

  engine->AsyncRpc("test", &req, resp, [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_TRUE(stat.ok());
  });

  boost::asio::deadline_timer timer(io_service->GetRaw());
  timer.expires_from_now(boost::posix_time::hours(100));
  timer.async_wait([](const boost::system::error_code & err){(void)err; ASSERT_FALSE("Timed out"); });

  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestConnectionFailure)
{
  auto producer = std::make_shared<SharedConnectionData>();
  producer->checkProducerForConnect = true;
  SharedMockConnection::SetSharedConnectionData(producer);

  // Error and no retry
  std::shared_ptr<IoService> io_service = IoService::MakeShared();

  bool complete = false;

  Options options;
  options.max_rpc_retries = 0;
  options.rpc_retry_delay_ms = 0;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);

  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(std::make_pair(make_error_code(boost::asio::error::connection_reset), "")));

  engine->Connect("", make_endpoint(), [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_FALSE(stat.ok());
  });
  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestConnectionFailureRetryAndFailure)
{
  auto producer = std::make_shared<SharedConnectionData>();
  producer->checkProducerForConnect = true;
  SharedMockConnection::SetSharedConnectionData(producer);

  std::shared_ptr<IoService> io_service = IoService::MakeShared();

  bool complete = false;

  Options options;
  options.max_rpc_retries = 2;
  options.rpc_retry_delay_ms = 0;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);
  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(std::make_pair(make_error_code(boost::asio::error::connection_reset), "")))
      .WillOnce(Return(std::make_pair(make_error_code(boost::asio::error::connection_reset), "")))
      .WillOnce(Return(std::make_pair(make_error_code(boost::asio::error::connection_reset), "")));

  engine->Connect("", make_endpoint(), [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_FALSE(stat.ok());
  });
  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestConnectionFailureAndRecover)
{
  auto producer = std::make_shared<SharedConnectionData>();
  producer->checkProducerForConnect = true;
  SharedMockConnection::SetSharedConnectionData(producer);

  std::shared_ptr<IoService> io_service = IoService::MakeShared();

  bool complete = false;

  Options options;
  options.max_rpc_retries = 1;
  options.rpc_retry_delay_ms = 0;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);
  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(std::make_pair(make_error_code(boost::asio::error::connection_reset), "")))
      .WillOnce(Return(std::make_pair(boost::system::error_code(), "")))
      .WillOnce(Return(std::make_pair(boost::asio::error::would_block, "")));

  engine->Connect("", make_endpoint(), [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_TRUE(stat.ok());
  });
  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestEventCallbacks)
{
  std::shared_ptr<IoService> io_service = IoService::MakeShared();

  Options options;
  options.max_rpc_retries = 99;
  options.rpc_retry_delay_ms = 0;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);

  // Normally determined during RpcEngine::Connect, but in this case options
  // provides enough info to determine policy here.
  engine->TEST_SetRetryPolicy(engine->TEST_GenerateRetryPolicyUsingOptions());

  // Set up event callbacks
  int calls = 0;
  std::vector<std::string> callbacks;
  engine->SetFsEventCallback([&calls, &callbacks] (const char * event,
                    const char * cluster,
                    int64_t value) {
    (void)cluster; (void)value;
    callbacks.push_back(event);

    // Allow connect and fail first read
    calls++;
    if (calls == 1 || calls == 3) // First connect and first read
      return event_response::test_err(Status::Error("Test"));

    return event_response::make_ok();
  });



  EchoResponseProto server_resp;
  server_resp.set_message("foo");

  auto producer = std::make_shared<SharedConnectionData>();
  producer->checkProducerForConnect = true;
  RpcResponseHeaderProto h;
  h.set_callid(1);
  h.set_status(RpcResponseHeaderProto::SUCCESS);
  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(std::make_pair(boost::system::error_code(), ""))) // subverted by callback
      .WillOnce(Return(std::make_pair(boost::system::error_code(), "")))
      .WillOnce(Return(RpcResponse(h, "b"))) // subverted by callback
      .WillOnce(Return(RpcResponse(h, server_resp.SerializeAsString())));
  SharedMockConnection::SetSharedConnectionData(producer);


  EchoRequestProto req;
  req.set_message("foo");
  std::shared_ptr<EchoResponseProto> resp(new EchoResponseProto());

  bool complete = false;
  engine->AsyncRpc("test", &req, resp, [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_TRUE(stat.ok());
  });

  // If you're adding event hooks you'll most likely need to update this.
  // It's a brittle test but makes it hard to miss control flow changes in RPC retry.
  for(const auto& m : callbacks) {
    std::cerr << m << std::endl;
  }
  io_service->Run();
  ASSERT_TRUE(complete);
  ASSERT_EQ(9, callbacks.size());
  ASSERT_EQ(FS_NN_CONNECT_EVENT, callbacks[0]); // error
  ASSERT_EQ(FS_NN_PRE_RPC_RETRY_EVENT, callbacks[1]); // figure out retry decision
  ASSERT_EQ(FS_NN_CONNECT_EVENT, callbacks[2]); // reconnect
  ASSERT_EQ(FS_NN_PRE_RPC_RETRY_EVENT, callbacks[3]); // makes an error
  ASSERT_EQ(FS_NN_CONNECT_EVENT, callbacks[4]); // reconnect
  for (int i=5; i < 8; i++)
    ASSERT_EQ(FS_NN_READ_EVENT, callbacks[i]);
}



TEST(RpcEngineTest, TestConnectionFailureAndAsyncRecover)
{
  // Error and async recover
  auto producer = std::make_shared<SharedConnectionData>();
  producer->checkProducerForConnect = true;
  SharedMockConnection::SetSharedConnectionData(producer);

  std::shared_ptr<IoService> io_service = IoService::MakeShared();

  bool complete = false;

  Options options;
  options.max_rpc_retries = 1;
  options.rpc_retry_delay_ms = 1;
  auto engine = std::make_shared<SharedConnectionEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);
  EXPECT_CALL(*producer, Produce())
      .WillOnce(Return(std::make_pair(make_error_code(boost::asio::error::connection_reset), "")))
      .WillOnce(Return(std::make_pair(boost::system::error_code(), "")))
      .WillOnce(Return(std::make_pair(boost::asio::error::would_block, "")));

  engine->Connect("", make_endpoint(), [&complete, io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_TRUE(stat.ok());
  });

  boost::asio::deadline_timer timer(io_service->GetRaw());
  timer.expires_from_now(boost::posix_time::hours(100));
  timer.async_wait([](const boost::system::error_code & err){(void)err; ASSERT_FALSE("Timed out"); });

  io_service->Run();
  ASSERT_TRUE(complete);
}

TEST(RpcEngineTest, TestTimeout) {
  std::shared_ptr<IoService> io_service = IoService::MakeShared();
  Options options;
  options.rpc_timeout = 1;
  auto engine = std::make_shared<RpcEngine>(
      io_service, options, std::make_shared<std::string>("foo"), "", "protocol",
      1);
  auto conn =
      std::make_shared<RpcConnectionImpl<MockRPCConnection> >(engine);
  conn->TEST_set_connected(true);
  conn->StartReading();

    EXPECT_CALL(conn->TEST_get_mutable_socket(), Produce())
        .WillOnce(Return(std::make_pair(boost::asio::error::would_block, "")));

  std::shared_ptr<RpcConnection> conn_ptr(conn);
  engine->TEST_SetRpcConnection(conn_ptr);

  bool complete = false;

  EchoRequestProto req;
  req.set_message("foo");
  std::shared_ptr<EchoResponseProto> resp(new EchoResponseProto());
  engine->AsyncRpc("test", &req, resp, [resp, &complete,&io_service](const Status &stat) {
    complete = true;
    io_service->Stop();
    ASSERT_FALSE(stat.ok());
  });

  boost::asio::deadline_timer timer(io_service->GetRaw());
  timer.expires_from_now(boost::posix_time::hours(100));
  timer.async_wait([](const boost::system::error_code & err){(void)err; ASSERT_FALSE("Timed out"); });

  io_service->Run();
  ASSERT_TRUE(complete);
}

int main(int argc, char *argv[]) {
  // The following line must be executed to initialize Google Mock
  // (and Google Test) before running the tests.
  ::testing::InitGoogleMock(&argc, argv);
  int exit_code = RUN_ALL_TESTS();

  // Clean up static data and prevent valgrind memory leaks
  google::protobuf::ShutdownProtobufLibrary();
  return exit_code;
}
