[mle] update Avd trickle timer interval based on router neighbor count (#9307)

This commit updates the `TrickleTimer` to allow `IntervalMax` and
`IntervalMin` to be changed while the timer is running. In particular,
when `IntervalMax` is changed to a value that is shorter than the
current interval being used by the timer, the timer will adapt the
new shorter interval and may fire immediately.

A unit test `test_trickle_timer` has been added to validate the
behavior of `TrickleTimer` in detail. All different scenarios where
`IntervalMax` or `IntervalMin` are changed are covered by the
unit test.

The new mechanism to change the trickle timer `IntervalMax` is
used to update the MLE Advertisement trickle timer. The `IntervalMax`
is determined based on the number of router neighbors of the device
with link quality 2 or better. If the device has fewer router neighbors,
it will use a shorter `IntervalMax`. As new links are established
with routers, the `IntervalMax` is recalculated and updated on the
Advertisement trickle timer.

This commit also adds a new test `test-024-mle-adv-imax-change.py` to
validate updates to the `IntervalMax` value based on the number of
router neighbors.

Co-authored-by: David Smith <david.smith@mmbnetworks.com>
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index 7b0edb4..3be5a11 100644
--- a/include/openthread/instance.h
+++ b/include/openthread/instance.h
@@ -53,7 +53,7 @@
  * @note This number versions both OpenThread platform and user APIs.
  *
  */
-#define OPENTHREAD_API_VERSION (348)
+#define OPENTHREAD_API_VERSION (349)
 
 /**
  * @addtogroup api-instance
diff --git a/include/openthread/thread_ftd.h b/include/openthread/thread_ftd.h
index 24154e4..9302623 100644
--- a/include/openthread/thread_ftd.h
+++ b/include/openthread/thread_ftd.h
@@ -882,6 +882,16 @@
 otError otThreadSetRouterIdRange(otInstance *aInstance, uint8_t aMinRouterId, uint8_t aMaxRouterId);
 
 /**
+ * Gets the current Interval Max value used by Advertisement trickle timer.
+ *
+ * This API requires `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE`, and is intended for testing only.
+ *
+ * @returns The Interval Max of Advertisement trickle timer in milliseconds.
+ *
+ */
+uint32_t otThreadGetAdvertisementTrickleIntervalMax(otInstance *aInstance);
+
+/**
  * Indicates whether or not a Router ID is currently allocated.
  *
  * @param[in]  aInstance     A pointer to an OpenThread instance.
diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp
index 5ab340b..fcfee7b 100644
--- a/src/cli/cli.cpp
+++ b/src/cli/cli.cpp
@@ -4135,6 +4135,23 @@
 #endif
 #endif // OPENTHREAD_FTD
 
+#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+/**
+ * @cli mleadvimax
+ * @code
+ * mleadvimax
+ * 12000
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetAdvertisementTrickleIntervalMax
+ */
+template <> otError Interpreter::Process<Cmd("mleadvimax")>(Arg aArgs[])
+{
+    return ProcessGet(aArgs, otThreadGetAdvertisementTrickleIntervalMax);
+}
+#endif
+
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 /**
  * @cli mliid
@@ -8046,6 +8063,9 @@
 #if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
         CmdEntry("meshdiag"),
 #endif
+#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+        CmdEntry("mleadvimax"),
+#endif
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
         CmdEntry("mliid"),
 #endif
diff --git a/src/core/api/thread_ftd_api.cpp b/src/core/api/thread_ftd_api.cpp
index 8b8504b..814e80a 100644
--- a/src/core/api/thread_ftd_api.cpp
+++ b/src/core/api/thread_ftd_api.cpp
@@ -357,6 +357,7 @@
 }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+
 void otThreadSendAddressNotification(otInstance               *aInstance,
                                      otIp6Address             *aDestination,
                                      otIp6Address             *aTarget,
@@ -386,9 +387,7 @@
 {
     AsCoreType(aInstance).Get<Mle::MleRouter>().SetThreadVersionCheckEnabled(aEnabled);
 }
-#endif
 
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 void otThreadGetRouterIdRange(otInstance *aInstance, uint8_t *aMinRouterId, uint8_t *aMaxRouterId)
 {
     AssertPointerIsNotNull(aMinRouterId);
@@ -401,7 +400,13 @@
 {
     return AsCoreType(aInstance).Get<RouterTable>().SetRouterIdRange(aMinRouterId, aMaxRouterId);
 }
-#endif
+
+uint32_t otThreadGetAdvertisementTrickleIntervalMax(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Mle::MleRouter>().GetAdvertisementTrickleIntervalMax();
+}
+
+#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 
 bool otThreadIsRouterIdAllocated(otInstance *aInstance, uint8_t aRouterId)
 {
diff --git a/src/core/common/trickle_timer.cpp b/src/core/common/trickle_timer.cpp
index 7258cd2..2f86f80 100644
--- a/src/core/common/trickle_timer.cpp
+++ b/src/core/common/trickle_timer.cpp
@@ -35,6 +35,7 @@
 
 #include "common/code_utils.hpp"
 #include "common/debug.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 
 namespace ot {
@@ -53,6 +54,158 @@
 {
 }
 
+TimeMilli TrickleTimer::GetStartTimeOfCurrentInterval(void) const
+{
+    // Determines and returns the start time of the current
+    // interval.
+
+    TimeMilli startTime = TimerMilli::GetFireTime();
+
+    if (mMode == kModePlainTimer)
+    {
+        startTime -= mInterval;
+        ExitNow();
+    }
+
+    switch (mPhase)
+    {
+    case kBeforeRandomTime:
+        startTime -= mTimeInInterval;
+        break;
+
+    case kAfterRandomTime:
+        startTime -= mInterval;
+        break;
+    }
+
+exit:
+    return startTime;
+}
+
+void TrickleTimer::SetIntervalMin(uint32_t aIntervalMin)
+{
+    VerifyOrExit(IsRunning());
+
+    mIntervalMin = aIntervalMin;
+
+    if (mIntervalMax < mIntervalMin)
+    {
+        SetIntervalMax(mIntervalMin);
+    }
+
+exit:
+    return;
+}
+
+void TrickleTimer::SetIntervalMax(uint32_t aIntervalMax)
+{
+    TimeMilli endOfInterval;
+
+    VerifyOrExit(IsRunning());
+
+    aIntervalMax = Max(mIntervalMin, aIntervalMax);
+    VerifyOrExit(aIntervalMax != mIntervalMax);
+
+    mIntervalMax = aIntervalMax;
+
+    // If the new `aIntervalMax` is greater than the current
+    // `mInterval`, no action is needed. The new `aIntervalMax` will be
+    // used as the `mInterval` grows.
+
+    VerifyOrExit(mIntervalMax < mInterval);
+
+    // Determine the end of the interval as if the new and shorter
+    // `mIntervalMax` would have been used. The calculated time may
+    // be in the past. In this case, `FireAt(endOfInterval)` will
+    // cause the timer to fire immediately.
+
+    endOfInterval = GetStartTimeOfCurrentInterval() + mIntervalMax;
+
+    if (mMode == kModePlainTimer)
+    {
+        TimerMilli::FireAt(endOfInterval);
+        ExitNow();
+    }
+
+    // Trickle mode possible scenarios:
+    //
+    // We are in `kBeforeRandomTime` phase.
+    //
+    // a) If the new `aIntervalMax < mTimeInInterval`:
+    //    - Reschedule the timer to fire at new `endOfInterval`
+    //      (which may fire immediately).
+    //    - Set `mTimeInInterval = aIntervalMax`
+    //    - Set `mInterval` to use the shorter `aIntervalMax`.
+    //
+    //   |<---- mInterval ----------------^------------------>|
+    //   |<---- mTimeInInterval ----------^---->|             |
+    //   |<---- aIntervalMax -------->|   ^     |             |
+    //   |                            |  now    |             |
+    //
+    // b) If the new `aIntervalMax >= mTimeInInterval`:
+    //    - Keep timer unchanged to fire at `mTimeInInterval`
+    //    - Keep `mTimeInInterval` unchanged.
+    //    - Set `mInterval` to use the shorter `aIntervalMax`.
+    //
+    //   |<---- mInterval ----------------^------------------>|
+    //   |<---- mTimeInInterval ----------^---->|    |        |
+    //   |<---- aIntervalMax -------------^--------->|        |
+    //   |                              now          |        |
+    //
+    // We are in `kAfterRandomTime` phase.
+    //
+    // c) If the new `aIntervalMax < mTimeInInterval`:
+    //    - Act as if current interval is already finished.
+    //    - Reschedule the timer to fire at new `endOfInterval`
+    //      (which should fire immediately).
+    //    - Set `mInterval` to use the shorter `aIntervalMax`.
+    //    - The `mTimeInInterval` value does not matter as we
+    //      are in `kAfterRandomTime` phase. Keep it unchanged.
+    //
+    //   |<---- mInterval ---------------------------^------->|
+    //   |<---- mTimeInInterval --------------->|    ^        |
+    //   |<---- aIntervalMax -------->|         |    ^        |
+    //   |                            |         |   now       |
+    //
+    // d) If the new `aIntervalMax >= mTimeInInterval`:
+    //    - Reschedule the timer to fire at new `endOfInterval`
+    //    - Set `mInterval` to use the shorter `aIntervalMax`.
+    //    - The `mTimeInInterval` value does not matter as we
+    //      are in `kAfterRandomTime` phase. Keep it unchanged.
+    //
+    //   |<---- mInterval ---------------------------^------->|
+    //   |<---- mTimeInInterval --------------->|    ^   |    |
+    //   |<---- aIntervalMax ------------------------^-->|    |
+    //   |                                      |   now  |    |
+
+    // In all cases we need to set `mInterval` to the new
+    // shorter `aIntervalMax`.
+
+    mInterval = aIntervalMax;
+
+    switch (mPhase)
+    {
+    case kBeforeRandomTime:
+        if (aIntervalMax < mTimeInInterval)
+        {
+            mTimeInInterval = aIntervalMax;
+        }
+        else
+        {
+            break;
+        }
+
+        OT_FALL_THROUGH;
+
+    case kAfterRandomTime:
+        TimerMilli::FireAt(endOfInterval);
+        break;
+    }
+
+exit:
+    return;
+}
+
 void TrickleTimer::Start(Mode aMode, uint32_t aIntervalMin, uint32_t aIntervalMax, uint16_t aRedundancyConstant)
 {
     OT_ASSERT((aIntervalMax >= aIntervalMin) && (aIntervalMin > 0));
diff --git a/src/core/common/trickle_timer.hpp b/src/core/common/trickle_timer.hpp
index df2a251..7c3e0ff 100644
--- a/src/core/common/trickle_timer.hpp
+++ b/src/core/common/trickle_timer.hpp
@@ -57,6 +57,8 @@
  */
 class TrickleTimer : public TimerMilli
 {
+    friend class TrickleTimerTester;
+
 public:
     /**
      * Defines the modes of operation for the `TrickleTimer`.
@@ -110,7 +112,45 @@
     Mode GetMode(void) const { return mMode; }
 
     /**
-     * Starts the trickle timer.
+     * Gets the interval min value of the trickle timer.
+     *
+     * @returns The interval min value in milliseconds.
+     *
+     */
+    uint32_t GetIntervalMin(void) const { return mIntervalMin; }
+
+    /**
+     * Sets the interval min value of the trickle timer while timer is running.
+     *
+     * If @p aIntervalMin is smaller than the current `GetIntervalMax()` the interval max value is also updated to
+     * the new @p aIntervalMin (as if `SetIntervalMax(aIntervalMin)` was called).
+     *
+     * @param[in]  aIntervalMin   The minimum interval in milliseconds.
+     *
+     */
+    void SetIntervalMin(uint32_t aIntervalMin);
+
+    /**
+     * Gets the interval max value of the trickle timer.
+     *
+     * @returns The interval max value in milliseconds.
+     *
+     */
+    uint32_t GetIntervalMax(void) const { return mIntervalMax; }
+
+    /**
+     * Sets the interval max value of the trickle timer while timer is running.
+     *
+     * If the given @p aIntervalMax is smaller than the current `GetIntervalMin()`, the interval min value will be
+     * used instead.
+     *
+     * @param[in]  aIntervalMax  The maximum interval in milliseconds.
+     *
+     */
+    void SetIntervalMax(uint32_t aIntervalMax);
+
+    /**
+     * This method starts the trickle timer.
      *
      * @param[in]  aMode                The operation mode of timer (trickle or plain periodic mode).
      * @param[in]  aIntervalMin         The minimum interval for the timer in milliseconds.
@@ -162,6 +202,7 @@
     void        HandleTimer(void);
     void        HandleEndOfTimeInInterval(void);
     void        HandleEndOfInterval(void);
+    TimeMilli   GetStartTimeOfCurrentInterval(void) const;
 
     // Shadow base class `TimerMilli` methods to ensure they are hidden.
     void StartAt(void) {}
diff --git a/src/core/net/ip6_mpl.cpp b/src/core/net/ip6_mpl.cpp
index f39d833..616b031 100644
--- a/src/core/net/ip6_mpl.cpp
+++ b/src/core/net/ip6_mpl.cpp
@@ -340,12 +340,13 @@
     Message *messageCopy = nullptr;
     Metadata metadata;
     uint8_t  hopLimit = 0;
+    uint8_t  interval;
 
 #if OPENTHREAD_CONFIG_MPL_DYNAMIC_INTERVAL_ENABLE
     // adjust the first MPL forward interval dynamically according to the network scale
-    uint8_t interval = (kDataMessageInterval / Mle::kMaxRouters) * Get<RouterTable>().GetNeighborCount();
+    interval = (kDataMessageInterval / Mle::kMaxRouters) * Get<RouterTable>().GetNeighborCount(kLinkQuality1);
 #else
-    uint8_t interval = kDataMessageInterval;
+    interval = kDataMessageInterval;
 #endif
 
     VerifyOrExit(GetTimerExpirations() > 0);
diff --git a/src/core/thread/mle_router.cpp b/src/core/thread/mle_router.cpp
index 7b58175..ddcca05 100644
--- a/src/core/thread/mle_router.cpp
+++ b/src/core/thread/mle_router.cpp
@@ -482,14 +482,38 @@
 
 void MleRouter::StopAdvertiseTrickleTimer(void) { mAdvertiseTrickleTimer.Stop(); }
 
+uint32_t MleRouter::DetermineAdvertiseIntervalMax(void) const
+{
+    uint32_t interval;
+
+#if OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
+    interval = kAdvIntervalMaxLogRoutes;
+#else
+    // Determine the interval based on the number of router neighbors
+    // with link quality 2 or higher.
+
+    interval = (Get<RouterTable>().GetNeighborCount(kLinkQuality2) + 1) * kAdvIntervalNeighborMultiplier;
+    interval = Clamp(interval, kAdvIntervalMaxLowerBound, kAdvIntervalMaxUpperBound);
+#endif
+
+    return interval;
+}
+
+void MleRouter::UpdateAdvertiseInterval(void)
+{
+    if (IsRouterOrLeader() && mAdvertiseTrickleTimer.IsRunning())
+    {
+        mAdvertiseTrickleTimer.SetIntervalMax(DetermineAdvertiseIntervalMax());
+    }
+}
+
 void MleRouter::ResetAdvertiseInterval(void)
 {
     VerifyOrExit(IsRouterOrLeader());
 
     if (!mAdvertiseTrickleTimer.IsRunning())
     {
-        mAdvertiseTrickleTimer.Start(TrickleTimer::kModeTrickle, Time::SecToMsec(kAdvertiseIntervalMin),
-                                     Time::SecToMsec(kAdvertiseIntervalMax));
+        mAdvertiseTrickleTimer.Start(TrickleTimer::kModeTrickle, kAdvIntervalMin, DetermineAdvertiseIntervalMax());
     }
 
     mAdvertiseTrickleTimer.IndicateInconsistent();
@@ -1312,7 +1336,7 @@
             VerifyOrExit(router != nullptr);
 
             if (!router->IsStateValid() && !router->IsStateLinkRequest() &&
-                (mRouterTable.GetNeighborCount() < mChildRouterLinks))
+                (mRouterTable.GetNeighborCount(kLinkQuality1) < mChildRouterLinks))
             {
                 InitNeighbor(*router, aRxInfo);
                 router->SetState(Neighbor::kStateLinkRequest);
diff --git a/src/core/thread/mle_router.hpp b/src/core/thread/mle_router.hpp
index 801722b..2424789 100644
--- a/src/core/thread/mle_router.hpp
+++ b/src/core/thread/mle_router.hpp
@@ -502,6 +502,14 @@
      */
     void ResetAdvertiseInterval(void);
 
+    /**
+     * Updates the MLE Advertisement Trickle timer max interval (if timer is running).
+     *
+     * This is called when there is change in router table.
+     *
+     */
+    void UpdateAdvertiseInterval(void);
+
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     /**
      * Generates an MLE Time Synchronization message.
@@ -532,6 +540,7 @@
     uint8_t GetMaxChildIpAddresses(void) const;
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+
     /**
      * Sets/restores the maximum number of IP addresses that each MTD child may register with this
      * device as parent.
@@ -560,9 +569,27 @@
      *
      */
     void SetThreadVersionCheckEnabled(bool aEnabled) { mThreadVersionCheckEnabled = aEnabled; }
-#endif
+
+    /**
+     * Gets the current Interval Max value used by Advertisement trickle timer.
+     *
+     * @returns The Interval Max of Advertisement trickle timer in milliseconds.
+     *
+     */
+    uint32_t GetAdvertisementTrickleIntervalMax(void) const { return mAdvertiseTrickleTimer.GetIntervalMax(); }
+
+#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 
 private:
+    // Advertisement trickle timer constants - all times are in milliseconds.
+    static constexpr uint32_t kAdvIntervalMin                = 1000;  // I_MIN
+    static constexpr uint32_t kAdvIntervalNeighborMultiplier = 4000;  // Multiplier for I_MAX per router neighbor
+    static constexpr uint32_t kAdvIntervalMaxLowerBound      = 12000; // Lower bound for I_MAX
+    static constexpr uint32_t kAdvIntervalMaxUpperBound      = 32000; // Upper bound for I_MAX
+#if OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
+    constexpr uint32_t kAdvIntervalMaxLogRoutes = 5000;
+#endif
+
     static constexpr uint16_t kDiscoveryMaxJitter            = 250; // Max jitter delay Discovery Responses (in msec).
     static constexpr uint16_t kChallengeTimeout              = 2;   // Challenge timeout (in sec).
     static constexpr uint16_t kUnsolicitedDataResponseJitter = 500; // Max delay for unsol Data Response (in msec).
@@ -606,7 +633,9 @@
     Error ProcessRouteTlv(const RouteTlv &aRouteTlv, RxInfo &aRxInfo);
     Error ReadAndProcessRouteTlvOnFed(RxInfo &aRxInfo, uint8_t aParentId);
 
-    void  StopAdvertiseTrickleTimer(void);
+    void     StopAdvertiseTrickleTimer(void);
+    uint32_t DetermineAdvertiseIntervalMax(void) const;
+
     Error SendAddressSolicit(ThreadStatusTlv::Status aStatus);
     void  SendAddressSolicitResponse(const Coap::Message    &aRequest,
                                      ThreadStatusTlv::Status aResponseStatus,
diff --git a/src/core/thread/mle_types.hpp b/src/core/thread/mle_types.hpp
index c959f9a..32f0ff1 100644
--- a/src/core/thread/mle_types.hpp
+++ b/src/core/thread/mle_types.hpp
@@ -81,7 +81,7 @@
 // Extra one for core Backbone Router Service.
 constexpr uint8_t kMaxServiceAlocs = OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_MAX_ALOCS + 1;
 #else
-constexpr uint8_t  kMaxServiceAlocs      = OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_MAX_ALOCS;
+constexpr uint8_t kMaxServiceAlocs      = OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_MAX_ALOCS;
 #endif
 
 constexpr uint16_t kUdpPort = 19788; ///< MLE UDP Port
@@ -124,7 +124,7 @@
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 constexpr uint8_t kLinkAcceptMaxRouters = 3; ///< Max Route TLV entries in a Link Accept message
 #else
-constexpr uint8_t  kLinkAcceptMaxRouters = 20; ///< Max Route TLV entries in a Link Accept message
+constexpr uint8_t kLinkAcceptMaxRouters = 20; ///< Max Route TLV entries in a Link Accept message
 #endif
 constexpr uint8_t kLinkAcceptSequenceRollback = 64; ///< Route Sequence value rollback in a Link Accept message.
 
@@ -141,13 +141,6 @@
  * Routing Protocol Constants
  *
  */
-constexpr uint32_t kAdvertiseIntervalMin = 1; ///< Min Advertise interval (in sec)
-#if OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
-constexpr uint32_t kAdvertiseIntervalMax = 5; ///< Max Advertise interval (in sec)
-#else
-constexpr uint32_t kAdvertiseIntervalMax = 32; ///< Max Advertise interval (in sec)
-#endif
-
 constexpr uint8_t kFailedRouterTransmissions = 4;
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 constexpr uint8_t kFailedCslDataPollTransmissions = 15;
@@ -160,7 +153,7 @@
 #if OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
 constexpr uint8_t kMaxRouteCost = 127;
 #else
-constexpr uint8_t  kMaxRouteCost         = 16;
+constexpr uint8_t kMaxRouteCost         = 16;
 #endif
 
 constexpr uint8_t kMaxRouterId           = OT_NETWORK_MAX_ROUTER_ID; ///< Max Router ID
diff --git a/src/core/thread/neighbor_table.cpp b/src/core/thread/neighbor_table.cpp
index 46ab1c7..d5f679b 100644
--- a/src/core/thread/neighbor_table.cpp
+++ b/src/core/thread/neighbor_table.cpp
@@ -310,6 +310,13 @@
 #endif
         break;
 
+#if OPENTHREAD_FTD
+    case kRouterAdded:
+    case kRouterRemoved:
+        Get<RouterTable>().SignalTableChanged();
+        break;
+#endif
+
     default:
         break;
     }
diff --git a/src/core/thread/router_table.cpp b/src/core/thread/router_table.cpp
index 407f511..4270000 100644
--- a/src/core/thread/router_table.cpp
+++ b/src/core/thread/router_table.cpp
@@ -343,13 +343,13 @@
     return (!mRouters.IsEmpty()) ? Time::MsecToSec(TimerMilli::GetNow() - mRouterIdSequenceLastUpdated) : 0xffffffff;
 }
 
-uint8_t RouterTable::GetNeighborCount(void) const
+uint8_t RouterTable::GetNeighborCount(LinkQuality aLinkQuality) const
 {
     uint8_t count = 0;
 
     for (const Router &router : mRouters)
     {
-        if (router.IsStateValid())
+        if (router.IsStateValid() && (router.GetLinkQualityIn() >= aLinkQuality))
         {
             count++;
         }
@@ -881,6 +881,8 @@
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
     Get<Utils::HistoryTracker>().RecordRouterTableChange();
 #endif
+
+    Get<Mle::MleRouter>().UpdateAdvertiseInterval();
 }
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
diff --git a/src/core/thread/router_table.hpp b/src/core/thread/router_table.hpp
index bed25ab..7419489 100644
--- a/src/core/thread/router_table.hpp
+++ b/src/core/thread/router_table.hpp
@@ -329,12 +329,14 @@
     bool IsRouteTlvIdSequenceMoreRecent(const Mle::RouteTlv &aRouteTlv) const;
 
     /**
-     * Returns the number of neighbor links.
+     * Gets the number of router neighbors with `GetLinkQualityIn()` better than or equal to a given threshold.
      *
-     * @returns The number of neighbor links.
+     * @param[in] aLinkQuality  Link quality threshold.
+     *
+     * @returns Number of router neighbors with link quality of @o aLinkQuality or better.
      *
      */
-    uint8_t GetNeighborCount(void) const;
+    uint8_t GetNeighborCount(LinkQuality aLinkQuality) const;
 
     /**
      * Indicates whether or not a Router ID is allocated.
diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py
index a086c42..39427cd 100644
--- a/tests/toranj/cli/cli.py
+++ b/tests/toranj/cli/cli.py
@@ -458,6 +458,12 @@
         return counter
 
     #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # Misc
+
+    def get_mle_adv_imax(self):
+        return self._cli_single_output('mleadvimax')
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # UDP
 
     def udp_open(self):
diff --git a/tests/toranj/cli/test-024-mle-adv-imax-change.py b/tests/toranj/cli/test-024-mle-adv-imax-change.py
new file mode 100755
index 0000000..b981622
--- /dev/null
+++ b/tests/toranj/cli/test-024-mle-adv-imax-change.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2023, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+
+from cli import verify
+from cli import verify_within
+import cli
+import time
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test description:
+#
+# Validate changes to `IntervalMax` for MLE Advertisement Trickle Timer based on number of
+# router neighbors of the device.
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 20
+cli.Node.set_time_speedup_factor(speedup)
+
+leader = cli.Node()
+routers = []
+for num in range(0, 9):
+    routers.append(cli.Node())
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+leader.form('mle-adv-imax')
+
+verify(leader.get_state() == 'leader')
+
+# The Imax is determined as `Clamp((n + 1) * 4, 12, 32)` with `n` as
+# number of router neighbors with link quality 2 or higher
+
+verify(int(leader.get_mle_adv_imax()) == 12000)
+
+expected_neighbor_count = 0
+
+
+def check_leader_has_expected_number_of_neighbors():
+    verify(len(leader.get_neighbor_table()) == expected_neighbor_count)
+
+
+# Add two routers one by one and check that Imax
+# remains at 12 seconds.
+
+for num in range(0, 2):
+    r = routers[num]
+
+    r.join(leader)
+    verify(r.get_state() == 'router')
+
+    expected_neighbor_count += 1
+    verify_within(check_leader_has_expected_number_of_neighbors, 10)
+
+    verify(int(leader.get_mle_adv_imax()) == 12000)
+
+# Adding the third router, we should see Imax increasing
+# to 16 seconds.
+
+r = routers[2]
+r.join(leader)
+verify(r.get_state() == 'router')
+
+expected_neighbor_count += 1
+verify_within(check_leader_has_expected_number_of_neighbors, 10)
+
+verify(int(leader.get_mle_adv_imax()) == 16000)
+
+# Adding a neighbor with poor link quality which should not
+# count.
+
+r_poor_lqi = routers[3]
+leader.set_macfilter_lqi_to_node(r_poor_lqi, 1)
+
+r_poor_lqi.join(leader)
+verify(r_poor_lqi.get_state() == 'router')
+
+expected_neighbor_count += 1
+verify_within(check_leader_has_expected_number_of_neighbors, 10)
+verify(int(leader.get_mle_adv_imax()) == 16000)
+
+expected_imax = 16000
+
+# Add four new routers one by one and check that Imax is
+# increased by 4 second for each new router neighbor up to
+# 32 seconds.
+
+for num in range(4, 8):
+    r = routers[num]
+
+    r.join(leader)
+    verify(r.get_state() == 'router')
+
+    expected_neighbor_count += 1
+    verify_within(check_leader_has_expected_number_of_neighbors, 10)
+    expected_imax += 4000
+    verify(int(leader.get_mle_adv_imax()) == expected_imax)
+
+# Check that Imax does not increase beyond 32 seconds.
+
+r = routers[8]
+
+r.join(leader)
+verify(r.get_state() == 'router')
+
+expected_neighbor_count += 1
+verify_within(check_leader_has_expected_number_of_neighbors, 10)
+
+verify(int(leader.get_mle_adv_imax()) == 32000)
+
+# Check that all routers see each other as neighbor and they are all also
+# using 32 seconds as Imax.
+
+
+def check_all_routers_have_expected_number_of_neighbors():
+    for r in routers:
+        verify(len(r.get_neighbor_table()) == expected_neighbor_count)
+
+
+verify_within(check_all_routers_have_expected_number_of_neighbors, 10)
+
+for r in routers:
+    verify(int(r.get_mle_adv_imax()) == 32000)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh
index 33766ae..609faf7 100755
--- a/tests/toranj/start.sh
+++ b/tests/toranj/start.sh
@@ -188,6 +188,7 @@
     run cli/test-021-br-route-prf.py
     run cli/test-022-netdata-full.py
     run cli/test-023-mesh-diag.py
+    run cli/test-024-mle-adv-imax-change.py
     run cli/test-400-srp-client-server.py
     run cli/test-601-channel-manager-channel-change.py
     # Skip the "channel-select" test on a TREL only radio link, since it
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index ca963d2..51f4b0f 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -1082,6 +1082,27 @@
 
 add_test(NAME ot-test-timer COMMAND ot-test-timer)
 
+add_executable(ot-test-trickle-timer
+    test_trickle_timer.cpp
+)
+
+target_include_directories(ot-test-trickle-timer
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-trickle-timer
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-trickle-timer
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-trickle-timer COMMAND ot-test-trickle-timer)
+
 add_executable(ot-test-tlv
     test_tlv.cpp
 )
diff --git a/tests/unit/test_trickle_timer.cpp b/tests/unit/test_trickle_timer.cpp
new file mode 100644
index 0000000..f5851f2
--- /dev/null
+++ b/tests/unit/test_trickle_timer.cpp
@@ -0,0 +1,516 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "test_platform.h"
+
+#include "common/code_utils.hpp"
+#include "common/debug.hpp"
+#include "common/instance.hpp"
+#include "common/num_utils.hpp"
+#include "common/trickle_timer.hpp"
+
+static ot::Instance *sInstance;
+
+static uint32_t sNow = 0;
+static uint32_t sAlarmTime;
+static bool     sAlarmOn = false;
+
+extern "C" {
+
+void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; }
+
+void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
+{
+    sAlarmOn   = true;
+    sAlarmTime = aT0 + aDt;
+}
+
+uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
+
+} // extern "C"
+
+namespace ot {
+
+void AdvanceTime(uint32_t aDuration)
+{
+    uint32_t time = sNow + aDuration;
+
+    while (TimeMilli(sAlarmTime) <= TimeMilli(time))
+    {
+        sNow = sAlarmTime;
+        otPlatAlarmMilliFired(sInstance);
+    }
+
+    sNow = time;
+}
+
+class TrickleTimerTester : public TrickleTimer
+{
+public:
+    explicit TrickleTimerTester(Instance &aInstance)
+        : TrickleTimer(aInstance, HandleTimerFired)
+        , mDidFire(false)
+    {
+    }
+
+    Time     GetFireTime(void) const { return TimerMilli::GetFireTime(); }
+    uint32_t GetInterval(void) const { return TrickleTimer::mInterval; }
+    uint32_t GetTimeInInterval(void) const { return TrickleTimer::mTimeInInterval; }
+
+    void VerifyTimerDidFire(void)
+    {
+        VerifyOrQuit(mDidFire);
+        mDidFire = false;
+    }
+
+    void VerifyTimerDidNotFire(void) const { VerifyOrQuit(!mDidFire); }
+
+    static void RemoveAll(Instance &aInstance) { TimerMilli::RemoveAll(aInstance); }
+
+private:
+    static void HandleTimerFired(TrickleTimer &aTimer) { static_cast<TrickleTimerTester &>(aTimer).HandleTimerFired(); }
+    void        HandleTimerFired(void) { mDidFire = true; }
+
+    bool mDidFire;
+};
+
+void AlarmFired(otInstance *aInstance) { otPlatAlarmMilliFired(aInstance); }
+
+void TestTrickleTimerPlainMode(void)
+{
+    static constexpr uint32_t kMinInterval = 2000;
+    static constexpr uint32_t kMaxInterval = 5000;
+
+    Instance          *instance = testInitInstance();
+    TrickleTimerTester timer(*instance);
+    uint32_t           interval;
+
+    sInstance = instance;
+    TrickleTimerTester::RemoveAll(*instance);
+
+    printf("TestTrickleTimerPlainMode() ");
+
+    // Validate that timer picks a random interval between min and max
+    // on start.
+
+    sNow = 1000;
+    timer.Start(TrickleTimer::kModePlainTimer, kMinInterval, kMaxInterval, 0);
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMax() == kMaxInterval);
+    VerifyOrQuit(timer.GetIntervalMin() == kMinInterval);
+
+    interval = timer.GetInterval();
+    VerifyOrQuit((interval >= kMinInterval) && (interval <= kMaxInterval));
+
+    for (uint8_t iter = 0; iter <= 10; iter++)
+    {
+        AdvanceTime(interval);
+
+        timer.VerifyTimerDidFire();
+
+        // The plain mode trickle timer restarts with a new random
+        // interval between min and max.
+
+        VerifyOrQuit(timer.IsRunning());
+        interval = timer.GetInterval();
+        VerifyOrQuit((interval >= kMinInterval) && (interval <= kMaxInterval));
+    }
+
+    printf(" --> PASSED\n");
+
+    testFreeInstance(instance);
+}
+
+void TestTrickleTimerTrickleMode(uint32_t aRedundancyConstant, uint32_t aConsistentCalls)
+{
+    static constexpr uint32_t kMinInterval = 1000;
+    static constexpr uint32_t kMaxInterval = 9000;
+
+    Instance          *instance = testInitInstance();
+    TrickleTimerTester timer(*instance);
+    uint32_t           interval;
+    uint32_t           t;
+
+    sInstance = instance;
+    TrickleTimerTester::RemoveAll(*instance);
+
+    printf("TestTrickleTimerTrickleMode(aRedundancyConstant:%u, aConsistentCalls:%u) ", aRedundancyConstant,
+           aConsistentCalls);
+
+    sNow = 1000;
+    timer.Start(TrickleTimer::kModeTrickle, kMinInterval, kMaxInterval, aRedundancyConstant);
+
+    // Validate that trickle timer starts with random interval between
+    // min/max.
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMax() == kMaxInterval);
+    VerifyOrQuit(timer.GetIntervalMin() == kMinInterval);
+
+    interval = timer.GetInterval();
+    VerifyOrQuit((kMinInterval <= interval) && (interval <= kMaxInterval));
+    t = timer.GetTimeInInterval();
+    VerifyOrQuit((interval / 2 <= t) && (t <= interval));
+
+    // After `IndicateInconsistent()` should go back to min
+    // interval.
+
+    timer.IndicateInconsistent();
+
+    VerifyOrQuit(timer.IsRunning());
+    interval = timer.GetInterval();
+    VerifyOrQuit(interval == kMinInterval);
+    t = timer.GetTimeInInterval();
+    VerifyOrQuit((interval / 2 <= t) && (t <= interval));
+
+    for (uint8_t iter = 0; iter < 10; iter++)
+    {
+        for (uint32_t index = 0; index < aConsistentCalls; index++)
+        {
+            timer.IndicateConsistent();
+        }
+
+        AdvanceTime(t);
+
+        if (aConsistentCalls < aRedundancyConstant)
+        {
+            timer.VerifyTimerDidFire();
+        }
+        else
+        {
+            timer.VerifyTimerDidNotFire();
+        }
+
+        AdvanceTime(interval - t);
+
+        // Verify that interval is doubling each time up
+        // to max interval.
+
+        VerifyOrQuit(timer.IsRunning());
+        VerifyOrQuit(timer.GetInterval() == Min(interval * 2, kMaxInterval));
+
+        interval = timer.GetInterval();
+        t        = timer.GetTimeInInterval();
+        VerifyOrQuit((interval / 2 <= t) && (t <= interval));
+    }
+
+    AdvanceTime(t);
+
+    timer.IndicateInconsistent();
+
+    VerifyOrQuit(timer.IsRunning());
+    interval = timer.GetInterval();
+    VerifyOrQuit(interval == kMinInterval);
+
+    printf(" --> PASSED\n");
+
+    testFreeInstance(instance);
+}
+
+void TestTrickleTimerMinMaxIntervalChange(void)
+{
+    Instance          *instance = testInitInstance();
+    TrickleTimerTester timer(*instance);
+    TimeMilli          fireTime;
+    uint32_t           interval;
+    uint32_t           t;
+
+    sInstance = instance;
+    TrickleTimerTester::RemoveAll(*instance);
+
+    printf("TestTrickleTimerMinMaxIntervalChange()");
+
+    sNow = 1000;
+    timer.Start(TrickleTimer::kModeTrickle, 2000, 4000);
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMin() == 2000);
+    VerifyOrQuit(timer.GetIntervalMax() == 4000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that `SetIntervalMin()` to a larger value than
+    // previously set does not impact the current interval.
+
+    timer.IndicateInconsistent();
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+    fireTime = timer.GetFireTime();
+
+    VerifyOrQuit(interval == 2000);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+
+    // Change `IntervalMin` before time `t`.
+
+    timer.SetIntervalMin(3000);
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMin() == 3000);
+    VerifyOrQuit(timer.GetIntervalMax() == 4000);
+
+    VerifyOrQuit(interval == timer.GetInterval());
+    VerifyOrQuit(t == timer.GetTimeInInterval());
+    VerifyOrQuit(fireTime == timer.GetFireTime());
+
+    AdvanceTime(t);
+    timer.VerifyTimerDidFire();
+    fireTime = timer.GetFireTime();
+
+    // Change `IntervalMin` after time `t`.
+
+    timer.SetIntervalMin(3500);
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMin() == 3500);
+    VerifyOrQuit(timer.GetIntervalMax() == 4000);
+
+    VerifyOrQuit(interval == timer.GetInterval());
+    VerifyOrQuit(t == timer.GetTimeInInterval());
+    VerifyOrQuit(fireTime == timer.GetFireTime());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that `SetIntervalMin()` to a smaller value
+    // also does not impact the current interval.
+
+    timer.IndicateInconsistent();
+
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+    fireTime = timer.GetFireTime();
+
+    VerifyOrQuit(interval == 3500);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+
+    // Change `IntervalMin` before time `t`.
+
+    timer.SetIntervalMin(3000);
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMin() == 3000);
+    VerifyOrQuit(timer.GetIntervalMax() == 4000);
+
+    VerifyOrQuit(interval == timer.GetInterval());
+    VerifyOrQuit(t == timer.GetTimeInInterval());
+    VerifyOrQuit(fireTime == timer.GetFireTime());
+
+    AdvanceTime(t);
+    timer.VerifyTimerDidFire();
+    fireTime = timer.GetFireTime();
+
+    // Change `IntervalMin` after time `t`.
+
+    timer.SetIntervalMin(2000);
+
+    VerifyOrQuit(timer.IsRunning());
+    VerifyOrQuit(timer.GetIntervalMin() == 2000);
+    VerifyOrQuit(timer.GetIntervalMax() == 4000);
+
+    VerifyOrQuit(interval == timer.GetInterval());
+    VerifyOrQuit(t == timer.GetTimeInInterval());
+    VerifyOrQuit(fireTime == timer.GetFireTime());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that changing `IntervalMax` to a larger value
+    // than the current interval being used by timer, does not
+    // impact the current internal.
+
+    timer.IndicateInconsistent();
+
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+    fireTime = timer.GetFireTime();
+
+    VerifyOrQuit(interval == 2000);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+
+    // Change `IntervalMax` before time `t`.
+
+    timer.SetIntervalMax(2500);
+
+    VerifyOrQuit(timer.GetIntervalMax() == 2500);
+    VerifyOrQuit(timer.IsRunning());
+
+    VerifyOrQuit(interval == timer.GetInterval());
+    VerifyOrQuit(t == timer.GetTimeInInterval());
+    VerifyOrQuit(fireTime == timer.GetFireTime());
+
+    AdvanceTime(t);
+
+    timer.VerifyTimerDidFire();
+
+    fireTime = timer.GetFireTime();
+
+    // Change `IntervalMax` after time `t`.
+
+    timer.SetIntervalMax(3000);
+
+    VerifyOrQuit(interval == timer.GetInterval());
+    VerifyOrQuit(t == timer.GetTimeInInterval());
+    VerifyOrQuit(fireTime == timer.GetFireTime());
+
+    timer.Stop();
+    VerifyOrQuit(!timer.IsRunning());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check behavior when the new `IntervalMax` is smaller
+    // than the current interval being used by timer.
+
+    // New `Imax` is smaller than `t` and before now.
+    //
+    //   |<---- interval --^-------------------------------->|
+    //   |<---- t ---------^------------------>|             |
+    //   |<---- new Imax --^--->|              |             |
+    //   |                now   |              |             |
+
+    timer.Start(TrickleTimer::kModeTrickle, 2000, 2000);
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+    fireTime = timer.GetFireTime();
+
+    VerifyOrQuit(interval == 2000);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+    timer.SetIntervalMin(500);
+
+    AdvanceTime(100);
+    timer.VerifyTimerDidNotFire();
+
+    timer.SetIntervalMax(500);
+
+    VerifyOrQuit(timer.GetInterval() == 500);
+    VerifyOrQuit(timer.GetTimeInInterval() == 500);
+    VerifyOrQuit(timer.GetFireTime() != fireTime);
+    timer.VerifyTimerDidNotFire();
+
+    AdvanceTime(400);
+    timer.VerifyTimerDidFire();
+
+    // New `Imax` is smaller than `t` and after now.
+    //
+    //   |<---- interval --------------^-------------------->|
+    //   |<---- t ---------------------^------>|             |
+    //   |<---- new Imax ------>|      ^       |             |
+    //   |                      |     now      |             |
+
+    timer.Start(TrickleTimer::kModeTrickle, 2000, 2000);
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+    fireTime = timer.GetFireTime();
+
+    VerifyOrQuit(interval == 2000);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+    timer.SetIntervalMin(500);
+
+    AdvanceTime(800);
+    timer.VerifyTimerDidNotFire();
+
+    timer.SetIntervalMax(500);
+
+    VerifyOrQuit(timer.GetInterval() == 500);
+    VerifyOrQuit(timer.GetTimeInInterval() == 500);
+    VerifyOrQuit(timer.GetFireTime() != fireTime);
+    timer.VerifyTimerDidNotFire();
+
+    AdvanceTime(0);
+    timer.VerifyTimerDidFire();
+
+    // New `Imax` is larger than `t` and before now.
+    //
+    //   |<---- interval --------------------------------^-->|
+    //   |<---- t ---------------------------->|         ^   |
+    //   |<---- new Imax --------------------------->|   ^   |
+    //   |                                     |     |  now  |
+
+    timer.Start(TrickleTimer::kModeTrickle, 2000, 2000);
+
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+
+    VerifyOrQuit(interval == 2000);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+    timer.SetIntervalMin(500);
+
+    AdvanceTime(1999);
+    timer.VerifyTimerDidFire();
+
+    timer.SetIntervalMax(t + 1);
+
+    VerifyOrQuit(timer.GetInterval() == t + 1);
+    fireTime = timer.GetFireTime();
+
+    // Check that new interval is started immediately.
+    AdvanceTime(0);
+    timer.VerifyTimerDidNotFire();
+    VerifyOrQuit(fireTime != timer.GetFireTime());
+    VerifyOrQuit(timer.GetInterval() == timer.GetIntervalMax());
+
+    // New `Imax` is larger than `t` and after now.
+    //
+    //   |<---- interval -------------------------^--------->|
+    //   |<---- t ---------------------------->|  ^          |
+    //   |<---- new Imax -------------------------^->|       |
+    //   |                                     | now |       |
+
+    timer.Start(TrickleTimer::kModeTrickle, 2000, 2000);
+
+    interval = timer.GetInterval();
+    t        = timer.GetTimeInInterval();
+
+    VerifyOrQuit(interval == 2000);
+    VerifyOrQuit((interval / 2 <= t) && (t < interval));
+    timer.SetIntervalMin(500);
+
+    AdvanceTime(t);
+    timer.VerifyTimerDidFire();
+
+    timer.SetIntervalMax(t + 1);
+
+    VerifyOrQuit(timer.GetInterval() == t + 1);
+    fireTime = timer.GetFireTime();
+
+    AdvanceTime(1);
+    timer.VerifyTimerDidNotFire();
+    VerifyOrQuit(fireTime != timer.GetFireTime());
+    VerifyOrQuit(timer.GetInterval() == timer.GetIntervalMax());
+
+    printf(" --> PASSED\n");
+
+    testFreeInstance(instance);
+}
+
+} // namespace ot
+
+int main(void)
+{
+    ot::TestTrickleTimerPlainMode();
+    ot::TestTrickleTimerTrickleMode(/* aRedundancyConstant */ 5, /* aConsistentCalls */ 3);
+    ot::TestTrickleTimerTrickleMode(/* aRedundancyConstant */ 3, /* aConsistentCalls */ 3);
+    ot::TestTrickleTimerMinMaxIntervalChange();
+
+    printf("All tests passed\n");
+    return 0;
+}