#include "dbus-sdr/sensorutils.hpp"

#include <cmath>

#include "gtest/gtest.h"

// There is a surprising amount of slop in the math,
// thanks to all the rounding and conversion.
// The "x" byte value can drift by up to 2 away, I have seen.
static constexpr int8_t expectedSlopX = 2;

// Unlike expectedSlopX, this is a ratio, not an integer
// It scales based on the range of "y"
static constexpr double expectedSlopY = 0.01;

// The algorithm here was copied from ipmitool
// sdr_convert_sensor_reading() function
// https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
double ipmitool_y_from_x(uint8_t x, int m, int k2_rExp, int b, int k1_bExp,
                         bool bSigned)
{
    double result;

    // Rename to exactly match names and types (except analog) from ipmitool
    uint8_t val = x;
    double k1 = k1_bExp;
    double k2 = k2_rExp;
    int analog = bSigned ? 2 : 0;

    // Begin paste here
    // Only change is to comment out complicated structure in switch statement

    switch (/*sensor->cmn.unit.*/ analog)
    {
        case 0:
            result = (double)(((m * val) + (b * pow(10, k1))) * pow(10, k2));
            break;
        case 1:
            if (val & 0x80)
                val++;
            /* Deliberately fall through to case 2. */
            [[fallthrough]];
        case 2:
            result =
                (double)(((m * (int8_t)val) + (b * pow(10, k1))) * pow(10, k2));
            break;
        default:
            /* Oops! This isn't an analog sensor. */
            return 0.0;
    }

    // End paste here
    // Ignoring linearization curves and postprocessing that follows,
    // assuming all sensors are perfectly linear
    return result;
}

void testValue(int x, double y, int16_t M, int8_t rExp, int16_t B, int8_t bExp,
               bool bSigned, double yRange)
{
    double yRoundtrip;
    int result;

    // There is intentionally no exception catching here,
    // because if getSensorAttributes() returned true,
    // it is a promise that all of these should work.
    if (bSigned)
    {
        int8_t expect = x;
        int8_t actual =
            ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp, bSigned);

        result = actual;
        yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned);

        EXPECT_NEAR(actual, expect, expectedSlopX);
    }
    else
    {
        uint8_t expect = x;
        uint8_t actual =
            ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp, bSigned);

        result = actual;
        yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned);

        EXPECT_NEAR(actual, expect, expectedSlopX);
    }

    // Scale the amount of allowed slop in y based on range, so ratio similar
    double yTolerance = yRange * expectedSlopY;
    // double yError = std::abs(y - yRoundtrip);

    EXPECT_NEAR(y, yRoundtrip, yTolerance);

    char szFormat[1024];
    sprintf(szFormat,
            "Value | xExpect %4d | xResult %4d "
            "| M %5d | rExp %3d "
            "| B %5d | bExp %3d | bSigned %1d | y %18.3f | yRoundtrip %18.3f\n",
            x, result, M, (int)rExp, B, (int)bExp, (int)bSigned, y, yRoundtrip);
    std::cout << szFormat;
}

void testBounds(double yMin, double yMax, bool bExpectedOutcome = true)
{
    int16_t mValue;
    int8_t rExp;
    int16_t bValue;
    int8_t bExp;
    bool bSigned;
    bool result;

    result = ipmi::getSensorAttributes(yMax, yMin, mValue, rExp, bValue, bExp,
                                       bSigned);
    EXPECT_EQ(result, bExpectedOutcome);

    if (!result)
    {
        return;
    }

    char szFormat[1024];
    sprintf(szFormat,
            "Bounds | yMin %18.3f | yMax %18.3f | M %5d"
            " | rExp %3d | B %5d | bExp %3d | bSigned %1d\n",
            yMin, yMax, mValue, (int)rExp, bValue, (int)bExp, (int)bSigned);
    std::cout << szFormat;

    double y50p = (yMin + yMax) / 2.0;

    // Average the average
    double y25p = (yMin + y50p) / 2.0;
    double y75p = (y50p + yMax) / 2.0;

    // This range value is only used for tolerance checking, not computation
    double yRange = yMax - yMin;

    if (bSigned)
    {
        int8_t xMin = -128;
        int8_t x25p = -64;
        int8_t x50p = 0;
        int8_t x75p = 64;
        int8_t xMax = 127;

        testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange);
    }
    else
    {
        uint8_t xMin = 0;
        uint8_t x25p = 64;
        uint8_t x50p = 128;
        uint8_t x75p = 192;
        uint8_t xMax = 255;

        testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange);
        testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange);
    }
}

void testRanges(void)
{
    // The ranges from the main TEST function
    testBounds(0x0, 0xFF);
    testBounds(-128, 127);
    testBounds(0, 16000);
    testBounds(0, 20);
    testBounds(8000, 16000);
    testBounds(-10, 10);
    testBounds(0, 277);
    testBounds(0, 0, false);
    testBounds(10, 12);

    // Additional test cases recommended to me by hardware people
    testBounds(-40, 150);
    testBounds(0, 1);
    testBounds(0, 2);
    testBounds(0, 4);
    testBounds(0, 8);
    testBounds(35, 65);
    testBounds(0, 18);
    testBounds(0, 25);
    testBounds(0, 80);
    testBounds(0, 500);

    // Additional sanity checks
    testBounds(0, 255);
    testBounds(-255, 0);
    testBounds(-255, 255);
    testBounds(0, 1000);
    testBounds(-1000, 0);
    testBounds(-1000, 1000);
    testBounds(0, 255000);
    testBounds(-128000000, 127000000);
    testBounds(-50000, 0);
    testBounds(-40000, 10000);
    testBounds(-30000, 20000);
    testBounds(-20000, 30000);
    testBounds(-10000, 40000);
    testBounds(0, 50000);
    testBounds(-1e3, 1e6);
    testBounds(-1e6, 1e3);

    // Extreme ranges are now possible
    testBounds(0, 1e10);
    testBounds(0, 1e11);
    testBounds(0, 1e12);
    testBounds(0, 1e13, false);
    testBounds(-1e10, 0);
    testBounds(-1e11, 0);
    testBounds(-1e12, 0);
    testBounds(-1e13, 0, false);
    testBounds(-1e9, 1e9);
    testBounds(-1e10, 1e10);
    testBounds(-1e11, 1e11);
    testBounds(-1e12, 1e12, false);

    // Large multiplier but small offset
    testBounds(1e4, 1e4 + 255);
    testBounds(1e5, 1e5 + 255);
    testBounds(1e6, 1e6 + 255);
    testBounds(1e7, 1e7 + 255);
    testBounds(1e8, 1e8 + 255);
    testBounds(1e9, 1e9 + 255);
    testBounds(1e10, 1e10 + 255, false);

    // Input validation against garbage
    testBounds(0, INFINITY, false);
    testBounds(-INFINITY, 0, false);
    testBounds(-INFINITY, INFINITY, false);
    testBounds(0, NAN, false);
    testBounds(NAN, 0, false);
    testBounds(NAN, NAN, false);

    // Noteworthy binary integers
    testBounds(0, std::pow(2.0, 32.0) - 1.0);
    testBounds(0, std::pow(2.0, 32.0));
    testBounds(0.0 - std::pow(2.0, 31.0), std::pow(2.0, 31.0));
    testBounds((0.0 - std::pow(2.0, 31.0)) - 1.0, std::pow(2.0, 31.0));

    // Similar but negative (note additional commented-out below)
    testBounds(-1e1, (-1e1) + 255);
    testBounds(-1e2, (-1e2) + 255);

    // Ranges of negative numbers (note additional commented-out below)
    testBounds(-10400, -10000);
    testBounds(-15000, -14000);
    testBounds(-10000, -9000);
    testBounds(-1000, -900);
    testBounds(-1000, -800);
    testBounds(-1000, -700);
    testBounds(-1000, -740);

    // Very small ranges (note additional commented-out below)
    testBounds(0, 0.1);
    testBounds(0, 0.01);
    testBounds(0, 0.001);
    testBounds(0, 0.0001);
    testBounds(0, 0.000001, false);

#if 0
    // TODO(): The algorithm in this module is better than it was before,
    // but the resulting value of X is still wrong under certain conditions,
    // such as when the range between min and max is around 255,
    // and the offset is fairly extreme compared to the multiplier.
    // Not sure why this is, but these ranges are contrived,
    // and real-world examples would most likely never be this way.
    testBounds(-10290, -10000);
    testBounds(-10280, -10000);
    testBounds(-10275,-10000);
    testBounds(-10270,-10000);
    testBounds(-10265,-10000);
    testBounds(-10260,-10000);
    testBounds(-10255,-10000);
    testBounds(-10250,-10000);
    testBounds(-10245,-10000);
    testBounds(-10256,-10000);
    testBounds(-10512, -10000);
    testBounds(-11024, -10000);

    // TODO(): This also fails, due to extreme small range, loss of precision
    testBounds(0, 0.00001);

    // TODO(): Interestingly, if bSigned is forced false,
    // causing "x" to have range of (0,255) instead of (-128,127),
    // these test cases change from failing to passing!
    // Not sure why this is, perhaps a mathematician might know.
    testBounds(-10300, -10000);
    testBounds(-1000,-750);
    testBounds(-1e3, (-1e3) + 255);
    testBounds(-1e4, (-1e4) + 255);
    testBounds(-1e5, (-1e5) + 255);
    testBounds(-1e6, (-1e6) + 255);
#endif
}

TEST(sensorutils, TranslateToIPMI)
{
    /*bool getSensorAttributes(double maxValue, double minValue, int16_t
       &mValue, int8_t &rExp, int16_t &bValue, int8_t &bExp, bool &bSigned); */
    // normal unsigned sensor
    double maxValue = 0xFF;
    double minValue = 0x0;
    int16_t mValue;
    int8_t rExp;
    int16_t bValue;
    int8_t bExp;
    bool bSigned;
    bool result;

    uint8_t scaledVal;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);
    if (result)
    {
        EXPECT_EQ(bSigned, false);
        EXPECT_EQ(mValue, 1);
        EXPECT_EQ(rExp, 0);
        EXPECT_EQ(bValue, 0);
        EXPECT_EQ(bExp, 0);
    }
    double expected = 0x50;
    scaledVal = ipmi::scaleIPMIValueFromDouble(0x50, mValue, rExp, bValue, bExp,
                                               bSigned);
    EXPECT_NEAR(scaledVal, expected, expected * 0.01);

    // normal signed sensor
    maxValue = 127;
    minValue = -128;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);

    if (result)
    {
        EXPECT_EQ(bSigned, true);
        EXPECT_EQ(mValue, 1);
        EXPECT_EQ(rExp, 0);
        EXPECT_EQ(bValue, 0);
        EXPECT_EQ(bExp, 0);
    }

    // check negative values
    expected = 236; // 2s compliment -20
    scaledVal = ipmi::scaleIPMIValueFromDouble(-20, mValue, rExp, bValue, bExp,
                                               bSigned);
    EXPECT_NEAR(scaledVal, expected, expected * 0.01);

    // fan example
    maxValue = 16000;
    minValue = 0;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);
    if (result)
    {
        EXPECT_EQ(bSigned, false);
        EXPECT_EQ(mValue, floor((16000.0 / 0xFF) + 0.5));
        EXPECT_EQ(rExp, 0);
        EXPECT_EQ(bValue, 0);
        EXPECT_EQ(bExp, 0);
    }

    // voltage sensor example
    maxValue = 20;
    minValue = 0;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);
    if (result)
    {
        EXPECT_EQ(bSigned, false);
        EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
        EXPECT_EQ(rExp, -3);
        EXPECT_EQ(bValue, 0);
        EXPECT_EQ(bExp, 0);
    }
    scaledVal = ipmi::scaleIPMIValueFromDouble(12.2, mValue, rExp, bValue, bExp,
                                               bSigned);

    expected = 12.2 / (mValue * std::pow(10, rExp));
    EXPECT_NEAR(scaledVal, expected, expected * 0.01);

    // shifted fan example
    maxValue = 16000;
    minValue = 8000;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);

    if (result)
    {
        EXPECT_EQ(bSigned, false);
        EXPECT_EQ(mValue, floor(((8000.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
        EXPECT_EQ(rExp, -1);
        EXPECT_EQ(bValue, 8);
        EXPECT_EQ(bExp, 4);
    }

    // signed voltage sensor example
    maxValue = 10;
    minValue = -10;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);
    if (result)
    {
        EXPECT_EQ(bSigned, true);
        EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
        EXPECT_EQ(rExp, -3);
        // Although this seems like a weird magic number,
        // it is because the range (-128,127) is not symmetrical about zero,
        // unlike the range (-10,10), so this introduces some distortion.
        EXPECT_EQ(bValue, 392);
        EXPECT_EQ(bExp, -1);
    }

    scaledVal =
        ipmi::scaleIPMIValueFromDouble(5, mValue, rExp, bValue, bExp, bSigned);

    expected = 5 / (mValue * std::pow(10, rExp));
    EXPECT_NEAR(scaledVal, expected, expected * 0.01);

    // reading = max example
    maxValue = 277;
    minValue = 0;

    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);
    if (result)
    {
        EXPECT_EQ(bSigned, false);
    }

    scaledVal = ipmi::scaleIPMIValueFromDouble(maxValue, mValue, rExp, bValue,
                                               bExp, bSigned);

    expected = 0xFF;
    EXPECT_NEAR(scaledVal, expected, expected * 0.01);

    // 0, 0 failure
    maxValue = 0;
    minValue = 0;
    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, false);

    // too close *success* (was previously failure!)
    maxValue = 12;
    minValue = 10;
    result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
                                       bExp, bSigned);
    EXPECT_EQ(result, true);
    if (result)
    {
        EXPECT_EQ(bSigned, false);
        EXPECT_EQ(mValue, floor(((2.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
        EXPECT_EQ(rExp, -4);
        EXPECT_EQ(bValue, 1);
        EXPECT_EQ(bExp, 5);
    }
}

TEST(sensorUtils, TestRanges)
{
    // Additional test ranges, each running through a series of values,
    // to make sure the values of "x" and "y" go together and make sense,
    // for the resulting scaling attributes from each range.
    // Unlike the TranslateToIPMI test, exact matches of the
    // getSensorAttributes() results (the coefficients) are not required,
    // because they are tested through actual use, relating "x" to "y".
    testRanges();
}