// Copyright 2020 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#include <chrono>

#include "gtest/gtest.h"
#include "pw_chrono/system_clock.h"
#include "pw_chrono/system_timer.h"
#include "pw_sync/thread_notification.h"

using namespace std::chrono_literals;

namespace pw::chrono {
namespace {

// We can't control the SystemClock's period configuration, so just in case
// duration cannot be accurately expressed in integer ticks, round the
// duration up.
constexpr SystemClock::duration kRoundedArbitraryShortDuration =
    SystemClock::for_at_least(42ms);
constexpr SystemClock::duration kRoundedArbitraryLongDuration =
    SystemClock::for_at_least(1s);

void ShouldNotBeInvoked(SystemClock::time_point) { FAIL(); }

TEST(SystemTimer, CancelInactive) {
  SystemTimer timer(ShouldNotBeInvoked);
  timer.Cancel();
}

TEST(SystemTimer, CancelExplicitly) {
  SystemTimer timer(ShouldNotBeInvoked);
  timer.InvokeAfter(kRoundedArbitraryLongDuration);
  timer.Cancel();
}

TEST(SystemTimer, CancelThroughDestruction) {
  SystemTimer timer(ShouldNotBeInvoked);
  timer.InvokeAfter(kRoundedArbitraryLongDuration);
}

TEST(SystemTimer, CancelThroughRescheduling) {
  SystemTimer timer(ShouldNotBeInvoked);
  timer.InvokeAfter(kRoundedArbitraryLongDuration);
  // Cancel the first with this rescheduling.
  timer.InvokeAfter(kRoundedArbitraryLongDuration);
  timer.Cancel();
}

// Helper class to let test cases easily instantiate a timer with a handler
// and its own context.
class TimerWithHandler {
 public:
  TimerWithHandler()
      : timer_([this](SystemClock::time_point expired_deadline) {
          this->OnExpiryCallback(expired_deadline);
        }) {}
  virtual ~TimerWithHandler() = default;

  // To be implemented by the test case.
  virtual void OnExpiryCallback(SystemClock::time_point expired_deadline) = 0;

  SystemTimer& timer() { return timer_; }

 private:
  SystemTimer timer_;
};

TEST(SystemTimer, StaticInvokeAt) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point expired_deadline) override {
      EXPECT_GE(SystemClock::now(), expired_deadline);
      EXPECT_EQ(expired_deadline, expected_deadline);
      callback_ran_notification.release();
    };

    SystemClock::time_point expected_deadline;
    sync::ThreadNotification callback_ran_notification;
  };
  static TimerWithContext uut;

  uut.expected_deadline = SystemClock::now() + kRoundedArbitraryShortDuration;
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callback_ran_notification.acquire();

  // Ensure you can re-use the timer.
  uut.expected_deadline = SystemClock::now() + kRoundedArbitraryShortDuration;
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callback_ran_notification.acquire();
}

TEST(SystemTimer, InvokeAt) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point expired_deadline) override {
      EXPECT_GE(SystemClock::now(), expired_deadline);
      EXPECT_EQ(expired_deadline, expected_deadline);
      callback_ran_notification.release();
    };

    SystemClock::time_point expected_deadline;
    sync::ThreadNotification callback_ran_notification;
  };
  TimerWithContext uut;

  uut.expected_deadline = SystemClock::now() + kRoundedArbitraryShortDuration;
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callback_ran_notification.acquire();

  // Ensure you can re-use the timer.
  uut.expected_deadline = SystemClock::now() + kRoundedArbitraryShortDuration;
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callback_ran_notification.acquire();

  // Ensure scheduling it in the past causes it to execute immediately.
  uut.expected_deadline = SystemClock::now() - SystemClock::duration(1);
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callback_ran_notification.acquire();
}

TEST(SystemTimer, InvokeAfter) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point expired_deadline) override {
      EXPECT_GE(SystemClock::now(), expired_deadline);
      EXPECT_GE(expired_deadline, expected_min_deadline);
      callback_ran_notification.release();
    };

    SystemClock::time_point expected_min_deadline;
    sync::ThreadNotification callback_ran_notification;
  };
  TimerWithContext uut;

  uut.expected_min_deadline =
      SystemClock::TimePointAfterAtLeast(kRoundedArbitraryShortDuration);
  uut.timer().InvokeAfter(kRoundedArbitraryShortDuration);
  uut.callback_ran_notification.acquire();

  // Ensure you can re-use the timer.
  uut.expected_min_deadline =
      SystemClock::TimePointAfterAtLeast(kRoundedArbitraryShortDuration);
  uut.timer().InvokeAfter(kRoundedArbitraryShortDuration);
  uut.callback_ran_notification.acquire();

  // Ensure scheduling it immediately works.
  uut.expected_min_deadline = SystemClock::now();
  uut.timer().InvokeAfter(SystemClock::duration(0));
  uut.callback_ran_notification.acquire();
}

TEST(SystemTimer, CancelFromCallback) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point) override {
      timer().Cancel();
      callback_ran_notification.release();
    };

    sync::ThreadNotification callback_ran_notification;
  };
  TimerWithContext uut;

  uut.timer().InvokeAfter(kRoundedArbitraryShortDuration);
  uut.callback_ran_notification.acquire();
}

TEST(SystemTimer, RescheduleAndCancelFromCallback) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point) override {
      timer().InvokeAfter(kRoundedArbitraryShortDuration);
      timer().Cancel();
      callback_ran_notification.release();
    };

    sync::ThreadNotification callback_ran_notification;
  };
  TimerWithContext uut;

  uut.timer().InvokeAfter(kRoundedArbitraryShortDuration);
  uut.callback_ran_notification.acquire();
}

TEST(SystemTimer, RescheduleFromCallback) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point expired_deadline) override {
      EXPECT_GE(SystemClock::now(), expired_deadline);

      EXPECT_EQ(expired_deadline, expected_deadline);
      invocation_count++;
      ASSERT_LE(invocation_count, kRequiredInvocations);
      if (invocation_count < kRequiredInvocations) {
        expected_deadline = expired_deadline + kPeriod;
        timer().InvokeAt(expected_deadline);
      } else {
        callbacks_done_notification.release();
      }
    };

    const uint8_t kRequiredInvocations = 5;
    const SystemClock::duration kPeriod = kRoundedArbitraryShortDuration;
    uint8_t invocation_count = 0;
    SystemClock::time_point expected_deadline;
    sync::ThreadNotification callbacks_done_notification;
  };
  TimerWithContext uut;

  uut.expected_deadline = SystemClock::now() + kRoundedArbitraryShortDuration;
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callbacks_done_notification.acquire();
}

TEST(SystemTimer, DoubleRescheduleFromCallback) {
  class TimerWithContext : public TimerWithHandler {
   public:
    void OnExpiryCallback(SystemClock::time_point expired_deadline) override {
      EXPECT_GE(SystemClock::now(), expired_deadline);

      EXPECT_EQ(expired_deadline, expected_deadline);
      invocation_count++;
      ASSERT_LE(invocation_count, kExpectedInvocations);
      if (invocation_count == 1) {
        expected_deadline = expired_deadline + kPeriod;
        timer().InvokeAt(expected_deadline);
        timer().InvokeAt(expected_deadline);
      } else {
        callbacks_done_notification.release();
      }
    };

    const uint8_t kExpectedInvocations = 2;
    const SystemClock::duration kPeriod = kRoundedArbitraryShortDuration;
    uint8_t invocation_count = 0;
    SystemClock::time_point expected_deadline;
    sync::ThreadNotification callbacks_done_notification;
  };
  TimerWithContext uut;

  uut.expected_deadline = SystemClock::now() + kRoundedArbitraryShortDuration;
  uut.timer().InvokeAt(uut.expected_deadline);
  uut.callbacks_done_notification.acquire();
}

}  // namespace
}  // namespace pw::chrono