/*
 * Copyright (C) 2017 Open Source Robotics Foundation
 *
 * 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 <string>
#include <iostream>
#include <sstream>
#include <ignition/msgs.hh>

#include "gtest/gtest.h"
#include "ign.hh"
#include "ignition/transport/Node.hh"
#include "ignition/transport/test_config.h"

using namespace ignition;

// Global constants.
static const std::string g_topic   = "/topic"; // NOLINT(*)
static const std::string g_service = "/echo"; // NOLINT(*)
static const std::string g_intType = "ign_msgs.Int32"; // NOLINT(*)
static const std::string g_reqData = "data: 10"; // NOLINT(*)

// Global variables.
static std::string     g_partition; // NOLINT(*)
static std::streambuf *g_stdOutFile;
static std::streambuf *g_stdErrFile;

// \brief Redirect stdout and stderr to streams.
void redirectIO(std::stringstream &_stdOutBuffer,
                std::stringstream &_stdErrBuffer)
{
  g_stdOutFile = std::cout.rdbuf(_stdOutBuffer.rdbuf());
  g_stdErrFile = std::cerr.rdbuf(_stdErrBuffer.rdbuf());
}

// \brief Clear all streams (including state flags).
void clearIOStreams(std::stringstream &_stdOutBuffer,
                    std::stringstream &_stdErrBuffer)
{
  _stdOutBuffer.str("");
  _stdOutBuffer.clear();
  _stdErrBuffer.str("");
  _stdErrBuffer.clear();
}

/// \brief Restore stdout and stderr redirections.
void restoreIO()
{
  std::cout.rdbuf(g_stdOutFile);
  std::cerr.rdbuf(g_stdErrFile);
}

/// \brief Provide a service.
bool srvEcho(const ignition::msgs::Int32 &_req, ignition::msgs::Int32 &_rep)
{
  _rep.set_data(_req.data());
  return false;
}

//////////////////////////////////////////////////
/// \brief Check cmdTopicInfo running the advertiser on a the same process.
TEST(ignTest, cmdTopicInfo)
{
  std::stringstream stdOutBuffer;
  std::stringstream stdErrBuffer;
  redirectIO(stdOutBuffer, stdErrBuffer);

  transport::Node node;

  // A null topic name should generate an error message.
  cmdTopicInfo(nullptr);
  EXPECT_EQ(stdErrBuffer.str(), "Invalid topic. Topic must not be empty.\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // A topic without advertisers should show an empty list of publishers.
  cmdTopicInfo(g_topic.c_str());
  EXPECT_EQ(stdOutBuffer.str(), "No publishers on topic [/topic]\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  restoreIO();
}

//////////////////////////////////////////////////
/// \brief Check cmdServiceInfo running the advertiser on a the same process.
TEST(ignTest, cmdServiceInfo)
{
  std::stringstream stdOutBuffer;
  std::stringstream stdErrBuffer;
  redirectIO(stdOutBuffer, stdErrBuffer);

  transport::Node node;

  // A null service name should generate an error message.
  cmdServiceInfo(nullptr);
  EXPECT_EQ(stdErrBuffer.str(),
    "Invalid service. Service must not be empty.\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // A service without advertisers should show no service providers.
  cmdServiceInfo(g_service.c_str());
  EXPECT_EQ(stdOutBuffer.str(), "No service providers on service [/echo]\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  restoreIO();
}

//////////////////////////////////////////////////
/// \brief Check cmdTopicPub running the advertiser on a the same process.
TEST(ignTest, cmdTopicPub)
{
  std::stringstream stdOutBuffer;
  std::stringstream stdErrBuffer;
  redirectIO(stdOutBuffer, stdErrBuffer);

  transport::Node node;

  // A null topic name should generate an error message.
  cmdTopicPub(nullptr, g_intType.c_str(), g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(), "Topic name is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // A null msgType name should generate an error message.
  cmdTopicPub(g_topic.c_str(), nullptr, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(), "Message type is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // Null data should generate an error message.
  cmdTopicPub(g_topic.c_str(), g_intType.c_str(), nullptr);
  EXPECT_EQ(stdErrBuffer.str(), "Message data is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  restoreIO();
}

//////////////////////////////////////////////////
/// \brief Check cmdServiceReq running the advertiser on a the same process.
TEST(ignTest, cmdServiceReq)
{
  std::stringstream  stdOutBuffer;
  std::stringstream  stdErrBuffer;
  redirectIO(stdOutBuffer, stdErrBuffer);

  const std::string kUnknownType = "_unknown_type_";
  const int         kTimeout     = 10;

  transport::Node node;
  EXPECT_TRUE(node.Advertise(g_service, srvEcho));

  ignition::msgs::Int32 msg;
  msg.set_data(10);

  // A null service name should generate an error message.
  cmdServiceReq(nullptr, g_intType.c_str(), g_intType.c_str(),
    kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(), "Service name is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // A null service request type should generate an error message.
  cmdServiceReq(g_service.c_str(), nullptr, g_intType.c_str(),
    kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(), "Request type is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // A null service response type should generate an error message.
  cmdServiceReq(g_service.c_str(), g_intType.c_str(), nullptr,
    kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(), "Response type is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // Null data should generate an error message.
  cmdServiceReq(g_service.c_str(), g_intType.c_str(),
    g_intType.c_str(), kTimeout, nullptr);
  EXPECT_EQ(stdErrBuffer.str(), "Request data is null\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // It's not possible to request a service using a request parameter that is
  // not part of Ignition Messages.
  cmdServiceReq(g_service.c_str(), kUnknownType.c_str(),
    g_intType.c_str(), kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(),
    "Unable to create request of type[_unknown_type_] with data[data: 10].\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // It's not possible to request a service using a response type that is not
  // part of Ignition Messages.
  cmdServiceReq(g_service.c_str(), g_intType.c_str(),
    kUnknownType.c_str(), kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(),
    "Unable to create response of type[_unknown_type_].\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // The service request is valid, received and containing a "false" result.
  cmdServiceReq(g_service.c_str(), g_intType.c_str(),
    g_intType.c_str(), kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdOutBuffer.str(), "Service call failed\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  // The service request is valid but will expire because there's no service
  // available.
  cmdServiceReq("_unknown_service_", g_intType.c_str(),
    g_intType.c_str(), kTimeout, g_reqData.c_str());
  EXPECT_EQ(stdErrBuffer.str(), "Service call timed out\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  restoreIO();
}

//////////////////////////////////////////////////
/// \brief Check cmdTopicEcho running the advertiser on a the same process.
TEST(ignTest, cmdTopicEcho)
{
  std::stringstream  stdOutBuffer;
  std::stringstream  stdErrBuffer;
  redirectIO(stdOutBuffer, stdErrBuffer);

  const std::string kInvalidTopic = "/";
  transport::Node node;

  // Requesting a null topic should trigger an error message.
  cmdTopicEcho(nullptr, 10.00, 0);
  EXPECT_EQ(stdErrBuffer.str(), "Invalid topic. Topic must not be empty.\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  cmdTopicEcho(kInvalidTopic.c_str(), 5.00, 0);
  EXPECT_EQ(stdErrBuffer.str(), "Topic [/] is not valid.\n");
  clearIOStreams(stdOutBuffer, stdErrBuffer);

  restoreIO();
}

/////////////////////////////////////////////////
/// Main
int main(int argc, char **argv)
{
  // Get a random partition name.
  g_partition = testing::getRandomNumber();

  // Set the partition name for this process.
  setenv("IGN_PARTITION", g_partition.c_str(), 1);

  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
