#include "test.h"

#include <initializer_list>
#include <string>
#include <sstream>
#include <vector>

#include <errno.h>
#include <unistd.h>

namespace {

  constexpr const char* FAKE_HOST = "SomeHostname";

  constexpr int NUM_DB_PING_TRIALS = 10000;

  std::vector<std::string> ExtractStringVector(Tango::DeviceData &dd)
  {
    // requires cppTango main
    // BOOST_TEST_INFO("Device data: " << dd);
    std::vector<std::string> vec;
    BOOST_CHECK(dd >> vec);

    return vec;
  }

  Tango::DeviceData MakeDeviceDataStringArray(std::initializer_list<std::string> list = {})
  {
    int idx = 0;
    Tango::DeviceData dd;
    auto *arr = new Tango::DevVarStringArray;
    dd << arr;

    arr->length(list.size());

    for(const auto &elem : list)
    {
      (*arr)[idx++] = Tango::string_dup(elem.c_str());
    }

    return dd;
  }

  Tango::DeviceData MakeDeviceDataString(std::string str)
  {
    Tango::DeviceData dd;
    dd << str;

    return dd;
  }

  void ExecuteInShell(const std::string& str)
  {
    errno  = 0;
    int ret = system(str.c_str());
    BOOST_TEST_INFO("system errno: " << errno << ", error string: " << strerror(errno));
    BOOST_REQUIRE(!ret);
  }

} // anonymous

class Fixture
{
  public:
    Fixture()
    {
      ExecuteInShell("./start_tdb.sh");

      for(int i = 0;i < NUM_DB_PING_TRIALS; i++)
      {
        try
        {
          dp = new Tango::DeviceProxy("sys/database/2");

          dp->ping();
        }
        catch(Tango::DevFailed &)
        {
          // Databaseds is not yet ready
          usleep(1e3);
          continue;
        }

        return;
      }

      BOOST_FAIL("The database could not be reached.");
    }

    ~Fixture()
    {
      ExecuteInShell("./stop_tdb.sh");
    }

    Tango::DeviceProxy *dp;
};

class AddAndExportDeviceFixture : public Fixture
{
  public:
    AddAndExportDeviceFixture()
    {
      {
        auto dd = MakeDeviceDataStringArray({"Proc.py", device_name , "myClass"});
        BOOST_CHECK_NO_THROW(dp->command_inout("DbAddDevice", dd));
      }

      {
        auto dd = MakeDeviceDataStringArray({"myDevice", "tango://1/mydomain/myfamily/mymember", "myClass"});
        BOOST_CHECK_NO_THROW(dp->command_inout("DbAddServer", dd));
      }

      {
        // using the maximum PID on 64-bit systems see
        // https://unix.stackexchange.com/questions/16883/what-is-the-maximum-value-of-the-process-id
        // and `man 5 proc`

        //                                    device name, CORBA IOR, host name,     pid,       process version
        auto dd = MakeDeviceDataStringArray({device_name, "SomeIOR", FAKE_HOST, "4194303", "123"});

        BOOST_CHECK_NO_THROW(dp->command_inout("DbExportDevice", dd));
      }
    }

  constexpr static const char* device_name = "mydomain/myfamily/mymember";

  ~AddAndExportDeviceFixture() = default;
};

BOOST_FIXTURE_TEST_SUITE(AllTests, Fixture)

  BOOST_AUTO_TEST_CASE(ExportDeviceRequires5Args)
  {
    auto dd = MakeDeviceDataStringArray();

    BOOST_CHECK_THROW(dp->command_inout("DbExportDevice", dd), Tango::DevFailed);
  }

  BOOST_FIXTURE_TEST_CASE(PassedPIDIsParsedCorrectly, AddAndExportDeviceFixture)
  {
    auto dd = MakeDeviceDataString(device_name);
    Tango::DeviceData reply;
    BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetDeviceInfo", dd));

    std::vector<std::string> vs;
    std::vector<Tango::DevLong> vl;
    BOOST_CHECK(reply.extract(vl, vs));   // exported, pid
    std::vector<Tango::DevLong> vl_ref = {1,           4194303};
    BOOST_CHECK_EQUAL_COLLECTIONS(vl.begin(), vl.end(), vl_ref.begin(), vl_ref.end());

    // replace volatile data
    vs[5] = "fakeDate";
    std::vector<std::string> vs_ref = {device_name, "SomeIOR", "123", "Proc.py", FAKE_HOST, "fakeDate", "?", "myClass"};
    BOOST_CHECK_EQUAL_COLLECTIONS(vs.begin(), vs.end(), vs_ref.begin(), vs_ref.end());
  }

  BOOST_AUTO_TEST_CASE(AddDeviceWorks)
  {
    {
      auto dd = MakeDeviceDataString("*");

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetServerList", dd));

      auto vec = ExtractStringVector(reply);
      BOOST_CHECK_EQUAL(vec.size(), 4);
    }

    // add device
    {
      auto dd = MakeDeviceDataStringArray({"TangoTest/mytest", "TangoTest/mytest/1", "TangoTest"});
      BOOST_CHECK_NO_THROW(dp->command_inout("DbAddServer", dd));
    }

    // check that it now exists
    {
      auto dd = MakeDeviceDataString("*");

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetServerList", dd));

      auto vec = ExtractStringVector(reply);
      BOOST_CHECK_EQUAL(vec.size(), 5);

      auto it = std::find(std::begin(vec), std::end(vec), "TangoTest/mytest");
      BOOST_CHECK(it != std::end(vec));
    }
  }

  BOOST_AUTO_TEST_CASE(DBInfoDoesNotThrow)
  {
    Tango::DeviceData reply;
    BOOST_CHECK_NO_THROW(reply = dp->command_inout("DBInfo"));
    auto vec = ExtractStringVector(reply);
    BOOST_CHECK_GT(vec.size(), 0);
  }

  BOOST_FIXTURE_TEST_CASE(GetHostListWorks, AddAndExportDeviceFixture)
  {
    // all
    {
      auto dd = MakeDeviceDataString("*");

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetHostList", dd));
      auto vec = ExtractStringVector(reply);

      BOOST_CHECK_EQUAL(vec.size(), 3);
      BOOST_CHECK_NE(std::find(std::begin(vec), std::end(vec), FAKE_HOST), std::end(vec));
      BOOST_CHECK_NE(std::find(std::begin(vec), std::end(vec), "nada"), std::end(vec));
    }

    // no matches
    {
      auto dd = MakeDeviceDataString("");

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetHostList", dd));
      auto vec = ExtractStringVector(reply);
      BOOST_CHECK(vec.empty());
    }

    // single match
    {
      auto dd = MakeDeviceDataString(FAKE_HOST);

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetHostList", dd));
      auto vec = ExtractStringVector(reply);
      BOOST_CHECK_EQUAL(vec.size(), 1);

      std::vector<std::string> ref = {FAKE_HOST};
      BOOST_CHECK_EQUAL_COLLECTIONS(std::begin(vec), std::end(vec), std::begin(ref), std::end(ref));
    }
  }

  BOOST_AUTO_TEST_CASE(DbPutPropertyWorks)
  {
    const std::string objName{"globalObj"};
    const std::string propName{"propName"};
    const std::string propValue{"propValue"};

    // write free property
    {
      auto in = MakeDeviceDataStringArray({objName, "1", propName, "1", propValue});

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbPutProperty", in));
    }

    // read it back
    {
      auto in = MakeDeviceDataStringArray({objName, propName});

      Tango::DeviceData reply;
      BOOST_CHECK_NO_THROW(reply = dp->command_inout("DbGetProperty", in));

      auto vec = ExtractStringVector(reply);
      std::vector<std::string> ref = {objName, "1", propName, "1", propValue};
      BOOST_CHECK_EQUAL_COLLECTIONS(std::begin(vec), std::end(vec), std::begin(ref), std::end(ref));
    }
  }

BOOST_AUTO_TEST_SUITE_END() // AllTests
