#include "failsafeloggers/builder.hpp"
#include "failsafeloggers/failsafe_logger.hpp"
#include "failsafeloggers/failsafe_logger_utility.hpp"
#include "pid/ec/logging.hpp"
#include "pid/ec/pid.hpp"
#include "pid/zone.hpp"
#include "sensors/manager.hpp"
#include "test/controller_mock.hpp"
#include "test/helpers.hpp"
#include "test/sensor_mock.hpp"

#include <sdbusplus/test/sdbus_mock.hpp>

#include <chrono>
#include <cstring>
#include <unordered_map>
#include <vector>

#include <gmock/gmock.h>
#include <gtest/gtest.h>

namespace pid_control
{
namespace
{

using ::testing::_;
using ::testing::IsNull;
using ::testing::Return;
using ::testing::StrEq;

static std::string modeInterface = "xyz.openbmc_project.Control.Mode";
static std::string debugZoneInterface = "xyz.openbmc_project.Debug.Pid.Zone";
static std::string enableInterface = "xyz.openbmc_project.Object.Enable";
static std::string debugThermalPowerInterface =
    "xyz.openbmc_project.Debug.Pid.ThermalPower";

namespace
{

TEST(PidZoneConstructorTest, BoringConstructorTest)
{
    // Build a PID Zone.

    sdbusplus::SdBusMock sdbus_mock_passive, sdbus_mock_host, sdbus_mock_mode,
        sdbus_mock_enable;
    auto bus_mock_passive = sdbusplus::get_mocked_new(&sdbus_mock_passive);
    auto bus_mock_host = sdbusplus::get_mocked_new(&sdbus_mock_host);
    auto bus_mock_mode = sdbusplus::get_mocked_new(&sdbus_mock_mode);
    auto bus_mock_enable = sdbusplus::get_mocked_new(&sdbus_mock_enable);

    EXPECT_CALL(sdbus_mock_host,
                sd_bus_add_object_manager(
                    IsNull(), _, StrEq("/xyz/openbmc_project/extsensors")))
        .WillOnce(Return(0));

    SensorManager m(bus_mock_passive, bus_mock_host);

    bool defer = true;
    bool accSetPoint = false;
    const char* objPath = "/path/";
    int64_t zone = 1;
    double minThermalOutput = 1000.0;
    double failSafePercent = 100;
    conf::CycleTime cycleTime;

    double d;
    std::vector<std::string> properties;
    SetupDbusObject(&sdbus_mock_mode, defer, objPath, modeInterface, properties,
                    &d);
    SetupDbusObject(&sdbus_mock_mode, defer, objPath, debugZoneInterface,
                    properties, &d);

    std::string sensorname = "temp1";
    std::string pidsensorpath =
        "/xyz/openbmc_project/settings/fanctrl/zone1/" + sensorname;

    double de;
    std::vector<std::string> propertiesenable;
    SetupDbusObject(&sdbus_mock_enable, defer, pidsensorpath.c_str(),
                    enableInterface, propertiesenable, &de);

    DbusPidZone p(zone, minThermalOutput, failSafePercent, cycleTime, m,
                  bus_mock_mode, objPath, defer, accSetPoint);
    // Success.
}

} // namespace

class PidZoneTest : public ::testing::Test
{
  protected:
    PidZoneTest() :
        property_index(), properties(), sdbus_mock_passive(), sdbus_mock_host(),
        sdbus_mock_mode(), sdbus_mock_enable()
    {
        EXPECT_CALL(sdbus_mock_host,
                    sd_bus_add_object_manager(
                        IsNull(), _, StrEq("/xyz/openbmc_project/extsensors")))
            .WillOnce(Return(0));

        auto bus_mock_passive = sdbusplus::get_mocked_new(&sdbus_mock_passive);
        auto bus_mock_host = sdbusplus::get_mocked_new(&sdbus_mock_host);
        auto bus_mock_mode = sdbusplus::get_mocked_new(&sdbus_mock_mode);
        auto bus_mock_enable = sdbusplus::get_mocked_new(&sdbus_mock_enable);

        // Compiler weirdly not happy about just instantiating mgr(...);
        SensorManager m(bus_mock_passive, bus_mock_host);
        mgr = std::move(m);

        SetupDbusObject(&sdbus_mock_mode, defer, objPath, modeInterface,
                        properties, &property_index);
        SetupDbusObject(&sdbus_mock_mode, defer, objPath, debugZoneInterface,
                        properties, &property_index);

        SetupDbusObject(&sdbus_mock_enable, defer, pidsensorpath.c_str(),
                        enableInterface, propertiesenable,
                        &propertyenable_index);

        zone = std::make_unique<DbusPidZone>(
            zoneId, minThermalOutput, failSafePercent, cycleTime, mgr,
            bus_mock_mode, objPath, defer, accSetPoint);
    }

    // unused
    double property_index;
    std::vector<std::string> properties;
    double propertyenable_index;
    std::vector<std::string> propertiesenable;

    sdbusplus::SdBusMock sdbus_mock_passive;
    sdbusplus::SdBusMock sdbus_mock_host;
    sdbusplus::SdBusMock sdbus_mock_mode;
    sdbusplus::SdBusMock sdbus_mock_enable;
    int64_t zoneId = 1;
    double minThermalOutput = 1000.0;
    double failSafePercent = 100;
    double setpoint = 50.0;
    bool defer = true;
    bool accSetPoint = false;
    const char* objPath = "/path/";
    SensorManager mgr;
    conf::CycleTime cycleTime;

    std::string sensorname = "temp1";
    std::string sensorType = "temp";
    std::string pidsensorpath =
        "/xyz/openbmc_project/settings/fanctrl/zone1/" + sensorname;

    std::unique_ptr<DbusPidZone> zone;
};

TEST_F(PidZoneTest, GetZoneId_ReturnsExpected)
{
    // Verifies the zoneId returned is what we expect.

    EXPECT_EQ(zoneId, zone->getZoneID());
}

TEST_F(PidZoneTest, GetAndSetManualModeTest_BehavesAsExpected)
{
    // Verifies that the zone starts in manual mode.  Verifies that one can set
    // the mode.
    EXPECT_FALSE(zone->getManualMode());

    zone->setManualMode(true);
    EXPECT_TRUE(zone->getManualMode());
}

TEST_F(PidZoneTest, AddPidControlProcessGetAndSetEnableTest_BehavesAsExpected)
{
    // Verifies that the zone starts in enable mode.  Verifies that one can set
    // enable the mode.
    auto bus_mock_enable = sdbusplus::get_mocked_new(&sdbus_mock_enable);

    EXPECT_CALL(sdbus_mock_mode, sd_bus_emit_properties_changed_strv(
                                     IsNull(), StrEq(pidsensorpath.c_str()),
                                     StrEq(enableInterface), NotNull()))
        .Times(::testing::AnyNumber())
        .WillOnce(Invoke(
            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
                [[maybe_unused]] const char* interface, const char** names) {
                EXPECT_STREQ("Enable", names[0]);
                return 0;
            }));

    zone->addPidControlProcess(sensorname, sensorType, setpoint,
                               bus_mock_enable, pidsensorpath.c_str(), defer);
    EXPECT_TRUE(zone->isPidProcessEnabled(sensorname));
}

TEST_F(PidZoneTest, SetManualMode_RedundantWritesEnabledOnceAfterManualMode)
{
    // Tests adding a fan PID controller to the zone, and verifies it's
    // touched during processing.

    std::unique_ptr<PIDController> tpid =
        std::make_unique<ControllerMock>("fan1", zone.get());
    ControllerMock* tmock = reinterpret_cast<ControllerMock*>(tpid.get());

    // Access the internal pid configuration to clear it out (unrelated to the
    // test).
    [[maybe_unused]] ec::pid_info_t* info = tpid->getPIDInfo();

    zone->addFanPID(std::move(tpid));

    EXPECT_CALL(*tmock, setptProc()).WillOnce(Return(10.0));
    EXPECT_CALL(*tmock, inputProc()).WillOnce(Return(11.0));
    EXPECT_CALL(*tmock, outputProc(_));

    // while zone is in auto mode redundant writes should be disabled
    EXPECT_FALSE(zone->getRedundantWrite());

    // but switching from manual to auto enables a single redundant write
    zone->setManualMode(true);
    zone->setManualMode(false);
    EXPECT_TRUE(zone->getRedundantWrite());

    // after one iteration of a pid loop redundant write should be cleared
    zone->processFans();
    EXPECT_FALSE(zone->getRedundantWrite());
}

TEST_F(PidZoneTest, RpmSetPoints_AddMaxClear_BehaveAsExpected)
{
    // Tests addSetPoint, clearSetPoints, determineMaxSetPointRequest
    // and getMinThermalSetPoint.

    // Need to add pid control process for the zone that can enable
    // the process and add the set point.
    auto bus_mock_enable = sdbusplus::get_mocked_new(&sdbus_mock_enable);

    EXPECT_CALL(sdbus_mock_mode, sd_bus_emit_properties_changed_strv(
                                     IsNull(), StrEq(pidsensorpath.c_str()),
                                     StrEq(enableInterface), NotNull()))
        .Times(::testing::AnyNumber())
        .WillOnce(Invoke(
            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
                [[maybe_unused]] const char* interface, const char** names) {
                EXPECT_STREQ("Enable", names[0]);
                return 0;
            }));

    zone->addPidControlProcess(sensorname, sensorType, setpoint,
                               bus_mock_enable, pidsensorpath.c_str(), defer);

    // At least one value must be above the minimum thermal setpoint used in
    // the constructor otherwise it'll choose that value
    std::vector<double> values = {100, 200, 300, 400, 500, 5000};

    for (auto v : values)
    {
        zone->addSetPoint(v, sensorname);
    }

    // This will pull the maximum RPM setpoint request.
    zone->determineMaxSetPointRequest();
    EXPECT_EQ(5000, zone->getMaxSetPointRequest());

    // Clear the values, so it'll choose the minimum thermal setpoint.
    zone->clearSetPoints();

    // This will go through the RPM set point values and grab the maximum.
    zone->determineMaxSetPointRequest();
    EXPECT_EQ(zone->getMinThermalSetPoint(), zone->getMaxSetPointRequest());
}

TEST_F(PidZoneTest, RpmSetPoints_AddBelowMinimum_BehavesAsExpected)
{
    // Tests adding several RPM setpoints, however, they're all lower than the
    // configured minimal thermal setpoint RPM value.

    // Need to add pid control process for the zone that can enable
    // the process and add the set point.
    auto bus_mock_enable = sdbusplus::get_mocked_new(&sdbus_mock_enable);

    EXPECT_CALL(sdbus_mock_mode, sd_bus_emit_properties_changed_strv(
                                     IsNull(), StrEq(pidsensorpath.c_str()),
                                     StrEq(enableInterface), NotNull()))
        .Times(::testing::AnyNumber())
        .WillOnce(Invoke(
            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
                [[maybe_unused]] const char* interface, const char** names) {
                EXPECT_STREQ("Enable", names[0]);
                return 0;
            }));

    zone->addPidControlProcess(sensorname, sensorType, setpoint,
                               bus_mock_enable, pidsensorpath.c_str(), defer);

    std::vector<double> values = {100, 200, 300, 400, 500};

    for (auto v : values)
    {
        zone->addSetPoint(v, sensorname);
    }

    // This will pull the maximum RPM setpoint request.
    zone->determineMaxSetPointRequest();

    // Verifies the value returned in the minimal thermal rpm set point.
    EXPECT_EQ(zone->getMinThermalSetPoint(), zone->getMaxSetPointRequest());
}

TEST_F(PidZoneTest, GetFailSafePercent_SingleFailedReturnsExpected)
{
    // Tests when only one sensor failed and the sensor's failsafe duty is zero,
    // and verify that the sensor name is empty and failsafe duty is PID zone's
    // failsafe duty.

    std::vector<std::string> input1 = {"temp1"};
    std::vector<std::string> input2 = {"temp2"};
    std::vector<std::string> input3 = {"temp3"};
    std::vector<double> values = {0, 0, 0};

    zone->addPidFailSafePercent(input1, values[0]);
    zone->addPidFailSafePercent(input2, values[1]);
    zone->addPidFailSafePercent(input3, values[2]);

    zone->markSensorMissing("temp1");

    EXPECT_EQ(failSafePercent, zone->getFailSafePercent());
}

TEST_F(PidZoneTest, GetFailSafePercent_MultiFailedReturnsExpected)
{
    // Tests when multi sensor failed, and verify the final failsafe's sensor
    // name and duty as expected.

    std::vector<std::string> input1 = {"temp1"};
    std::vector<std::string> input2 = {"temp2"};
    std::vector<std::string> input3 = {"temp3"};
    std::vector<double> values = {60, 80, 70};

    zone->addPidFailSafePercent(input1, values[0]);
    zone->addPidFailSafePercent(input2, values[1]);
    zone->addPidFailSafePercent(input3, values[2]);

    zone->markSensorMissing("temp1");
    zone->markSensorMissing("temp2");
    zone->markSensorMissing("temp3");

    EXPECT_EQ(80, zone->getFailSafePercent());
}

TEST_F(PidZoneTest, ThermalInputs_FailsafeToValid_ReadsSensors)
{
    // This test will add a couple thermal inputs, and verify that the zone
    // initializes into failsafe mode, and will read each sensor.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    std::string name1 = "temp1";
    int64_t timeout = 1;

    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string name2 = "temp2";
    std::unique_ptr<Sensor> sensor2 =
        std::make_unique<SensorMock>(name2, timeout);
    SensorMock* sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
    mgr.addSensor(type, name2, std::move(sensor2));
    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);

    // Now that the sensors exist, add them to the zone.
    zone->addThermalInput(name1, false);
    zone->addThermalInput(name2, false);

    // Initialize Zone
    zone->initializeCache();

    // Verify now in failsafe mode.
    EXPECT_TRUE(zone->getFailSafeMode());

    ReadReturn r1;
    r1.value = 10.0;
    r1.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));

    ReadReturn r2;
    r2.value = 11.0;
    r2.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    // Read the sensors, this will put the values into the cache.
    zone->updateSensors();

    // We should no longer be in failsafe mode.
    EXPECT_FALSE(zone->getFailSafeMode());

    EXPECT_EQ(r1.value, zone->getCachedValue(name1));
    EXPECT_EQ(r2.value, zone->getCachedValue(name2));
}

TEST_F(PidZoneTest, FanInputTest_VerifiesFanValuesCached)
{
    // This will add a couple fan inputs, and verify the values are cached.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    std::string name1 = "fan1";
    int64_t timeout = 2;

    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string name2 = "fan2";
    std::unique_ptr<Sensor> sensor2 =
        std::make_unique<SensorMock>(name2, timeout);
    SensorMock* sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
    mgr.addSensor(type, name2, std::move(sensor2));
    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);

    // Now that the sensors exist, add them to the zone.
    zone->addFanInput(name1, false);
    zone->addFanInput(name2, false);

    // Initialize Zone
    zone->initializeCache();

    ReadReturn r1;
    r1.value = 10.0;
    r1.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));

    ReadReturn r2;
    r2.value = 11.0;
    r2.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    // Method under test will read through each fan sensor for the zone and
    // cache the values.
    zone->updateFanTelemetry();

    EXPECT_EQ(r1.value, zone->getCachedValue(name1));
    EXPECT_EQ(r2.value, zone->getCachedValue(name2));
}

TEST_F(PidZoneTest, ThermalInput_ValueTimeoutEntersFailSafeMode)
{
    // On the second updateSensors call, the updated timestamp will be beyond
    // the timeout limit.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    int64_t timeout = 1;

    std::string name1 = "temp1";
    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string name2 = "temp2";
    std::unique_ptr<Sensor> sensor2 =
        std::make_unique<SensorMock>(name2, timeout);
    SensorMock* sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
    mgr.addSensor(type, name2, std::move(sensor2));
    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);

    zone->addThermalInput(name1, false);
    zone->addThermalInput(name2, false);

    // Initialize Zone
    zone->initializeCache();

    // Verify now in failsafe mode.
    EXPECT_TRUE(zone->getFailSafeMode());

    ReadReturn r1;
    r1.value = 10.0;
    r1.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));

    ReadReturn r2;
    r2.value = 11.0;
    r2.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    zone->updateSensors();
    EXPECT_FALSE(zone->getFailSafeMode());

    // Ok, so we're not in failsafe mode, so let's set updated to the past.
    // sensor1 will have an updated field older than its timeout value, but
    // sensor2 will be fine. :D
    r1.updated -= std::chrono::seconds(3);
    r2.updated = std::chrono::high_resolution_clock::now();

    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    // Method under test will read each sensor.  One sensor's value is older
    // than the timeout for that sensor and this triggers failsafe mode.
    zone->updateSensors();
    EXPECT_TRUE(zone->getFailSafeMode());
}

TEST_F(PidZoneTest, ThermalInput_MissingIsAcceptableNoFailSafe)
{
    // This is similar to the above test, but because missingIsAcceptable
    // is set for sensor1, the zone should not enter failsafe mode when
    // only sensor1 goes missing.
    // However, sensor2 going missing should still trigger failsafe mode.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    int64_t timeout = 1;

    std::string name1 = "temp1";
    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string name2 = "temp2";
    std::unique_ptr<Sensor> sensor2 =
        std::make_unique<SensorMock>(name2, timeout);
    SensorMock* sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
    mgr.addSensor(type, name2, std::move(sensor2));
    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);

    // Only sensor1 has MissingIsAcceptable enabled for it
    zone->addThermalInput(name1, true);
    zone->addThermalInput(name2, false);

    // Initialize Zone
    zone->initializeCache();

    // As sensors are not initialized, zone should be in failsafe mode
    EXPECT_TRUE(zone->getFailSafeMode());

    // r1 not populated here, intentionally, to simulate a sensor that
    // is not available yet, perhaps takes a long time to start up.
    ReadReturn r1;
    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));

    ReadReturn r2;
    r2.value = 11.0;
    r2.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    zone->updateSensors();

    // Only sensor2 has been initialized here. Failsafe should be false,
    // because sensor1 MissingIsAcceptable so it is OK for it to go missing.
    EXPECT_FALSE(zone->getFailSafeMode());

    r1.value = 10.0;
    r1.updated = std::chrono::high_resolution_clock::now();

    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
    zone->updateSensors();

    // Both sensors are now properly initialized
    EXPECT_FALSE(zone->getFailSafeMode());

    // Ok, so we're not in failsafe mode, so let's set updated to the past.
    // sensor1 will have an updated field older than its timeout value, but
    // sensor2 will be fine. :D
    r1.updated -= std::chrono::seconds(3);
    r2.updated = std::chrono::high_resolution_clock::now();

    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
    zone->updateSensors();

    // MissingIsAcceptable is true for sensor1, so the zone should not be
    // thrown into failsafe mode.
    EXPECT_FALSE(zone->getFailSafeMode());

    // Do the same thing, but for the opposite sensors: r1 is good,
    // but r2 is set to some time in the past.
    r1.updated = std::chrono::high_resolution_clock::now();
    r2.updated -= std::chrono::seconds(3);

    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
    zone->updateSensors();

    // Now, the zone should be in failsafe mode, because sensor2 does not
    // have MissingIsAcceptable set true, it is still subject to failsafe.
    EXPECT_TRUE(zone->getFailSafeMode());

    r1.updated = std::chrono::high_resolution_clock::now();
    r2.updated = std::chrono::high_resolution_clock::now();

    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));
    zone->updateSensors();

    // The failsafe mode should cease, as both sensors are good again.
    EXPECT_FALSE(zone->getFailSafeMode());
}

TEST_F(PidZoneTest, FanInputTest_FailsafeToValid_ReadsSensors)
{
    // This will add a couple fan inputs, and verify the values are cached.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    std::string name1 = "fan1";
    int64_t timeout = 2;

    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string name2 = "fan2";
    std::unique_ptr<Sensor> sensor2 =
        std::make_unique<SensorMock>(name2, timeout);
    SensorMock* sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
    mgr.addSensor(type, name2, std::move(sensor2));
    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);

    // Now that the sensors exist, add them to the zone.
    zone->addFanInput(name1, false);
    zone->addFanInput(name2, false);

    // Initialize Zone
    zone->initializeCache();

    // Verify now in failsafe mode.
    EXPECT_TRUE(zone->getFailSafeMode());

    ReadReturn r1;
    r1.value = 10.0;
    r1.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));

    ReadReturn r2;
    r2.value = 11.0;
    r2.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    // Method under test will read through each fan sensor for the zone and
    // cache the values.
    zone->updateFanTelemetry();

    // We should no longer be in failsafe mode.
    EXPECT_FALSE(zone->getFailSafeMode());

    EXPECT_EQ(r1.value, zone->getCachedValue(name1));
    EXPECT_EQ(r2.value, zone->getCachedValue(name2));
}

TEST_F(PidZoneTest, FanInputTest_ValueTimeoutEntersFailSafeMode)
{
    // This will add a couple fan inputs, and verify the values are cached.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    std::string name1 = "fan1";
    int64_t timeout = 2;

    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string name2 = "fan2";
    std::unique_ptr<Sensor> sensor2 =
        std::make_unique<SensorMock>(name2, timeout);
    SensorMock* sensor_ptr2 = reinterpret_cast<SensorMock*>(sensor2.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);
    mgr.addSensor(type, name2, std::move(sensor2));
    EXPECT_EQ(mgr.getSensor(name2), sensor_ptr2);

    // Now that the sensors exist, add them to the zone.
    zone->addFanInput(name1, false);
    zone->addFanInput(name2, false);

    // Initialize Zone
    zone->initializeCache();

    // Verify now in failsafe mode.
    EXPECT_TRUE(zone->getFailSafeMode());

    ReadReturn r1;
    r1.value = 10.0;
    r1.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));

    ReadReturn r2;
    r2.value = 11.0;
    r2.updated = std::chrono::high_resolution_clock::now();
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    // Method under test will read through each fan sensor for the zone and
    // cache the values.
    zone->updateFanTelemetry();

    // We should no longer be in failsafe mode.
    EXPECT_FALSE(zone->getFailSafeMode());

    r1.updated -= std::chrono::seconds(3);
    r2.updated = std::chrono::high_resolution_clock::now();

    EXPECT_CALL(*sensor_ptr1, read()).WillOnce(Return(r1));
    EXPECT_CALL(*sensor_ptr2, read()).WillOnce(Return(r2));

    zone->updateFanTelemetry();
    EXPECT_TRUE(zone->getFailSafeMode());
}

TEST_F(PidZoneTest, GetSensorTest_ReturnsExpected)
{
    // One can grab a sensor from the manager through the zone.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    int64_t timeout = 1;

    std::string name1 = "temp1";
    std::unique_ptr<Sensor> sensor1 =
        std::make_unique<SensorMock>(name1, timeout);
    SensorMock* sensor_ptr1 = reinterpret_cast<SensorMock*>(sensor1.get());

    std::string type = "unchecked";
    mgr.addSensor(type, name1, std::move(sensor1));
    EXPECT_EQ(mgr.getSensor(name1), sensor_ptr1);

    zone->addThermalInput(name1, false);

    // Verify method under test returns the pointer we expect.
    EXPECT_EQ(mgr.getSensor(name1), zone->getSensor(name1));
}

TEST_F(PidZoneTest, AddThermalPIDTest_VerifiesThermalPIDsProcessed)
{
    // Tests adding a thermal PID controller to the zone, and verifies it's
    // touched during processing.

    std::unique_ptr<PIDController> tpid =
        std::make_unique<ControllerMock>("thermal1", zone.get());
    ControllerMock* tmock = reinterpret_cast<ControllerMock*>(tpid.get());

    // Access the internal pid configuration to clear it out (unrelated to the
    // test).
    [[maybe_unused]] ec::pid_info_t* info = tpid->getPIDInfo();

    zone->addThermalPID(std::move(tpid));

    EXPECT_CALL(*tmock, setptProc()).WillOnce(Return(10.0));
    EXPECT_CALL(*tmock, inputProc()).WillOnce(Return(11.0));
    EXPECT_CALL(*tmock, outputProc(_));

    // Method under test will, for each thermal PID, call setpt, input, and
    // output.
    zone->processThermals();
}

TEST_F(PidZoneTest, AddFanPIDTest_VerifiesFanPIDsProcessed)
{
    // Tests adding a fan PID controller to the zone, and verifies it's
    // touched during processing.

    std::unique_ptr<PIDController> tpid =
        std::make_unique<ControllerMock>("fan1", zone.get());
    ControllerMock* tmock = reinterpret_cast<ControllerMock*>(tpid.get());

    // Access the internal pid configuration to clear it out (unrelated to the
    // test).
    [[maybe_unused]] ec::pid_info_t* info = tpid->getPIDInfo();

    zone->addFanPID(std::move(tpid));

    EXPECT_CALL(*tmock, setptProc()).WillOnce(Return(10.0));
    EXPECT_CALL(*tmock, inputProc()).WillOnce(Return(11.0));
    EXPECT_CALL(*tmock, outputProc(_));

    // Method under test will, for each fan PID, call setpt, input, and output.
    zone->processFans();
}

TEST_F(PidZoneTest, ManualModeDbusTest_VerifySetManualBehavesAsExpected)
{
    // The manual(bool) method is inherited from the dbus mode interface.

    // Verifies that someone doesn't remove the internal call to the dbus
    // object from which we're inheriting.
    EXPECT_CALL(sdbus_mock_mode,
                sd_bus_emit_properties_changed_strv(
                    IsNull(), StrEq(objPath), StrEq(modeInterface), NotNull()))
        .WillOnce(Invoke(
            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
                [[maybe_unused]] const char* interface, const char** names) {
                EXPECT_STREQ("Manual", names[0]);
                return 0;
            }));

    // Method under test will set the manual mode to true and broadcast this
    // change on dbus.
    zone->manual(true);
    EXPECT_TRUE(zone->getManualMode());
}

TEST_F(PidZoneTest, FailsafeDbusTest_VerifiesReturnsExpected)
{
    // This property is implemented by us as read-only, such that trying to
    // write to it will have no effect.

    // Disable failsafe logger for the unit test.
    std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> empty_zone_map;
    buildFailsafeLoggers(empty_zone_map, 0);

    EXPECT_EQ(zone->failSafe(), zone->getFailSafeMode());
}

} // namespace
} // namespace pid_control