/*
 *  Copyright (c) 2025, 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 <stdarg.h>
#include <stdio.h>
#include <string.h>

#include "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"

namespace ot {
namespace Nexus {

using Admitter     = MeshCoP::BorderAgent::Admitter;
using Manager      = MeshCoP::BorderAgent::Manager;
using Commissioner = MeshCoP::Commissioner;
using Joiner       = MeshCoP::Joiner;

static LogLevel kLogLevel = kLogLevelCrit;

void TestBorderAdmitterPrimeSelection(void)
{
    Core                           nexus;
    Node                          &leader  = nexus.CreateNode();
    Node                          &node1   = nexus.CreateNode();
    Node                          &node2   = nexus.CreateNode();
    Node                          &node3   = nexus.CreateNode();
    Node                          *nodes[] = {&node1, &node2, &node3};
    NetworkData::Service::Iterator netDataIter(leader.GetInstance());
    uint16_t                       rloc16;
    bool                           found;

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

    nexus.AdvanceTime(0);

    SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevel));

    // Form the topology.

    leader.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);
    node1.Join(leader);
    node2.Join(leader);
    node3.Join(leader);

    nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);

    VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
    VerifyOrQuit(node1.Get<Mle::Mle>().IsRouter());
    VerifyOrQuit(node2.Get<Mle::Mle>().IsRouter());
    VerifyOrQuit(node3.Get<Mle::Mle>().IsRouter());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check Border Admitter initial state");

    VerifyOrQuit(!node1.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node2.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node3.Get<Admitter>().IsEnabled());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Admitter role on `node1` and validate that it becomes the Prime Admitter");

    node1.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(node1.Get<Admitter>().IsEnabled());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(node1.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!node1.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the NetworkData contains a single Admitter Service from `node1`");

    netDataIter.Reset();

    SuccessOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16));
    VerifyOrQuit(rloc16 == node1.Get<Mle::Mle>().GetRloc16());

    VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Admitter role on `node2` & `node3` and validate that `node1` remains the Prime Admitter");

    node2.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(node2.Get<Admitter>().IsEnabled());

    node3.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(node3.Get<Admitter>().IsEnabled());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(node1.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!node1.Get<Admitter>().IsActiveCommissioner());

    VerifyOrQuit(!node2.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!node3.Get<Admitter>().IsPrimeAdmitter());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the NetworkData contains the Admitter Service from `node1`");

    netDataIter.Reset();

    SuccessOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16));
    VerifyOrQuit(rloc16 == node1.Get<Mle::Mle>().GetRloc16());

    VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Disable Admitter role on `node1` and check that another Prime Admitter is elected");

    node1.Get<Admitter>().SetEnabled(false);
    VerifyOrQuit(!node1.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node1.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(75 * Time::kOneSecondInMsec);

    // We use `!=` to do an "exclusive or" logic check (either node2 or node3 is prime and not both)
    VerifyOrQuit(node2.Get<Admitter>().IsPrimeAdmitter() != node3.Get<Admitter>().IsPrimeAdmitter());

    VerifyOrQuit(!node2.Get<Admitter>().IsActiveCommissioner());
    VerifyOrQuit(!node3.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the NetworkData contains a single Admitter Service entry");

    netDataIter.Reset();

    SuccessOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16));

    VerifyOrQuit(rloc16 == (node2.Get<Admitter>().IsPrimeAdmitter() ? node2.Get<Mle::Mle>().GetRloc16()
                                                                    : node3.Get<Mle::Mle>().GetRloc16()));

    VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Disable Admitter role on all nodes (`node2` and `node3`)");

    node2.Get<Admitter>().SetEnabled(false);
    VerifyOrQuit(!node2.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node2.Get<Admitter>().IsPrimeAdmitter());

    node3.Get<Admitter>().SetEnabled(false);
    VerifyOrQuit(!node3.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node3.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(5 * Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the NetworkData contains no Admitter Service entry");

    netDataIter.Reset();
    VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound);

    nexus.AdvanceTime(10 * Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Admitter role on all 3 nodes at the same time");

    node1.Get<Admitter>().SetEnabled(true);
    node2.Get<Admitter>().SetEnabled(true);
    node3.Get<Admitter>().SetEnabled(true);

    VerifyOrQuit(node1.Get<Admitter>().IsEnabled());
    VerifyOrQuit(node2.Get<Admitter>().IsEnabled());
    VerifyOrQuit(node3.Get<Admitter>().IsEnabled());

    nexus.AdvanceTime(75 * Time::kOneSecondInMsec);

    Log("Validate that we end up with a single Prime Admitter");

    found = false;

    for (Node *node : nodes)
    {
        if (node->Get<Admitter>().IsPrimeAdmitter())
        {
            VerifyOrQuit(!found);
            found  = true;
            rloc16 = node->Get<Mle::Mle>().GetRloc16();
        }

        VerifyOrQuit(!node->Get<Admitter>().IsActiveCommissioner());
    }

    VerifyOrQuit(found);
}

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

enum AdmitterState : uint8_t
{
    kAdmitterUnavailable   = 0,
    kAdmitterReady         = 1,
    kAdmitterActive        = 2,
    kAdmitterConflictError = 3,
};

const char *AdmitterStateToString(uint8_t aState)
{
    const char *str = "Unknown";

    switch (aState)
    {
    case kAdmitterUnavailable:
        str = "AdmitterUnavailable";
        break;
    case kAdmitterReady:
        str = "AdmitterReady";
        break;
    case kAdmitterActive:
        str = "AdmitterActive";
        break;
    case kAdmitterConflictError:
        str = "AdmitterConflictError";
        break;
    default:
        break;
    }
    return str;
}

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

struct AdmitterInfo
{
    // Tracks information about Admitter from received
    // messages on an Enroller.

    void ParseAdmitterInfo(const Coap::Message &aResponse)
    {
        Error error;

        error = Tlv::Find<MeshCoP::AdmitterStateTlv>(aResponse, mAdmitterState);
        VerifyOrQuit(error == kErrorNone || error == kErrorNotFound);
        mHasAdmitterState = (error == kErrorNone);

        error = Tlv::Find<MeshCoP::CommissionerSessionIdTlv>(aResponse, mCommrSessionId);
        VerifyOrQuit(error == kErrorNone || error == kErrorNotFound);
        mHasCommrSessionId = (error == kErrorNone);

        error = Tlv::Find<MeshCoP::JoinerUdpPortTlv>(aResponse, mJoinerUdp);
        VerifyOrQuit(error == kErrorNone || error == kErrorNotFound);
        mHasJoinerUdp = (error == kErrorNone);
    }

    bool     mHasAdmitterState;
    bool     mHasCommrSessionId;
    bool     mHasJoinerUdp;
    uint8_t  mResponseState;
    uint8_t  mAdmitterState;
    uint16_t mCommrSessionId;
    uint16_t mJoinerUdp;
};

struct ResponseContext : public AdmitterInfo, public Clearable<ResponseContext>
{
    // Tracks information in a TMF response received by an enroller.
    // (populated by `HandleResponse()`).

    bool mReceived;
};

void HandleResponse(void *aContext, Coap::Msg *aMsg, Error aResult)
{
    OT_UNUSED_VARIABLE(aResult);

    ResponseContext *responseContext;

    VerifyOrQuit(aContext != nullptr);
    VerifyOrQuit(aMsg != nullptr);

    responseContext = static_cast<ResponseContext *>(aContext);

    VerifyOrQuit(!responseContext->mReceived); // Duplicate response
    responseContext->mReceived = true;

    SuccessOrQuit(Tlv::Find<MeshCoP::StateTlv>(aMsg->mMessage, responseContext->mResponseState));

    responseContext->ParseAdmitterInfo(aMsg->mMessage);

    Log("  Received response - %s",
        MeshCoP::StateTlv::StateToString(static_cast<MeshCoP::StateTlv::State>(responseContext->mResponseState)));
}

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

struct ReceiveContext
{
    // Tracks information from received TMF on an Enroller send by
    // Admitter.

    static constexpr uint8_t kMaxEntries = 16;

    void Clear(void)
    {
        mStateReports.Clear();
        mRelayRxMsgs.DequeueAndFreeAll();
        mProxyRxMsgs.DequeueAndFreeAll();
    }

    bool    HasReceivedReportState(void) const { return !mStateReports.IsEmpty(); }
    uint8_t GetLastReportedAdmitterState(void) { return mStateReports.Back()->mAdmitterState; }

    Array<AdmitterInfo, kMaxEntries> mStateReports;
    MessageQueue                     mRelayRxMsgs;
    MessageQueue                     mProxyRxMsgs;
};

bool HandleResource(void *aContext, Uri aUri, Coap::Msg &aMsg)
{
    bool                     didHandle = false;
    ReceiveContext          *recvContext;
    AdmitterInfo            *info;
    Message                 *msgClone;
    uint16_t                 joinerPort;
    Ip6::InterfaceIdentifier joinerIid;

    VerifyOrQuit(aContext != nullptr);
    recvContext = static_cast<ReceiveContext *>(aContext);

    switch (aUri)
    {
    case kUriEnrollerReportState:
        didHandle = true;
        info      = recvContext->mStateReports.PushBack();
        VerifyOrQuit(info != nullptr);
        info->ParseAdmitterInfo(aMsg.mMessage);
        VerifyOrQuit(info->mHasAdmitterState);
        Log("  Received `EnrollerReportState` with state %s", AdmitterStateToString(info->mAdmitterState));
        break;

    case kUriRelayRx:
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerUdpPortTlv>(aMsg.mMessage, joinerPort));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(aMsg.mMessage, joinerIid));
        Log("  Received `RelayRx` from joiner - port:%u iid:%s", joinerPort, joinerIid.ToString().AsCString());

        msgClone = aMsg.mMessage.Clone<kNoReservedHeader>();
        VerifyOrQuit(msgClone != nullptr);
        recvContext->mRelayRxMsgs.Enqueue(*msgClone);
        break;

    case kUriProxyRx:
        Log("  Received `ProxyRx`");
        msgClone = aMsg.mMessage.Clone<kNoReservedHeader>();
        VerifyOrQuit(msgClone != nullptr);
        recvContext->mProxyRxMsgs.Enqueue(*msgClone);
        break;
    default:
        Log("  Received unexpected URI %u", aUri);
        break;
    }

    return didHandle;
}

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

void TestBorderAdmitterEnrollerInteraction(void)
{
    static const char kEnrollerId[]    = "en00";
    static const char kEnrollerIdAlt[] = "en01";

    static const uint8_t kEnrollerTimeoutInSec = 50;

    Core                   nexus;
    Node                  &admitter = nexus.CreateNode();
    Node                  &enroller = nexus.CreateNode();
    Ip6::SockAddr          sockAddr;
    Pskc                   pskc;
    Admitter::Iterator     iter;
    Admitter::EnrollerInfo enrollerInfo;
    Admitter::JoinerInfo   joinerInfo;
    Coap::Message         *message;
    uint8_t                mode;
    MeshCoP::SteeringData  steeringData;
    MeshCoP::SteeringData  leaderSteeringData;
    ResponseContext        responseContext;
    ReceiveContext         recvContext;
    Manager::SessionInfo  *sessionInfo;
    uint16_t               rloc16;
    uint16_t               sessionId;

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

    nexus.AdvanceTime(0);

    // Form the topology:
    // - `admitter` forms its own network (acting as leader)
    // - `enroller` stays disconnected.

    admitter.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);
    VerifyOrQuit(admitter.Get<Mle::Mle>().IsLeader());

    SuccessOrQuit(enroller.Get<Mac::Mac>().SetPanChannel(admitter.Get<Mac::Mac>().GetPanChannel()));
    enroller.Get<Mac::Mac>().SetPanId(admitter.Get<Mac::Mac>().GetPanId());
    enroller.Get<ThreadNetif>().Up();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Border Admitter on `admitter`");

    admitter.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(admitter.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!admitter.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<Ip6::Filter>().AddUnsecurePort(admitter.Get<Manager>().GetUdpPort()));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Establish a DTLS connection from `enroller` to `admitter`");

    sockAddr.SetAddress(admitter.Get<Mle::Mle>().GetLinkLocalAddress());
    sockAddr.SetPort(admitter.Get<Manager>().GetUdpPort());

    admitter.Get<KeyManager>().GetPskc(pskc);
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));

    enroller.Get<Tmf::SecureAgent>().RegisterResourceHandler(HandleResource, &recvContext);

    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().Open(0));
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().Connect(sockAddr));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(enroller.Get<Tmf::SecureAgent>().IsConnected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check that Enroller session list on `admitter` is empty");

    iter.Init(admitter.GetInstance());
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);
    VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerRegister` message from `enroller` to `admitter`");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
    VerifyOrQuit(message != nullptr);

    mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;

    steeringData.SetToPermitAllJoiners();

    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContext.mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the enroller list on `admitter`");

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    sessionInfo = &enrollerInfo.mSessionInfo;
    VerifyOrQuit(sessionInfo->mIsConnected);
    VerifyOrQuit(!sessionInfo->mIsCommissioner);
    VerifyOrQuit(enroller.Get<ThreadNetif>().HasUnicastAddress(AsCoreType(&sessionInfo->mPeerSockAddr.mAddress)));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` becomes active commissioner");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `EnrollerReportState` is received with the updated Admitter state");

    VerifyOrQuit(recvContext.HasReceivedReportState());
    VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterActive);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that commissioner steering data and session ID are properly set");

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Ensure no changes before enroller timeout");

    recvContext.Clear();

    nexus.AdvanceTime((kEnrollerTimeoutInSec - 3) * Time::kOneSecondInMsec);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(enrollerInfo.mRegisterDuration >= 48 * Time::kOneSecondInMsec);

    VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerKeepAlive` message");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContext.mHasAdmitterState);
    VerifyOrQuit(responseContext.mAdmitterState == kAdmitterActive);

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);
    VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Wait until just before the timeout and validate that `admitter` remains active commissioner");

    nexus.AdvanceTime((kEnrollerTimeoutInSec - 2) * Time::kOneSecondInMsec);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));
    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerKeepAlive` message with an Enroller Mode TLV changing the mode");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx;

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContext.mHasAdmitterState);
    VerifyOrQuit(responseContext.mAdmitterState == kAdmitterActive);

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    nexus.AdvanceTime((kEnrollerTimeoutInSec - 2) * Time::kOneSecondInMsec);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerKeepAlive` message with Steering Data TLV");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(steeringData.Init(MeshCoP::SteeringData::kMaxLength));
    SuccessOrQuit(steeringData.UpdateBloomFilter(admitter.Get<Mac::Mac>().GetExtAddress()));

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContext.mHasAdmitterState);
    VerifyOrQuit(responseContext.mAdmitterState == kAdmitterActive);

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate Network Data is updated with the new Steering Data");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the enroller timeout");

    // Already 2 seconds has passed since sending last `EnrollerKeepAlive`
    nexus.AdvanceTime((kEnrollerTimeoutInSec - 2) * Time::kOneSecondInMsec - 50);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));
    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    nexus.AdvanceTime(75);

    iter.Init(admitter.GetInstance());
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the `admitter` resigns from being active commissioner");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    VerifyOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16) == kErrorNotFound);

    nexus.AdvanceTime(10 * Time::kOneSecondInMsec);

    VerifyOrQuit(!enroller.Get<Tmf::SecureAgent>().IsConnected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Re-establish DTLS session");

    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().Connect(sockAddr));
    nexus.AdvanceTime(Time::kOneSecondInMsec);
    VerifyOrQuit(enroller.Get<Tmf::SecureAgent>().IsConnected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Register as enroller again");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContext.mHasAdmitterState);

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));
    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` becomes active commissioner again");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);

    nexus.AdvanceTime((kEnrollerTimeoutInSec / 2) * Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerKeepAlive` message from `enroller` with `kReject` status, resigning enroller role");

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kReject));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject);

    iter.Init(admitter.GetInstance());
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    VerifyOrQuit(enroller.Get<Tmf::SecureAgent>().IsConnected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Register as enroller again");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContext.mHasAdmitterState);

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));
    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    nexus.AdvanceTime((kEnrollerTimeoutInSec / 2) * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerRegister` message while already registered, with different parameters");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
    VerifyOrQuit(message != nullptr);

    mode = 0;
    SuccessOrQuit(steeringData.Init(8));
    SuccessOrQuit(steeringData.UpdateBloomFilter(admitter.Get<Mac::Mac>().GetExtAddress()));

    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerIdAlt));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    Log("Validate that enroller info is updated accordingly");

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerIdAlt));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the Network Data (Commissioner Data) is also updated");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the `EnrollerRegister` extended the keep-alive timeout");

    recvContext.Clear();

    nexus.AdvanceTime((kEnrollerTimeoutInSec - 3) * Time::kOneSecondInMsec);

    VerifyOrQuit(!recvContext.HasReceivedReportState());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));
    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerIdAlt));
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an invalid `EnrollerKeepAlive` message without State TLV and validate that it is rejected");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check that enroller is removed on `admitter`, and it stops being active commissioner");

    iter.Init(admitter.GetInstance());
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    nexus.AdvanceTime(10 * Time::kOneSecondInMsec);

    VerifyOrQuit(!enroller.Get<Tmf::SecureAgent>().IsConnected());

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    VerifyOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Re-establish DTLS session");

    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().Connect(sockAddr));
    nexus.AdvanceTime(Time::kOneSecondInMsec);
    VerifyOrQuit(enroller.Get<Tmf::SecureAgent>().IsConnected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send `EnrollerRegister` with missing TLVs, validate that it is rejected");

    for (uint16_t testIter = 0; testIter < 3; testIter++)
    {
        message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
        VerifyOrQuit(message != nullptr);

        // Skip one of the required TLVs for each `testIter`.

        if (testIter != 0)
        {
            SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
        }

        if (testIter != 1)
        {
            SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
        }

        if (testIter != 2)
        {
            SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));
        }

        responseContext.Clear();
        SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

        nexus.AdvanceTime(250);

        VerifyOrQuit(responseContext.mReceived);
        VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject);

        iter.Init(admitter.GetInstance());
        VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);
    }

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

    Log("Register as enroller with invalid Steering Data, validate that Admitter rejects");

    for (uint8_t length = 1; length <= 16; length++)
    {
        // Steering Data Length 1 can only be used with
        // `SetPermitAllJoiners()` or empty. Lengths of 8
        // and 16 are valid.

        if ((length == 8) || (length == 16))
        {
            continue;
        }

        Log("Send `EnrollerRegister` with invalid Steering Data length %u, validate that it is rejected", length);

        SuccessOrQuit(steeringData.Init(length));
        SuccessOrQuit(steeringData.UpdateBloomFilter(admitter.Get<Mac::Mac>().GetExtAddress()));

        message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
        SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

        responseContext.Clear();
        SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

        nexus.AdvanceTime(250);

        VerifyOrQuit(responseContext.mReceived);
        VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject);

        iter.Init(admitter.GetInstance());
        VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);
    }

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

    Log("Register as enroller with empty Steering Data, validate that Admitter accepts");

    SuccessOrQuit(steeringData.Init(1));

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    responseContext.Clear();
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContext));

    nexus.AdvanceTime(250);

    VerifyOrQuit(responseContext.mReceived);
    VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept);

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));
    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);
    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);
}

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

void TestBorderAdmitterCommissionerConflictAndPetitionerRetry(void)
{
    static const char kEnrollerId[] = "TestEnroller1234";

    Core                   nexus;
    Node                  &admitter   = nexus.CreateNode();
    Node                  &enroller   = nexus.CreateNode();
    Node                  &otherCommr = nexus.CreateNode();
    Ip6::SockAddr          sockAddr;
    Pskc                   pskc;
    Admitter::Iterator     iter;
    Admitter::EnrollerInfo enrollerInfo;
    Coap::Message         *message;
    uint8_t                mode;
    MeshCoP::SteeringData  steeringData;
    MeshCoP::SteeringData  leaderSteeringData;
    ReceiveContext         recvContext;
    uint16_t               rloc16;
    uint16_t               sessionId;
    uint16_t               numStateChanges;

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

    nexus.AdvanceTime(0);

    // Form the topology:
    // - `admitter` forms the network (as leader)
    // - `otherCommr` joins the same network.
    // - `enroller` stays disconnected.

    admitter.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);

    otherCommr.Join(admitter);

    nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);

    VerifyOrQuit(admitter.Get<Mle::Mle>().IsLeader());
    VerifyOrQuit(otherCommr.Get<Mle::Mle>().IsRouter());

    SuccessOrQuit(enroller.Get<Mac::Mac>().SetPanChannel(admitter.Get<Mac::Mac>().GetPanChannel()));
    enroller.Get<Mac::Mac>().SetPanId(admitter.Get<Mac::Mac>().GetPanId());
    enroller.Get<ThreadNetif>().Up();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Border Admitter on `admitter`");

    admitter.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(admitter.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!admitter.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<Ip6::Filter>().AddUnsecurePort(admitter.Get<Manager>().GetUdpPort()));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Make `otherCommr` the active commissioner");

    SuccessOrQuit(otherCommr.Get<Commissioner>().Start(nullptr, nullptr, nullptr));

    nexus.AdvanceTime(2 * Time::kOneSecondInMsec);

    VerifyOrQuit(otherCommr.Get<Commissioner>().GetState() == Commissioner::kStateActive);

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == otherCommr.Get<Mle::Mle>().GetRloc16());

    nexus.AdvanceTime(5 * Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Establish a DTLS connection from `enroller` to `admitter`");

    sockAddr.SetAddress(admitter.Get<Mle::Mle>().GetLinkLocalAddress());
    sockAddr.SetPort(admitter.Get<Manager>().GetUdpPort());

    admitter.Get<KeyManager>().GetPskc(pskc);
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));

    enroller.Get<Tmf::SecureAgent>().RegisterResourceHandler(HandleResource, &recvContext);

    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().Open(0));
    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().Connect(sockAddr));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(enroller.Get<Tmf::SecureAgent>().IsConnected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerRegister` message from `enroller` to `admitter`");

    message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
    VerifyOrQuit(message != nullptr);

    mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;

    steeringData.SetToPermitAllJoiners();

    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerId));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
    SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

    SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the enroller list on `admitter`");

    iter.Init(admitter.GetInstance());
    SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

    VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
    VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
    VerifyOrQuit(enrollerInfo.mMode == mode);

    VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Since there is another commissioner active, validate that `admitter` fails to become active");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());
    VerifyOrQuit(admitter.Get<Admitter>().IsPetitionRejected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `EnrollerReportState` is received with `ConflictError` state");

    VerifyOrQuit(recvContext.HasReceivedReportState());
    VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterConflictError);

    nexus.AdvanceTime(10 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());
    VerifyOrQuit(admitter.Get<Admitter>().IsPetitionRejected());

    VerifyOrQuit(otherCommr.Get<Commissioner>().GetState() == Commissioner::kStateActive);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Stop `otherCommr` from acting as active commissioner");

    recvContext.Clear();

    SuccessOrQuit(otherCommr.Get<Commissioner>().Stop());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the `admitter` will detect this, petitions, and becomes active commissioner");

    nexus.AdvanceTime(2 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());
    VerifyOrQuit(!admitter.Get<Admitter>().IsPetitionRejected());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `EnrollerReportState` is received now with `Active` state");

    VerifyOrQuit(recvContext.HasReceivedReportState());
    VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterActive);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check the Network Data (Commissioner Data) to be properly set");

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("From `otherCommr` forcefully evict the current active commissioner (`admitter`)");

    recvContext.Clear();

    SuccessOrQuit(otherCommr.Get<Manager>().EvictActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send `EnrollerKeepAlive` three times, 20 seconds apart to maintain the enroller connection");

    for (uint8_t i = 0; i < 3; i++)
    {
        nexus.AdvanceTime(20 * Time::kOneSecondInMsec);

        message = enroller.Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

        SuccessOrQuit(enroller.Get<Tmf::SecureAgent>().SendMessage(*message));

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        iter.Init(admitter.GetInstance());
        SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo));

        VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId));
        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == mode);

        VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the eviction was properly detected, and petitioner retry mechanism did restore it");

    VerifyOrQuit(recvContext.HasReceivedReportState());
    VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterActive);

    numStateChanges = recvContext.mStateReports.GetLength();
    VerifyOrQuit(numStateChanges >= 2);
    VerifyOrQuit(recvContext.mStateReports[numStateChanges - 2].mAdmitterState == kAdmitterReady);
    VerifyOrQuit(recvContext.mStateReports[numStateChanges - 1].mAdmitterState == kAdmitterActive);
}

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

template <uint8_t kNumEnrollers>
uint8_t FindMatchingEnroller(const Admitter::EnrollerInfo &aInfo,
                             const char *(&aEnrollerIds)[kNumEnrollers],
                             BitSet<kNumEnrollers> &aFoundIndexes)
{
    // Finds a matching enroller by comparing `aInfo.mId` against
    // `aEnrollerIds[]` and returns the matched index. This function
    // ensures each enroller is found only once by checking that the
    // index is not already in `aFoundIndexes`, then updates
    // `aFoundIndexes`.

    uint8_t matchedIndex = kNumEnrollers;

    for (uint8_t index = 0; index < kNumEnrollers; index++)
    {
        if (StringMatch(aInfo.mId, aEnrollerIds[index]))
        {
            matchedIndex = index;
            break;
        }
    }

    VerifyOrQuit(matchedIndex < kNumEnrollers);

    VerifyOrQuit(!aFoundIndexes.Has(matchedIndex));
    aFoundIndexes.Add(matchedIndex);

    return matchedIndex;
}

template <uint8_t kNumEnrollers> bool DidFindAllEnrollers(const BitSet<kNumEnrollers> &aFoundIndexes)
{
    bool didFindAll = true;

    for (uint8_t index = 0; index < kNumEnrollers; index++)
    {
        if (!aFoundIndexes.Has(index))
        {
            didFindAll = false;
            break;
        }
    }

    return didFindAll;
}

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

void LogEnroller(const Admitter::EnrollerInfo &aInfo)
{
    Log("   Enroller - id:%s steeringData:%s mode:0x%02x[%c%c]", aInfo.mId,
        AsCoreType(&aInfo.mSteeringData).ToString().AsCString(), aInfo.mMode,
        aInfo.mMode & MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx ? 'J' : '-',
        aInfo.mMode & MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx ? 'U' : '-');
}

void LogJoiner(const Admitter::JoinerInfo &aInfo)
{
    Log("      Joiner - iid:%s, msec-till-expire:%lu", AsCoreType(&aInfo.mIid).ToString().AsCString(),
        ToUlong(aInfo.mMsecTillExpiration));
}

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

void TestBorderAdmitterMultipleEnrollers(void)
{
    static constexpr uint8_t kNumEnrollers = 4;

    static const char *kEnrollerIds[kNumEnrollers] = {"earth", "water", "wind", "fire"};

    Core                   nexus;
    Node                  &admitter = nexus.CreateNode();
    Node                  *enrollers[kNumEnrollers];
    Ip6::SockAddr          sockAddr;
    Pskc                   pskc;
    Admitter::Iterator     iter;
    Admitter::EnrollerInfo enrollerInfo;
    Admitter::JoinerInfo   joinerInfo;
    Coap::Message         *message;
    uint8_t                mode;
    MeshCoP::SteeringData  steeringData[kNumEnrollers];
    ReceiveContext         recvContext[kNumEnrollers];
    ResponseContext        responseContexts[kNumEnrollers];
    BitSet<kNumEnrollers>  foundEnrollers;
    MeshCoP::SteeringData  leaderSteeringData;
    MeshCoP::SteeringData  combinedSteeringData;
    uint16_t               rloc16;
    uint16_t               sessionId;
    Mac::ExtAddress        joinerIid;

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

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        enrollers[i] = &nexus.CreateNode();
    }

    nexus.AdvanceTime(0);

    // Form the topology:
    // - `admitter` forms the network (as leader)
    // - All enrollers stay disconnected.

    admitter.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Mle::Mle>().IsLeader());

    for (Node *enroller : enrollers)
    {
        SuccessOrQuit(enroller->Get<Mac::Mac>().SetPanChannel(admitter.Get<Mac::Mac>().GetPanChannel()));
        enroller->Get<Mac::Mac>().SetPanId(admitter.Get<Mac::Mac>().GetPanId());
        enroller->Get<ThreadNetif>().Up();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Border Admitter on `admitter`");

    admitter.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(admitter.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!admitter.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<Ip6::Filter>().AddUnsecurePort(admitter.Get<Manager>().GetUdpPort()));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Establish a DTLS connection from all `enrollers` to `admitter`");

    sockAddr.SetAddress(admitter.Get<Mle::Mle>().GetLinkLocalAddress());
    sockAddr.SetPort(admitter.Get<Manager>().GetUdpPort());

    admitter.Get<KeyManager>().GetPskc(pskc);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Node *enroller = enrollers[i];

        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));

        recvContext[i].Clear();
        enroller->Get<Tmf::SecureAgent>().RegisterResourceHandler(HandleResource, &recvContext[i]);

        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().Open(0));
        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().Connect(sockAddr));

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        VerifyOrQuit(enroller->Get<Tmf::SecureAgent>().IsConnected());
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Prepare Steering Data for each enroller");

    mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;

    steeringData[0].SetToPermitAllJoiners();

    SuccessOrQuit(steeringData[1].Init(16));

    for (uint8_t numJoiners = 0; numJoiners < 3; numJoiners++)
    {
        joinerIid.GenerateRandom();
        SuccessOrQuit(steeringData[1].UpdateBloomFilter(joinerIid));
    }

    SuccessOrQuit(steeringData[2].Init(8));
    joinerIid.GenerateRandom();
    SuccessOrQuit(steeringData[2].UpdateBloomFilter(joinerIid));

    SuccessOrQuit(steeringData[3].Init(8));
    joinerIid.GenerateRandom();
    SuccessOrQuit(steeringData[3].UpdateBloomFilter(joinerIid));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerRegister` message from all `enrollers`");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        message =
            enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerIds[i]));
        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, mode));
        SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData[i]));

        responseContexts[i].Clear();
        SuccessOrQuit(
            enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
    }

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check that all registrations were accepted");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        VerifyOrQuit(responseContexts[i].mReceived);
        VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
        VerifyOrQuit(responseContexts[i].mHasAdmitterState);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` becomes active commissioner");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData.GetLength() == 1);
    VerifyOrQuit(leaderSteeringData.PermitsAllJoiners());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `EnrollerReportState` is received with the updated Admitter state");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        // Some enrollers may already get the updated state in the Register response
        if (recvContext[i].HasReceivedReportState())
        {
            VerifyOrQuit(recvContext[i].GetLastReportedAdmitterState() == kAdmitterActive);
        }
    }

    // - - - - - - - - - - - - - - - - - - - - - - -. - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the enroller list on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData[matchedIndex]);
        VerifyOrQuit(enrollerInfo.mMode == mode);

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    nexus.AdvanceTime(10 * Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send a keep alive from first enroller with reject status (to unregister the enroller)");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kReject));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kReject);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check enroller info and that enroller 0 is no longer present");

    foundEnrollers.Clear();

    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);
        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData[matchedIndex]);
        VerifyOrQuit(enrollerInfo.mMode == mode);

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(!foundEnrollers.Has(0));
    VerifyOrQuit(foundEnrollers.Has(1));
    VerifyOrQuit(foundEnrollers.Has(2));
    VerifyOrQuit(foundEnrollers.Has(3));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` is still active commissioner and the steering data is updated");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    combinedSteeringData = steeringData[1];
    SuccessOrQuit(combinedSteeringData.MergeBloomFilterWith(steeringData[2]));
    SuccessOrQuit(combinedSteeringData.MergeBloomFilterWith(steeringData[3]));

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == combinedSteeringData);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Wait 20 seconds, send keep-alive from enrollers[2,3] but not from [1]");

    nexus.AdvanceTime(20 * Time::kOneSecondInMsec);

    for (uint8_t i = 2; i < kNumEnrollers; i++)
    {
        message =
            enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

        responseContexts[i].Clear();
        SuccessOrQuit(
            enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
    }

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    for (uint8_t i = 2; i < kNumEnrollers; i++)
    {
        VerifyOrQuit(responseContexts[i].mReceived);
        VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Wait 35 more seconds and check that enroller 1 is now removed");

    nexus.AdvanceTime(35 * Time::kOneSecondInMsec);

    foundEnrollers.Clear();

    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        LogEnroller(enrollerInfo);

        FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(!foundEnrollers.Has(0));
    VerifyOrQuit(!foundEnrollers.Has(1));
    VerifyOrQuit(foundEnrollers.Has(2));
    VerifyOrQuit(foundEnrollers.Has(3));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` updates the steering data");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    combinedSteeringData = steeringData[2];
    SuccessOrQuit(combinedSteeringData.MergeBloomFilterWith(steeringData[3]));

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == combinedSteeringData);
}

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

void TestBorderAdmitterJoinerEnrollerInteraction(void)
{
    static constexpr uint8_t kNumEnrollers = 4;
    static constexpr uint8_t kNumJoiners   = 2;

    static const char *kEnrollerIds[kNumEnrollers] = {"diamond", "ruby", "sapphire", "emerald"};

    static const char kPskd[] = "J01NME1234";

    Core                     nexus;
    Node                    &admitter = nexus.CreateNode();
    Node                    *enrollers[kNumEnrollers];
    Node                    *joiners[kNumJoiners];
    Ip6::SockAddr            sockAddr;
    Pskc                     pskc;
    Coap::Message           *message;
    uint8_t                  modes[kNumEnrollers];
    ResponseContext          responseContexts[kNumEnrollers];
    ReceiveContext           recvContext[kNumEnrollers];
    MeshCoP::SteeringData    steeringData;
    MeshCoP::SteeringData    leaderSteeringData;
    Ip6::InterfaceIdentifier joinerIids[kNumJoiners];
    Ip6::InterfaceIdentifier wildcardJoinerIid;
    Admitter::Iterator       iter;
    Admitter::EnrollerInfo   enrollerInfo;
    Admitter::JoinerInfo     joinerInfo;
    BitSet<kNumEnrollers>    foundEnrollers;
    BitSet<kNumJoiners>      foundJoiners;
    uint16_t                 sessionId;
    uint16_t                 rloc16;

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

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        enrollers[i] = &nexus.CreateNode();
    }

    for (uint8_t j = 0; j < kNumJoiners; j++)
    {
        joiners[j] = &nexus.CreateNode();
    }

    nexus.AdvanceTime(0);

    // Form the topology:
    // - `admitter` forms the network (as leader)
    // - All enrollers stay disconnected.

    admitter.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Mle::Mle>().IsLeader());

    for (Node *enroller : enrollers)
    {
        SuccessOrQuit(enroller->Get<Mac::Mac>().SetPanChannel(admitter.Get<Mac::Mac>().GetPanChannel()));
        enroller->Get<Mac::Mac>().SetPanId(admitter.Get<Mac::Mac>().GetPanId());
        enroller->Get<ThreadNetif>().Up();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Border Admitter on `admitter`");

    admitter.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(admitter.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!admitter.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<Ip6::Filter>().AddUnsecurePort(admitter.Get<Manager>().GetUdpPort()));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Establish a DTLS connection from all `enrollers` to `admitter`");

    sockAddr.SetAddress(admitter.Get<Mle::Mle>().GetLinkLocalAddress());
    sockAddr.SetPort(admitter.Get<Manager>().GetUdpPort());

    admitter.Get<KeyManager>().GetPskc(pskc);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Node *enroller = enrollers[i];

        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));

        recvContext[i].Clear();
        enroller->Get<Tmf::SecureAgent>().RegisterResourceHandler(HandleResource, &recvContext[i]);

        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().Open(0));
        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().Connect(sockAddr));

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        VerifyOrQuit(enroller->Get<Tmf::SecureAgent>().IsConnected());
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Prepare mode for each enroller");

    modes[0] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;
    modes[1] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;
    modes[2] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx;
    modes[3] = MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;

    steeringData.SetToPermitAllJoiners();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerRegister` message from all `enrollers`");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        message =
            enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerIds[i]));
        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, modes[i]));
        SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

        responseContexts[i].Clear();
        SuccessOrQuit(
            enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
    }

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check that all registrations were accepted");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        VerifyOrQuit(responseContexts[i].mReceived);
        VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
        VerifyOrQuit(responseContexts[i].mHasAdmitterState);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` becomes active commissioner");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);
    VerifyOrQuit(leaderSteeringData.PermitsAllJoiners());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the Enroller info on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[0]`");

    joiners[0]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[0]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    joinerIids[0].SetFromExtAddress(joiners[0]->Get<Joiner>().GetId());

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `joiner` `RelayRx` are forwarded to all `enrollers` with `kForwardJoinerRelayRx` mode flag");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx) == 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[0]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Stop `joiners[0]`");

    joiners[0]->Get<Joiner>().Stop();

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerAccept` message from `enrollers[0]` to `admitter` accepting `joiners[0]`");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[0]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the accepted `joiners[0]` is tracked by `enrollers[0]` entry on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo));
            VerifyOrQuit(AsCoreType(&joinerInfo.mIid) == joinerIids[0]);
            LogJoiner(joinerInfo);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[0]` again and validate that its `RelayRx` are only forwarded to `enrollers[0]`");

    joiners[0]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[0]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if (i != 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[0]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[0]->Get<Joiner>().Stop();

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[1]` and validate that its `RelayRx` are forwarded to all `enrollers`");

    joiners[1]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[1]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    joinerIids[1].SetFromExtAddress(joiners[1]->Get<Joiner>().GetId());

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx) == 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[1]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[1]->Get<Joiner>().Stop();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send `EnrollerKeepAlive` message from `enrollers[0]` change its mode to disallow `kForwardJoinerRelayRx`");

    modes[0] = MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, modes[0]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the `enrollers[0]` mode got changed on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo));
            VerifyOrQuit(AsCoreType(&joinerInfo.mIid) == joinerIids[0]);
            LogJoiner(joinerInfo);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[0]` again and validate that its `RelayRx` is still only forwarded to `enrollers[0]`");

    joiners[0]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[0]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if (i != 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[0]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[0]->Get<Joiner>().Stop();

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[1]` and validate that its `RelayRx` is not longer forwarded to `enrollers[0]`");

    joiners[1]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[1]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    joinerIids[1].SetFromExtAddress(joiners[1]->Get<Joiner>().GetId());

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx) == 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[1]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[1]->Get<Joiner>().Stop();

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send `EnrollerKeepAlive` message from `enrollers[0]` to revert its mode back to allowing both Joiner and UDP");

    modes[0] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));
    SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, modes[0]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the `enrollers[0]` mode got changed on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo));
            VerifyOrQuit(AsCoreType(&joinerInfo.mIid) == joinerIids[0]);
            LogJoiner(joinerInfo);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        message =
            enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerKeepAlive);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

        responseContexts[i].Clear();
        SuccessOrQuit(
            enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
    }

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        VerifyOrQuit(responseContexts[i].mReceived);
        VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
        VerifyOrQuit(responseContexts[i].mHasAdmitterState);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerAccept` message from `enrollers[0]` to `admitter` accepting `joiners[1]`");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[1]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that both accepted `joiners` are tracked by `enrollers[0]` on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);
                    }
                }
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[1]` again and validate that its `RelayRx` are only forwarded to `enrollers[0]`");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    joiners[1]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[1]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if (i != 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[1]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[1]->Get<Joiner>().Stop();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("From `enrollers[1]` send `EnrollerJoinerAccept` for `joiners[1]`");

    message =
        enrollers[1]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[1]));

    responseContexts[1].Clear();
    SuccessOrQuit(enrollers[1]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[1]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    Log("Validate that the request is rejected since `joiners[1]` is already accepted by `enrollers[0]`");

    VerifyOrQuit(responseContexts[1].mReceived);
    VerifyOrQuit(responseContexts[1].mResponseState == MeshCoP::StateTlv::kReject);
    VerifyOrQuit(responseContexts[1].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate `joiners[1]` is still accepted by `enrollers[0]`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);
                    }
                }
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[1]` again and validate that its `RelayRx` are only forwarded to `enrollers[0]`");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    joiners[1]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[1]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if (i != 0)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[1]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[1]->Get<Joiner>().Stop();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerAccept` message again accepting `joiners[1]` from `enrollers[0]`");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[1]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that there is no change in the `enrollers` list and the tracked `joiners` on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);
                    }
                }
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerRelease` message from `enrollers[0]` to `admitter` releasing `joiners[0]`");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[0]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that the released `joiners[0]` is removed on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 0)
        {
            SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo));
            VerifyOrQuit(AsCoreType(&joinerInfo.mIid) == joinerIids[1]);
            LogJoiner(joinerInfo);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerRelease` message again releasing `joiners[0]` from `enrollers[0]`");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[0]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    Log("Validate that `EnrollerJoinerRelease` is accepted, even though the given IID is already removed");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerRelease` message releasing `joiners[1]` from `enrollers[0]`");

    message =
        enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease);
    VerifyOrQuit(message != nullptr);

    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[1]));

    responseContexts[0].Clear();
    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[0]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[0].mReceived);
    VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[0].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send two `EnrollerJoinerAccept` messages from `enrollers[2]` accepting both `joiners`");

    for (uint8_t j = 0; j < 2; j++)
    {
        message = enrollers[2]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(
            kUriEnrollerJoinerAccept);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[j]));

        responseContexts[2].Clear();
        SuccessOrQuit(
            enrollers[2]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[2]));

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        VerifyOrQuit(responseContexts[2].mReceived);
        VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kAccept);
        VerifyOrQuit(responseContexts[2].mHasAdmitterState);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that both accepted joiners are tracked by `enrollers[2]` on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 2)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);
                    }
                }
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerJoinerRelease` message from `enrollers[2]` with wildcard IID releasing all joiners");

    message =
        enrollers[2]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease);
    VerifyOrQuit(message != nullptr);

    wildcardJoinerIid.Clear();
    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, wildcardJoinerIid));

    responseContexts[2].Clear();
    SuccessOrQuit(enrollers[2]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[2]));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[2].mReceived);
    VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kAccept);
    VerifyOrQuit(responseContexts[2].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that all previously accepted joiners by `enrollers[2]` on `admitter` are now removed");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an invalid `EnrollerJoinerAccept` message from enrollers[2] with wildcard IID");

    message =
        enrollers[2]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept);
    VerifyOrQuit(message != nullptr);

    wildcardJoinerIid.Clear();
    SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, wildcardJoinerIid));

    responseContexts[2].Clear();
    SuccessOrQuit(enrollers[2]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[2]));

    Log("Validate that the invalid `EnrollerJoinerAccept` is correctly rejected");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(responseContexts[2].mReceived);
    VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kReject);
    VerifyOrQuit(responseContexts[2].mHasAdmitterState);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send two `EnrollerJoinerAccept` messages from `enrollers[2]` accepting both `joiners`");

    for (uint8_t j = 0; j < 2; j++)
    {
        message = enrollers[2]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(
            kUriEnrollerJoinerAccept);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::JoinerIidTlv>(*message, joinerIids[j]));

        responseContexts[2].Clear();
        SuccessOrQuit(
            enrollers[2]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[2]));

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        VerifyOrQuit(responseContexts[2].mReceived);
        VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kAccept);
        VerifyOrQuit(responseContexts[2].mHasAdmitterState);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that both accepted Joiners are tracked by `enrollers[2]` on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 2)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);
                    }
                }

                VerifyOrQuit(joinerInfo.mMsecTillExpiration >= 6 * Time::kOneMinuteInMsec);
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Wait for 4 minutes, sending `EnrollerKeepAlive` every 30 seconds to maintain enroller connections");

    for (uint8_t interval = 0; interval < 4 * 2; interval++)
    {
        Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection");

        for (uint8_t i = 0; i < kNumEnrollers; i++)
        {
            message = enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(
                kUriEnrollerKeepAlive);
            VerifyOrQuit(message != nullptr);

            SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

            responseContexts[i].Clear();
            SuccessOrQuit(
                enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
        }

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        for (uint8_t i = 0; i < kNumEnrollers; i++)
        {
            VerifyOrQuit(responseContexts[i].mReceived);
            VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
            VerifyOrQuit(responseContexts[i].mHasAdmitterState);
        }

        nexus.AdvanceTime(29 * Time::kOneSecondInMsec);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the enroller list on `admitter` and that both joiners are still accepted by `enrollers[2]`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 2)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);
                    }
                }

                // Since we waited for 4 minutes, the joiner expiration time should be closer

                VerifyOrQuit(joinerInfo.mMsecTillExpiration < 6 * Time::kOneMinuteInMsec);
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Start `joiners[1]` and validate that its RelayRx are only forwarded to `enrollers[2]`");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        recvContext[i].Clear();
    }

    joiners[1]->Get<ThreadNetif>().Up();
    SuccessOrQuit(joiners[1]->Get<Joiner>().Start(kPskd,
                                                  /* aProvisioningUrl */ nullptr,
                                                  /* aVendorName */ nullptr,
                                                  /* aVendorModel */ nullptr,
                                                  /* aVendorSwVersion */ nullptr,
                                                  /* aVendorData */ nullptr,
                                                  /* aCallback */ nullptr,
                                                  /* aContext */ nullptr));

    nexus.AdvanceTime(8 * Time::kOneSecondInMsec);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::InterfaceIdentifier readIid;
        uint16_t                 joinerRouterRloc;

        message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead());

        if (i != 2)
        {
            VerifyOrQuit(message == nullptr);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerIidTlv>(*message, readIid));
        SuccessOrQuit(Tlv::Find<MeshCoP::JoinerRouterLocatorTlv>(*message, joinerRouterRloc));

        VerifyOrQuit(readIid == joinerIids[1]);
        VerifyOrQuit(joinerRouterRloc == admitter.Get<Mle::Mle>().GetRloc16());
    }

    joiners[1]->Get<Joiner>().Stop();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate expiration is extended for joiners[1] after it transmitted");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 2)
        {
            uint16_t numJoiners = 0;

            foundJoiners.Clear();

            while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone)
            {
                LogJoiner(joinerInfo);

                numJoiners++;

                for (uint8_t j = 0; j < 2; j++)
                {
                    if (joinerIids[j] == AsCoreType(&joinerInfo.mIid))
                    {
                        VerifyOrQuit(!foundJoiners.Has(j));
                        foundJoiners.Add(j);

                        // `joiners[0]` expiration should still tick down, while `joiners[1]`'s should be extended

                        if (j == 0)
                        {
                            VerifyOrQuit(joinerInfo.mMsecTillExpiration < 6 * Time::kOneMinuteInMsec);
                        }
                        else
                        {
                            VerifyOrQuit(joinerInfo.mMsecTillExpiration >= 6 * Time::kOneMinuteInMsec);
                        }
                    }
                }
            }

            VerifyOrQuit(numJoiners == 2);
        }
        else
        {
            VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
        }
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Wait for another 4 minutes, sending `EnrollerKeepAlive` every 30 seconds");

    for (uint8_t interval = 0; interval < 4 * 2; interval++)
    {
        Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection");

        for (uint8_t i = 0; i < kNumEnrollers; i++)
        {
            message = enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(
                kUriEnrollerKeepAlive);
            VerifyOrQuit(message != nullptr);

            SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

            responseContexts[i].Clear();
            SuccessOrQuit(
                enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
        }

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        for (uint8_t i = 0; i < kNumEnrollers; i++)
        {
            VerifyOrQuit(responseContexts[i].mReceived);
            VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
            VerifyOrQuit(responseContexts[i].mHasAdmitterState);
        }

        nexus.AdvanceTime(29 * Time::kOneSecondInMsec);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `joiners[0]` is timed out and removed on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        if (matchedIndex == 2)
        {
            SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo));

            LogJoiner(joinerInfo);

            VerifyOrQuit(joinerIids[1] == AsCoreType(&joinerInfo.mIid));
            VerifyOrQuit(joinerInfo.mMsecTillExpiration > 0);
        }

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Wait for another 4 minutes, sending `EnrollerKeepAlive` every 30 seconds");

    for (uint8_t interval = 0; interval < 4 * 2; interval++)
    {
        Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection");

        for (uint8_t i = 0; i < kNumEnrollers; i++)
        {
            message = enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(
                kUriEnrollerKeepAlive);
            VerifyOrQuit(message != nullptr);

            SuccessOrQuit(Tlv::Append<MeshCoP::StateTlv>(*message, MeshCoP::StateTlv::kAccept));

            responseContexts[i].Clear();
            SuccessOrQuit(
                enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
        }

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        for (uint8_t i = 0; i < kNumEnrollers; i++)
        {
            VerifyOrQuit(responseContexts[i].mReceived);
            VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
            VerifyOrQuit(responseContexts[i].mHasAdmitterState);
        }

        nexus.AdvanceTime(29 * Time::kOneSecondInMsec);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `joiners[1]` is also timed out and removed on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);
        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));
}

void TestBorderAdmitterForwardingUdpProxy(void)
{
    static constexpr uint8_t kNumEnrollers = 4;

    static const char *kEnrollerIds[kNumEnrollers] = {"1", "2", "3", "4"};

    static const uint8_t kDiagTlvs[] = {NetDiag::Tlv::kExtMacAddress, NetDiag::Tlv::kVersion};

    Core                               nexus;
    Node                              &admitter = nexus.CreateNode();
    Node                              *enrollers[kNumEnrollers];
    Ip6::SockAddr                      sockAddr;
    Pskc                               pskc;
    Coap::Message                     *message;
    Coap::Message                     *diagMessage;
    uint8_t                            modes[kNumEnrollers];
    ResponseContext                    responseContexts[kNumEnrollers];
    ReceiveContext                     recvContext[kNumEnrollers];
    MeshCoP::SteeringData              steeringData;
    MeshCoP::SteeringData              leaderSteeringData;
    Admitter::Iterator                 iter;
    Admitter::EnrollerInfo             enrollerInfo;
    Admitter::JoinerInfo               joinerInfo;
    BitSet<kNumEnrollers>              foundEnrollers;
    uint16_t                           sessionId;
    uint16_t                           rloc16;
    MeshCoP::UdpEncapsulationTlvHeader udpEncapHeader;

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

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        enrollers[i] = &nexus.CreateNode();
    }

    nexus.AdvanceTime(0);

    // Form the topology:
    // - `admitter` forms the network (as leader)
    // - All enrollers stay disconnected.

    admitter.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Mle::Mle>().IsLeader());

    for (Node *enroller : enrollers)
    {
        SuccessOrQuit(enroller->Get<Mac::Mac>().SetPanChannel(admitter.Get<Mac::Mac>().GetPanChannel()));
        enroller->Get<Mac::Mac>().SetPanId(admitter.Get<Mac::Mac>().GetPanId());
        enroller->Get<ThreadNetif>().Up();
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Border Admitter on `admitter`");

    admitter.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(admitter.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!admitter.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<Ip6::Filter>().AddUnsecurePort(admitter.Get<Manager>().GetUdpPort()));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Establish a DTLS connection from all `enrollers` to `admitter`");

    sockAddr.SetAddress(admitter.Get<Mle::Mle>().GetLinkLocalAddress());
    sockAddr.SetPort(admitter.Get<Manager>().GetUdpPort());

    admitter.Get<KeyManager>().GetPskc(pskc);

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Node *enroller = enrollers[i];

        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));

        recvContext[i].Clear();
        enroller->Get<Tmf::SecureAgent>().RegisterResourceHandler(HandleResource, &recvContext[i]);

        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().Open(0));
        SuccessOrQuit(enroller->Get<Tmf::SecureAgent>().Connect(sockAddr));

        nexus.AdvanceTime(Time::kOneSecondInMsec);

        VerifyOrQuit(enroller->Get<Tmf::SecureAgent>().IsConnected());
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Prepare mode for each enroller");

    modes[0] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;
    modes[1] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;
    modes[2] = MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx;
    modes[3] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx;

    steeringData.SetToPermitAllJoiners();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Send an `EnrollerRegister` message from all `enrollers`");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        message =
            enrollers[i]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityConfirmablePostMessage(kUriEnrollerRegister);
        VerifyOrQuit(message != nullptr);

        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerIdTlv>(*message, kEnrollerIds[i]));
        SuccessOrQuit(Tlv::Append<MeshCoP::EnrollerModeTlv>(*message, modes[i]));
        SuccessOrQuit(MeshCoP::SteeringDataTlv::AppendTo(*message, steeringData));

        responseContexts[i].Clear();
        SuccessOrQuit(
            enrollers[i]->Get<Tmf::SecureAgent>().SendMessage(*message, HandleResponse, &responseContexts[i]));
    }

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check that all registrations were accepted");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        VerifyOrQuit(responseContexts[i].mReceived);
        VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept);
        VerifyOrQuit(responseContexts[i].mHasAdmitterState);
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` becomes active commissioner");

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    VerifyOrQuit(admitter.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(admitter.Get<Admitter>().IsActiveCommissioner());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindBorderAgentRloc(rloc16));
    VerifyOrQuit(rloc16 == admitter.Get<Mle::Mle>().GetRloc16());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
    VerifyOrQuit(sessionId == admitter.Get<Admitter>().GetCommissionerSessionId());

    SuccessOrQuit(admitter.Get<NetworkData::Leader>().FindSteeringData(leaderSteeringData));
    VerifyOrQuit(leaderSteeringData == steeringData);
    VerifyOrQuit(leaderSteeringData.PermitsAllJoiners());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the Enroller info on `admitter`");

    foundEnrollers.Clear();
    iter.Init(admitter.GetInstance());

    while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone)
    {
        uint8_t matchedIndex;

        LogEnroller(enrollerInfo);

        matchedIndex = FindMatchingEnroller<kNumEnrollers>(enrollerInfo, kEnrollerIds, foundEnrollers);

        VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData);
        VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]);

        VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound);
    }

    VerifyOrQuit(DidFindAllEnrollers<kNumEnrollers>(foundEnrollers));

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Prepare a `DiagnosticGetQuery` message");

    diagMessage = enrollers[0]->Get<Tmf::Agent>().AllocateAndInitNonConfirmablePostMessage(kUriDiagnosticGetQuery);
    VerifyOrQuit(diagMessage != nullptr);
    SuccessOrQuit(Tlv::Append<NetDiag::TypeListTlv>(*diagMessage, kDiagTlvs, sizeof(kDiagTlvs)));
    diagMessage->WriteMessageId(0);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Embed the `DiagnosticGetQuery` into `ProxyTx` message and send it from `enrollers[0]`");

    message = enrollers[0]->Get<Tmf::SecureAgent>().AllocateAndInitPriorityNonConfirmablePostMessage(kUriProxyTx);
    VerifyOrQuit(message != nullptr);

    udpEncapHeader.SetSourcePort(Tmf::kUdpPort);
    udpEncapHeader.SetDestinationPort(Tmf::kUdpPort);

    SuccessOrQuit(Tlv::AppendTlvHeader(*message, MeshCoP::Tlv::kUdpEncapsulation,
                                       sizeof(udpEncapHeader) + diagMessage->GetLength()));
    SuccessOrQuit(message->Append(udpEncapHeader));
    SuccessOrQuit(message->AppendBytesFromMessage(*diagMessage, 0, diagMessage->GetLength()));
    diagMessage->Free();

    SuccessOrQuit(Tlv::Append<MeshCoP::Ip6AddressTlv>(*message, admitter.Get<Mle::Mle>().GetMeshLocalRloc()));

    SuccessOrQuit(enrollers[0]->Get<Tmf::SecureAgent>().SendMessage(*message));

    nexus.AdvanceTime(Time::kOneSecondInMsec);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate that `admitter` receives the `DiagnosticGetQuery` response");
    Log("And that it forwards it as `ProxyRx` messages to all `enrollers` with `kForwardUdpProxyRx` mode flag");

    for (uint8_t i = 0; i < kNumEnrollers; i++)
    {
        Ip6::Address senderAddr;
        OffsetRange  offsetRange;

        message = AsCoapMessagePtr(recvContext[i].mProxyRxMsgs.GetHead());

        if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx) == 0)
        {
            VerifyOrQuit(message == nullptr);
            Log("   Enroller %s does not set `kForwardUdpProxyRx` mode - so did not get `ProxyRx`", kEnrollerIds[i]);
            continue;
        }

        VerifyOrQuit(message != nullptr);

        VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable);
        VerifyOrQuit(message->ReadCode() == Coap::kCodePost);

        SuccessOrQuit(Tlv::FindTlvValueOffsetRange(*message, MeshCoP::Tlv::kUdpEncapsulation, offsetRange));

        SuccessOrQuit(Tlv::Find<MeshCoP::Ip6AddressTlv>(*message, senderAddr));
        VerifyOrQuit(senderAddr == admitter.Get<Mle::Mle>().GetMeshLocalRloc());

        Log("   Enroller %s received `ProxyRx` from %s", kEnrollerIds[i], senderAddr.ToString().AsCString());
    }
}

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

static constexpr uint32_t kInfraIfIndex = 1;

void ValidateAdmitterMdnsService(Node &aNode)
{
    static const char kDefaultServiceBaseName[] = OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME;

    Dns::Multicast::Core::Iterator  *iterator;
    Dns::Multicast::Core::Service    service;
    Dns::Multicast::Core::EntryState entryState;
    bool                             found = false;

    iterator = aNode.Get<Dns::Multicast::Core>().AllocateIterator();
    VerifyOrQuit(iterator != nullptr);

    while (aNode.Get<Dns::Multicast::Core>().GetNextService(*iterator, service, entryState) == kErrorNone)
    {
        Log("- - - - - - - - - - - - - - - - -");
        Log("  HostName: %s", service.mHostName);
        Log("  ServiceInstance: %s", service.mServiceInstance);
        Log("  ServiceType: %s", service.mServiceType);

        if (StringMatch(service.mServiceType, "_meshcop._udp"))
        {
            for (uint16_t i = 0; i < service.mSubTypeLabelsLength; i++)
            {
                Log("  SubType: %s", service.mSubTypeLabels[i]);
            }

            Log("  Port: %u", service.mPort);
            Log("  TTL: %lu", ToUlong(service.mTtl));

            VerifyOrQuit(StringStartsWith(service.mServiceInstance, kDefaultServiceBaseName));
            VerifyOrQuit(StringStartsWith(service.mHostName, "ot"));
            VerifyOrQuit(service.mPort == aNode.Get<MeshCoP::BorderAgent::Manager>().GetUdpPort());
            VerifyOrQuit(service.mTtl > 0);
            VerifyOrQuit(service.mInfraIfIndex == 1);
            VerifyOrQuit(entryState == OT_MDNS_ENTRY_STATE_REGISTERED);

            if (aNode.Get<Admitter>().IsPrimeAdmitter())
            {
                VerifyOrQuit(service.mSubTypeLabelsLength == 1);
                VerifyOrQuit(StringMatch(service.mSubTypeLabels[0], "_admitter"));
            }
            else
            {
                VerifyOrQuit(service.mSubTypeLabelsLength == 0);
            }

            found = true;
        }
    }

    VerifyOrQuit(found);

    aNode.Get<Dns::Multicast::Core>().FreeIterator(*iterator);
}

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

void TestBorderAdmitterDnssdService(void)
{
    Core  nexus;
    Node &node1 = nexus.CreateNode();
    Node &node2 = nexus.CreateNode();

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

    nexus.AdvanceTime(0);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Enable mDNS
    SuccessOrQuit(node1.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
    VerifyOrQuit(node1.Get<Dns::Multicast::Core>().IsEnabled());
    SuccessOrQuit(node2.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
    VerifyOrQuit(node2.Get<Dns::Multicast::Core>().IsEnabled());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Form the topology.

    node1.Form();
    nexus.AdvanceTime(50 * Time::kOneSecondInMsec);
    node2.Join(node1);

    nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);

    VerifyOrQuit(node1.Get<Mle::Mle>().IsLeader());
    VerifyOrQuit(node2.Get<Mle::Mle>().IsRouter());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Check Border Admitter initial state");

    VerifyOrQuit(!node1.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node2.Get<Admitter>().IsEnabled());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Admitter role on `node1` and validate that it becomes the Prime Admitter");

    node1.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(node1.Get<Admitter>().IsEnabled());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(node1.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!node1.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the registered mDNS MeshCop service by `node1` including `_admitter` sub-type");

    ValidateAdmitterMdnsService(node1);
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the registered mDNS MeshCop service by `node2` (should not have `_admitter` sub-type)");

    ValidateAdmitterMdnsService(node2);
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Enable Admitter role on `node2` and validate that `node1` remains the Prime Admitter");

    node2.Get<Admitter>().SetEnabled(true);
    VerifyOrQuit(node2.Get<Admitter>().IsEnabled());

    nexus.AdvanceTime(45 * Time::kOneSecondInMsec);

    VerifyOrQuit(node1.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!node1.Get<Admitter>().IsActiveCommissioner());
    VerifyOrQuit(!node2.Get<Admitter>().IsPrimeAdmitter());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the registered mDNS MeshCop service by `node1` including `_admitter` sub-type");

    ValidateAdmitterMdnsService(node1);
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the registered mDNS MeshCop service by `node2` (should not have `_admitter` sub-type)");
    ValidateAdmitterMdnsService(node2);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Disable Admitter role on `node1` and check that `node2` becomes the Prime Admitter");

    node1.Get<Admitter>().SetEnabled(false);
    VerifyOrQuit(!node1.Get<Admitter>().IsEnabled());
    VerifyOrQuit(!node1.Get<Admitter>().IsPrimeAdmitter());

    nexus.AdvanceTime(75 * Time::kOneSecondInMsec);

    VerifyOrQuit(node2.Get<Admitter>().IsPrimeAdmitter());
    VerifyOrQuit(!node2.Get<Admitter>().IsActiveCommissioner());

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the registered mDNS MeshCop service by `node1` (no longer publishing `_admitter` sub-type)");

    ValidateAdmitterMdnsService(node1);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Log("Validate the registered mDNS MeshCop service by `node2` (now should include `_admitter` sub-type)");
    ValidateAdmitterMdnsService(node2);
}

} // namespace Nexus
} // namespace ot

int main(void)
{
    ot::Nexus::TestBorderAdmitterPrimeSelection();
    ot::Nexus::TestBorderAdmitterEnrollerInteraction();
    ot::Nexus::TestBorderAdmitterCommissionerConflictAndPetitionerRetry();
    ot::Nexus::TestBorderAdmitterMultipleEnrollers();
    ot::Nexus::TestBorderAdmitterJoinerEnrollerInteraction();
    ot::Nexus::TestBorderAdmitterForwardingUdpProxy();
    ot::Nexus::TestBorderAdmitterDnssdService();

    printf("\nAll tests passed\n");
    return 0;
}
