/**
 * 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 <gtest/gtest.h>
#include <pulsar/Client.h>

#include <thread>

#include "HttpHelper.h"
#include "PulsarFriend.h"
#include "lib/Future.h"
#include "lib/Latch.h"
#include "lib/LogUtils.h"
#include "lib/ProducerImpl.h"
#include "lib/Utils.h"
DECLARE_LOG_OBJECT()

using namespace pulsar;

static const std::string serviceUrl = "pulsar://localhost:6650";
static const std::string adminUrl = "http://localhost:8080/";

// See the `maxMessageSize` config in test-conf/standalone-ssl.conf
static constexpr size_t maxMessageSize = 1024000;

TEST(ProducerTest, producerNotInitialized) {
    Producer producer;

    Message msg = MessageBuilder().setContent("test").build();

    ASSERT_EQ(ResultProducerNotInitialized, producer.send(msg));

    Promise<Result, MessageId> promise;
    producer.sendAsync(msg, WaitForCallbackValue<MessageId>(promise));

    MessageId mi;
    ASSERT_EQ(ResultProducerNotInitialized, promise.getFuture().get(mi));

    ASSERT_EQ(ResultProducerNotInitialized, producer.close());

    Promise<bool, Result> promiseClose;
    producer.closeAsync(WaitForCallback(promiseClose));

    Result result;
    promiseClose.getFuture().get(result);
    ASSERT_EQ(ResultProducerNotInitialized, result);

    ASSERT_TRUE(producer.getTopic().empty());
}

TEST(ProducerTest, exactlyOnceWithProducerNameSpecified) {
    Client client(serviceUrl);

    std::string topicName = "persistent://public/default/exactlyOnceWithProducerNameSpecified";

    Producer producer1;
    ProducerConfiguration producerConfiguration1;
    producerConfiguration1.setProducerName("p-name-1");

    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration1, producer1));

    Producer producer2;
    ProducerConfiguration producerConfiguration2;
    producerConfiguration2.setProducerName("p-name-2");
    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration2, producer2));

    Producer producer3;
    Result result = client.createProducer(topicName, producerConfiguration2, producer3);
    ASSERT_EQ(ResultProducerBusy, result);
}

TEST(ProducerTest, testSynchronouslySend) {
    Client client(serviceUrl);
    const std::string topic = "ProducerTestSynchronouslySend";

    Consumer consumer;
    ASSERT_EQ(ResultOk, client.subscribe(topic, "sub-name", consumer));

    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topic, producer));
    MessageId messageId;
    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent("hello").build(), messageId));
    LOG_INFO("Send message to " << messageId);

    Message receivedMessage;
    ASSERT_EQ(ResultOk, consumer.receive(receivedMessage, 3000));
    LOG_INFO("Received message from " << receivedMessage.getMessageId());
    ASSERT_EQ(receivedMessage.getMessageId(), messageId);
    ASSERT_EQ(ResultOk, consumer.acknowledge(receivedMessage));

    client.close();
}

TEST(ProducerTest, testIsConnected) {
    Client client(serviceUrl);
    const std::string nonPartitionedTopic =
        "testProducerIsConnectedNonPartitioned-" + std::to_string(time(nullptr));
    const std::string partitionedTopic =
        "testProducerIsConnectedPartitioned-" + std::to_string(time(nullptr));

    Producer producer;
    ASSERT_FALSE(producer.isConnected());
    // ProducerImpl
    ASSERT_EQ(ResultOk, client.createProducer(nonPartitionedTopic, producer));
    ASSERT_TRUE(producer.isConnected());
    ASSERT_EQ(ResultOk, producer.close());
    ASSERT_FALSE(producer.isConnected());

    int res = makePutRequest(
        adminUrl + "admin/v2/persistent/public/default/" + partitionedTopic + "/partitions", "2");
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    // PartitionedProducerImpl
    ASSERT_EQ(ResultOk, client.createProducer(partitionedTopic, producer));
    ASSERT_TRUE(producer.isConnected());
    ASSERT_EQ(ResultOk, producer.close());
    ASSERT_FALSE(producer.isConnected());

    client.close();
}

TEST(ProducerTest, testSendAsyncAfterCloseAsyncWithLazyProducers) {
    Client client(serviceUrl);
    const std::string partitionedTopic =
        "testProducerIsConnectedPartitioned-" + std::to_string(time(nullptr));

    int res = makePutRequest(
        adminUrl + "admin/v2/persistent/public/default/" + partitionedTopic + "/partitions", "10");
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    ProducerConfiguration producerConfiguration;
    producerConfiguration.setLazyStartPartitionedProducers(true);
    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(partitionedTopic, producerConfiguration, producer));

    Message msg = MessageBuilder().setContent("test").build();

    Promise<bool, Result> promiseClose;
    producer.closeAsync(WaitForCallback(promiseClose));

    Promise<Result, MessageId> promise;
    producer.sendAsync(msg, WaitForCallbackValue<MessageId>(promise));

    MessageId mi;
    ASSERT_EQ(ResultAlreadyClosed, promise.getFuture().get(mi));

    Result result;
    promiseClose.getFuture().get(result);
    ASSERT_EQ(ResultOk, result);
}

TEST(ProducerTest, testGetNumOfChunks) {
    ASSERT_EQ(ProducerImpl::getNumOfChunks(11, 5), 3);
    ASSERT_EQ(ProducerImpl::getNumOfChunks(10, 5), 2);
    ASSERT_EQ(ProducerImpl::getNumOfChunks(8, 5), 2);
    ASSERT_EQ(ProducerImpl::getNumOfChunks(4, 5), 1);
    ASSERT_EQ(ProducerImpl::getNumOfChunks(1, 0), 1);
}

TEST(ProducerTest, testBacklogQuotasExceeded) {
    std::string ns = "public/test-backlog-quotas";
    std::string topic = ns + "/testBacklogQuotasExceeded" + std::to_string(time(nullptr));

    int res = makePutRequest(adminUrl + "admin/v2/persistent/" + topic + "/partitions", "5");
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;
    LOG_INFO("Created topic " << topic << " with 5 partitions");

    auto setBacklogPolicy = [&ns](const std::string& policy, int limitSize) {
        const auto body = R"({"policy":")" + policy + R"(","limitSize":)" + std::to_string(limitSize) + "}";
        int res = makePostRequest(adminUrl + "admin/v2/namespaces/" + ns + "/backlogQuota", body);
        LOG_INFO(res << " | Change the backlog policy to: " << body);
        ASSERT_TRUE(res == 204 || res == 409);
        std::this_thread::sleep_for(std::chrono::seconds(1));
    };

    Client client(serviceUrl);

    // Create a topic with backlog size that is greater than 1024
    Consumer consumer;
    ASSERT_EQ(ResultOk, client.subscribe(topic, "sub", consumer));  // create a cursor
    Producer producer;

    const auto partition = topic + "-partition-0";
    ASSERT_EQ(ResultOk, client.createProducer(partition, producer));
    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent(std::string(1024L, 'a')).build()));
    ASSERT_EQ(ResultOk, producer.close());

    setBacklogPolicy("producer_request_hold", 1024);
    ASSERT_EQ(ResultProducerBlockedQuotaExceededError, client.createProducer(topic, producer));
    ASSERT_EQ(ResultProducerBlockedQuotaExceededError, client.createProducer(partition, producer));

    setBacklogPolicy("producer_exception", 1024);
    ASSERT_EQ(ResultProducerBlockedQuotaExceededException, client.createProducer(topic, producer));
    ASSERT_EQ(ResultProducerBlockedQuotaExceededException, client.createProducer(partition, producer));

    setBacklogPolicy("consumer_backlog_eviction", 1024);
    ASSERT_EQ(ResultOk, client.createProducer(topic, producer));
    ASSERT_EQ(ResultOk, client.createProducer(partition, producer));

    client.close();
}

TEST(ProducerTest, testCreateProducerAfterTopicTermination) {
    const auto topicName = "testCreateProducerAfterTopicTermination-" + std::to_string(time(nullptr));
    const auto topic = "persistent://public/default/" + topicName;

    Client client(serviceUrl, ClientConfiguration().setOperationTimeoutSeconds(1));

    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topic, producer));
    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent("content").build()));
    ASSERT_EQ(ResultOk, producer.close());

    const auto httpCode =
        makePostRequest(adminUrl + "admin/v2/persistent/public/default/" + topicName + "/terminate", "");
    ASSERT_EQ(200, httpCode) << "httpCode: " << httpCode;

    Producer terminatedProducer;
    ASSERT_EQ(ResultTopicTerminated, client.createProducer(topic, terminatedProducer));

    client.close();
}

TEST(ProducerTest, testSendAfterTopicTerminationReconnect) {
    const auto topicName = "testSendAfterTopicTerminationReconnect-" + std::to_string(time(nullptr));
    const auto topic = "persistent://public/default/" + topicName;

    Client client(serviceUrl, ClientConfiguration().setOperationTimeoutSeconds(1));

    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topic, producer));
    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent("before-terminate").build()));

    const auto httpCode =
        makePostRequest(adminUrl + "admin/v2/persistent/public/default/" + topicName + "/terminate", "");
    ASSERT_EQ(200, httpCode) << "httpCode: " << httpCode;

    PulsarFriend::getProducerImpl(producer).disconnectProducer();
    ASSERT_TRUE(
        waitUntil(std::chrono::seconds(3), [&producer] { return PulsarFriend::isTerminated(producer); }));

    ASSERT_EQ(ResultTopicTerminated, producer.send(MessageBuilder().setContent("after-terminate").build()));

    client.close();
}

class ProducerTest : public ::testing::TestWithParam<bool> {};

TEST_P(ProducerTest, testMaxMessageSize) {
    Client client(serviceUrl);

    const auto topic = std::string("ProducerTest-NoBatchMaxMessageSize-") +
                       (GetParam() ? "batch-" : "-no-batch-") + std::to_string(time(nullptr));

    Consumer consumer;
    ASSERT_EQ(ResultOk, client.subscribe(topic, "sub", consumer));

    Producer producer;
    ProducerConfiguration conf;
    conf.setBatchingEnabled(GetParam());
    ASSERT_EQ(ResultOk, client.createProducer(topic, conf, producer));

    std::string msg = std::string(maxMessageSize / 2, 'a');
    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent(msg).build()));
    Message message;
    ASSERT_EQ(ResultOk, consumer.receive(message));
    ASSERT_EQ(msg, message.getDataAsString());

    std::string orderKey = std::string(maxMessageSize, 'a');
    ASSERT_EQ(ResultMessageTooBig, producer.send(MessageBuilder().setOrderingKey(orderKey).build()));

    ASSERT_EQ(ResultMessageTooBig,
              producer.send(MessageBuilder().setContent(std::string(maxMessageSize, 'b')).build()));

    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent(msg).build()));

    client.close();
}

TEST(ProducerTest, testChunkingMaxMessageSize) {
    Client client(serviceUrl);

    const auto topic = std::string("ProducerTest-ChunkingMaxMessageSize-") + std::to_string(time(nullptr));

    Consumer consumer;
    ASSERT_EQ(ResultOk, client.subscribe(topic, "sub", consumer));

    Producer producer;
    ProducerConfiguration conf;
    conf.setBatchingEnabled(false);
    conf.setChunkingEnabled(true);
    ASSERT_EQ(ResultOk, client.createProducer(topic, conf, producer));

    std::string orderKey = std::string(maxMessageSize, 'a');
    ASSERT_EQ(ResultMessageTooBig, producer.send(MessageBuilder().setOrderingKey(orderKey).build()));

    std::string msg = std::string(2 * maxMessageSize + 10, 'b');
    Message message;
    ASSERT_EQ(ResultOk, producer.send(MessageBuilder().setContent(msg).build()));
    ASSERT_EQ(ResultOk, consumer.receive(message));
    ASSERT_EQ(msg, message.getDataAsString());
    ASSERT_LE(1L, message.getMessageId().entryId());

    client.close();
}

TEST(ProducerTest, testExclusiveProducer) {
    Client client(serviceUrl);

    std::string topicName =
        "persistent://public/default/testExclusiveProducer" + std::to_string(time(nullptr));

    Producer producer1;
    ProducerConfiguration producerConfiguration1;
    producerConfiguration1.setProducerName("p-name-1");
    producerConfiguration1.setAccessMode(ProducerConfiguration::Exclusive);

    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration1, producer1));

    Producer producer2;
    ProducerConfiguration producerConfiguration2;
    producerConfiguration2.setProducerName("p-name-2");
    producerConfiguration2.setAccessMode(ProducerConfiguration::Exclusive);
    ASSERT_EQ(ResultProducerFenced, client.createProducer(topicName, producerConfiguration2, producer2));

    Producer producer3;
    ProducerConfiguration producerConfiguration3;
    producerConfiguration3.setProducerName("p-name-3");
    ASSERT_EQ(ResultProducerBusy, client.createProducer(topicName, producerConfiguration3, producer3));
}

TEST(ProducerTest, testWaitForExclusiveProducer) {
    Client client(serviceUrl);

    std::string topicName =
        "persistent://public/default/testWaitForExclusiveProducer" + std::to_string(time(nullptr));

    Producer producer1;
    ProducerConfiguration producerConfiguration1;
    producerConfiguration1.setProducerName("p-name-1");
    producerConfiguration1.setAccessMode(ProducerConfiguration::Exclusive);

    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration1, producer1));

    ASSERT_EQ(ResultOk, producer1.send(MessageBuilder().setContent("content").build()));

    Producer producer2;
    ProducerConfiguration producerConfiguration2;
    producerConfiguration2.setProducerName("p-name-2");
    producerConfiguration2.setAccessMode(ProducerConfiguration::WaitForExclusive);

    Latch latch(1);
    client.createProducerAsync(topicName, producerConfiguration2,
                               [&latch, &producer2](Result res, const Producer& producer) {
                                   ASSERT_EQ(ResultOk, res);
                                   producer2 = producer;
                                   latch.countdown();
                               });

    // when p1 close, p2 success created.
    producer1.close();
    latch.wait();
    ASSERT_EQ(ResultOk, producer2.send(MessageBuilder().setContent("content").build()));

    producer2.close();
}

TEST(ProducerTest, testExclusiveWithFencingProducer) {
    Client client(serviceUrl);

    std::string topicName =
        "persistent://public/default/testExclusiveWithFencingProducer" + std::to_string(time(nullptr));

    Producer producer1;
    ProducerConfiguration producerConfiguration1;
    producerConfiguration1.setProducerName("p-name-1");
    producerConfiguration1.setAccessMode(ProducerConfiguration::Exclusive);

    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration1, producer1));
    producer1.send(MessageBuilder().setContent("content").build());

    Producer producer2;
    ProducerConfiguration producerConfiguration2;
    producerConfiguration2.setProducerName("p-name-2");
    producerConfiguration2.setAccessMode(ProducerConfiguration::WaitForExclusive);

    Latch latch(1);
    client.createProducerAsync(topicName, producerConfiguration2,
                               [&latch, &producer2](Result res, const Producer& producer) {
                                   // producer2 will be fenced
                                   ASSERT_EQ(ResultProducerFenced, res);
                                   latch.countdown();
                                   producer2 = producer;
                               });
    // wait for all the Producers to be enqueued in order to prevent races
    sleep(1);

    // producer3 will create success.
    Producer producer3;
    ProducerConfiguration producerConfiguration3;
    producerConfiguration3.setProducerName("p-name-3");
    producerConfiguration3.setAccessMode(ProducerConfiguration::ExclusiveWithFencing);
    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration3, producer3));
    ASSERT_EQ(ResultOk, producer3.send(MessageBuilder().setContent("content").build()));

    latch.wait();
    // producer1 will be fenced
    ASSERT_EQ(ResultProducerFenced, producer1.send(MessageBuilder().setContent("content").build()));

    // Again create producer4 with WaitForExclusive
    Producer producer4;
    ProducerConfiguration producerConfiguration4;
    producerConfiguration2.setProducerName("p-name-4");
    producerConfiguration2.setAccessMode(ProducerConfiguration::WaitForExclusive);
    Latch latch2(1);
    client.createProducerAsync(topicName, producerConfiguration2,
                               [&latch2, &producer4](Result res, const Producer& producer) {
                                   // producer4 will be success
                                   ASSERT_EQ(ResultOk, res);
                                   producer4 = producer;
                                   latch2.countdown();
                               });
    ASSERT_EQ(ResultProducerNotInitialized, producer4.send(MessageBuilder().setContent("content").build()));

    // When producer3 is close, producer4 will be create success
    producer3.close();
    latch2.wait();
    ASSERT_EQ(ResultOk, producer4.send(MessageBuilder().setContent("content").build()));

    client.close();
}

TEST_P(ProducerTest, testFlushNoBatch) {
    Client client(serviceUrl);

    auto partitioned = GetParam();
    const auto topicName = std::string("testFlushNoBatch") +
                           (partitioned ? "partitioned-" : "-no-partitioned-") +
                           std::to_string(time(nullptr));

    if (partitioned) {
        // call admin api to make it partitioned
        std::string url = adminUrl + "admin/v2/persistent/public/default/" + topicName + "/partitions";
        int res = makePutRequest(url, "5");
        LOG_INFO("res = " << res);
        ASSERT_FALSE(res != 204 && res != 409);
    }

    ProducerConfiguration producerConfiguration;
    producerConfiguration.setBatchingEnabled(false);

    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration, producer));

    std::atomic_int needCallBack(100);
    auto cb = [&needCallBack](Result code, const MessageId& msgId) {
        ASSERT_EQ(code, ResultOk);
        needCallBack.fetch_sub(1);
    };

    for (int i = 0; i < 100; ++i) {
        Message msg = MessageBuilder().setContent("content").build();
        producer.sendAsync(msg, cb);
    }

    producer.flush();
    ASSERT_EQ(needCallBack.load(), 0);
    producer.close();

    client.close();
}

// Verifies that getLastSequenceId() is correct after sendAsync + flush when batching is enabled.
// Previously the batch used the last message's sequence_id, causing lastSequenceIdPublished_ to be
// doubled (e.g. 3 messages yielded 4 instead of 2). The batch must use the first message's
// sequence_id so that lastSequenceIdPublished_ = sequenceId + messagesCount - 1 is correct.
TEST(ProducerTest, testGetLastSequenceIdAfterBatchFlush) {
    Client client(serviceUrl);

    const std::string topicName =
        "persistent://public/default/testGetLastSequenceIdAfterBatchFlush-" + std::to_string(time(nullptr));

    ProducerConfiguration producerConfiguration;
    producerConfiguration.setBatchingEnabled(true);
    producerConfiguration.setBatchingMaxMessages(10);
    producerConfiguration.setBatchingMaxPublishDelayMs(60000);

    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration, producer));

    // Send 3 messages in a batch, then flush. Sequence ids are [0, 1, 2], so getLastSequenceId() must be 2.
    for (int i = 0; i < 3; i++) {
        Message msg = MessageBuilder().setContent("content").build();
        producer.sendAsync(msg, nullptr);
    }
    ASSERT_EQ(ResultOk, producer.flush());
    ASSERT_EQ(producer.getLastSequenceId(), 2) << "After 3 messages, last sequence id should be 2";

    // Send 2 more (total 5), flush. Sequence ids for these are [3, 4], so getLastSequenceId() must be 4.
    for (int i = 0; i < 2; i++) {
        Message msg = MessageBuilder().setContent("content").build();
        producer.sendAsync(msg, nullptr);
    }
    ASSERT_EQ(ResultOk, producer.flush());
    ASSERT_EQ(producer.getLastSequenceId(), 4) << "After 5 messages total, last sequence id should be 4";

    producer.close();
    client.close();
}

TEST_P(ProducerTest, testFlushBatch) {
    Client client(serviceUrl);

    auto partitioned = GetParam();
    const auto topicName = std::string("testFlushNoBatch") +
                           (partitioned ? "partitioned-" : "-no-partitioned-") +
                           std::to_string(time(nullptr));

    if (partitioned) {
        // call admin api to make it partitioned
        std::string url = adminUrl + "admin/v2/persistent/public/default/" + topicName + "/partitions";
        int res = makePutRequest(url, "5");
        LOG_INFO("res = " << res);
        ASSERT_FALSE(res != 204 && res != 409);
    }

    ProducerConfiguration producerConfiguration;
    producerConfiguration.setBatchingEnabled(true);
    producerConfiguration.setBatchingMaxMessages(10);
    producerConfiguration.setBatchingMaxPublishDelayMs(1000);
    producerConfiguration.setBatchingMaxAllowedSizeInBytes(4 * 1024 * 1024);

    // test all messages in batch has been sent
    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration, producer));

    std::atomic_int needCallBack(100);
    auto cb = [&needCallBack](Result code, const MessageId& msgId) {
        ASSERT_EQ(code, ResultOk);
        needCallBack.fetch_sub(1);
    };

    for (int i = 0; i < 100; ++i) {
        Message msg = MessageBuilder().setContent("content").build();
        producer.sendAsync(msg, cb);
    }

    auto assertFlushCallbackOnce = [&producer] {
        Latch latch{1};
        std::mutex mutex;
        std::vector<Result> results;
        producer.flushAsync([&](Result result) {
            {
                std::lock_guard<std::mutex> lock{mutex};
                results.emplace_back(result);
            }
            latch.countdown();
        });
        latch.wait();
        std::lock_guard<std::mutex> lock{mutex};
        ASSERT_EQ(results, (std::vector<Result>{ResultOk}));
    };

    assertFlushCallbackOnce();
    ASSERT_EQ(needCallBack.load(), 0);
    producer.close();

    // test remain messages in batch not send
    ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration, producer));

    std::atomic_int needCallBack2(105);
    auto cb2 = [&needCallBack2](Result code, const MessageId& msgId) {
        ASSERT_EQ(code, ResultOk);
        needCallBack2.fetch_sub(1);
    };

    for (int i = 0; i < 105; ++i) {
        Message msg = MessageBuilder().setContent("content").build();
        producer.sendAsync(msg, cb2);
    }

    assertFlushCallbackOnce();
    ASSERT_EQ(needCallBack2.load(), 0);
    producer.close();

    client.close();
}

TEST(ProducerTest, testCloseSubProducerWhenFail) {
    Client client(serviceUrl);

    std::string ns = "test-close-sub-producer-when-fail";
    std::string localName = std::string("testCloseSubProducerWhenFail") + std::to_string(time(nullptr));
    std::string topicName = "persistent://public/" + ns + '/' + localName;
    const int maxProducersPerTopic = 10;
    const int partitionNum = 5;

    // call admin api to create namespace with max prodcuer limit
    std::string url = adminUrl + "admin/v2/namespaces/public/" + ns;
    int res =
        makePutRequest(url, "{\"max_producers_per_topic\": " + std::to_string(maxProducersPerTopic) + "}");
    ASSERT_TRUE(res == 204 || res == 409) << "res:" << res;

    // call admin api to create partitioned topic
    res = makePutRequest(adminUrl + "admin/v2/persistent/public/" + ns + "/" + localName + "/partitions",
                         std::to_string(partitionNum));
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    ProducerConfiguration producerConfiguration;
    producerConfiguration.setBatchingEnabled(false);

    // create producers for partition-0 up to max producer limit
    std::vector<Producer> producers;
    for (int i = 0; i < maxProducersPerTopic; ++i) {
        Producer producer;
        ASSERT_EQ(ResultOk,
                  client.createProducer(topicName + "-partition-0", producerConfiguration, producer));
        producers.push_back(producer);
    }

    // create partitioned producer, should fail because partition-0 already reach max producer limit
    for (int i = 0; i < maxProducersPerTopic; ++i) {
        Producer producer;
        ASSERT_EQ(ResultProducerBusy, client.createProducer(topicName, producer));
    }

    std::this_thread::sleep_for(std::chrono::seconds(1));

    // create producer for partition-1, should succeed
    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topicName + "-partition-1", producerConfiguration, producer));
    producers.push_back(producer);

    for (auto& producer : producers) {
        producer.close();
    }
    client.close();
}

TEST(ProducerTest, testCloseProducerBeforeCreated) {
    Client client(serviceUrl);

    std::string ns = "test-close-producer-before-created";
    std::string localName = std::string("testCloseProducerBeforeCreated") + std::to_string(time(nullptr));
    std::string topicName = "persistent://public/" + ns + '/' + localName;
    const int maxProducersPerTopic = 10;
    const int partitionNum = 5;

    // call admin api to create namespace with max prodcuer limit
    std::string url = adminUrl + "admin/v2/namespaces/public/" + ns;
    int res =
        makePutRequest(url, "{\"max_producers_per_topic\": " + std::to_string(maxProducersPerTopic) + "}");
    ASSERT_TRUE(res == 204 || res == 409) << "res:" << res;

    // call admin api to create partitioned topic
    res = makePutRequest(adminUrl + "admin/v2/persistent/public/" + ns + "/" + localName + "/partitions",
                         std::to_string(partitionNum));
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    ProducerConfiguration producerConfiguration;
    producerConfiguration.setLazyStartPartitionedProducers(true);
    producerConfiguration.setPartitionsRoutingMode(ProducerConfiguration::RoundRobinDistribution);
    producerConfiguration.setBatchingEnabled(false);

    Message msg = MessageBuilder().setContent("test").build();
    for (int i = 0; i < maxProducersPerTopic * 100; ++i) {
        Producer producer;
        ASSERT_EQ(ResultOk, client.createProducer(topicName, producerConfiguration, producer));
        // trigger lazy producer creation
        for (int j = 0; j < partitionNum; ++j) {
            producer.sendAsync(msg, [](pulsar::Result, const pulsar::MessageId&) {});
        }
        producer.close();
    }

    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer(topicName, {}, producer));
    producer.close();

    client.close();
}

TEST(ProducerTest, testNoDeadlockWhenClosingPartitionedProducerAfterPartitionsUpdate) {
    const std::string topic = "public/default/partition-test" + std::to_string(time(nullptr));
    std::string topicOperateUrl = adminUrl + "admin/v2/persistent/" + topic + "/partitions";

    int res = makePutRequest(topicOperateUrl, "2");
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    ClientConfiguration clientConf;
    clientConf.setPartititionsUpdateInterval(1);
    Client client(serviceUrl, clientConf);
    ProducerConfiguration conf;
    Producer producer;
    client.createProducer(topic, conf, producer);
    PartitionedProducerImpl& partitionedProducer = PulsarFriend::getPartitionedProducerImpl(producer);

    // TODO: Replace by producer interceptor to reproduce the issue then we can remove
    // PulsarFriend::updatePartitions
    PulsarFriend::updatePartitions(partitionedProducer, 3);

    producer.close();
    client.close();
}

TEST(ProducerTest, testReconnectMultiConnectionsPerBroker) {
    ClientConfiguration conf;
    conf.setConnectionsPerBroker(10);

    Client client(serviceUrl, conf);
    Producer producer;
    ASSERT_EQ(ResultOk, client.createProducer("producer-test-reconnect-twice", producer));

    for (int i = 0; i < 5; i++) {
        ASSERT_TRUE(PulsarFriend::reconnect(producer)) << "i: " << i;
    }

    client.close();
}

TEST(ProducerTest, testFailedToCreateNewPartitionProducer) {
    const std::string topic =
        "public/default/testFailedToCreateNewPartitionProducer" + std::to_string(time(nullptr));
    std::string topicOperateUrl = adminUrl + "admin/v2/persistent/" + topic + "/partitions";

    int res = makePutRequest(topicOperateUrl, "2");
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    ClientConfiguration clientConf;
    clientConf.setPartititionsUpdateInterval(1);
    Client client(serviceUrl, clientConf);
    ProducerConfiguration conf;
    Producer producer;
    client.createProducer(topic, conf, producer);
    ASSERT_TRUE(waitUntil(std::chrono::seconds(1), [&producer]() -> bool { return producer.isConnected(); }));

    PartitionedProducerImpl& partitionedProducer = PulsarFriend::getPartitionedProducerImpl(producer);
    PulsarFriend::updatePartitions(partitionedProducer, 3);
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    auto& newProducer = PulsarFriend::getInternalProducerImpl(producer, 2);
    ASSERT_FALSE(newProducer.isConnected());  // should fail with topic not found

    res = makePostRequest(topicOperateUrl, "3");
    ASSERT_TRUE(res == 204 || res == 409) << "res: " << res;

    ASSERT_TRUE(
        waitUntil(std::chrono::seconds(5), [&newProducer]() -> bool { return newProducer.isConnected(); }));

    producer.close();
    client.close();
}

TEST(ProducerTest, testLargeProperties) {
    const std::string topic = "producer-test-large-properties-" + std::to_string(time(nullptr));
    Client client(serviceUrl);
    Producer producer;
    ProducerConfiguration conf;
    conf.setBatchingEnabled(false);
    ASSERT_EQ(ResultOk, client.createProducer(topic, conf, producer));
    Consumer consumer;
    ASSERT_EQ(ResultOk, client.subscribe(topic, "sub", consumer));

    MessageBuilder::StringMap properties;
    constexpr int propertyCount = 20000;
    auto builder = MessageBuilder().setContent("msg");
    for (int i = 0; i < propertyCount; i++) {
        builder.setProperty("key" + std::to_string(i), "value-" + std::to_string(i));
    }

    // ASSERT_EQ(ResultOk,
    // producer.send(MessageBuilder().setContent("msg").setProperties(properties).build()));
    ASSERT_EQ(ResultOk, producer.send(builder.build()));

    Message msg;
    ASSERT_EQ(ResultOk, consumer.receive(msg, 3000));
    ASSERT_EQ(msg.getProperties().size(), propertyCount);
    for (int i = 0; i < propertyCount; i++) {
        auto it = msg.getProperties().find("key" + std::to_string(i));
        ASSERT_NE(it, msg.getProperties().cend());
    }
    client.close();
}

INSTANTIATE_TEST_CASE_P(Pulsar, ProducerTest, ::testing::Values(true, false));
