/*
 *  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/num_utils.hpp"
#include "common/trickle_timer.hpp"
#include "instance/instance.hpp"

namespace ot {

static 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"

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;
    uint32_t           newIntervalMax;

    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();

    newIntervalMax = Min(t + 1, interval - 1);

    timer.SetIntervalMax(newIntervalMax);

    VerifyOrQuit(timer.GetInterval() == newIntervalMax);
    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();

    newIntervalMax = Min(t + 1, interval - 1);

    timer.SetIntervalMax(newIntervalMax);

    VerifyOrQuit(timer.GetInterval() == newIntervalMax);
    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;
}
