#include <unity.h>
#include "SDS011.h"
#include "SDS018.h"
#include "SDS021.h"
#include "SDS198.h"

#include <cstring>
#include "../mock/MockUARTInterface.h"

void setUp(void) {}
void tearDown(void) {}

void test_get_sensor_name(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor1(mockUart);
    GuL::SDS018 sensor2(mockUart);
    GuL::SDS021 sensor3(mockUart);
    GuL::SDS198 sensor4(mockUart);

    TEST_ASSERT_EQUAL_STRING("SDS011", sensor1.getSensorName());
    TEST_ASSERT_EQUAL_STRING("SDS018", sensor2.getSensorName());
    TEST_ASSERT_EQUAL_STRING("SDS021", sensor3.getSensorName());
    TEST_ASSERT_EQUAL_STRING("SDS198", sensor4.getSensorName());
}

void test_sleep_wakeup_send_commands(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Test sleep command
    mockUart.enableWrites();
    TEST_ASSERT_TRUE(sensor.sleep());

    const uint8_t expectedSleepCmd[] = {0xAA, 0xB4, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x08, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedSleepCmd, mockUart.getOutput(), sizeof(expectedSleepCmd));

    // Test wakeup command
    mockUart.resetOutput(); // Clear previous output
    mockUart.enableWrites();
    TEST_ASSERT_TRUE(sensor.wakeup());
    const uint8_t expectedWakeupCmd[] = {0xAA, 0xB4, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x09, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedWakeupCmd, mockUart.getOutput(), sizeof(expectedWakeupCmd));
}

void test_set_reporting_mode_send_commands(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Test set to active reporting
    mockUart.enableWrites();
    TEST_ASSERT_TRUE(sensor.setToPassiveReporting());
    const uint8_t expectedPassiveCmd[] = {0xAA, 0xB4, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x05, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedPassiveCmd, mockUart.getOutput(), sizeof(expectedPassiveCmd));

    mockUart.resetOutput(); // Clear previous output
    mockUart.enableWrites();
    TEST_ASSERT_TRUE(sensor.setToActiveReporting());
    const uint8_t expectedActiveCmd[] = {0xAA, 0xB4, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x04, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedActiveCmd, mockUart.getOutput(), sizeof(expectedActiveCmd));
}

void test_polling_and_reading_data(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xFFFF); // Use a specific ID for testing

    mockUart.enableWrites();
    mockUart.resetOutput(); // Clear previous output
    TEST_ASSERT_TRUE(sensor.poll());
    uint8_t expectedPollCmd[] = {0xAA, 0xB4, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x02, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedPollCmd, mockUart.getOutput(), sizeof(expectedPollCmd));

    // Simulate receiving a data frame from the sensor
    // With PM2.5=123.6 (0xD4 0x04), PM10=261.8 (0x3A 0x0A), ID = 0xA160
    uint8_t dataFrame[] = {0xAA, 0xC0, 0xD4, 0x04, 0x3A, 0x0A, 0xA1, 0x60, 0x1D, 0xAB};
    mockUart.setInput(dataFrame, sizeof(dataFrame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 123.6, sensor.getPM2_5());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 261.8, sensor.getPM10());
    TEST_ASSERT_FLOAT_WITHIN(0.1, -1, sensor.getPM100()); // PM100 should be -1 as it's not included in this frame
}

void test_reading_data100_frame(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xFFFF); // Use a specific ID for testing

    // Simulate receiving a data100 frame from the sensor
    //
    uint8_t data100Frame[] = {0xAA, 0xCF, 0x00, 0x00, 0x3A, 0x0A, 0xA1, 0x60, 0x45, 0xAB};
    mockUart.setInput(data100Frame, sizeof(data100Frame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_FLOAT_WITHIN(0.1, -1, sensor.getPM2_5());
    TEST_ASSERT_FLOAT_WITHIN(0.1, -1, sensor.getPM10());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 2618, sensor.getPM100());
}

void test_set_working_period_send_commands(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Test set working period to 30 seconds
    mockUart.enableWrites();
    TEST_ASSERT_TRUE(sensor.setWorkingPeriod(1)); // Set to 1 minute (60 seconds)
    const uint8_t expectedCmd[] = {0xAA, 0xB4, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x0B, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedCmd, mockUart.getOutput(), sizeof(expectedCmd));
}

void test_poll_firmware_version(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    mockUart.enableWrites();
    mockUart.resetOutput(); // Clear previous output
    TEST_ASSERT_TRUE(sensor.pollFirmwareVersion());
    uint8_t expectedCmd[] = {0xAA, 0xB4, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x08, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedCmd, mockUart.getOutput(), sizeof(expectedCmd));
}

void test_poll_reporting_mode(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xFFFF); // Use a specific ID for testing

    mockUart.enableWrites();
    mockUart.resetOutput(); // Clear previous output
    TEST_ASSERT_TRUE(sensor.pollReportingMode());
    uint8_t expectedCmd[] = {0xAA, 0xB4, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedCmd, mockUart.getOutput(), sizeof(expectedCmd));
}

void test_poll_working_mode(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    mockUart.enableWrites();
    mockUart.resetOutput(); // Clear previous output
    TEST_ASSERT_TRUE(sensor.pollWorkingMode());
    uint8_t expectedCmd[] = {0xAA, 0xB4, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x07, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedCmd, mockUart.getOutput(), sizeof(expectedCmd));
}

void test_poll_working_period(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    mockUart.enableWrites();
    mockUart.resetOutput(); // Clear previous output
    TEST_ASSERT_TRUE(sensor.pollWorkingPeriod());
    uint8_t expectedCmd[] = {0xAA, 0xB4, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x09, 0xAB};
    TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedCmd, mockUart.getOutput(), sizeof(expectedCmd));
}

void test_read_firmware_version(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Simulate receiving a firmware version frame from the sensor
    uint8_t firmwareFrame[] = {0xAA, 0xC5, 0x07, 0x0F, 0x07, 0x0A, 0xA1, 0x60, 0x28, 0xAB};
    mockUart.setInput(firmwareFrame, sizeof(firmwareFrame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_EQUAL_UINT8(15, sensor.getVersion().year);
    TEST_ASSERT_EQUAL_UINT8(7, sensor.getVersion().month);
    TEST_ASSERT_EQUAL_UINT8(10, sensor.getVersion().day);
}

void test_get_work_mode(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Simulate receiving a working mode frame from the sensor (work mode = WAKEUP)
    uint8_t workModeFrame[] = {0xAA, 0xC5, 0x06, 0x01, 0x01, 0x00, 0xA1, 0x60, 0x09, 0xAB};
    mockUart.setInput(workModeFrame, sizeof(workModeFrame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::WAKEUP, sensor.getWorkMode());
    uint8_t workModeFrame2[] = {0xAA, 0xC5, 0x06, 0x00, 0x01, 0x00, 0xA1, 0x60, 0x08, 0xAB};
    mockUart.setInput(workModeFrame2, sizeof(workModeFrame2));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::WAKEUP, sensor.getWorkMode());
    workModeFrame2[4] = 0x00; // Set to SLEEP
    workModeFrame2[8] = 0x07; // Update checksum accordingly
    mockUart.setInput(workModeFrame2, sizeof(workModeFrame2));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::SLEEP, sensor.getWorkMode());
}

void test_get_reporting_mode(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Simulate receiving a reporting mode frame from the sensor (reporting mode = ACTIVE)
    uint8_t reportingModeFrame[] = {0xAA, 0xC5, 0x02, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x03, 0xAB};
    mockUart.setInput(reportingModeFrame, sizeof(reportingModeFrame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::ACTIVE, sensor.getReportingMode());
    reportingModeFrame[4] = 0x01; // Set to PASSIVE
    reportingModeFrame[8] = 0x04; // Update checksum accordingly
    mockUart.setInput(reportingModeFrame, sizeof(reportingModeFrame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::PASSIVE, sensor.getReportingMode());
}

void test_error_codes_set(void)
{
    MockUartInterface mockUart;
    GuL::NovaFitness sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Simulate receiving a frame with invalid checksum
    uint8_t invalidChecksumFrame[] = {0xAA, 0xC0, 0xD4, 0x04, 0x3A, 0x0A, 0xA1, 0x60, 0x00, 0xAB}; // Checksum byte set to 0x00 (invalid)
    mockUart.setInput(invalidChecksumFrame, sizeof(invalidChecksumFrame));
    TEST_ASSERT_FALSE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::CHECKSUM_ERROR, sensor.getLastFailureReason());

    // Simulate receiving an incomplete frame (only header and command ID)
    uint8_t incompleteFrame[] = {0xAA, 0xC0}; // Incomplete frame
    mockUart.setInput(incompleteFrame, sizeof(incompleteFrame));
    TEST_ASSERT_FALSE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::INCOMPLETE_FRAME, sensor.getLastFailureReason());

    // Simulate receiving a frame with unknown command ID
    uint8_t unknownCommandFrame[] = {0xAA, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xA1, 0x60, 0x01, 0xAB}; // Command ID set to 0xFF (unknown)
    mockUart.setInput(unknownCommandFrame, sizeof(unknownCommandFrame));
    TEST_ASSERT_FALSE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::UNKNOWN_COMMAND_ID, sensor.getLastFailureReason());
}

void test_interrupting_frame_parsing(void)
{
    MockUartInterface mockUart;
    GuL::SDS011 sensor(mockUart, 0xA160); // Use a specific ID for testing

    // Simulate receiving a valid data frame followed by an incomplete frame
    uint8_t validFrame[] = {0xAA, 0xC0, 0xD4, 0x04, 0x3A, 0x0A, 0xA1, 0x60, 0x1D, 0xAB};
    uint8_t incompleteFrame[] = {0xAA, 0xC0};                                   // Incomplete frame
    uint8_t residualFrame[] = {0x3B, 0x0A, 0x23, 0x21, 0xA1, 0x60, 0x8A, 0xAB}; // Residual data from incomplete frame if not handled properly
    uint8_t combinedFrames[sizeof(validFrame) + sizeof(incompleteFrame)];
    memcpy(combinedFrames, validFrame, sizeof(validFrame));
    memcpy(combinedFrames + sizeof(validFrame), incompleteFrame, sizeof(incompleteFrame));
    mockUart.setInput(combinedFrames, sizeof(combinedFrames));

    // First read should successfully parse the valid frame
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 123.6, sensor.getPM2_5());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 261.8, sensor.getPM10());

    // Second read should fail due to incomplete frame
    TEST_ASSERT_FALSE(sensor.read());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::INCOMPLETE_FRAME, sensor.getLastFailureReason());

    // Now simulate receiving the remaining bytes of the incomplete frame
    mockUart.setInput(residualFrame, sizeof(residualFrame));
    TEST_ASSERT_TRUE(sensor.read());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 261.9, sensor.getPM2_5());
    TEST_ASSERT_FLOAT_WITHIN(0.1, 848.3, sensor.getPM10());
    TEST_ASSERT_EQUAL(GuL::NovaFitness::NONE, sensor.getLastFailureReason()); // After successfully parsing the frame, the failure reason should be reset to NONE
}

int processTests()
{
    UNITY_BEGIN();
    RUN_TEST(test_get_sensor_name);
    RUN_TEST(test_sleep_wakeup_send_commands);
    RUN_TEST(test_set_reporting_mode_send_commands);
    RUN_TEST(test_polling_and_reading_data);
    RUN_TEST(test_reading_data100_frame);
    RUN_TEST(test_set_working_period_send_commands);
    RUN_TEST(test_poll_firmware_version);
    RUN_TEST(test_poll_reporting_mode);
    RUN_TEST(test_poll_working_mode);
    RUN_TEST(test_poll_working_period);
    RUN_TEST(test_read_firmware_version);
    RUN_TEST(test_get_work_mode);
    RUN_TEST(test_get_reporting_mode);
    RUN_TEST(test_error_codes_set);
    RUN_TEST(test_interrupting_frame_parsing);
    return UNITY_END();
}

#if defined(ARDUINO)
void setup()
{
    Serial.begin(115200);
    delay(2000);
    processTests();
}

void loop() {}
#else
int main(int argc, char *argv[])
{
    return processTests();
}
#endif