#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Sleep.h>

#include "client/mgmtd/MgmtdClient.h"
#include "fbs/mgmtd/NodeConversion.h"
#include "tests/GtestHelpers.h"
#include "tests/stubs/DummyMgmtdServiceStub.h"

namespace hf3fs::client::tests {
namespace {
class MgmtdClientTest : public ::testing::Test {
 protected:
  MgmtdClientTest() {}
};

#define CO_START_CLIENT(client) \
  co_await (client).start();    \
  co_await folly::coro::co_scope_exit([&]() -> CoTask<void> { co_await (client).stop(); })

std::vector<std::pair<String, String>> convert(const std::vector<std::pair<String, net::Address>> &v) {
  std::vector<std::pair<String, String>> ret;
  for (const auto &[method, addr] : v) {
    ret.emplace_back(method, addr.toString());
  }
  std::sort(ret.begin(), ret.end());
  return ret;
}

#define CO_ASSERT_RECORDS_EQ(expectedRecords)      \
  do {                                             \
    auto _expected = convert(expectedRecords);     \
    auto _actual = convert(Machine::visitRecords); \
    CO_ASSERT_EQ(_actual, _expected);              \
  } while (false)

flat::PersistentNodeInfo makePrimary(flat::NodeId id, net::Address addr) {
  flat::PersistentNodeInfo info;
  info.nodeId = id;
  flat::ServiceGroupInfo sgi;
  sgi.services.insert("Mgmtd");
  sgi.endpoints.push_back(addr);
  info.serviceGroups.push_back(sgi);
  return info;
}

struct Machine {
  Machine(flat::NodeId id, std::vector<net::Address> addrs) {
    selfInfo = makePrimary(id, addrs[0]);
    for (size_t i = 1; i < addrs.size(); ++i) {
      selfInfo.serviceGroups[0].endpoints.push_back(addrs[i]);
    }
  }

  std::unique_ptr<mgmtd::DummyMgmtdServiceStub> createStub(net::Address addr) {
    const auto &endpoints = selfInfo.serviceGroups[0].endpoints;
    auto it = std::find(endpoints.begin(), endpoints.end(), addr);
    if (it == endpoints.end()) return nullptr;

    auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
    stub->set_getPrimaryMgmtdFunc([this, addr](const mgmtd::GetPrimaryMgmtdReq &) -> Result<mgmtd::GetPrimaryMgmtdRsp> {
          visitRecords.emplace_back("GetPrimaryMgmtd", addr);
          if (primary.hasError())
            return makeError(primary.error());
          else if (*primary)
            return mgmtd::GetPrimaryMgmtdRsp::create(**primary);
          return mgmtd::GetPrimaryMgmtdRsp::create(std::nullopt);
        })
        .set_getRoutingInfoFunc([this, addr](const mgmtd::GetRoutingInfoReq &) -> Result<mgmtd::GetRoutingInfoRsp> {
          visitRecords.emplace_back("GetRoutingInfo", addr);
          if (primary.hasError()) return makeError(primary.error());
          if (!*primary) return makeError(MgmtdCode::kNotPrimary);
          if (**primary != selfInfo) return makeError(MgmtdCode::kNotPrimary, fmt::format("{}", (*primary)->nodeId));
          if (routingInfo.hasError()) return makeError(routingInfo.error());
          if (*routingInfo) {
            return mgmtd::GetRoutingInfoRsp::create(**routingInfo);
          }
          return mgmtd::GetRoutingInfoRsp::create(std::nullopt);
        });
    return stub;
  }

  flat::PersistentNodeInfo selfInfo;
  Result<flat::PersistentNodeInfo *> primary = makeError(RPCCode::kConnectFailed);
  Result<flat::RoutingInfo *> routingInfo = makeError(RPCCode::kConnectFailed);
  static std::vector<std::pair<String, net::Address>> visitRecords;
};

std::vector<std::pair<String, net::Address>> Machine::visitRecords;

TEST_F(MgmtdClientTest, testStartStopWithWrongConfig) {
  MgmtdClient::Config config;
  config.set_auto_refresh_interval(1_ms);
  config.set_auto_heartbeat_interval(1_ms);
  MgmtdClient client("", nullptr, config);
  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    co_await folly::coro::sleep(std::chrono::milliseconds(100));
  }());
}

TEST_F(MgmtdClientTest, testRepetitiveStartStop) {
  MgmtdClient::Config config;
  config.set_auto_refresh_interval(1_ms);
  config.set_auto_heartbeat_interval(1_ms);
  MgmtdClient client("", nullptr, config);
  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    co_await folly::coro::sleep(std::chrono::milliseconds(10));
    co_await client.stop();

    co_await client.start();
    co_await folly::coro::sleep(std::chrono::milliseconds(10));
    co_await client.stop();

    co_await client.start();
    co_await folly::coro::sleep(std::chrono::milliseconds(10));
    co_await client.stop();
  }());
}

TEST_F(MgmtdClientTest, testGetRoutingInfoBeforeStart) {
  MgmtdClient::Config config;
  MgmtdClient client("", nullptr, config);
  auto info = client.getRoutingInfo();
  ASSERT_TRUE(!info);
}

net::Address ada = net::Address::from("127.0.0.1:8080").value();
net::Address adb = net::Address::from("192.168.0.1:9000").value();
net::Address adc = net::Address::from("192.168.0.2:8000").value();
net::Address add = net::Address::from("192.168.0.3:8800").value();
net::Address ade;  // invalid addr
net::Address adf = net::Address::from("RDMA://127.0.0.1:8080").value();
net::Address adg = net::Address::from("RDMA://192.168.0.1:9000").value();
net::Address adh = net::Address::from("RDMA://192.168.0.2:8000").value();
net::Address adi = net::Address::from("RDMA://192.168.0.3:8800").value();
net::Address adj = net::Address::from("192.168.0.4:8888").value();

TEST_F(MgmtdClientTest, testWhenNoPrimary) {
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
      return std::make_unique<mgmtd::DummyMgmtdServiceStub>();
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({ada, adb, adc});
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(), config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
    CO_ASSERT_TRUE(!client.getRoutingInfo());
  }());
}

TEST_F(MgmtdClientTest, testInvalidAddress) {
  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({ada, ade});
  MgmtdClient client("", nullptr, config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), StatusCode::kInvalidConfig);
  }());
}

TEST_F(MgmtdClientTest, testAddressTypeMismatch) {
  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({ada, adf});
  config.set_network_type(net::Address::TCP);
  MgmtdClient client("", nullptr, config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), StatusCode::kInvalidConfig);
  }());
}

TEST_F(MgmtdClientTest, testWhenLastIsPrimary) {
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
      if (addr == adc) {
        stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
              return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), adc));
            })
            .set_getRoutingInfoFunc(
                [](const mgmtd::GetRoutingInfoReq &) { return mgmtd::GetRoutingInfoRsp::create(std::nullopt); });
      }
      return stub;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({ada, adb, adc});
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(), config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    auto ri = client.getRoutingInfo();
    CO_ASSERT_TRUE(ri);
    CO_ASSERT_TRUE(!ri->raw());
  }());
}

TEST_F(MgmtdClientTest, testWhenPrimaryNotInConfig) {
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
      if (addr == adc) {
        stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
          return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), add));
        });
      } else if (addr == add) {
        stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
              return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), add));
            })
            .set_getRoutingInfoFunc(
                [](const mgmtd::GetRoutingInfoReq &) { return mgmtd::GetRoutingInfoRsp::create(std::nullopt); });
      }
      return stub;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({ada, adb, adc});
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(), config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    auto ri = client.getRoutingInfo();
    CO_ASSERT_TRUE(ri);
    CO_ASSERT_TRUE(!ri->raw());
  }());
}

TEST_F(MgmtdClientTest, testWhenGetPrimaryLoop) {
  std::vector<net::Address> addresses = {ada, adb, adc};
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::vector<net::Address> addresses;

    explicit StubFactory(std::vector<net::Address> v)
        : addresses(std::move(v)) {}

    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
      for (size_t i = 0; i < addresses.size(); ++i) {
        if (addr == addresses[i]) {
          if (i + 1 == addresses.size()) {
            stub->set_getPrimaryMgmtdFunc([this](const mgmtd::GetPrimaryMgmtdReq &) {
              return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), addresses[0]));
            });
          } else {
            stub->set_getPrimaryMgmtdFunc([this, i](const mgmtd::GetPrimaryMgmtdReq &) {
              return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(i + 2), addresses[i + 1]));
            });
          }
          break;
        }
      }
      return stub;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses(addresses);
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(addresses), config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
    CO_ASSERT_TRUE(!client.getRoutingInfo());
  }());
}

TEST_F(MgmtdClientTest, testRetryOnRefreshFail) {
  std::vector<net::Address> addresses = {ada, adb, adc};
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    int counta = 0;
    int countb = 0;

    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
      if (addr == ada) {
        stub->set_getPrimaryMgmtdFunc([this](const mgmtd::GetPrimaryMgmtdReq &) -> Result<mgmtd::GetPrimaryMgmtdRsp> {
              if (++counta == 1) {
                return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
              } else {
                return makeError(RPCCode::kConnectFailed);
              }
            })
            .set_getRoutingInfoFunc([this](const mgmtd::GetRoutingInfoReq &) -> Result<mgmtd::GetRoutingInfoRsp> {
              if (++countb == 1)
                return mgmtd::GetRoutingInfoRsp::create(std::nullopt);
              else
                return makeError(RPCCode::kConnectFailed);
            });
      } else if (addr == adb) {
        stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
              return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(2), adb));
            })
            .set_getRoutingInfoFunc([](const mgmtd::GetRoutingInfoReq &) {
              flat::RoutingInfo ri;
              ri.routingInfoVersion = flat::RoutingInfoVersion(2);
              return mgmtd::GetRoutingInfoRsp::create(ri);
            });
      }
      return stub;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses(addresses);
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(), config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    auto ri = client.getRoutingInfo();
    CO_ASSERT_TRUE(ri && !ri->raw());
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    ri = client.getRoutingInfo();
    CO_ASSERT_TRUE(ri && ri->raw());
    CO_ASSERT_EQ(ri->raw()->routingInfoVersion, flat::RoutingInfoVersion(2));
  }());
}

TEST_F(MgmtdClientTest, testRefreshRoutingInfoCallback) {
  std::vector<net::Address> addresses = {ada};
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
      auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
      stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
            return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
          })
          .set_getRoutingInfoFunc([](const mgmtd::GetRoutingInfoReq &req) {
            flat::RoutingInfo ri;
            ri.routingInfoVersion = flat::RoutingInfoVersion(req.routingInfoVersion + 1);
            return mgmtd::GetRoutingInfoRsp::create(ri);
          });
      return stub;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses(addresses);
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(), config);

  std::vector<std::shared_ptr<RoutingInfo>> ris;
  client.addRoutingInfoListener("test", [&](std::shared_ptr<RoutingInfo> ri) { ris.push_back(std::move(ri)); });

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    for (int i = 0; i < 10; ++i) CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
  }());

  ASSERT_EQ(ris.size(), 10);
  for (int i = 0; i < 10; ++i) {
    const auto &ri = ris[i];
    ASSERT_TRUE(ri && ri->raw());
    ASSERT_EQ(ri->raw()->routingInfoVersion, flat::RoutingInfoVersion(i + 1));
    if (i > 0) ASSERT_TRUE(ri->lastRefreshTime() > ris[i - 1]->lastRefreshTime());
  }
}

TEST_F(MgmtdClientTest, testRetryAllAvailableAddresses) {
  Machine::visitRecords.clear();
  Machine ma(flat::NodeId(1), {ada, adb, adc});
  Machine mb(flat::NodeId(2), {add, adf});
  Machine mc(flat::NodeId(3), {adj});

  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::vector<Machine *> machines;

    explicit StubFactory(std::vector<Machine *> v)
        : machines(std::move(v)) {}

    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      for (auto *m : machines) {
        auto stub = m->createStub(addr);
        if (stub) return stub;
      }
      return nullptr;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({adj});
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  config.set_network_type(net::Address::TCP);
  MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);

  flat::RoutingInfo ri;
  ri.routingInfoVersion = flat::RoutingInfoVersion(2);
  ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
  ri.nodes[mb.selfInfo.nodeId] = flat::toNode(mb.selfInfo);
  ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    mc.primary = &mc.selfInfo;
    mc.routingInfo = &ri;
    CO_START_CLIENT(client);

    Machine::visitRecords.clear();
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    {
      std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
      CO_ASSERT_RECORDS_EQ(expected);
    }

    mc.primary = makeError(RPCCode::kConnectFailed);
    Machine::visitRecords.clear();
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
    {
      std::vector<std::pair<String, net::Address>> expected = {
          {"GetRoutingInfo", adj},
          {"GetPrimaryMgmtd", ada},
          {"GetPrimaryMgmtd", adb},
          {"GetPrimaryMgmtd", adc},
          {"GetPrimaryMgmtd", add},
      };
      CO_ASSERT_RECORDS_EQ(expected);
    }
  }());
}

TEST_F(MgmtdClientTest, testRetryEndWhenNoPrimary) {
  Machine::visitRecords.clear();
  Machine ma(flat::NodeId(1), {ada, adb, adc});
  Machine mb(flat::NodeId(2), {add, adf});
  Machine mc(flat::NodeId(3), {adj});

  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::vector<Machine *> machines;

    explicit StubFactory(std::vector<Machine *> v)
        : machines(std::move(v)) {}

    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      for (auto *m : machines) {
        auto stub = m->createStub(addr);
        if (stub) return stub;
      }
      return nullptr;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({adj});
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  config.set_network_type(net::Address::TCP);
  MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);

  flat::RoutingInfo ri;
  ri.routingInfoVersion = flat::RoutingInfoVersion(2);
  ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
  ri.nodes[mb.selfInfo.nodeId] = flat::toNode(mb.selfInfo);
  ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    mc.primary = &mc.selfInfo;
    mc.routingInfo = &ri;
    CO_START_CLIENT(client);

    Machine::visitRecords.clear();
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    {
      std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
      CO_ASSERT_RECORDS_EQ(expected);
    }

    ma.primary = nullptr;
    mc.primary = makeError(RPCCode::kConnectFailed);
    Machine::visitRecords.clear();
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
    {
      std::vector<std::pair<String, net::Address>> expected = {
          {"GetRoutingInfo", adj},
          {"GetPrimaryMgmtd", ada},
      };
      CO_ASSERT_RECORDS_EQ(expected);
    }
  }());
}

TEST_F(MgmtdClientTest, testRetryUnknownAddrs) {
  Machine::visitRecords.clear();
  Machine ma(flat::NodeId(1), {ada, adb, adc});
  Machine mb(flat::NodeId(2), {add, adf});
  Machine mc(flat::NodeId(3), {adj});

  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::vector<Machine *> machines;

    explicit StubFactory(std::vector<Machine *> v)
        : machines(std::move(v)) {}

    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
      for (auto *m : machines) {
        auto stub = m->createStub(addr);
        if (stub) return stub;
      }
      return nullptr;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses({adj, add});
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  config.set_network_type(net::Address::TCP);
  MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);

  flat::RoutingInfo ri;
  ri.routingInfoVersion = flat::RoutingInfoVersion(2);
  ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
  ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    mc.primary = &mc.selfInfo;
    mc.routingInfo = &ri;
    CO_START_CLIENT(client);

    Machine::visitRecords.clear();
    CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
    {
      std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
      CO_ASSERT_RECORDS_EQ(expected);
    }

    mc.primary = makeError(RPCCode::kConnectFailed);
    Machine::visitRecords.clear();
    CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
    {
      std::vector<std::pair<String, net::Address>> expected = {
          {"GetRoutingInfo", adj},
          {"GetPrimaryMgmtd", ada},
          {"GetPrimaryMgmtd", adb},
          {"GetPrimaryMgmtd", adc},
          {"GetPrimaryMgmtd", add},
      };
      CO_ASSERT_RECORDS_EQ(expected);
    }
  }());
}

TEST_F(MgmtdClientTest, testSetGetConfigViaInvoke) {
  std::vector<net::Address> addresses = {ada};
  struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
    std::map<flat::NodeType, flat::ConfigInfo> configs;
    std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
      auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
      stub->set_setConfigFunc([this](const mgmtd::SetConfigReq &req) -> Result<mgmtd::SetConfigRsp> {
            auto &info = configs[req.nodeType];
            auto cv = flat::ConfigVersion(info.configVersion + 1);
            info = flat::ConfigInfo::create(cv, req.content, req.desc);
            return mgmtd::SetConfigRsp::create(cv);
          })
          .set_getConfigFunc([this](const mgmtd::GetConfigReq &req) -> Result<mgmtd::GetConfigRsp> {
            if (configs.contains(req.nodeType)) return mgmtd::GetConfigRsp::create(configs[req.nodeType]);
            return mgmtd::GetConfigRsp::create(std::nullopt);
          })
          .set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
            return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
          });
      return stub;
    }
  };

  MgmtdClient::Config config;
  config.set_mgmtd_server_addresses(addresses);
  config.set_enable_auto_refresh(false);
  config.set_enable_auto_heartbeat(false);
  MgmtdClient client("", std::make_unique<StubFactory>(), config);

  folly::coro::blockingWait([&]() -> CoTask<void> {
    CO_START_CLIENT(client);
    {
      auto res = co_await client.setConfig(flat::UserInfo{}, flat::NodeType::MGMTD, String("abcd"), "desc");
      CO_ASSERT_OK(res);
    }
    {
      auto res = co_await client.getConfig(flat::NodeType::MGMTD, flat::ConfigVersion{0});
      CO_ASSERT_OK(res);
      CO_ASSERT_TRUE(res->has_value());
      CO_ASSERT_EQ(res->value().configVersion, flat::ConfigVersion{1});
      CO_ASSERT_EQ(res->value().content, "abcd");
      CO_ASSERT_EQ(res->value().desc, "desc");

      res = co_await client.getConfig(flat::NodeType::META, flat::ConfigVersion{0});
      CO_ASSERT_OK(res);
      CO_ASSERT_TRUE(!res->has_value());
    }
  }());
}

}  // namespace
}  // namespace hf3fs::client::tests
