#include "TestUtil.h"
#include "TransmitHistory.h"
#include "gps/RTC.h"
#include <Throttle.h>
#include <unity.h>

// Reset the singleton between tests
static void resetTransmitHistory()
{
    if (transmitHistory) {
        delete transmitHistory;
        transmitHistory = nullptr;
    }
    transmitHistory = TransmitHistory::getInstance();
}

void setUp(void)
{
    resetTransmitHistory();
}

void tearDown(void) {}

static void test_setLastSentToMesh_stores_millis()
{
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);

    uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    TEST_ASSERT_NOT_EQUAL(0, result);

    // The stored millis value should be very close to current millis()
    uint32_t diff = millis() - result;
    TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms
}

static void test_set_overwrites_previous_value()
{
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
    uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);

    testDelay(50);

    transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
    uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);

    // The second value should be newer (larger millis)
    TEST_ASSERT_GREATER_THAN(first, second);
}

// --- Throttle integration ---

static void test_throttle_blocks_within_interval()
{
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
    uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);

    // Should be within a 10-minute interval (just set it)
    bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
    TEST_ASSERT_TRUE(withinInterval);
}

static void test_throttle_allows_after_interval()
{
    // Unknown key returns 0 — throttle should NOT block
    uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    TEST_ASSERT_EQUAL_UINT32(0, lastMs);

    // When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending
    bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000);
    TEST_ASSERT_TRUE(shouldSend);
}

static void test_throttle_blocks_after_set_then_zero_does_not()
{
    // Set it — now throttle should block
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);
    uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);
    bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000);
    TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval)

    // Different key — should allow
    uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
    bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000);
    TEST_ASSERT_TRUE(otherShouldSend);
}

// --- Multiple keys ---

static void test_multiple_keys_stored_independently()
{
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
    uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    testDelay(20);
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
    uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
    testDelay(20);
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP);

    uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP);
    uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP);

    // All should be non-zero
    TEST_ASSERT_NOT_EQUAL(0, nodeInfo);
    TEST_ASSERT_NOT_EQUAL(0, position);
    TEST_ASSERT_NOT_EQUAL(0, telemetry);

    // Updating other keys should not overwrite earlier key timestamps
    TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo);
    TEST_ASSERT_EQUAL_UINT32(positionInitial, position);
}

// --- Singleton ---

static void test_getInstance_returns_same_instance()
{
    TransmitHistory *a = TransmitHistory::getInstance();
    TransmitHistory *b = TransmitHistory::getInstance();
    TEST_ASSERT_EQUAL_PTR(a, b);
}

static void test_getInstance_creates_global()
{
    if (transmitHistory) {
        delete transmitHistory;
        transmitHistory = nullptr;
    }
    TEST_ASSERT_NULL(transmitHistory);

    TransmitHistory::getInstance();
    TEST_ASSERT_NOT_NULL(transmitHistory);
}

// --- Persistence round-trip (loadFromDisk / saveToDisk) ---

static void test_save_and_load_round_trip()
{
    // Set some values
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
    testDelay(10);
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);

    uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
    uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);

    // Force save
    transmitHistory->saveToDisk();

    // Reset and reload
    delete transmitHistory;
    transmitHistory = nullptr;
    transmitHistory = TransmitHistory::getInstance();
    transmitHistory->loadFromDisk();

    // Epoch values should be restored (if RTC was available when set)
    uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
    uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP);

    TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo);
    TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition);

    // After loadFromDisk, millis should be seeded (non-zero) for stored entries
    uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    if (restoredNodeInfo > 0) {
        // If epoch was stored (set seconds ago), epoch-conversion gives elapsed ≈ 0 s,
        // so getLastSentToMeshMillis() should return a non-zero value.
        TEST_ASSERT_NOT_EQUAL(0, restoredMillis);
    }
}

// --- Boot without RTC scenario ---

// Crash-reboot protection: a send that happened moments before the reboot must still
// throttle after reload. This works because getLastSentToMeshMillis() reconstructs
// a millis()-relative timestamp from the stored epoch, and Throttle uses unsigned
// subtraction so the age survives wraparound even when uptime is near zero.
static void test_boot_after_recent_send_still_throttles()
{
    transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
    transmitHistory->saveToDisk();

    // Simulate reboot
    delete transmitHistory;
    transmitHistory = nullptr;
    transmitHistory = TransmitHistory::getInstance();
    transmitHistory->loadFromDisk();

    // Epoch was set seconds ago; reconstructed age is still within the 10-min window.
    uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
    if (epoch == 0) {
        TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
        return;
    }

    TEST_ASSERT_NOT_EQUAL(0, result);
    bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000);
    TEST_ASSERT_TRUE(withinInterval);
}

// Regression test for issue #9901:
// A device powered off for longer than the throttle window must broadcast NodeInfo
// on its next boot — it must not be silenced because loadFromDisk() once treated
// every loaded entry as "just sent" by seeding lastMillis to millis() at boot.
static void test_boot_after_long_gap_allows_nodeinfo()
{
    if (getRTCQuality() <= RTCQualityNone) {
        TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test");
        return;
    }

    uint32_t now = getTime();

    // Simulate: last NodeInfo sent 30 minutes ago (outside the 10-min throttle window)
    transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (30 * 60));
    transmitHistory->saveToDisk();

    // Simulate reboot
    delete transmitHistory;
    transmitHistory = nullptr;
    transmitHistory = TransmitHistory::getInstance();
    transmitHistory->loadFromDisk();

    uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
    if (restoredEpoch == 0) {
        TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
        return;
    }

    uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000);
    TEST_ASSERT_FALSE_MESSAGE(throttled, "NodeInfo must not be throttled after a 30-min gap (#9901)");
}

// Complementary: a rapid reboot must still throttle (crash-loop protection), even
// though the reconstructed lastMs may wrap because current uptime is small.
static void test_boot_within_throttle_window_still_throttles()
{
    if (getRTCQuality() <= RTCQualityNone) {
        TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test");
        return;
    }

    uint32_t now = getTime();

    // Simulate: last NodeInfo sent 5 minutes ago (inside the 10-min throttle window)
    transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (5 * 60));
    transmitHistory->saveToDisk();

    // Simulate reboot
    delete transmitHistory;
    transmitHistory = nullptr;
    transmitHistory = TransmitHistory::getInstance();
    transmitHistory->loadFromDisk();

    uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP);
    if (restoredEpoch == 0) {
        TEST_IGNORE_MESSAGE("Epoch not persisted; skipping");
        return;
    }

    uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000);
    TEST_ASSERT_TRUE_MESSAGE(throttled, "NodeInfo must still be throttled when last send was within the 10-min window");
}

static void test_boot_without_time_source_still_throttles_recent_restart()
{
    setBootRelativeTimeForUnitTest(32);
    transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32);
    transmitHistory->saveToDisk();

    delete transmitHistory;
    transmitHistory = nullptr;
    transmitHistory = TransmitHistory::getInstance();

    setBootRelativeTimeForUnitTest(31);
    transmitHistory->loadFromDisk();

    uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000);
    TEST_ASSERT_TRUE_MESSAGE(throttled, "Recent no-RTC reboots should still suppress duplicate NodeInfo");
}

static void test_boot_without_time_source_expires_boot_relative_history()
{
    setBootRelativeTimeForUnitTest(32);
    transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32);
    transmitHistory->saveToDisk();

    delete transmitHistory;
    transmitHistory = nullptr;
    transmitHistory = TransmitHistory::getInstance();

    setBootRelativeTimeForUnitTest(400);
    transmitHistory->loadFromDisk();

    uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP);
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, restoredMs, "Boot-relative history should only suppress near-term restarts");
}

void setup()
{
    initializeTestEnvironment();

    // Wait for portduino's millis() clock to start ticking before tests run
    testDelay(10);
    testDelay(2000);

    UNITY_BEGIN();

    RUN_TEST(test_setLastSentToMesh_stores_millis);
    RUN_TEST(test_set_overwrites_previous_value);

    RUN_TEST(test_throttle_blocks_within_interval);
    RUN_TEST(test_throttle_allows_after_interval);
    RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not);

    RUN_TEST(test_multiple_keys_stored_independently);

    // Singleton
    RUN_TEST(test_getInstance_returns_same_instance);
    RUN_TEST(test_getInstance_creates_global);

    // Persistence
    RUN_TEST(test_save_and_load_round_trip);
    RUN_TEST(test_boot_after_recent_send_still_throttles);

    // Issue #9901 regression tests
    RUN_TEST(test_boot_after_long_gap_allows_nodeinfo);
    RUN_TEST(test_boot_within_throttle_window_still_throttles);

    // No-RTC regression tests
    RUN_TEST(test_boot_without_time_source_still_throttles_recent_restart);
    RUN_TEST(test_boot_without_time_source_expires_boot_relative_history);

    exit(UNITY_END());
}

void loop() {}
