/*
 *  Copyright (c) 2023, The OpenThread Authors.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

#include <openthread/config.h>

#include "test_platform.h"
#include "test_util.hpp"

#include <openthread/dataset_ftd.h>
#include <openthread/dns_client.h>
#include <openthread/srp_client.h>
#include <openthread/srp_server.h>
#include <openthread/thread.h>

#include "common/arg_macros.hpp"
#include "common/array.hpp"
#include "common/clearable.hpp"
#include "common/string.hpp"
#include "common/time.hpp"
#include "instance/instance.hpp"

#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE &&                 \
    OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE && OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE && \
    OPENTHREAD_CONFIG_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE &&                                   \
    !OPENTHREAD_CONFIG_TIME_SYNC_ENABLE && !OPENTHREAD_PLATFORM_POSIX
#define ENABLE_DNS_TEST 1
#else
#define ENABLE_DNS_TEST 0
#endif

#if ENABLE_DNS_TEST

using namespace ot;

// Logs a message and adds current time (sNow) as "<hours>:<min>:<secs>.<msec>"
#define Log(...)                                                                                          \
    printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 36000000), (sNow / 60000) % 60, \
           (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__))

static constexpr uint16_t kMaxRaSize = 800;

static ot::Instance *sInstance;

static uint32_t sNow = 0;
static uint32_t sAlarmTime;
static bool     sAlarmOn = false;

static otRadioFrame sRadioTxFrame;
static uint8_t      sRadioTxFramePsdu[OT_RADIO_FRAME_MAX_SIZE];
static bool         sRadioTxOngoing = false;

//----------------------------------------------------------------------------------------------------------------------
// Function prototypes

void ProcessRadioTxAndTasklets(void);
void AdvanceTime(uint32_t aDuration);

//----------------------------------------------------------------------------------------------------------------------
// `otPlatRadio`

extern "C" {

otRadioCaps otPlatRadioGetCaps(otInstance *) { return OT_RADIO_CAPS_ACK_TIMEOUT | OT_RADIO_CAPS_CSMA_BACKOFF; }

otError otPlatRadioTransmit(otInstance *, otRadioFrame *)
{
    sRadioTxOngoing = true;

    return OT_ERROR_NONE;
}

otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { return &sRadioTxFrame; }

//----------------------------------------------------------------------------------------------------------------------
// `otPlatAlaram`

void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; }

void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
{
    sAlarmOn   = true;
    sAlarmTime = aT0 + aDt;
}

uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }

//----------------------------------------------------------------------------------------------------------------------

Array<void *, 500> sHeapAllocatedPtrs;

#if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
void *otPlatCAlloc(size_t aNum, size_t aSize)
{
    void *ptr = calloc(aNum, aSize);

    SuccessOrQuit(sHeapAllocatedPtrs.PushBack(ptr));

    return ptr;
}

void otPlatFree(void *aPtr)
{
    if (aPtr != nullptr)
    {
        void **entry = sHeapAllocatedPtrs.Find(aPtr);

        VerifyOrQuit(entry != nullptr, "A heap allocated item is freed twice");
        sHeapAllocatedPtrs.Remove(*entry);
    }

    free(aPtr);
}
#endif

#if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED
#if OPENTHREAD_CONFIG_LOG_INSTANCE_AWARE_API_ENABLE
void otPlatLogOutput(otInstance *, otLogLevel, const char *aLogLine) { printf("   %s\n", aLogLine); }
#else
void otPlatLog(otLogLevel, otLogRegion, const char *aFormat, ...)
{
    va_list args;

    printf("   ");
    va_start(args, aFormat);
    vprintf(aFormat, args);
    va_end(args);
    printf("\n");
}
#endif
#endif

} // extern "C"

//---------------------------------------------------------------------------------------------------------------------

void ProcessRadioTxAndTasklets(void)
{
    do
    {
        if (sRadioTxOngoing)
        {
            sRadioTxOngoing = false;
            otPlatRadioTxStarted(sInstance, &sRadioTxFrame);
            otPlatRadioTxDone(sInstance, &sRadioTxFrame, nullptr, OT_ERROR_NONE);
        }

        otTaskletsProcess(sInstance);
    } while (otTaskletsArePending(sInstance));
}

void AdvanceTime(uint32_t aDuration)
{
    uint32_t time = sNow + aDuration;

    Log("AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000);

    while (TimeMilli(sAlarmTime) <= TimeMilli(time))
    {
        ProcessRadioTxAndTasklets();
        sNow = sAlarmTime;
        otPlatAlarmMilliFired(sInstance);
    }

    ProcessRadioTxAndTasklets();
    sNow = time;
}

void InitTest(void)
{
    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Initialize OT instance.

    sNow      = 0;
    sAlarmOn  = false;
    sInstance = static_cast<Instance *>(testInitInstance());

    memset(&sRadioTxFrame, 0, sizeof(sRadioTxFrame));
    sRadioTxFrame.mPsdu = sRadioTxFramePsdu;
    sRadioTxOngoing     = false;

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Initialize Border Router and start Thread operation.

    otOperationalDataset     dataset;
    otOperationalDatasetTlvs datasetTlvs;

    SuccessOrQuit(otDatasetCreateNewNetwork(sInstance, &dataset));
    otDatasetConvertToTlvs(&dataset, &datasetTlvs);
    SuccessOrQuit(otDatasetSetActiveTlvs(sInstance, &datasetTlvs));

    SuccessOrQuit(otIp6SetEnabled(sInstance, true));
    SuccessOrQuit(otThreadSetEnabled(sInstance, true));

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Ensure device starts as leader.

    AdvanceTime(10000);

    VerifyOrQuit(otThreadGetDeviceRole(sInstance) == OT_DEVICE_ROLE_LEADER);
}

void FinalizeTest(void)
{
    AdvanceTime(30 * 1000);

    SuccessOrQuit(otIp6SetEnabled(sInstance, false));
    SuccessOrQuit(otThreadSetEnabled(sInstance, false));
    // Make sure there is no message/buffer leak
    VerifyOrQuit(sInstance->Get<MessagePool>().GetFreeBufferCount() ==
                 sInstance->Get<MessagePool>().GetTotalBufferCount());
    SuccessOrQuit(otInstanceErasePersistentInfo(sInstance));
    testFreeInstance(sInstance);
}

//---------------------------------------------------------------------------------------------------------------------

static const char kHostName[]        = "elden";
static const char kHostFullName[]    = "elden.default.service.arpa.";
static const char kNonExistingName[] = "noname.nodomain.";

static const char kService1Name[]      = "_srv._udp";
static const char kService1FullName[]  = "_srv._udp.default.service.arpa.";
static const char kInstance1Label[]    = "srv-instance";
static const char kInstance1FullName[] = "srv-instance._srv._udp.default.service.arpa.";

static const char kService2Name[]            = "_game._udp";
static const char kService2FullName[]        = "_game._udp.default.service.arpa.";
static const char kService2SubTypeFullName[] = "_best._sub._game._udp.default.service.arpa.";
static const char kInstance2Label[]          = "last-ninja";
static const char kInstance2FullName[]       = "last-ninja._game._udp.default.service.arpa.";

void PrepareService1(Srp::Client::Service &aService)
{
    static const char          kSub1[]       = "_sub1";
    static const char          kSub2[]       = "_V1234567";
    static const char          kSub3[]       = "_XYZWS";
    static const char         *kSubLabels[]  = {kSub1, kSub2, kSub3, nullptr};
    static const char          kTxtKey1[]    = "ABCD";
    static const uint8_t       kTxtValue1[]  = {'a', '0'};
    static const char          kTxtKey2[]    = "Z0";
    static const uint8_t       kTxtValue2[]  = {'1', '2', '3'};
    static const char          kTxtKey3[]    = "D";
    static const uint8_t       kTxtValue3[]  = {0};
    static const otDnsTxtEntry kTxtEntries[] = {
        {kTxtKey1, kTxtValue1, sizeof(kTxtValue1)},
        {kTxtKey2, kTxtValue2, sizeof(kTxtValue2)},
        {kTxtKey3, kTxtValue3, sizeof(kTxtValue3)},
    };

    memset(&aService, 0, sizeof(aService));
    aService.mName          = kService1Name;
    aService.mInstanceName  = kInstance1Label;
    aService.mSubTypeLabels = kSubLabels;
    aService.mTxtEntries    = kTxtEntries;
    aService.mNumTxtEntries = 3;
    aService.mPort          = 777;
    aService.mWeight        = 1;
    aService.mPriority      = 2;
}

void PrepareService2(Srp::Client::Service &aService)
{
    static const char  kSub4[]       = "_best";
    static const char *kSubLabels2[] = {kSub4, nullptr};

    memset(&aService, 0, sizeof(aService));
    aService.mName          = kService2Name;
    aService.mInstanceName  = kInstance2Label;
    aService.mSubTypeLabels = kSubLabels2;
    aService.mTxtEntries    = nullptr;
    aService.mNumTxtEntries = 0;
    aService.mPort          = 555;
    aService.mWeight        = 0;
    aService.mPriority      = 3;
}

void ValidateHost(Srp::Server &aServer, const char *aHostName)
{
    // Validate that only a host with `aHostName` is
    // registered on SRP server.

    const Srp::Server::Host *host;
    const char              *name;

    Log("ValidateHost()");

    host = aServer.GetNextHost(nullptr);
    VerifyOrQuit(host != nullptr);

    name = host->GetFullName();
    Log("Hostname: %s", name);

    VerifyOrQuit(StringStartsWith(name, aHostName, kStringCaseInsensitiveMatch));
    VerifyOrQuit(name[strlen(aHostName)] == '.');

    // Only one host on server
    VerifyOrQuit(aServer.GetNextHost(host) == nullptr);
}

//---------------------------------------------------------------------------------------------------------------------

void LogServiceInfo(const Dns::Client::ServiceInfo &aInfo)
{
    Log("   TTL: %u", aInfo.mTtl);
    Log("   Port: %u", aInfo.mPort);
    Log("   Weight: %u", aInfo.mWeight);
    Log("   HostName: %s", aInfo.mHostNameBuffer);
    Log("   HostAddr: %s", AsCoreType(&aInfo.mHostAddress).ToString().AsCString());
    Log("   TxtDataLength: %u", aInfo.mTxtDataSize);
    Log("   TxtDataTTL: %u", aInfo.mTxtDataTtl);
}

const char *ServiceModeToString(Dns::Client::QueryConfig::ServiceMode aMode)
{
    static const char *const kServiceModeStrings[] = {
        "unspec",      // kServiceModeUnspecified     (0)
        "srv",         // kServiceModeSrv             (1)
        "txt",         // kServiceModeTxt             (2)
        "srv_txt",     // kServiceModeSrvTxt          (3)
        "srv_txt_sep", // kServiceModeSrvTxtSeparate  (4)
        "srv_txt_opt", // kServiceModeSrvTxtOptimize  (5)
    };

    static_assert(Dns::Client::QueryConfig::kServiceModeUnspecified == 0, "Unspecified value is incorrect");
    static_assert(Dns::Client::QueryConfig::kServiceModeSrv == 1, "Srv value is incorrect");
    static_assert(Dns::Client::QueryConfig::kServiceModeTxt == 2, "Txt value is incorrect");
    static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxt == 3, "SrvTxt value is incorrect");
    static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate == 4, "SrvTxtSeparate value is incorrect");
    static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize == 5, "SrvTxtOptimize value is incorrect");

    return kServiceModeStrings[aMode];
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static constexpr uint16_t kMaxAddresses = 10;

struct AddressInfo
{
    void Reset(void) { ClearAllBytes(*this); }

    uint16_t          mCallbackCount;
    Error             mError;
    Dns::Name::Buffer mHostName;
    Ip6::Address      mHostAddresses[kMaxAddresses];
    uint8_t           mNumHostAddresses;
};

static AddressInfo sAddressInfo;

void AddressCallback(otError aError, const otDnsAddressResponse *aResponse, void *aContext)
{
    const Dns::Client::AddressResponse &response = AsCoreType(aResponse);

    Log("AddressCallback");
    Log("   Error: %s", ErrorToString(aError));

    VerifyOrQuit(aContext == sInstance);

    sAddressInfo.mCallbackCount++;
    sAddressInfo.mError = aError;

    SuccessOrExit(aError);

    SuccessOrQuit(response.GetHostName(sAddressInfo.mHostName, sizeof(sAddressInfo.mHostName)));
    Log("   HostName: %s", sAddressInfo.mHostName);

    for (uint16_t index = 0;; index++)
    {
        Error    error;
        uint32_t ttl;

        VerifyOrQuit(index < kMaxAddresses);

        error = response.GetAddress(index, sAddressInfo.mHostAddresses[index], ttl);

        if (error == kErrorNotFound)
        {
            sAddressInfo.mNumHostAddresses = index;
            break;
        }

        SuccessOrQuit(error);

        Log("  %2u) %s ttl:%lu", index + 1, sAddressInfo.mHostAddresses[index].ToString().AsCString(), ToUlong(ttl));
    }

exit:
    return;
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct BrowseInfo
{
    void Reset(void) { mCallbackCount = 0; }

    uint16_t          mCallbackCount;
    Error             mError;
    Dns::Name::Buffer mServiceName;
    uint16_t          mNumInstances;
};

static BrowseInfo sBrowseInfo;

void BrowseCallback(otError aError, const otDnsBrowseResponse *aResponse, void *aContext)
{
    const Dns::Client::BrowseResponse &response = AsCoreType(aResponse);

    Log("BrowseCallback");
    Log("   Error: %s", ErrorToString(aError));

    VerifyOrQuit(aContext == sInstance);

    sBrowseInfo.mCallbackCount++;
    sBrowseInfo.mError = aError;

    SuccessOrExit(aError);

    SuccessOrQuit(response.GetServiceName(sBrowseInfo.mServiceName, sizeof(sBrowseInfo.mServiceName)));
    Log("   ServiceName: %s", sBrowseInfo.mServiceName);

    for (uint16_t index = 0;; index++)
    {
        Dns::Name::LabelBuffer instLabel;
        Error                  error;

        error = response.GetServiceInstance(index, instLabel, sizeof(instLabel));

        if (error == kErrorNotFound)
        {
            sBrowseInfo.mNumInstances = index;
            break;
        }

        SuccessOrQuit(error);

        Log("  %2u) %s", index + 1, instLabel);
    }

exit:
    return;
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static constexpr uint8_t  kMaxHostAddresses = 10;
static constexpr uint16_t kMaxTxtBuffer     = 256;

struct ResolveServiceInfo
{
    void Reset(void)
    {
        memset(this, 0, sizeof(*this));
        mInfo.mHostNameBuffer     = mNameBuffer;
        mInfo.mHostNameBufferSize = sizeof(mNameBuffer);
        mInfo.mTxtData            = mTxtBuffer;
        mInfo.mTxtDataSize        = sizeof(mTxtBuffer);
    };

    uint16_t                 mCallbackCount;
    Error                    mError;
    Dns::Client::ServiceInfo mInfo;
    Dns::Name::Buffer        mNameBuffer;
    uint8_t                  mTxtBuffer[kMaxTxtBuffer];
    Ip6::Address             mHostAddresses[kMaxHostAddresses];
    uint8_t                  mNumHostAddresses;
};

static ResolveServiceInfo sResolveServiceInfo;

void ServiceCallback(otError aError, const otDnsServiceResponse *aResponse, void *aContext)
{
    const Dns::Client::ServiceResponse &response = AsCoreType(aResponse);
    Dns::Name::LabelBuffer              instLabel;
    Dns::Name::Buffer                   serviceName;

    Log("ServiceCallback");
    Log("   Error: %s", ErrorToString(aError));

    VerifyOrQuit(aContext == sInstance);

    SuccessOrQuit(response.GetServiceName(instLabel, sizeof(instLabel), serviceName, sizeof(serviceName)));
    Log("   InstLabel: %s", instLabel);
    Log("   ServiceName: %s", serviceName);

    sResolveServiceInfo.mCallbackCount++;
    sResolveServiceInfo.mError = aError;

    SuccessOrExit(aError);
    SuccessOrQuit(response.GetServiceInfo(sResolveServiceInfo.mInfo));

    for (uint8_t index = 0; index < kMaxHostAddresses; index++)
    {
        Error    error;
        uint32_t ttl;

        error = response.GetHostAddress(sResolveServiceInfo.mInfo.mHostNameBuffer, index,
                                        sResolveServiceInfo.mHostAddresses[index], ttl);

        if (error == kErrorNotFound)
        {
            sResolveServiceInfo.mNumHostAddresses = index;
            break;
        }

        SuccessOrQuit(error);
    }

    LogServiceInfo(sResolveServiceInfo.mInfo);
    Log("   NumHostAddresses: %u", sResolveServiceInfo.mNumHostAddresses);

    for (uint8_t index = 0; index < sResolveServiceInfo.mNumHostAddresses; index++)
    {
        Log("      %s", sResolveServiceInfo.mHostAddresses[index].ToString().AsCString());
    }

exit:
    return;
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static constexpr uint16_t kMaxRecords = 16;

struct QueryRecordInfo
{
    struct Record : public Dns::Client::RecordInfo
    {
        static constexpr uint16_t kMaxRecordDataSize = 500;

        void Init(void)
        {
            ClearAllBytes(*this);
            mNameBuffer     = mName;
            mNameBufferSize = sizeof(mName);
            mDataBuffer     = mData;
            mDataBufferSize = sizeof(mData);
        }

        uint8_t mData[kMaxRecordDataSize];
        char    mName[Dns::Name::kMaxNameSize];
    };

    void Reset(void) { memset(this, 0, sizeof(*this)); };

    uint16_t mCallbackCount;
    Error    mError;
    char     mQueryName[Dns::Name::kMaxNameSize];
    Record   mRecords[kMaxRecords];
    uint16_t mNumRecords;
};

static QueryRecordInfo sQueryRecordInfo;

void RecordCallback(otError aError, const otDnsRecordResponse *aResponse, void *aContext)
{
    static constexpr uint16_t kMaxStringSize = 400;

    const Dns::Client::RecordResponse &response = AsCoreType(aResponse);

    Log("RecordCallback");
    Log("   Error: %s", ErrorToString(aError));

    VerifyOrQuit(aContext == sInstance);

    sQueryRecordInfo.mCallbackCount++;
    sQueryRecordInfo.mError      = aError;
    sQueryRecordInfo.mNumRecords = 0;

    SuccessOrExit(aError);

    SuccessOrQuit(response.GetQueryName(sQueryRecordInfo.mQueryName, sizeof(sQueryRecordInfo.mQueryName)));
    Log("   QueryName: %s", sQueryRecordInfo.mQueryName);

    for (uint8_t index = 0; index < kMaxRecords; index++)
    {
        Error    error;
        uint32_t ttl;

        sQueryRecordInfo.mRecords[index].Init();

        error = response.GetRecordInfo(index, sQueryRecordInfo.mRecords[index]);

        if (error == kErrorNotFound)
        {
            sQueryRecordInfo.mNumRecords = index;
            break;
        }

        SuccessOrQuit(error);
    }

    Log("   NumRecords: %u", sQueryRecordInfo.mNumRecords);

    for (uint16_t index = 0; index < sQueryRecordInfo.mNumRecords; index++)
    {
        const QueryRecordInfo::Record &record = sQueryRecordInfo.mRecords[index];
        String<kMaxStringSize>         string;
        uint16_t                       rrType;

        string.AppendHexBytes(record.mDataBuffer, record.mDataBufferSize);
        rrType = record.mRecordType;

        Log("   Record %u", index);
        Log("      Name: %s", record.mNameBuffer);
        Log("      Type: %u (%s)", rrType, Dns::ResourceRecord::TypeToString(rrType).AsCString());
        Log("      Data: %s", string.AsCString());
    }

exit:
    return;
}

void ValidateSrvRecordData(const QueryRecordInfo::Record &aRecord, const char *aFullHostName)
{
    // Validate that the read SRV record data contains
    // the uncompressed host name.

    Message *data   = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
    uint16_t offset = sizeof(Dns::SrvRecord) - sizeof(Dns::ResourceRecord);

    VerifyOrQuit(data != nullptr);
    SuccessOrQuit(data->AppendBytes(aRecord.mDataBuffer, aRecord.mRecordLength));

    SuccessOrQuit(Dns::Name::CompareName(*data, offset, aFullHostName));
    VerifyOrQuit(offset == data->GetLength());

    data->Free();
}

void ValidatePtrRecordData(const QueryRecordInfo::Record &aRecord, const char *aFullInstanceName)
{
    // Validate that the read PTR record data contains
    // the uncompressed service instance name.

    Message *data   = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
    uint16_t offset = 0;

    VerifyOrQuit(data != nullptr);
    SuccessOrQuit(data->AppendBytes(aRecord.mDataBuffer, aRecord.mRecordLength));

    SuccessOrQuit(Dns::Name::CompareName(*data, offset, aFullInstanceName));
    VerifyOrQuit(offset == data->GetLength());

    data->Free();
}

void ValidateSoaRecordData(const QueryRecordInfo::Record &aRecord, const char *aServerName)
{
    static constexpr uint32_t kSoaSerial  = 0;
    static constexpr uint32_t kSoaRefresh = 7200;
    static constexpr uint32_t kSoaRetry   = 3600;
    static constexpr uint32_t kSoaExpire  = 86400;
    static constexpr uint32_t kSoaMinimum = 10;

    uint16_t          offset;
    Message          *message;
    Dns::Name::Buffer name;

    VerifyOrQuit(StringMatch(aRecord.mNameBuffer, "default.service.arpa."));
    VerifyOrQuit(aRecord.mRecordType == Dns::ResourceRecord::kTypeSoa);
    VerifyOrQuit(aRecord.mTtl == 7200);

    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
    VerifyOrQuit(message != nullptr);

    VerifyOrQuit(aRecord.mRecordLength == aRecord.mDataBufferSize);
    SuccessOrQuit(message->AppendBytes(aRecord.mDataBuffer, aRecord.mDataBufferSize));

    // Validate the SOA record data.

    offset = 0;

    // MNAME field:
    SuccessOrQuit(Dns::Name::ReadName(*message, offset, name));
    VerifyOrQuit(StringMatch(name, aServerName, kStringCaseInsensitiveMatch));

    // RNAME: must be "postmaster.default.service.arpa."
    SuccessOrQuit(Dns::Name::ReadName(*message, offset, name));
    SuccessOrQuit(Dns::Name::StripName(name, "default.service.arpa."));
    VerifyOrQuit(StringMatch(name, "postmaster"));

    // SERIAL
    VerifyOrQuit(message->Compare<uint32_t>(offset, BigEndian::HostSwap32(kSoaSerial)));
    offset += sizeof(uint32_t);

    // REFRESH
    VerifyOrQuit(message->Compare<uint32_t>(offset, BigEndian::HostSwap32(kSoaRefresh)));
    offset += sizeof(uint32_t);

    // RETRY
    VerifyOrQuit(message->Compare<uint32_t>(offset, BigEndian::HostSwap32(kSoaRetry)));
    offset += sizeof(uint32_t);

    // EXPIRE
    VerifyOrQuit(message->Compare<uint32_t>(offset, BigEndian::HostSwap32(kSoaExpire)));
    offset += sizeof(uint32_t);

    // MINIMUM
    VerifyOrQuit(message->Compare<uint32_t>(offset, BigEndian::HostSwap32(kSoaMinimum)));
    offset += sizeof(uint32_t);

    VerifyOrQuit(offset == message->GetLength());

    message->Free();
}

void ValidateNsRecordData(const QueryRecordInfo::Record &aRecord, const char *aServerName)
{
    uint16_t          offset;
    Message          *message;
    Dns::Name::Buffer name;

    VerifyOrQuit(StringMatch(aRecord.mNameBuffer, "default.service.arpa."));
    VerifyOrQuit(aRecord.mRecordType == Dns::ResourceRecord::kTypeNs);
    VerifyOrQuit(aRecord.mTtl == 7200);

    message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
    VerifyOrQuit(message != nullptr);

    VerifyOrQuit(aRecord.mRecordLength == aRecord.mDataBufferSize);
    SuccessOrQuit(message->AppendBytes(aRecord.mDataBuffer, aRecord.mDataBufferSize));

    // Validate the NS data

    offset = 0;

    SuccessOrQuit(Dns::Name::ReadName(*message, offset, name));
    VerifyOrQuit(StringMatch(name, aServerName, kStringCaseInsensitiveMatch));

    VerifyOrQuit(offset == message->GetLength());

    message->Free();
}

//----------------------------------------------------------------------------------------------------------------------

void TestDnsClient(void)
{
    static constexpr uint8_t kNumAddresses = 2;

    static const char *const kAddresses[kNumAddresses] = {"2001::beef:cafe", "fd00:1234:5678:9abc::1"};

    const Dns::Client::QueryConfig::ServiceMode kServiceModes[] = {
        Dns::Client::QueryConfig::kServiceModeSrv,
        Dns::Client::QueryConfig::kServiceModeTxt,
        Dns::Client::QueryConfig::kServiceModeSrvTxt,
        Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate,
        Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize,
    };

    Array<Ip6::Address, kNumAddresses>      addresses;
    NetworkData::ExternalRouteConfig        routeConfig;
    Srp::Server                            *srpServer;
    Srp::Client                            *srpClient;
    Srp::Client::Service                    service1;
    Srp::Client::Service                    service2;
    Dns::Client                            *dnsClient;
    Dns::Client::QueryConfig                queryConfig;
    Dns::ServiceDiscovery::Server          *dnsServer;
    Dns::ServiceDiscovery::Server::Counters oldServerCounters;
    Dns::ServiceDiscovery::Server::Counters newServerCounters;
    uint16_t                                heapAllocations;

    Log("--------------------------------------------------------------------------------------------");
    Log("TestDnsClient");

    InitTest();

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("Add a route prefix (with NAT64 flag) to network data");

    routeConfig.Clear();
    SuccessOrQuit(AsCoreType(&routeConfig.mPrefix.mPrefix).FromString("64:ff9b::"));
    routeConfig.mPrefix.mLength = 96;
    routeConfig.mPreference     = NetworkData::kRoutePreferenceMedium;
    routeConfig.mNat64          = true;
    routeConfig.mStable         = true;

    SuccessOrQuit(otBorderRouterAddRoute(sInstance, &routeConfig));
    SuccessOrQuit(otBorderRouterRegister(sInstance));

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("Add addresses on Thread netif");

    for (const char *addrString : kAddresses)
    {
        otNetifAddress netifAddr;

        memset(&netifAddr, 0, sizeof(netifAddr));
        SuccessOrQuit(AsCoreType(&netifAddr.mAddress).FromString(addrString));
        netifAddr.mPrefixLength  = 64;
        netifAddr.mAddressOrigin = OT_ADDRESS_ORIGIN_MANUAL;
        netifAddr.mPreferred     = true;
        netifAddr.mValid         = true;
        SuccessOrQuit(otIp6AddUnicastAddress(sInstance, &netifAddr));

        SuccessOrQuit(addresses.PushBack(AsCoreType(&netifAddr.mAddress)));
    }

    srpServer = &sInstance->Get<Srp::Server>();
    srpClient = &sInstance->Get<Srp::Client>();
    dnsClient = &sInstance->Get<Dns::Client>();
    dnsServer = &sInstance->Get<Dns::ServiceDiscovery::Server>();

    heapAllocations = sHeapAllocatedPtrs.GetLength();

    PrepareService1(service1);
    PrepareService2(service2);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Start SRP server.

    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);

    srpServer->SetEnabled(true);
    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);

    AdvanceTime(10000);
    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Start SRP client.

    srpClient->EnableAutoStartMode(nullptr, nullptr);
    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());

    AdvanceTime(2000);
    VerifyOrQuit(srpClient->IsRunning());

    SuccessOrQuit(srpClient->SetHostName(kHostName));
    SuccessOrQuit(srpClient->EnableAutoHostAddress());

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Register two services on SRP.

    SuccessOrQuit(srpClient->AddService(service1));
    SuccessOrQuit(srpClient->AddService(service2));

    AdvanceTime(2 * 1000);

    VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered);
    VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered);
    ValidateHost(*srpServer, kHostName);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Check DNS Client's default config

    VerifyOrQuit(dnsClient->GetDefaultConfig().GetServiceMode() ==
                 Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `ResolveAddress()`

    sAddressInfo.Reset();
    Log("ResolveAddress(%s)", kHostFullName);
    SuccessOrQuit(dnsClient->ResolveAddress(kHostFullName, AddressCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sAddressInfo.mCallbackCount == 1);
    SuccessOrQuit(sAddressInfo.mError);
    VerifyOrQuit(sAddressInfo.mNumHostAddresses == GetArrayLength(kAddresses));

    for (uint8_t index = 0; index < sAddressInfo.mNumHostAddresses; index++)
    {
        VerifyOrQuit(addresses.Contains(sAddressInfo.mHostAddresses[index]));
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `ResolveAddress()` for an invalid (non-existing) name

    sAddressInfo.Reset();
    Log("ResolveAddress(%s)", kNonExistingName);
    SuccessOrQuit(dnsClient->ResolveAddress(kNonExistingName, AddressCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sAddressInfo.mCallbackCount == 1);
    VerifyOrQuit(sAddressInfo.mError == kErrorNotFound);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `ResolveIp4Address()`

    sAddressInfo.Reset();
    Log("ResolveIp4Address(%s)", kHostFullName);
    SuccessOrQuit(dnsClient->ResolveIp4Address(kHostFullName, AddressCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sAddressInfo.mCallbackCount == 1);
    SuccessOrQuit(sAddressInfo.mError);
    VerifyOrQuit(sAddressInfo.mNumHostAddresses == 0);

    sAddressInfo.Reset();
    Log("ResolveIp4Address(%s)", "badname");
    SuccessOrQuit(dnsClient->ResolveIp4Address("badname", AddressCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sAddressInfo.mCallbackCount == 1);
    VerifyOrQuit(sAddressInfo.mError != kErrorNone);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for host name and KEY record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for KEY RR", kHostFullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeKey, kHostName, "default.service.arpa.",
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);

    VerifyOrQuit(!strcmp(sQueryRecordInfo.mRecords[0].mNameBuffer, kHostFullName));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypeKey);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength == sizeof(Dns::Ecdsa256KeyRecord));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sizeof(Dns::Ecdsa256KeyRecord));
    VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for misc RR", kHostFullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeCname, kHostName, "default.service.arpa.",
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 0);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for host name and ANY record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for ANY RR", kHostFullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeAny, kHostName, "default.service.arpa.",
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 3);

    for (uint8_t index = 0; index < 3; index++)
    {
        const QueryRecordInfo::Record &record = sQueryRecordInfo.mRecords[index];

        VerifyOrQuit(StringMatch(record.mNameBuffer, kHostFullName));
        VerifyOrQuit(MapEnum(record.mSection) == Dns::Client::RecordInfo::kSectionAnswer);
        VerifyOrQuit(record.mTtl > 0);

        if (record.mRecordType == Dns::ResourceRecord::kTypeKey)
        {
            VerifyOrQuit(record.mRecordLength == sizeof(Dns::Ecdsa256KeyRecord));
            VerifyOrQuit(record.mDataBufferSize == sizeof(Dns::Ecdsa256KeyRecord));
        }
        else if (record.mRecordType == Dns::ResourceRecord::kTypeAaaa)
        {
            VerifyOrQuit(record.mRecordLength == sizeof(Ip6::Address));
            VerifyOrQuit(addresses.Contains(*reinterpret_cast<const Ip6::Address *>(record.mDataBuffer)));
        }
        else
        {
            // Unexpected record type.
            VerifyOrQuit(false);
        }
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for service instance name and KEY record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for KEY RR", kInstance1FullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeKey, kInstance1Label, kService1FullName,
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);

    VerifyOrQuit(!strcmp(sQueryRecordInfo.mRecords[0].mNameBuffer, kInstance1FullName));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypeKey);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength == sizeof(Dns::Ecdsa256KeyRecord));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sizeof(Dns::Ecdsa256KeyRecord));
    VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for misc RR", kInstance1FullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeCname, kInstance1Label, kService1FullName,
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 0);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for service instance name and SRV record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for SRV record", kInstance1FullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeSrv, kInstance1Label, kService1FullName,
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 4);

    VerifyOrQuit(!strcmp(sQueryRecordInfo.mRecords[0].mNameBuffer, kInstance1FullName));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypeSrv);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sQueryRecordInfo.mRecords[0].mRecordLength);
    VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);

    ValidateSrvRecordData(sQueryRecordInfo.mRecords[0], kHostFullName);

    // Validate the records in additional data (TXT and two AAAA).

    for (uint8_t index = 1; index < 4; index++)
    {
        const QueryRecordInfo::Record &record = sQueryRecordInfo.mRecords[index];

        VerifyOrQuit(record.mRecordLength > 0);
        VerifyOrQuit(record.mTtl > 0);
        VerifyOrQuit(record.mDataBufferSize == record.mRecordLength);
        VerifyOrQuit(MapEnum(record.mSection) == Dns::Client::RecordInfo::kSectionAdditional);

        switch (record.mRecordType)
        {
        case Dns::ResourceRecord::kTypeTxt:
            VerifyOrQuit(!strcmp(record.mNameBuffer, kInstance1FullName));
            break;
        case Dns::ResourceRecord::kTypeAaaa:
            VerifyOrQuit(!strcmp(record.mNameBuffer, kHostFullName));
            VerifyOrQuit(record.mRecordLength == sizeof(Ip6::Address));
            break;
        default:
            VerifyOrQuit(false);
            break;
        }
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for service instance name and ANY record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for ANY record", kInstance1FullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeAny, kInstance1Label, kService1FullName,
                                         RecordCallback, sInstance));
    AdvanceTime(100);

    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 3);

    for (uint8_t index = 1; index < 3; index++)
    {
        const QueryRecordInfo::Record &record = sQueryRecordInfo.mRecords[index];

        VerifyOrQuit(StringMatch(record.mNameBuffer, kInstance1FullName));
        VerifyOrQuit(record.mRecordLength > 0);
        VerifyOrQuit(record.mTtl > 0);
        VerifyOrQuit(record.mDataBufferSize == record.mRecordLength);
        VerifyOrQuit(MapEnum(record.mSection) == Dns::Client::RecordInfo::kSectionAnswer);

        switch (record.mRecordType)
        {
        case Dns::ResourceRecord::kTypeKey:
        case Dns::ResourceRecord::kTypeTxt:
        case Dns::ResourceRecord::kTypeSrv:
            break;
        default:
            VerifyOrQuit(false);
            break;
        }
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for PTR record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for PTR record", kService1FullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypePtr, "_srv", "_udp.default.service.arpa.",
                                         RecordCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 5);

    VerifyOrQuit(!strcmp(sQueryRecordInfo.mRecords[0].mNameBuffer, kService1FullName));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypePtr);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sQueryRecordInfo.mRecords[0].mRecordLength);
    VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);

    ValidatePtrRecordData(sQueryRecordInfo.mRecords[0], kInstance1FullName);

    // Validate the records in additional data (SRV, TXT and two AAAA).

    for (uint8_t index = 1; index < 5; index++)
    {
        const QueryRecordInfo::Record &record = sQueryRecordInfo.mRecords[index];

        VerifyOrQuit(record.mRecordLength > 0);
        VerifyOrQuit(record.mTtl > 0);
        VerifyOrQuit(record.mDataBufferSize == record.mRecordLength);
        VerifyOrQuit(MapEnum(record.mSection) == Dns::Client::RecordInfo::kSectionAdditional);

        switch (record.mRecordType)
        {
        case Dns::ResourceRecord::kTypeSrv:
            VerifyOrQuit(!strcmp(record.mNameBuffer, kInstance1FullName));
            ValidateSrvRecordData(record, kHostFullName);
            break;
        case Dns::ResourceRecord::kTypeTxt:
            VerifyOrQuit(!strcmp(record.mNameBuffer, kInstance1FullName));
            break;
        case Dns::ResourceRecord::kTypeAaaa:
            VerifyOrQuit(!strcmp(record.mNameBuffer, kHostFullName));
            VerifyOrQuit(record.mRecordLength == sizeof(Ip6::Address));
            break;
        default:
            VerifyOrQuit(false);
            break;
        }
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for service name and ANY record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for ANY record", kInstance1FullName);
    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeAny, "_srv", "_udp.default.service.arpa.",
                                         RecordCallback, sInstance));
    AdvanceTime(100);

    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);

    VerifyOrQuit(StringMatch(sQueryRecordInfo.mRecords[0].mNameBuffer, kService1FullName));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypePtr);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sQueryRecordInfo.mRecords[0].mRecordLength);
    VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `QueryRecord()` for sub-type service name and ANY record

    sQueryRecordInfo.Reset();
    Log("QueryRecord(%s) for ANY record", kService2SubTypeFullName);

    SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeAny, "_best",
                                         "_sub._game._udp.default.service.arpa.", RecordCallback, sInstance));
    AdvanceTime(100);

    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);

    VerifyOrQuit(StringMatch(sQueryRecordInfo.mRecords[0].mNameBuffer, kService2SubTypeFullName));
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypePtr);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl > 0);
    VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sQueryRecordInfo.mRecords[0].mRecordLength);
    VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `Browse()`

    sBrowseInfo.Reset();
    Log("Browse(%s)", kService1FullName);
    SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    SuccessOrQuit(sBrowseInfo.mError);
    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);

    sBrowseInfo.Reset();

    Log("Browse(%s)", kService2FullName);
    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    SuccessOrQuit(sBrowseInfo.mError);
    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);

    sBrowseInfo.Reset();

    Log("Browse(%s)", kService2SubTypeFullName);
    SuccessOrQuit(dnsClient->Browse(kService2SubTypeFullName, BrowseCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    SuccessOrQuit(sBrowseInfo.mError);
    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);

    sBrowseInfo.Reset();
    Log("Browse() for unknown service");
    SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    VerifyOrQuit(sBrowseInfo.mError == kErrorNotFound);

    Log("Issue four parallel `Browse()` at the same time");
    sBrowseInfo.Reset();
    SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance));
    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
    SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance));
    SuccessOrQuit(dnsClient->Browse("_unknown2._udp.default.service.arpa.", BrowseCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sBrowseInfo.mCallbackCount == 4);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `ResolveService()` using all service modes

    for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes)
    {
        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
        Log("ResolveService(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
            ServiceModeToString(mode));

        queryConfig.Clear();
        queryConfig.mServiceMode = static_cast<otDnsServiceMode>(mode);

        sResolveServiceInfo.Reset();
        SuccessOrQuit(
            dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
        AdvanceTime(100);

        VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
        SuccessOrQuit(sResolveServiceInfo.mError);

        if (mode != Dns::Client::QueryConfig::kServiceModeTxt)
        {
            VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
            VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
            VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
            VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);

            VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == kNumAddresses);
            VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress) == sResolveServiceInfo.mHostAddresses[0]);

            for (uint8_t index = 0; index < kNumAddresses; index++)
            {
                VerifyOrQuit(addresses.Contains(sResolveServiceInfo.mHostAddresses[index]));
            }
        }

        if (mode != Dns::Client::QueryConfig::kServiceModeSrv)
        {
            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
        }
    }

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    Log("Set TestMode on server to reject multi-question queries and send error");
    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeRejectMultiQuestionQuery);

    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize));

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);

    sResolveServiceInfo.Reset();
    SuccessOrQuit(
        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
    AdvanceTime(200);

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    SuccessOrQuit(sResolveServiceInfo.mError);

    // Use `kServiceModeSrvTxt` and check that server does reject two questions.

    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxt));

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxt);

    sResolveServiceInfo.Reset();
    SuccessOrQuit(
        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
    AdvanceTime(200);

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    VerifyOrQuit(sResolveServiceInfo.mError != kErrorNone);

    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    Log("Set TestMode on server to ignore multi-question queries (send no response)");
    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeIgnoreMultiQuestionQuery);

    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize));

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);

    sResolveServiceInfo.Reset();
    SuccessOrQuit(
        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));

    AdvanceTime(10 * 1000); // Wait longer than client response timeout.

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    SuccessOrQuit(sResolveServiceInfo.mError);

    // Use `kServiceModeSrvTxt` and check that server does ignore two questions.

    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxt));

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxt);

    sResolveServiceInfo.Reset();
    SuccessOrQuit(
        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));

    // Wait for the client to time out after exhausting all retry attempts, and
    // ensure that a `kErrorResponseTimeout` error is reported.

    AdvanceTime(45 * 1000);

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    VerifyOrQuit(sResolveServiceInfo.mError == kErrorResponseTimeout);

    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `ResolveService()` using all service modes
    // when sever does not provide any RR in the addition data section.

    for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes)
    {
        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
        Log("Set TestMode on server to not include any RR in additional section");
        dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeEmptyAdditionalSection);
        Log("ResolveService(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
            ServiceModeToString(mode));

        queryConfig.Clear();
        queryConfig.mServiceMode = static_cast<otDnsServiceMode>(mode);

        sResolveServiceInfo.Reset();
        SuccessOrQuit(
            dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
        AdvanceTime(100);

        VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
        SuccessOrQuit(sResolveServiceInfo.mError);

        if (mode != Dns::Client::QueryConfig::kServiceModeTxt)
        {
            VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
            VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
            VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
            VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);
        }

        if (mode != Dns::Client::QueryConfig::kServiceModeSrv)
        {
            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
        }

        // Since server is using `kTestModeEmptyAdditionalSection`, there
        // should be no AAAA records for host address.

        VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress).IsUnspecified());
        VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == 0);
    }

    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Validate DNS Client `ResolveServiceAndHostAddress()` using all service modes
    // with different TestMode configs on server:
    // - Normal behavior when server provides AAAA records for host in
    //   additional section.
    // - Server provides no records in additional section. We validate that
    //   client will send separate query to resolve host address.

    for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes)
    {
        for (uint8_t testIter = 0; testIter <= 1; testIter++)
        {
            Error error;

            Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

            if (testIter == 1)
            {
                Log("Set TestMode on server to not include any RR in additional section");
                dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeEmptyAdditionalSection);
            }
            else
            {
                dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);
            }

            Log("ResolveServiceAndHostAddress(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
                ServiceModeToString(mode));

            queryConfig.Clear();
            queryConfig.mServiceMode = static_cast<otDnsServiceMode>(mode);

            sResolveServiceInfo.Reset();
            error = dnsClient->ResolveServiceAndHostAddress(kInstance1Label, kService1FullName, ServiceCallback,
                                                            sInstance, &queryConfig);

            if (mode == Dns::Client::QueryConfig::kServiceModeTxt)
            {
                Log("ResolveServiceAndHostAddress() with ServiceMode: %s failed correctly", ServiceModeToString(mode));
                VerifyOrQuit(error == kErrorInvalidArgs);
                continue;
            }

            SuccessOrQuit(error);

            AdvanceTime(100);

            VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
            SuccessOrQuit(sResolveServiceInfo.mError);

            if (mode != Dns::Client::QueryConfig::kServiceModeTxt)
            {
                VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
                VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
                VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
                VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);

                VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == kNumAddresses);
                VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress) ==
                             sResolveServiceInfo.mHostAddresses[0]);

                for (uint8_t index = 0; index < kNumAddresses; index++)
                {
                    VerifyOrQuit(addresses.Contains(sResolveServiceInfo.mHostAddresses[index]));
                }
            }

            if (mode != Dns::Client::QueryConfig::kServiceModeSrv)
            {
                VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
                VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
            }
        }
    }

    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("Set TestMode on server to not include any RR in additional section AND to only accept single question");
    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeEmptyAdditionalSection +
                           Dns::ServiceDiscovery::Server::kTestModeRejectMultiQuestionQuery);

    Log("ResolveServiceAndHostAddress(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize));

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);

    oldServerCounters = dnsServer->GetCounters();

    sResolveServiceInfo.Reset();
    SuccessOrQuit(dnsClient->ResolveServiceAndHostAddress(kInstance1Label, kService1FullName, ServiceCallback,
                                                          sInstance, &queryConfig));

    AdvanceTime(100);

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    SuccessOrQuit(sResolveServiceInfo.mError);

    VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
    VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
    VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
    VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);

    VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
    VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);

    VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == kNumAddresses);
    VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress) == sResolveServiceInfo.mHostAddresses[0]);

    for (uint8_t index = 0; index < kNumAddresses; index++)
    {
        VerifyOrQuit(addresses.Contains(sResolveServiceInfo.mHostAddresses[index]));
    }

    newServerCounters = dnsServer->GetCounters();

    Log("Validate (using server counter) that client first tried to query SRV/TXT together and failed");
    Log("and then send separate queries (for SRV, TXT and AAAA)");
    Log("  Total : %2u -> %2u", oldServerCounters.GetTotalQueries(), newServerCounters.GetTotalQueries());
    Log("  Failed: %2u -> %2u", oldServerCounters.GetTotalFailedQueries(), newServerCounters.GetTotalFailedQueries());

    VerifyOrQuit(newServerCounters.GetTotalFailedQueries() == 1 + oldServerCounters.GetTotalFailedQueries());
    VerifyOrQuit(newServerCounters.GetTotalQueries() == 4 + oldServerCounters.GetTotalQueries());

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("Resolve service again now using `kServiceModeSrvTxtOptimize` as default config");
    Log("Client should already know that server is not capable of handling multi-question query");

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);

    dnsClient->SetDefaultConfig(queryConfig);

    Log("ResolveService(%s,%s)", kInstance1Label, kService1FullName);

    oldServerCounters = dnsServer->GetCounters();

    sResolveServiceInfo.Reset();
    SuccessOrQuit(dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, nullptr));

    AdvanceTime(100);

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    SuccessOrQuit(sResolveServiceInfo.mError);

    VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
    VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
    VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
    VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);

    VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
    VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);

    newServerCounters = dnsServer->GetCounters();

    Log("Client should already know that server is not capable of handling multi-question query");
    Log("Check server counters to validate that client did send separate queries for TXT and SRV");
    Log("  Total : %2u -> %2u", oldServerCounters.GetTotalQueries(), newServerCounters.GetTotalQueries());
    Log("  Failed: %2u -> %2u", oldServerCounters.GetTotalFailedQueries(), newServerCounters.GetTotalFailedQueries());

    VerifyOrQuit(newServerCounters.GetTotalFailedQueries() == oldServerCounters.GetTotalFailedQueries());
    VerifyOrQuit(newServerCounters.GetTotalQueries() == 2 + oldServerCounters.GetTotalQueries());

    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    Log("Stop DNS-SD server");
    dnsServer->Stop();

    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate));

    queryConfig.Clear();
    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate);

    sResolveServiceInfo.Reset();
    SuccessOrQuit(
        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
    AdvanceTime(25 * 1000);

    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
    VerifyOrQuit(sResolveServiceInfo.mError == kErrorResponseTimeout);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Disable SRP server, verify that all heap allocations by SRP server
    // and/or by DNS Client are freed.

    Log("Disabling SRP server");

    srpServer->SetEnabled(false);
    AdvanceTime(100);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("Remove the route prefix (with NAT64 flag) from network data");

    // This ensures the device is no longer tracked as a BR. This is
    // required to release the associated heap allocation in
    // `NetDataBrTracker`, which would otherwise cause the
    // `heapAllocations` check to fail.

    SuccessOrQuit(otBorderRouterRemoveRoute(sInstance, &routeConfig.mPrefix));
    SuccessOrQuit(otBorderRouterRegister(sInstance));
    AdvanceTime(1000);

    VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength());

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Finalize OT instance and validate all heap allocations are freed.

    Log("Finalizing OT instance");
    FinalizeTest();

    VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty());

    Log("End of TestDnsClient");
}

//----------------------------------------------------------------------------------------------------------------------

Dns::Name::Buffer sLastSubscribeName;
Dns::Name::Buffer sLastUnsubscribeName;

void QuerySubscribe(void *aContext, const char *aFullName)
{
    uint16_t length = StringLength(aFullName, Dns::Name::kMaxNameSize);

    Log("QuerySubscribe(%s)", aFullName);

    VerifyOrQuit(aContext == sInstance);
    VerifyOrQuit(length < Dns::Name::kMaxNameSize);
    strcpy(sLastSubscribeName, aFullName);
}

void QueryUnsubscribe(void *aContext, const char *aFullName)
{
    uint16_t length = StringLength(aFullName, Dns::Name::kMaxNameSize);

    Log("QueryUnsubscribe(%s)", aFullName);

    VerifyOrQuit(aContext == sInstance);
    VerifyOrQuit(length < Dns::Name::kMaxNameSize);
    strcpy(sLastUnsubscribeName, aFullName);
}

void TestDnssdServerProxyCallback(void)
{
    Srp::Server                   *srpServer;
    Srp::Client                   *srpClient;
    Dns::Client                   *dnsClient;
    Dns::ServiceDiscovery::Server *dnsServer;
    otDnssdServiceInstanceInfo     instanceInfo;

    Log("--------------------------------------------------------------------------------------------");
    Log("TestDnssdServerProxyCallback");

    InitTest();

    srpServer = &sInstance->Get<Srp::Server>();
    srpClient = &sInstance->Get<Srp::Client>();
    dnsClient = &sInstance->Get<Dns::Client>();
    dnsServer = &sInstance->Get<Dns::ServiceDiscovery::Server>();

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Start SRP server.

    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);

    srpServer->SetEnabled(true);
    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);

    AdvanceTime(10000);
    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Start SRP client.

    srpClient->EnableAutoStartMode(nullptr, nullptr);
    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());

    AdvanceTime(2000);
    VerifyOrQuit(srpClient->IsRunning());

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Set the query subscribe/unsubscribe callbacks on server

    dnsServer->SetQueryCallbacks(QuerySubscribe, QueryUnsubscribe, sInstance);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    sLastSubscribeName[0]   = '\0';
    sLastUnsubscribeName[0] = '\0';

    sBrowseInfo.Reset();
    Log("Browse(%s)", kService1FullName);
    SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance));
    AdvanceTime(10);

    VerifyOrQuit(strcmp(sLastSubscribeName, kService1FullName) == 0);
    VerifyOrQuit(strcmp(sLastUnsubscribeName, "") == 0);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 0);

    Log("Invoke subscribe callback");

    memset(&instanceInfo, 0, sizeof(instanceInfo));
    instanceInfo.mFullName = kInstance1FullName;
    instanceInfo.mHostName = kHostFullName;
    instanceInfo.mPort     = 200;

    dnsServer->HandleDiscoveredServiceInstance(kService1FullName, instanceInfo);

    AdvanceTime(10);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    SuccessOrQuit(sBrowseInfo.mError);
    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);

    VerifyOrQuit(strcmp(sLastUnsubscribeName, kService1FullName) == 0);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    sLastSubscribeName[0]   = '\0';
    sLastUnsubscribeName[0] = '\0';

    sBrowseInfo.Reset();
    Log("Browse(%s)", kService2FullName);
    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
    AdvanceTime(10);

    VerifyOrQuit(strcmp(sLastSubscribeName, kService2FullName) == 0);
    VerifyOrQuit(strcmp(sLastUnsubscribeName, "") == 0);

    Log("Invoke subscribe callback for wrong name");

    memset(&instanceInfo, 0, sizeof(instanceInfo));
    instanceInfo.mFullName = kInstance1FullName;
    instanceInfo.mHostName = kHostFullName;
    instanceInfo.mPort     = 200;

    dnsServer->HandleDiscoveredServiceInstance(kService1FullName, instanceInfo);

    AdvanceTime(10);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 0);

    Log("Invoke subscribe callback for correct name");

    memset(&instanceInfo, 0, sizeof(instanceInfo));
    instanceInfo.mFullName = kInstance2FullName;
    instanceInfo.mHostName = kHostFullName;
    instanceInfo.mPort     = 200;

    dnsServer->HandleDiscoveredServiceInstance(kService2FullName, instanceInfo);

    AdvanceTime(10);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    SuccessOrQuit(sBrowseInfo.mError);
    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);

    VerifyOrQuit(strcmp(sLastUnsubscribeName, kService2FullName) == 0);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    sLastSubscribeName[0]   = '\0';
    sLastUnsubscribeName[0] = '\0';

    sBrowseInfo.Reset();
    Log("Browse(%s)", kService2FullName);
    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
    AdvanceTime(10);

    VerifyOrQuit(strcmp(sLastSubscribeName, kService2FullName) == 0);
    VerifyOrQuit(strcmp(sLastUnsubscribeName, "") == 0);

    Log("Do not invoke subscribe callback and let query to timeout");

    // Query timeout is set to 6 seconds

    AdvanceTime(5000);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 0);

    AdvanceTime(2000);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    SuccessOrQuit(sBrowseInfo.mError);
    VerifyOrQuit(sBrowseInfo.mNumInstances == 0);

    VerifyOrQuit(strcmp(sLastUnsubscribeName, kService2FullName) == 0);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    sLastSubscribeName[0]   = '\0';
    sLastUnsubscribeName[0] = '\0';

    sBrowseInfo.Reset();
    Log("Browse(%s)", kService2FullName);
    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
    AdvanceTime(10);

    VerifyOrQuit(strcmp(sLastSubscribeName, kService2FullName) == 0);
    VerifyOrQuit(strcmp(sLastUnsubscribeName, "") == 0);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 0);

    Log("Do not invoke subscribe callback and stop server");

    dnsServer->Stop();

    AdvanceTime(10);

    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
    VerifyOrQuit(sBrowseInfo.mError != kErrorNone);

    VerifyOrQuit(strcmp(sLastUnsubscribeName, kService2FullName) == 0);

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Finalize OT instance and validate all heap allocations are freed.

    Log("Finalizing OT instance");
    FinalizeTest();

    Log("End of TestDnssdServerProxyCallback");
}

void TestDnssdSoaNsResponse(void)
{
    Srp::Server      *srpServer;
    Srp::Client      *srpClient;
    Dns::Client      *dnsClient;
    Dns::Name::Buffer serverName;
    StringWriter      writer(serverName, sizeof(serverName));

    Log("--------------------------------------------------------------------------------------------");
    Log("TestDnssdSoaNsResponse");

    InitTest();

    srpServer = &sInstance->Get<Srp::Server>();
    srpClient = &sInstance->Get<Srp::Client>();
    dnsClient = &sInstance->Get<Dns::Client>();

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Start SRP server.

    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);

    srpServer->SetEnabled(true);
    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);

    AdvanceTime(10000);
    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Start SRP client.

    srpClient->EnableAutoStartMode(nullptr, nullptr);
    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());

    AdvanceTime(2000);
    VerifyOrQuit(srpClient->IsRunning());

    writer.Append("otDNS%s.default.service.arpa.", sInstance->Get<Mac::Mac>().GetExtAddress().ToString().AsCString());

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("SOA Query");

    for (uint8_t iter = 0; iter < 2; iter++)
    {
        sQueryRecordInfo.Reset();

        // First iteration: Query for `default.service.arpa.` directly, and
        // validate that we see the SOA record in the Answer section.
        // Second iteration: Query for `myhost.default.service.arpa.`, and
        // validate that we see the SOA record in the Authority section.

        if (iter == 0)
        {
            Log("QueryRecord(%s) for SOA RR", "default.service.arpa.");
            SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeSoa, "default", "service.arpa.",
                                                 RecordCallback, sInstance));
        }
        else
        {
            Log("QueryRecord(%s) for SOA RR", "myhost.default.service.arpa.");
            SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeSoa, "myhost", "default.service.arpa.",
                                                 RecordCallback, sInstance));
        }

        AdvanceTime(100);
        VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
        SuccessOrQuit(sQueryRecordInfo.mError);
        VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);

        if (iter == 0)
        {
            VerifyOrQuit(StringMatch(sQueryRecordInfo.mQueryName, "default.service.arpa."));
            VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);
        }
        else
        {
            VerifyOrQuit(StringMatch(sQueryRecordInfo.mQueryName, "myhost.default.service.arpa."));
            VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAuthority);
        }

        ValidateSoaRecordData(sQueryRecordInfo.mRecords[0], serverName);

        AdvanceTime(1000);
    }

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
    Log("NS Query");

    for (uint8_t iter = 0; iter < 2; iter++)
    {
        sQueryRecordInfo.Reset();

        // First iteration: Query for `default.service.arpa.` directly and
        // validate that we see the NS response in the Answer section. Second
        // iteration: Query for `myhost.default.service.arpa.` and validate
        // that we see the SOA record instead in the Authority section in
        // the response.

        if (iter == 0)
        {
            Log("QueryRecord(%s) for NS RR", "default.service.arpa.");
            SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeNs, "default", "service.arpa.",
                                                 RecordCallback, sInstance));
        }
        else
        {
            Log("QueryRecord(%s) for NS RR", "myhost.default.service.arpa.");
            SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeNs, "myhost", "default.service.arpa.",
                                                 RecordCallback, sInstance));
        }

        AdvanceTime(100);
        VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
        SuccessOrQuit(sQueryRecordInfo.mError);
        VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1);

        if (iter == 0)
        {
            VerifyOrQuit(StringMatch(sQueryRecordInfo.mQueryName, "default.service.arpa."));
            VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer);
            ValidateNsRecordData(sQueryRecordInfo.mRecords[0], serverName);
        }
        else
        {
            VerifyOrQuit(StringMatch(sQueryRecordInfo.mQueryName, "myhost.default.service.arpa."));
            VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAuthority);
            ValidateSoaRecordData(sQueryRecordInfo.mRecords[0], serverName);
        }

        AdvanceTime(1000);
    }

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    sQueryRecordInfo.Reset();

    Log("QueryRecord(%s) for ANY RR", "default.service.arpa.");
    SuccessOrQuit(
        dnsClient->QueryRecord(Dns::ResourceRecord::kTypeAny, "default", "service.arpa.", RecordCallback, sInstance));

    AdvanceTime(100);
    VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1);
    SuccessOrQuit(sQueryRecordInfo.mError);
    VerifyOrQuit(sQueryRecordInfo.mNumRecords == 2);

    VerifyOrQuit(StringMatch(sQueryRecordInfo.mQueryName, "default.service.arpa."));

    for (uint8_t index = 0; index < 2; index++)
    {
        QueryRecordInfo::Record &record = sQueryRecordInfo.mRecords[index];

        VerifyOrQuit(MapEnum(record.mSection) == Dns::Client::RecordInfo::kSectionAnswer);

        switch (record.mRecordType)
        {
        case Dns::ResourceRecord::kTypeSoa:
            ValidateSoaRecordData(record, serverName);
            break;
        case Dns::ResourceRecord::kTypeNs:
            ValidateNsRecordData(record, serverName);
            break;
        default:
            VerifyOrQuit(false);
            break;
        }

        AdvanceTime(1000);
    }

    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");

    sAddressInfo.Reset();
    Log("ResolveAddress(%s)", serverName);
    SuccessOrQuit(dnsClient->ResolveAddress(serverName, AddressCallback, sInstance));
    AdvanceTime(100);
    VerifyOrQuit(sAddressInfo.mCallbackCount >= 1);
    SuccessOrQuit(sAddressInfo.mError);
    VerifyOrQuit(sAddressInfo.mHostAddresses[0] == sInstance->Get<Mle::Mle>().GetMeshLocalEid());

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Finalize OT instance and validate all heap allocations are freed.

    Log("Finalizing OT instance");
    FinalizeTest();

    Log("End of TestDnssdSoaNsResponse");
}

#endif // ENABLE_DNS_TEST

int main(void)
{
#if ENABLE_DNS_TEST
    TestDnsClient();
    TestDnssdServerProxyCallback();
    TestDnssdSoaNsResponse();
    printf("All tests passed\n");
#else
    printf("DNS_CLIENT or DSNSSD_SERVER feature is not enabled\n");
#endif

    return 0;
}
