/****************************************************************************
 *
 *   Copyright (c) 2020 ECL Development Team. 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 PX4 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 OWNER 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 <gtest/gtest.h>
#include <math.h>
#include "EKF/common.h"
#include "EKF/sensor_range_finder.hpp"
#include <matrix/math.hpp>

using estimator::rangeSample;
using matrix::Dcmf;
using matrix::Eulerf;
using namespace estimator::sensor;

class SensorRangeFinderTest : public ::testing::Test {
public:
	// Setup the Ekf with synthetic measurements
	void SetUp() override
	{
		_range_finder.setPitchOffset(0.f);
		_range_finder.setCosMaxTilt(0.707f);
		_range_finder.setLimits(_min_range, _max_range);
	}

	// Use this method to clean up any memory, network etc. after each test
	void TearDown() override
	{
	}

protected:
	SensorRangeFinder _range_finder{};
	const rangeSample _good_sample{1.f, (uint64_t)2e6, 100}; // {range, time_us, quality}
	const float _min_range{0.5f};
	const float _max_range{10.f};

	void updateSensorAtRate(uint64_t duration_us, uint64_t dt_update_us, uint64_t dt_sensor_us);
	void testTilt(const Eulerf &euler, bool should_pass);
};

void SensorRangeFinderTest::updateSensorAtRate(uint64_t duration_us, uint64_t dt_update_us, uint64_t dt_sensor_us)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	rangeSample new_sample = _good_sample;
	uint64_t t_now_us = _good_sample.time_us;
	for (int i = 0; i < int(duration_us / dt_update_us); i++) {
		t_now_us += dt_update_us;
		if ((i % int(dt_sensor_us / dt_update_us)) == 0) {
			new_sample.rng += 0.2f; // update the range to not trigger the stuck detection
			if (new_sample.rng > _max_range) {
				new_sample.rng = _min_range;
			}
			new_sample.time_us = t_now_us;
			_range_finder.setSample(new_sample);
		}
		_range_finder.runChecks(t_now_us, attitude);
	}
}

void SensorRangeFinderTest::testTilt(const Eulerf &euler, bool should_pass)
{
	const Dcmf attitude{euler};
	_range_finder.setSample(_good_sample);
	_range_finder.runChecks(_good_sample.time_us, attitude);

	if (should_pass) {
		EXPECT_TRUE(_range_finder.isDataHealthy());
		EXPECT_TRUE(_range_finder.isHealthy());

	} else {
		EXPECT_FALSE(_range_finder.isDataHealthy());
		EXPECT_FALSE(_range_finder.isHealthy());
	}
}

TEST_F(SensorRangeFinderTest, setRange)
{
	rangeSample sample{};
	sample.rng = 1.f;
	sample.time_us = 1e6;
	sample.quality = 9;

	_range_finder.setRange(sample.rng);
	_range_finder.setDataReadiness(true);
	_range_finder.setValidity(true);
	EXPECT_TRUE(_range_finder.isDataHealthy());
	EXPECT_TRUE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, goodData)
{
	// WHEN: the drone is leveled and the data is good
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
	_range_finder.setSample(_good_sample);
	_range_finder.runChecks(_good_sample.time_us, attitude);

	// THEN: the data can be used for aiding
	EXPECT_TRUE(_range_finder.isDataHealthy());
	EXPECT_TRUE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, tiltExceeded)
{
	const Eulerf zero(0.f, 0.f, 0.f);
	const Eulerf pitch_46(0.f, 0.8f, 0.f);
	const Eulerf pitch_minus46(0.f, -0.8f, 0.f);
	const Eulerf pitch_40(0.f, 0.7f, 1.f);
	const Eulerf pitch_minus40(0.f, -0.7f, 0.f);
	const Eulerf roll_46(0.8f, 0.f, 0.f);
	const Eulerf roll_minus46(-0.8f, 0.f, 0.f);
	const Eulerf roll_40(0.7f, 0.f, 2.f);
	const Eulerf roll_minus40(-0.7f, 0.f, 3.f);
	const Eulerf roll_28_pitch_minus28(0.5f, -0.5f, 4.f);
	const Eulerf roll_46_pitch_minus46(0.8f, -0.8f, 4.f);

	testTilt(zero, true);
	testTilt(pitch_46, false);
	testTilt(pitch_minus46, false);
	testTilt(pitch_40, true);
	testTilt(pitch_minus40, true);
	testTilt(roll_46, false);
	testTilt(roll_minus46, false);
	testTilt(roll_40, true);
	testTilt(roll_minus40, true);
	testTilt(roll_28_pitch_minus28, true);
	testTilt(roll_46_pitch_minus46, false);

}

TEST_F(SensorRangeFinderTest, rangeMaxExceeded)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	// WHEN: the measured range is larger than the maximum
	rangeSample bad_sample = _good_sample;
	bad_sample.rng = _max_range + 0.01f;
	_range_finder.setSample(bad_sample);
	_range_finder.runChecks(bad_sample.time_us, attitude);

	// THEN: the data should be marked as unhealthy
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, rangeMinExceeded)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	// WHEN: the measured range is shorter than the minimum
	rangeSample bad_sample = _good_sample;
	bad_sample.rng = _min_range - 0.01f;
	_range_finder.setSample(bad_sample);
	_range_finder.runChecks(bad_sample.time_us, attitude);

	// THEN: the data should be marked as unhealthy
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, outOfDate)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	// WHEN: the data is outdated
	rangeSample outdated_sample = _good_sample;
	outdated_sample.time_us = 0;
	uint64_t t_now = _good_sample.time_us;
	_range_finder.setSample(outdated_sample);
	_range_finder.runChecks(t_now, attitude);

	// THEN: the data should be marked as unhealthy
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, rangeStuck)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	// WHEN: the data is first not valid and then
	// constantly the same
	rangeSample new_sample = _good_sample;
	const uint64_t dt = 3e5;
	const uint64_t stuck_timeout = 11e6;
	new_sample.quality = 0;
	for (int i = 0; i < int(stuck_timeout / dt); i++) {
		_range_finder.setSample(new_sample);
		_range_finder.runChecks(new_sample.time_us, attitude);
		new_sample.time_us += dt;
	}
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());

	new_sample.quality = 100;
	// we need a few sample to pass the hysteresis check
	for (int i = 0; i < int(2e6 / dt); i++) {
		_range_finder.setSample(new_sample);
		_range_finder.runChecks(new_sample.time_us, attitude);
		new_sample.time_us += dt;
	}

	// THEN: the data should be marked as unhealthy
	// because the sensor is "stuck"
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, qualityHysteresis)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	// WHEN: the data is first bad and then good
	rangeSample new_sample = _good_sample;

	new_sample.quality = 0;
	_range_finder.setSample(new_sample);
	_range_finder.runChecks(new_sample.time_us, attitude);
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());

	new_sample.quality = _good_sample.quality;
	_range_finder.setSample(new_sample);
	_range_finder.runChecks(new_sample.time_us, attitude);
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());

	// AND: we need to put enough good data to pass the hysteresis
	const uint64_t dt = 3e5;
	const uint64_t hyst_time = 1e6;
	for (int i = 0; i < int(hyst_time / dt) + 2; i++) {
		_range_finder.setSample(new_sample);
		_range_finder.runChecks(new_sample.time_us, attitude);
		new_sample.time_us += dt;
	}

	// THEN: the data is again declared healthy
	EXPECT_TRUE(_range_finder.isDataHealthy());
	EXPECT_TRUE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, continuity)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};

	// WHEN: the data rate is too slow
	const uint64_t dt_update_us = 10e3;
	uint64_t dt_sensor_us = 4e6;
	uint64_t duration_us = 8e6;
	updateSensorAtRate(duration_us, dt_update_us, dt_sensor_us);

	// THEN: the data should be marked as unhealthy
	// Note that it also fails the out-of-date test here
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());

	// AND WHEN: the data rate is acceptable
	dt_sensor_us = 3e5;
	duration_us = 5e5;
	updateSensorAtRate(duration_us, dt_update_us, dt_sensor_us);

	// THEN: it should still fail until the filter converge
	// to the new datarate
	EXPECT_FALSE(_range_finder.isDataHealthy());
	EXPECT_FALSE(_range_finder.isHealthy());

	updateSensorAtRate(duration_us, dt_update_us, dt_sensor_us);
	EXPECT_TRUE(_range_finder.isDataHealthy());
	EXPECT_TRUE(_range_finder.isHealthy());
}

TEST_F(SensorRangeFinderTest, distBottom)
{
	const Dcmf attitude{Eulerf(0.f, 0.f, 0.f)};
	rangeSample sample{};
	sample.rng = 1.f;
	sample.time_us = 1e6;
	sample.quality = 9;

	_range_finder.setSample(sample);
	_range_finder.runChecks(sample.time_us, attitude);
	EXPECT_FLOAT_EQ(_range_finder.getDistBottom(), sample.rng);

	const Dcmf attitude20{Eulerf(-0.35f, 0.f, 0.f)};
	_range_finder.runChecks(sample.time_us, attitude20);
	EXPECT_FLOAT_EQ(_range_finder.getDistBottom(), sample.rng * cosf(-0.35));
}
