// Copyright 2020 Embedded Artistry LLC
// SPDX-License-Identifier: Licensed for use by MBARI and its subsidiaries

#include <TimerManager.hpp>
#include <atomic>
#include <bsp.hpp>
#include <_bsp_private.hpp> // Exception granted: this is for a test
#include <CppUTest/CommandLineTestRunner.h>
#include <STM32Timer.hpp>
#include <system_time.hpp>

static std::atomic<bool> called_ = false;
static std::atomic<unsigned> count_ = 0;

static void cb_called()
{
	called_ = true;
}

static void cb_called_count()
{
	count_++;
}

static void __attribute__((unused)) led_timer_callback()
{
	count_++;
	toggleLED(LED::OK);
	if(0 == count_ % 5)
	{
		toggleLED(LED::ERROR);
	}
}

TEST_GROUP(TimerManagerTests){};

TEST(TimerManagerTests, CreateTimerManager)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager timer_mgr(timer);
	TimerManager<0> timer_mgr2(timer);
}

TEST(TimerManagerTests, CreateStaticTimerManager)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10, Timer::period_t> timer_mgr(timer);
}

TEST_GROUP(BasicTimerHandleTest){};

TEST(BasicTimerHandleTest, CreateTimerHandle)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);

	auto timer_handle = timer_mgr.allocate();
	CHECK(true == timer_handle.valid());
}

TEST(BasicTimerHandleTest, MoveInvalidatesTimerHandle)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);

	auto timer_handle = timer_mgr.allocate();
	auto timer_handle2 = std::move(timer_handle);

	CHECK(true == timer_handle2.valid());
	CHECK(false == timer_handle.valid());
}

TEST(BasicTimerHandleTest, CancelUnscheduledTimer)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);

	auto timer_handle = timer_mgr.allocate();
	timer_handle.stop();

	CHECK(Timer::State::stopped == timer_handle.state());
}

TEST(BasicTimerHandleTest, ScheduleThenCancelDelay)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);
	timer_mgr.start();

	count_ = 0;
	auto timer_handle = timer_mgr.allocate();
	timer_handle.config(Timer::Config::periodic);
	timer_handle.period(std::chrono::milliseconds(1));
	timer_handle.registerCallback(cb_called_count);
	timer_handle.start();
	systime::delay(std::chrono::milliseconds(1));
	timer_handle.stop();

	CHECK(Timer::State::stopped == timer_handle.state());
	CHECK(3 >= count_);
}

TEST_GROUP(TimerHandleSchedulingTests){};

TEST(TimerHandleSchedulingTests, ScheduleAsyncDelay)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);
	timer_mgr.start();

	called_ = false;
	auto timer_handle = timer_mgr.allocate();
	timer_handle.config(Timer::Config::oneshot);
	timer_handle.period(std::chrono::milliseconds(1));
	timer_handle.registerCallback(cb_called);
	timer_handle.start();

	systime::delay(std::chrono::milliseconds(2));

	CHECK(true == called_);
}

TEST(TimerHandleSchedulingTests, ScheduleMultipleAsyncDelays)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);
	timer_mgr.start();
	count_ = 0;

	auto timer_handle = timer_mgr.allocate();
	timer_handle.registerCallback(cb_called_count);
	timer_handle.period(std::chrono::milliseconds(3));
	auto timer_handle2 = timer_mgr.allocate();
	timer_handle2.registerCallback(cb_called_count);
	timer_handle2.period(std::chrono::milliseconds(1));
	auto timer_handle3 = timer_mgr.allocate();
	timer_handle3.registerCallback(cb_called_count);
	timer_handle3.period(std::chrono::milliseconds(2));

	timer_handle.start();
	timer_handle2.start();
	timer_handle3.start();

	systime::delay(std::chrono::milliseconds(5));

	CHECK(3 == count_);
}

TEST(TimerHandleSchedulingTests, SchedulePeriodicDelay)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);
	timer_mgr.start();

	count_ = 0;
	auto timer_handle = timer_mgr.allocate();
	timer_handle.config(Timer::Config::periodic);
	timer_handle.period(std::chrono::milliseconds(1));
	timer_handle.registerCallback(cb_called_count);

	timer_handle.start();
	systime::delay(std::chrono::milliseconds(10));
	timer_handle.stop();

	// We give it some wiggle room - but not too much
	CHECK(8 <= count_);
	CHECK(11 >= count_);
}

// This test can be used for a visual confirmation of the timer
// accuracy, or at least, you can get a sense of it...
IGNORE_TEST(TimerHandleSchedulingTests, PeriodicLEDVisualTest)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<0> timer_mgr(timer);
	timer_mgr.start();
	bsp_private::initializeLEDs();

	count_ = 0;
	auto timer_handle = timer_mgr.allocate();
	timer_handle.config(Timer::Config::periodic);
	timer_handle.period(std::chrono::seconds(1));
	timer_handle.registerCallback(led_timer_callback);

	timer_handle.start();
	systime::delay(std::chrono::seconds(11));
	timer_handle.stop();

	// We give it some wiggle room - but not too much
	CHECK(9 <= count_);
	CHECK(11 >= count_);
}

TEST_GROUP(StaticBasicTimerHandleTests){};

TEST(StaticBasicTimerHandleTests, CreateTimerHandle)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10, Timer::period_t> timer_mgr(timer);

	auto timer_handle = timer_mgr.allocate();
	CHECK(true == timer_handle.valid());
}

TEST(StaticBasicTimerHandleTests, MoveInvalidatesTimerHandle)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10, Timer::period_t> timer_mgr(timer);

	auto timer_handle = timer_mgr.allocate();
	auto timer_handle2 = std::move(timer_handle);

	CHECK(true == timer_handle2.valid());
	CHECK(false == timer_handle.valid());
}

TEST(StaticBasicTimerHandleTests, CancelUnscheduledTimer)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10, Timer::period_t> timer_mgr(timer);

	auto timer_handle = timer_mgr.allocate();

	CHECK(Timer::State::stopped == timer_handle.state());
}

TEST(StaticBasicTimerHandleTests, ScheduleThenCancelDelay)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10, Timer::period_t> timer_mgr(timer);
	timer_mgr.start();
	count_ = 0;

	auto timer_handle = timer_mgr.allocate();
	timer_handle.config(Timer::Config::periodic);
	timer_handle.period(std::chrono::milliseconds(1));
	timer_handle.registerCallback(cb_called_count);
	timer_handle.start();
	systime::delay(std::chrono::milliseconds(1));
	timer_handle.stop();

	CHECK(Timer::State::stopped == timer_handle.state());
	CHECK(3 >= count_);
}

TEST_GROUP(StaticTimerHandlerSchedulingTests){};

TEST(StaticTimerHandlerSchedulingTests, ScheduleAsyncDelay)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10> timer_mgr(timer);
	timer_mgr.start();

	called_ = false;
	auto timer_handle = timer_mgr.allocate();
	timer_handle.period(std::chrono::milliseconds(1));
	timer_handle.registerCallback(cb_called);

	timer_handle.start();
	systime::delay(std::chrono::milliseconds(5));

	CHECK(true == called_);
}

TEST(StaticTimerHandlerSchedulingTests, ScheduleMultipleAsyncDelays)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10> timer_mgr(timer);
	timer_mgr.start();
	count_ = 0;

	auto timer_handle = timer_mgr.allocate();
	timer_handle.registerCallback(cb_called_count);
	timer_handle.period(std::chrono::milliseconds(3));
	auto timer_handle2 = timer_mgr.allocate();
	timer_handle2.registerCallback(cb_called_count);
	timer_handle2.period(std::chrono::milliseconds(1));
	auto timer_handle3 = timer_mgr.allocate();
	timer_handle3.registerCallback(cb_called_count);
	timer_handle3.period(std::chrono::milliseconds(2));

	timer_handle.start();
	timer_handle2.start();
	timer_handle3.start();

	systime::delay(std::chrono::milliseconds(5));

	CHECK(3 == count_);
}

TEST(StaticTimerHandlerSchedulingTests, SchedulePeriodicDelay)
{
	STM32Timer timer(STM32Timer::Device::CH5, STM32Timer::TickRate::microsecond);
	TimerManager<10> timer_mgr(timer);
	timer_mgr.start();

	count_ = 0;
	auto timer_handle = timer_mgr.allocate();
	timer_handle.config(Timer::Config::periodic);
	timer_handle.period(std::chrono::milliseconds(1));
	timer_handle.registerCallback(cb_called_count);

	timer_handle.start();
	systime::delay(std::chrono::milliseconds(10));
	timer_handle.stop();

	// We give it some wiggle room - but not too much
	CHECK(8 <= count_);
	CHECK(11 >= count_);
}
