/**
 * Tests for the radio configuration validation and clamping functions
 * introduced in the radio_interface_cherrypick branch.
 *
 * Targets:
 *  1. getRegion()
 *  2. RadioInterface::validateConfigRegion()
 *  3. RadioInterface::validateConfigLora()
 *  4. RadioInterface::clampConfigLora()
 *  5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF)
 *  6. Channel spacing calculation (placeholder for future protobuf changes)
 */

#include "Channels.h"
#include "DisplayFormatters.h"
#include "FSCommon.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RadioInterface.h"
#include "TestUtil.h"
#include "graphics/draw/MenuHandler.h"
#include "mesh/Channels.h"
#include "modules/AdminModule.h"
#include "modules/NodeInfoModule.h"
#include <pb_decode.h>
#include <pb_encode.h>
#include <string>
#include <unity.h>
#include <vector>

#include "meshtastic/config.pb.h"
#include "support/AdminModuleTestShim.h"

// hash() is a file-scope function in RadioInterface.cpp; link it in for slot-formula tests
extern uint32_t hash(const char *str);

// Every client notification the AdminModule emits flows through sendClientNotification();
// capture each formatted message so the warning/coalescing tests can assert on the exact
// set of messages produced by a sequence of admin messages. This shadows test/support/MockMeshService.h's
// release-only stub because these tests need to inspect the captured message text, not just avoid leaks.
static std::vector<std::string> capturedWarnings;

class MockMeshService : public MeshService
{
  public:
    void sendClientNotification(meshtastic_ClientNotification *n) override
    {
        capturedWarnings.push_back(n->message);
        releaseClientNotificationToPool(n);
    }
};

static MockMeshService *mockMeshService;

// -----------------------------------------------------------------------
// getRegion() tests
// -----------------------------------------------------------------------

static void test_getRegion_returnsCorrectRegion_US()
{
    const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    TEST_ASSERT_NOT_NULL(r);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code);
    TEST_ASSERT_EQUAL_STRING("US", r->name);
}

static void test_getRegion_returnsCorrectRegion_EU868()
{
    const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_NOT_NULL(r);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code);
    TEST_ASSERT_EQUAL_STRING("EU_868", r->name);
}

static void test_getRegion_returnsCorrectRegion_LORA24()
{
    const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
    TEST_ASSERT_NOT_NULL(r);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code);
    TEST_ASSERT_TRUE(r->wideLora);
}

static void test_getRegion_unsetCodeReturnsUnsetEntry()
{
    const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
    TEST_ASSERT_NOT_NULL(r);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
    TEST_ASSERT_EQUAL_STRING("UNSET", r->name);
}

static void test_getRegion_unknownCodeFallsToUnset()
{
    // A code not in the table should iterate to the UNSET sentinel
    const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255);
    TEST_ASSERT_NOT_NULL(r);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
}

// -----------------------------------------------------------------------
// validateConfigRegion() tests
// -----------------------------------------------------------------------

static void test_validateConfigRegion_validRegionReturnsTrue()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;

    // Ensure owner is not licensed (should not matter for non-licensed-only regions)
    devicestate.owner.is_licensed = false;

    TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
}

static void test_validateConfigRegion_unsetRegionReturnsTrue()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;

    devicestate.owner.is_licensed = false;

    // UNSET region has licensedOnly=false, so should pass
    TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
}

static void test_validateConfigRegion_unknownCodeReturnsFalse()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)255;

    devicestate.owner.is_licensed = false;

    // Unknown code is not in the regions table; getRegion() returns the UNSET sentinel,
    // whose .code != 255, so validateConfigRegion should reject it.
    TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
}

static void test_validateConfigRegion_anotherUnknownCodeReturnsFalse()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)99;

    devicestate.owner.is_licensed = true;

    // Unknown code should be rejected even when owner is licensed.
    TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
}

// -----------------------------------------------------------------------
// Shadow tables for testing (preset lists → profiles → regions → lookup)
// -----------------------------------------------------------------------

// A minimal preset list with only one entry
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = {
    meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
    MODEM_PRESET_END,
};

// A preset list that includes all turbo variants only
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = {
    meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
    meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,
    MODEM_PRESET_END,
};

// A restricted list simulating a hypothetical tight-regulation region
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = {
    meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
    meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
    MODEM_PRESET_END,
};

// Mirrors PROFILE_STD but with non-zero spacing/padding for testing
static const RegionProfile TEST_PROFILE_SPACED = {
    TEST_PRESETS_SINGLE,
    /* spacing */ 0.025f,
    /* padding */ 0.010f,
    /* audioPermitted */ true,
    /* licensedOnly */ false,
    /* textThrottle */ 0,
    /* positionThrottle */ 0,
    /* telemetryThrottle */ 0,
};

// A licensed-only profile for testing access control
static const RegionProfile TEST_PROFILE_LICENSED = {
    TEST_PRESETS_RESTRICTED,
    /* spacing */ 0.0f,
    /* padding */ 0.0f,
    /* audioPermitted */ false,
    /* licensedOnly */ true,
    /* textThrottle */ 5,
    /* positionThrottle */ 10,
    /* telemetryThrottle */ 10,
};

// Turbo-only profile
static const RegionProfile TEST_PROFILE_TURBO = {
    TEST_PRESETS_TURBO_ONLY,
    /* spacing */ 0.0f,
    /* padding */ 0.0f,
    /* audioPermitted */ true,
    /* licensedOnly */ false,
    /* textThrottle */ 0,
    /* positionThrottle */ 0,
    /* telemetryThrottle */ 0,
};

// A preset list for the preset-hash override slot test (LONG_FAST + MEDIUM_FAST)
static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_PRESET_HASH[] = {
    meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
    meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
    MODEM_PRESET_END,
};

// Profile with overrideSlot = OVERRIDE_SLOT_PRESET_HASH (-1):
// slot selection always uses hash(presetDisplayName), ignoring the primary channel name.
static const RegionProfile TEST_PROFILE_PRESET_HASH = {
    TEST_PRESETS_PRESET_HASH,
    /* spacing */ 0.0f,
    /* padding */ 0.0f,
    /* audioPermitted */ true,
    /* licensedOnly */ false,
    /* textThrottle */ 0,
    /* positionThrottle */ 0,
    /* telemetryThrottle */ 0,
};

// Standalone test region using US frequencies (26 MHz span → 104 slots at 250 kHz BW)
// Used to verify OVERRIDE_SLOT_PRESET_HASH slot formula; not inserted into testRegions[].
static const RegionInfo TEST_REGION_PRESET_HASH = {
    meshtastic_Config_LoRaConfig_RegionCode_US,
    902.0f,
    928.0f,
    100,
    30,
    false,
    false,
    &TEST_PROFILE_PRESET_HASH,
    meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
    OVERRIDE_SLOT_PRESET_HASH,
    "TEST_PRESET_HASH",
};

static const RegionInfo testRegions[] = {
    // A wide US-like region with spacing + padding
    {meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
     meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_US_SPACED"},

    // A narrow band simulating tight EU regulation
    {meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED,
     meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, 3, "TEST_EU_LICENSED"},

    // A wide-LoRa region with turbo-only presets
    {meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
     meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, 0, "TEST_LORA24_TURBO"},

    // Sentinel - must be last
    {meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
     meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_UNSET"},
};

static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
{
    const RegionInfo *r = testRegions;
    while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
        if (r->code == code)
            return r;
        r++;
    }
    return r; // Returns the UNSET sentinel
}

// -----------------------------------------------------------------------
// Shadow table tests
// -----------------------------------------------------------------------

// Helper: replicate the numFreqSlots formula from RadioInterface so tests can compute expected values.
static uint32_t testComputeNumFreqSlots(const RegionInfo *r, float bw_kHz)
{
    float w = r->profile->spacing + (r->profile->padding * 2) + (bw_kHz / 1000.0f);
    return (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / w) + 0.5f);
}

static void test_shadowTable_spacedProfileHasNonZeroSpacing()
{
    const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name);
    TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing);
    TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding);
}

static void test_shadowTable_licensedProfileFlagsCorrect()
{
    const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_TRUE(r->profile->licensedOnly);
    TEST_ASSERT_FALSE(r->profile->audioPermitted);
    TEST_ASSERT_EQUAL(3, r->overrideSlot);
}

static void test_shadowTable_presetCountMatchesExpected()
{
    const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    TEST_ASSERT_EQUAL(1, spaced->getNumPresets());

    const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_EQUAL(2, licensed->getNumPresets());

    const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
    TEST_ASSERT_EQUAL(2, turbo->getNumPresets());
}

static void test_shadowTable_defaultPresetIsFirstInList()
{
    const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset());

    const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset());

    const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset());
}

static void test_shadowTable_channelSpacingWithPadding()
{
    // Verify channel count when spacing + padding are non-zero
    const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
    float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f);

    // spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250
    // channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz
    TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing);

    uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f);
    // (928 - 902 + 0.025) / 0.295 = 88.2 → 88
    TEST_ASSERT_EQUAL_UINT32(88, numChannels);
}

static void test_shadowTable_turboOnlyOnWideLora()
{
    const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
    TEST_ASSERT_TRUE(r->wideLora);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset());

    // Verify wide-LoRa bandwidth for SHORT_TURBO
    float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora);
    TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode
}

static void test_shadowTable_unknownCodeFallsToSentinel()
{
    const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code);
    TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name);
}

static void test_shadowTable_presetHashProfileHasCorrectOverrideSlot()
{
    TEST_ASSERT_EQUAL(OVERRIDE_SLOT_PRESET_HASH, TEST_REGION_PRESET_HASH.overrideSlot);
    TEST_ASSERT_EQUAL(-1, TEST_REGION_PRESET_HASH.overrideSlot);
    TEST_ASSERT_EQUAL(2, TEST_REGION_PRESET_HASH.getNumPresets());
}

// -----------------------------------------------------------------------
// OVERRIDE_SLOT_PRESET_HASH (-1) slot formula tests
//
// Property under test:
//   overrideSlot = -1  → slot = hash(presetDisplayName) % numSlots
//     regardless of what the primary channel is named
//   overrideSlot = 0   → slot = hash(channelName) % numSlots
//     when channel name = preset display name, these two modes give identical slots
// -----------------------------------------------------------------------

static void test_overrideSlotPresetHash_longFast_customChannelMatchesDefaultNameSlot()
{
    // US + LONG_FAST: spacing=0, padding=0, bw=250 kHz
    // numSlots = round((928-902+0)/0.250) = 104
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
    uint32_t numSlots = testComputeNumFreqSlots(us, bw);
    TEST_ASSERT_EQUAL_UINT32(104, numSlots); // sanity

    const char *presetName =
        DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);

    // OVERRIDE_SLOT_PRESET_HASH (-1):
    //   channel is "MyCustomNetwork" but slot still uses preset name hash
    uint32_t slotPresetHashMode = hash(presetName) % numSlots;

    // OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH (0) with channel name = preset name (user never renamed it):
    //   channelName == presetName → same hash → same slot
    const char *defaultChannelName = presetName;
    uint32_t slotChannelHashModeDefaultName = hash(defaultChannelName) % numSlots;

    TEST_ASSERT_EQUAL_UINT32(slotPresetHashMode, slotChannelHashModeDefaultName);

    // Confirm a different custom channel name gives a different hash INPUT
    // (so mode 0 would diverge while mode -1 stays locked)
    TEST_ASSERT_TRUE(strcmp(presetName, "MyCustomNetwork") != 0);
}

static void test_overrideSlotPresetHash_mediumFast_customChannelMatchesDefaultNameSlot()
{
    // US + MEDIUM_FAST: bw=250 kHz → same 104 slots as LONG_FAST for US
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, us->wideLora);
    uint32_t numSlots = testComputeNumFreqSlots(us, bw);
    TEST_ASSERT_EQUAL_UINT32(104, numSlots); // sanity

    const char *presetName =
        DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);

    // Mode -1: slot = hash(presetName) % numSlots (channel name irrelevant)
    uint32_t slotPresetHashMode = hash(presetName) % numSlots;

    // Mode 0 + default name (channel name = preset display name):
    uint32_t slotChannelHashModeDefaultName = hash(presetName) % numSlots;

    TEST_ASSERT_EQUAL_UINT32(slotPresetHashMode, slotChannelHashModeDefaultName);

    TEST_ASSERT_TRUE(strcmp(presetName, "MyCustomNetwork") != 0);
}

static void test_overrideSlotPresetHash_longFast_slotIsStableAcrossCustomNames()
{
    // Mode -1 must give the same slot for LONG_FAST regardless of which custom name is in use.
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
    uint32_t numSlots = testComputeNumFreqSlots(us, bw);

    const char *presetName =
        DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
    uint32_t expectedSlot = hash(presetName) % numSlots;

    // Simulate three different custom channel names; mode -1 ignores all of them
    const char *customNames[] = {"AlphaNet", "BetaMesh", "GammaMesh"};
    for (int i = 0; i < 3; i++) {
        uint32_t slotForCustom = hash(presetName) % numSlots; // mode -1: presetName only
        TEST_ASSERT_EQUAL_UINT32(expectedSlot, slotForCustom);
        // Confirm input would have differed in mode 0
        TEST_ASSERT_TRUE(strcmp(presetName, customNames[i]) != 0);
    }
}

static void test_overrideSlotPresetHash_mediumFast_slotIsStableAcrossCustomNames()
{
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, us->wideLora);
    uint32_t numSlots = testComputeNumFreqSlots(us, bw);

    const char *presetName =
        DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);
    uint32_t expectedSlot = hash(presetName) % numSlots;

    const char *customNames[] = {"AlphaNet", "BetaMesh", "GammaMesh"};
    for (int i = 0; i < 3; i++) {
        uint32_t slotForCustom = hash(presetName) % numSlots; // mode -1: presetName only
        TEST_ASSERT_EQUAL_UINT32(expectedSlot, slotForCustom);
        TEST_ASSERT_TRUE(strcmp(presetName, customNames[i]) != 0);
    }
}

static void test_overrideSlotPresetHash_longFastAndMediumFast_slotsAreDifferentPresets()
{
    // LONG_FAST and MEDIUM_FAST have different display names → likely different hash slots.
    // This verifies the two presets genuinely occupy distinct positions, so the equivalence
    // tests above are not trivially vacuous.
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw_lf = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false);
    float bw_mf = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false);
    uint32_t numSlots_lf = testComputeNumFreqSlots(us, bw_lf);
    uint32_t numSlots_mf = testComputeNumFreqSlots(us, bw_mf);

    const char *nameLF =
        DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, false, true);
    const char *nameMF =
        DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, false, true);

    TEST_ASSERT_TRUE(strcmp(nameLF, nameMF) != 0);

    uint32_t slotLF = hash(nameLF) % numSlots_lf;
    uint32_t slotMF = hash(nameMF) % numSlots_mf;
    // They use the same numSlots (both 250 kHz on US), so a difference in display name
    // should produce a different slot.
    TEST_ASSERT_NOT_EQUAL(slotLF, slotMF);
}

// -----------------------------------------------------------------------
// validateConfigLora() tests
// -----------------------------------------------------------------------

static void test_validateConfigLora_validPresetForUS()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;

    TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
}

static void test_validateConfigLora_allStdPresetsValidForUS()
{
    meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,     meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
        meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW,   meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW,    meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,    meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
    };

    for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
        meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
        cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
        cfg.use_preset = true;
        cfg.modem_preset = stdPresets[i];
        TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US");
    }
}

static void test_validateConfigLora_turboPresetsInvalidForEU868()
{
    // EU_868 has PRESETS_EU_868 which excludes the 500 kHz turbo presets
    // (SHORT_TURBO, LONG_TURBO, MEDIUM_TURBO)
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    cfg.use_preset = true;

    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
    TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868");

    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO;
    TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868");

    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO;
    TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_TURBO should be invalid for EU_868");
}

static void test_validateConfigLora_validPresetsForEU868()
{
    meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = {
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,     meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
        meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW,   meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW,    meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE,
    };

    for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) {
        meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
        cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
        cfg.use_preset = true;
        cfg.modem_preset = eu868Presets[i];
        TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868");
    }
}

static void test_validateConfigLora_customBandwidthTooWideForEU868()
{
    // EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz
    // A 500 kHz custom BW should be rejected
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    cfg.use_preset = false;
    cfg.bandwidth = 500;
    cfg.spread_factor = 11;
    cfg.coding_rate = 5;

    TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
}

static void test_validateConfigLora_customBandwidthFitsUS()
{
    // US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    cfg.use_preset = false;
    cfg.bandwidth = 250;
    cfg.spread_factor = 11;
    cfg.coding_rate = 5;

    TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
}

static void test_validateConfigLora_customBandwidthFitsEU868()
{
    // EU_868 spans 250 kHz, 125 kHz BW should fit
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    cfg.use_preset = false;
    cfg.bandwidth = 125;
    cfg.spread_factor = 12;
    cfg.coding_rate = 8;

    TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg));
}

static void test_validateConfigLora_bogusPresetRejected()
{
    // A fabricated preset value not in any list should be rejected
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    cfg.use_preset = true;
    cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;

    TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
}

static void test_validateConfigLora_unsetRegionAcceptsAnyRealPreset()
{
    // UNSET is "no region chosen yet", not a regulatory domain, so it must not invalidate
    // a preset the user already picked - whichever region that preset belongs to.
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    cfg.use_preset = true;

    const meshtastic_Config_LoRaConfig_ModemPreset realPresets[] = {
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,   meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
        meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST,   meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW,
        meshtastic_Config_LoRaConfig_ModemPreset_TINY_FAST,
    };
    for (auto preset : realPresets) {
        cfg.modem_preset = preset;
        char msg[64];
        snprintf(msg, sizeof(msg), "preset %d should be valid for UNSET", (int)preset);
        TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), msg);
    }

    // A value no region offers is still invalid, so the clamp can repair it.
    cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;
    TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "bogus preset should be invalid for UNSET");
}

static void test_isKnownModemPreset_matchesRegionTable()
{
    // Every preset some region offers is "known"...
    TEST_ASSERT_TRUE(isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
    TEST_ASSERT_TRUE(isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO));
    TEST_ASSERT_TRUE(isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW));
    TEST_ASSERT_TRUE(isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW));

    // ...and nothing else is, including the retired VERY_LONG_SLOW enum value.
    TEST_ASSERT_FALSE(isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset_VERY_LONG_SLOW));
    TEST_ASSERT_FALSE(isKnownModemPreset((meshtastic_Config_LoRaConfig_ModemPreset)99));
}

static void test_validateConfigLora_allPresetsValidForLORA24()
{
    // LORA_24 uses PROFILE_STD (10 presets) with wideLora=true
    meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = {
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,     meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
        meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW,   meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW,    meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
        meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO,    meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO,
    };

    for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) {
        meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
        cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
        cfg.use_preset = true;
        cfg.modem_preset = stdPresets[i];
        TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24");
    }
}

// -----------------------------------------------------------------------
// clampConfigLora() tests
// -----------------------------------------------------------------------

static void test_clampConfigLora_invalidPresetClampedToDefault()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list

    RadioInterface::clampConfigLora(cfg);

    const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset);
}

static void test_clampConfigLora_validPresetUnchanged()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
}

static void test_clampConfigLora_customBwTooWideClampedToDefaultBw()
{
    // EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW.
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    cfg.use_preset = false;
    cfg.bandwidth = 500;
    cfg.spread_factor = 11;
    cfg.coding_rate = 5;

    RadioInterface::clampConfigLora(cfg);

    const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora);
    TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth);
}

static void test_clampConfigLora_customBwValidLeftUnchanged()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    cfg.use_preset = false;
    cfg.bandwidth = 125;
    cfg.spread_factor = 12;
    cfg.coding_rate = 8;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
}

static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast()
{
    // UNSET's default preset is LONG_FAST; a value no region offers clamps to it
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    cfg.use_preset = true;
    cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
}

static void test_clampConfigLora_unsetRegionKeepsRealPreset()
{
    // The boot-time clamp (NodeDB::loadFromDisk) runs on every boot. While the region is
    // unset it must leave a real preset alone rather than rewriting it to LONG_FAST.
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, cfg.modem_preset);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, cfg.region);
}

static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault()
{
    // LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD)
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
    cfg.use_preset = true;
    cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99;

    RadioInterface::clampConfigLora(cfg);

    const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24);
    TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset);
}

// -----------------------------------------------------------------------
// Region-locked preset swap tests (EU_868 / EU_866 / EU_N_868 trio)
// -----------------------------------------------------------------------

static void test_clampConfigLora_narrowPresetOnEU866SwapsToEUN868()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868, cfg.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST, cfg.modem_preset);
}

static void test_clampConfigLora_litePresetOnEU868SwapsToEU866()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_866, cfg.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW, cfg.modem_preset);
}

static void test_clampConfigLora_eu868PresetOnEUN868SwapsToEU868()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_N_868;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, cfg.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
}

static void test_clampConfigLora_litePresetOnUSDoesNotSwap()
{
    // Previous region is not one of the swappable trio, so the preset clamps to the
    // region default instead of swapping regions.
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST;

    RadioInterface::clampConfigLora(cfg);

    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, cfg.region);
    TEST_ASSERT_EQUAL(us->getDefaultPreset(), cfg.modem_preset);
}

static void test_clampConfigLora_narrowPresetOnHam125cmDoesNotSwap()
{
    // ITU2_125CM shares the NARROW presets, so they are valid there and nothing changes
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW;

    RadioInterface::clampConfigLora(cfg);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM, cfg.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW, cfg.modem_preset);
}

static void test_validateConfigLora_siblingLockedPresetStillFailsValidation()
{
    // Validation (no clamp) must keep failing so callers route into clampConfigLora,
    // which performs the region swap.
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
    cfg.use_preset = true;
    cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST;

    TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg));
}

// -----------------------------------------------------------------------
// RegionInfo preset list integrity tests
// -----------------------------------------------------------------------

static void test_presetsStd_hasTenEntries()
{
    // PROFILE_STD should have exactly 10 presets (adds MEDIUM_TURBO to the turbo cluster)
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    TEST_ASSERT_EQUAL(10, us->getNumPresets());
    TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets());
}

static void test_presetsEU868_hasSevenEntries()
{
    const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_EQUAL(7, eu->getNumPresets());
    TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets());
}

static void test_presetsUndef_hasOneEntry()
{
    const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
    TEST_ASSERT_EQUAL(1, unset->getNumPresets());
    TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets());
}

static void test_defaultPresetIsInAvailablePresets()
{
    // For every region, the defaultPreset must appear in its own availablePresets list
    const RegionInfo *r = regions;
    while (true) {
        bool found = false;
        for (size_t i = 0; i < r->getNumPresets(); i++) {
            if (r->getAvailablePresets()[i] == r->getDefaultPreset()) {
                found = true;
                break;
            }
        }
        char msg[80];
        snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name);
        TEST_ASSERT_TRUE_MESSAGE(found, msg);

        if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
            break; // UNSET is the sentinel, stop after it
        r++;
    }
}

static void test_regionFieldsAreSane()
{
    // Basic sanity check: all regions have freqEnd > freqStart and a non-null name
    const RegionInfo *r = regions;
    while (true) {
        char msg[80];
        snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name);
        TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg);
        TEST_ASSERT_NOT_NULL(r->name);
        TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0");
        TEST_ASSERT_NOT_NULL(r->getAvailablePresets());

        if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
            break;
        r++;
    }
}

static void test_onlyLORA24HasWideLora()
{
    // Verify that LORA_24 is the only region with wideLora=true
    const RegionInfo *r = regions;
    while (true) {
        char msg[80];
        if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
            snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name);
            TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg);
        } else {
            snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name);
            TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg);
        }

        if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
            break;
        r++;
    }
}

// -----------------------------------------------------------------------
// Channel spacing calculation (placeholder for future protobuf updates)
// -----------------------------------------------------------------------

static void test_channelSpacingCalculation_US_LONG_FAST()
{
    // Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000)
    // US: spacing=0, padding=0
    // LONG_FAST on non-wide region: bw=250 kHz
    // channelSpacing = 0 + 0 + 0.250 = 0.250 MHz
    // numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104
    const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
    float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora);
    float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f);
    uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f);

    TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
    TEST_ASSERT_EQUAL_UINT32(104, numChannels);
}

static void test_channelSpacingCalculation_EU868_LONG_FAST()
{
    // EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0
    // LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz
    // numChannels = round((0.25 + 0) / 0.250) = 1
    const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora);
    float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f);
    uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f);

    TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing);
    TEST_ASSERT_EQUAL_UINT32(1, numChannels);
}

// Placeholder: when protobuf region definitions include non-zero padding/spacing,
// add tests here to verify the channel count and frequency calculations.
static void test_channelSpacingCalculation_placeholder()
{
    // TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values,
    // verify:
    //  - Channel count matches expected value for each (region, preset) pair
    //  - First channel frequency = freqStart + (bw/2000) + padding
    //  - Nth channel frequency = first + (n * channelSpacing)
    //  - overrideSlot, when non-zero, forces the channel_num
    TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields");
}

// -----------------------------------------------------------------------
// handleSetConfig fromOthers dispatch tests
// -----------------------------------------------------------------------

// AdminModuleTestShim comes from test/support - the friend seam AdminModule.h declares.
static AdminModuleTestShim *testAdmin;
static NodeDB *savedNodeDB;
static NodeDB *replacementNodeDB;
static NodeInfoModule *savedNodeInfoModule;
static meshtastic_DeviceState savedDeviceState;
static meshtastic_User savedOwner;
static meshtastic_LocalConfig savedConfig;
static meshtastic_ChannelFile savedChannelFile;

// Called from setUp/tearDown for every test, not opted into by a handful. A shared NodeDB plus
// unrestored config/owner/devicestate/channelFile means each test inherits whatever its
// predecessors left, and the admin handlers under test write all four.
static void replaceAdminRadioGlobals()
{
    savedNodeDB = nodeDB;
    savedNodeInfoModule = nodeInfoModule;
    savedDeviceState = devicestate;
    savedOwner = owner;
    savedConfig = config;
    savedChannelFile = channelFile;
    replacementNodeDB = new NodeDB();
    nodeDB = replacementNodeDB;
}

static void restoreAdminRadioGlobals()
{
    nodeInfoModule = savedNodeInfoModule;
    nodeDB = savedNodeDB;
    delete replacementNodeDB;
    replacementNodeDB = nullptr;
    devicestate = savedDeviceState;
    owner = savedOwner;
    config = savedConfig;
    channelFile = savedChannelFile;
    initRegion();
}

static void installEncryptedAndAdminChannels()
{
    channels.initDefaults();
    meshtastic_Channel admin = meshtastic_Channel_init_zero;
    admin.index = 1;
    admin.role = meshtastic_Channel_Role_SECONDARY;
    admin.has_settings = true;
    strncpy(admin.settings.name, Channels::adminChannel, sizeof(admin.settings.name));
    admin.settings.psk.size = 16;
    memset(admin.settings.psk.bytes, 0xA5, admin.settings.psk.size);
    channels.setChannel(admin);

    meshtastic_Channel secondary = meshtastic_Channel_init_zero;
    secondary.index = 2;
    secondary.role = meshtastic_Channel_Role_SECONDARY;
    secondary.has_settings = true;
    strncpy(secondary.settings.name, "private", sizeof(secondary.settings.name));
    secondary.settings.psk.size = 32;
    memset(secondary.settings.psk.bytes, 0x5A, secondary.settings.psk.size);
    channels.setChannel(secondary);
}

static void assertLicensedChannelsSanitized()
{
    TEST_ASSERT_EQUAL(0, channels.getByIndex(0).settings.psk.size);
    TEST_ASSERT_EQUAL(meshtastic_Channel_Role_DISABLED, channels.getByIndex(1).role);
    TEST_ASSERT_EQUAL(0, channels.getByIndex(1).settings.psk.size);
    TEST_ASSERT_EQUAL(0, channels.getByIndex(2).settings.psk.size);
}

static void test_handleSetOwner_persistsLicensedChannelSanitation()
{
    owner = meshtastic_User_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    installEncryptedAndAdminChannels();

    meshtastic_User licensed = meshtastic_User_init_zero;
    licensed.is_licensed = true;
    testAdmin->deferSaves();
    nodeInfoModule = reinterpret_cast<NodeInfoModule *>(1); // reloadOwner(false) only checks presence
    testAdmin->handleSetOwner(licensed);

    TEST_ASSERT_TRUE(testAdmin->savedSegments() & SEGMENT_CHANNELS);
    assertLicensedChannelsSanitized();

    uint8_t encoded[meshtastic_ChannelFile_size];
    const size_t encodedSize = pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_ChannelFile_msg, &channelFile);
    TEST_ASSERT_GREATER_THAN(0, encodedSize);
    meshtastic_ChannelFile reloaded = meshtastic_ChannelFile_init_zero;
    TEST_ASSERT_TRUE(pb_decode_from_bytes(encoded, encodedSize, &meshtastic_ChannelFile_msg, &reloaded));
    channelFile = reloaded;
    assertLicensedChannelsSanitized();
    TEST_ASSERT_FALSE_MESSAGE(channels.ensureLicensedOperation(), "sanitized reload must not trigger another persistence write");
}

static void test_bootDefense_sanitizesStaleLicensedChannelsOnce()
{
    owner = meshtastic_User_init_zero;
    owner.is_licensed = true;
    installEncryptedAndAdminChannels();

    TEST_ASSERT_TRUE(channels.ensureLicensedOperation());
    assertLicensedChannelsSanitized();
    TEST_ASSERT_FALSE_MESSAGE(channels.ensureLicensedOperation(), "boot sanitation must be idempotent");
}

static void test_restorePreferences_sanitizesLicensedBackupBeforeReturn()
{
    NodeDB *savedNodeDB = nodeDB;
    nodeDB = new NodeDB();
    const meshtastic_DeviceState savedDeviceState = devicestate;
    const meshtastic_ChannelFile savedChannelFile = channelFile;

    owner = meshtastic_User_init_zero;
    owner.is_licensed = true;
    installEncryptedAndAdminChannels();
    TEST_ASSERT_TRUE(nodeDB->backupPreferences(meshtastic_AdminMessage_BackupLocation_FLASH));

    owner.is_licensed = false;
    channels.initDefaults();
    TEST_ASSERT_TRUE(
        nodeDB->restorePreferences(meshtastic_AdminMessage_BackupLocation_FLASH, SEGMENT_DEVICESTATE | SEGMENT_CHANNELS));
    TEST_ASSERT_TRUE(owner.is_licensed);
    assertLicensedChannelsSanitized();
    TEST_ASSERT_FALSE_MESSAGE(channels.ensureLicensedOperation(), "restored licensed channels must remain sanitized");

    devicestate = savedDeviceState;
    channelFile = savedChannelFile;
    nodeDB->saveToDisk(SEGMENT_DEVICESTATE | SEGMENT_CHANNELS);
    FSCom.remove(backupFileName);
    delete nodeDB;
    nodeDB = savedNodeDB;
}

static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset,
                                           meshtastic_Config_LoRaConfig_ModemPreset preset)
{
    meshtastic_Config c = meshtastic_Config_init_zero;
    c.which_payload_variant = meshtastic_Config_lora_tag;
    c.payload_variant.lora.region = region;
    c.payload_variant.lora.use_preset = usePreset;
    c.payload_variant.lora.modem_preset = preset;
    return c;
}

static void test_handleSetConfig_persistsLicensedFirstRegionIdentity()
{
    owner = meshtastic_User_init_zero;
    owner.is_licensed = true;
    config.security = meshtastic_Config_SecurityConfig_init_zero;
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    initRegion();

    testAdmin->deferSaves();
    const meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
    testAdmin->handleSetConfig(c, false);

    const int expectedSegments = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE;
    TEST_ASSERT_EQUAL_INT(expectedSegments, testAdmin->savedSegments());
    TEST_ASSERT_EQUAL(32, config.security.private_key.size);
    TEST_ASSERT_EQUAL(32, owner.public_key.size);
}

static void test_handleSetConfig_fromOthers_invalidPresetRejected()
{
    // Set up a known-good baseline in the global config
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    // Build an admin set_config with an invalid preset for EU_868
    meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
                                            meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);

    testAdmin->handleSetConfig(c, true); // fromOthers = true

    // fromOthers=true: invalid preset should be rejected, old preset preserved
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
}

static void test_handleSetConfig_fromLocal_invalidPresetClamped()
{
    // Set up a known-good baseline
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    // Build an admin set_config with an invalid preset for EU_868
    meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
                                            meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);

    testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)

    // fromOthers=false: invalid preset should be clamped to the region's default
    const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset);
}

static void test_handleSetConfig_fromOthers_validPresetAccepted()
{
    // Set up baseline
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    // Build an admin set_config with a valid preset for EU_868
    meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true,
                                            meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);

    testAdmin->handleSetConfig(c, true); // fromOthers = true

    // Valid preset should be accepted regardless of fromOthers
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset);
}

static void test_handleSetConfig_fromOthers_invalidChannelNumFullyRejected()
{
    // Rejecting a remote config must reject ALL of it: an invalid channel_num must not
    // leak into config.lora alongside the restored region/preset.
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    config.lora.channel_num = 0;
    initRegion();

    meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
    c.payload_variant.lora.channel_num = 5000; // far beyond US slot count

    testAdmin->handleSetConfig(c, true); // fromOthers = true

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, config.lora.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
    TEST_ASSERT_EQUAL_UINT32(0, config.lora.channel_num);
}

// clampBandwidthCode: an unset (0) bandwidth code maps to the default; any other code is left as-is.
static void test_clampBandwidthCode_zeroMapsToDefaultOthersUnchanged()
{
    TEST_ASSERT_NOT_EQUAL_UINT16(0, clampBandwidthCode(0)); // the point of the fix: 0 must not stay 0
    TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), clampBandwidthCode(0));
    TEST_ASSERT_EQUAL_UINT16(250, clampBandwidthCode(250));
    TEST_ASSERT_EQUAL_UINT16(125, clampBandwidthCode(125));
    TEST_ASSERT_EQUAL_UINT16(31, clampBandwidthCode(31));
}

// A custom (non-preset) config that leaves bandwidth at its proto zero-value must not persist as 0.
// Pre-fix it slipped past validateConfigLora() and the radio silently ran at the default while
// get_config still reported bandwidth 0. It is now coerced to the default code on ingest.
static void test_handleSetConfig_fromLocal_customBandwidthZeroClampedToDefault()
{
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
    c.payload_variant.lora.spread_factor = 11;
    c.payload_variant.lora.coding_rate = 5;
    c.payload_variant.lora.bandwidth = 0; // the footgun: unset custom bandwidth

    testAdmin->handleSetConfig(c, false); // fromOthers = false (local client)

    TEST_ASSERT_FALSE(config.lora.use_preset);
    TEST_ASSERT_NOT_EQUAL_UINT16(0, config.lora.bandwidth); // must not persist as 0
    TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), config.lora.bandwidth);
}

// Remote admin (fromOthers) is subject to the same ingest clamp: a custom bandwidth 0 from another
// node is normalized to the default rather than persisted as 0 (it does not weaken the wholesale
// rejection of configs that actually fail validation - a 0 bandwidth already passed validation).
static void test_handleSetConfig_fromOthers_customBandwidthZeroClampedToDefault()
{
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
    c.payload_variant.lora.spread_factor = 11;
    c.payload_variant.lora.coding_rate = 5;
    c.payload_variant.lora.bandwidth = 0;

    testAdmin->handleSetConfig(c, true); // fromOthers = true

    TEST_ASSERT_FALSE(config.lora.use_preset);
    TEST_ASSERT_EQUAL_UINT16(bwKHzToCode(LORA_BW_DEFAULT_KHZ), config.lora.bandwidth);
}

// In preset mode bandwidth 0 is the norm (the preset supplies it); the ingest clamp must leave it
// untouched so preset configs still read back bandwidth 0.
static void test_handleSetConfig_fromLocal_presetBandwidthZeroLeftUntouched()
{
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
    c.payload_variant.lora.bandwidth = 0;

    testAdmin->handleSetConfig(c, false);

    TEST_ASSERT_TRUE(config.lora.use_preset);
    TEST_ASSERT_EQUAL_UINT16(0, config.lora.bandwidth);
}

// A custom (non-preset) config with an already-valid bandwidth must be preserved verbatim.
static void test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved()
{
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, false, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
    c.payload_variant.lora.spread_factor = 11;
    c.payload_variant.lora.coding_rate = 5;
    c.payload_variant.lora.bandwidth = 125;

    testAdmin->handleSetConfig(c, false);

    TEST_ASSERT_FALSE(config.lora.use_preset);
    TEST_ASSERT_EQUAL_UINT16(125, config.lora.bandwidth);
}

// A security-config SET that omits the private key (partial/legacy client editing some other security field)
// must NOT regenerate our keypair: our NodeNum is crc32(public_key), so a new keypair would silently change
// our identity. The existing keypair has to be preserved.
static void test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted()
{
    config.security = meshtastic_Config_SecurityConfig_init_zero;
    config.security.private_key.size = 32;
    memset(config.security.private_key.bytes, 0x11, 32);
    config.security.public_key.size = 32;
    memset(config.security.public_key.bytes, 0x22, 32);

    // Incoming SET carries no private/public key, just another security field.
    meshtastic_Config c = meshtastic_Config_init_zero;
    c.which_payload_variant = meshtastic_Config_security_tag;
    c.payload_variant.security.serial_enabled = true;

    testAdmin->deferSaves();
    testAdmin->handleSetConfig(c, false);

    uint8_t expectedPriv[32];
    memset(expectedPriv, 0x11, 32);
    uint8_t expectedPub[32];
    memset(expectedPub, 0x22, 32);
    TEST_ASSERT_EQUAL_UINT(32, config.security.private_key.size);
    TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
    TEST_ASSERT_EQUAL_UINT(32, config.security.public_key.size);
    TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32);
    // The non-key field still applies.
    TEST_ASSERT_TRUE(config.security.serial_enabled);
}

// A SET that DOES supply a full 32-byte keypair (legitimate key import) must apply it, not preserve the old one.
static void test_handleSetConfig_security_acceptsSuppliedKeypair()
{
    config.security = meshtastic_Config_SecurityConfig_init_zero;
    config.security.private_key.size = 32;
    memset(config.security.private_key.bytes, 0x11, 32);
    config.security.public_key.size = 32;
    memset(config.security.public_key.bytes, 0x22, 32);

    meshtastic_Config c = meshtastic_Config_init_zero;
    c.which_payload_variant = meshtastic_Config_security_tag;
    c.payload_variant.security.private_key.size = 32;
    memset(c.payload_variant.security.private_key.bytes, 0x33, 32);
    c.payload_variant.security.public_key.size = 32;
    memset(c.payload_variant.security.public_key.bytes, 0x44, 32);

    testAdmin->deferSaves();
    testAdmin->handleSetConfig(c, false);

    uint8_t expectedPriv[32];
    memset(expectedPriv, 0x33, 32);
    uint8_t expectedPub[32];
    memset(expectedPub, 0x44, 32);
    TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);
    TEST_ASSERT_EQUAL_MEMORY(expectedPub, config.security.public_key.bytes, 32);
}

// Issue #11073: "regenerate keys" sends a blank SecurityConfig holding only the new private key. Replacing
// the whole struct with it wiped the admin keys, locking the owner out of remote admin.
static void test_handleSetConfig_security_rotationPreservesAdminKeys()
{
    config.security = meshtastic_Config_SecurityConfig_init_zero;
    config.security.private_key.size = 32;
    memset(config.security.private_key.bytes, 0x11, 32);
    config.security.public_key.size = 32;
    memset(config.security.public_key.bytes, 0x22, 32);
    config.security.admin_key_count = 2;
    config.security.admin_key[0].size = 32;
    memset(config.security.admin_key[0].bytes, 0xAA, 32);
    config.security.admin_key[1].size = 32;
    memset(config.security.admin_key[1].bytes, 0xBB, 32);
    config.security.is_managed = true;
    config.security.serial_enabled = true;
    config.security.packet_signature_policy =
        meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;

    // Exactly what the regenerate dialog emits.
    meshtastic_Config c = meshtastic_Config_init_zero;
    c.which_payload_variant = meshtastic_Config_security_tag;
    c.payload_variant.security.private_key.size = 32;
    memset(c.payload_variant.security.private_key.bytes, 0x33, 32);

    testAdmin->deferSaves();
    testAdmin->handleSetConfig(c, false);

    uint8_t expectedPriv[32];
    memset(expectedPriv, 0x33, 32);
    TEST_ASSERT_EQUAL_UINT(32, config.security.private_key.size);
    TEST_ASSERT_EQUAL_MEMORY(expectedPriv, config.security.private_key.bytes, 32);

    uint8_t expectedAdmin0[32], expectedAdmin1[32];
    memset(expectedAdmin0, 0xAA, 32);
    memset(expectedAdmin1, 0xBB, 32);
    TEST_ASSERT_EQUAL_UINT(2, config.security.admin_key_count);
    TEST_ASSERT_EQUAL_UINT(32, config.security.admin_key[0].size);
    TEST_ASSERT_EQUAL_MEMORY(expectedAdmin0, config.security.admin_key[0].bytes, 32);
    TEST_ASSERT_EQUAL_UINT(32, config.security.admin_key[1].size);
    TEST_ASSERT_EQUAL_MEMORY(expectedAdmin1, config.security.admin_key[1].bytes, 32);
    TEST_ASSERT_TRUE(config.security.is_managed);
    TEST_ASSERT_TRUE(config.security.serial_enabled);
    TEST_ASSERT_EQUAL(meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT,
                      config.security.packet_signature_policy);
}

// The escape hatch: a SET that leaves the private key alone still clears admin keys.
static void test_handleSetConfig_security_clearsAdminKeysWhenKeypairUnchanged()
{
    config.security = meshtastic_Config_SecurityConfig_init_zero;
    config.security.private_key.size = 32;
    memset(config.security.private_key.bytes, 0x11, 32);
    config.security.public_key.size = 32;
    memset(config.security.public_key.bytes, 0x22, 32);
    config.security.admin_key_count = 1;
    config.security.admin_key[0].size = 32;
    memset(config.security.admin_key[0].bytes, 0xAA, 32);

    // Same private key we already hold, empty admin key list.
    meshtastic_Config c = meshtastic_Config_init_zero;
    c.which_payload_variant = meshtastic_Config_security_tag;
    c.payload_variant.security.private_key.size = 32;
    memset(c.payload_variant.security.private_key.bytes, 0x11, 32);
    c.payload_variant.security.public_key.size = 32;
    memset(c.payload_variant.security.public_key.bytes, 0x22, 32);

    testAdmin->deferSaves();
    testAdmin->handleSetConfig(c, false);

    TEST_ASSERT_EQUAL_UINT(0, config.security.admin_key_count);
    TEST_ASSERT_EQUAL_UINT(0, config.security.admin_key[0].size);
}

static void test_regionInfo_supportsPreset()
{
    const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
    TEST_ASSERT_TRUE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
    TEST_ASSERT_FALSE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO));
    TEST_ASSERT_FALSE(eu868->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST));

    const RegionInfo *eu866 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
    TEST_ASSERT_TRUE(eu866->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW));
    TEST_ASSERT_FALSE(eu866->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));

    // UNSET enforces nothing (the radio is silent regardless), so it supports every real
    // preset - not just the LONG_FAST its own profile advertises as the default.
    const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET);
    TEST_ASSERT_TRUE(unset->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST));
    TEST_ASSERT_TRUE(unset->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO));
    TEST_ASSERT_TRUE(unset->supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST));
    TEST_ASSERT_FALSE(unset->supportsPreset((meshtastic_Config_LoRaConfig_ModemPreset)99));
}

static void test_checkConfigRegion_quietCheckReportsReason()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    TEST_ASSERT_TRUE(RadioInterface::checkConfigRegion(cfg));

    cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)254;
    char err[160] = {0};
    TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg, err, sizeof(err)));
    TEST_ASSERT_TRUE_MESSAGE(strlen(err) > 0, "Expected a failure reason in errBuf");
}

static void test_checkConfigRegion_allowsProspectiveLicensedOwner()
{
    meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
    cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M;
    devicestate.owner.is_licensed = false;

    TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg));
    TEST_ASSERT_TRUE(RadioInterface::checkConfigRegion(cfg, nullptr, 0, true));
}

static void test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion()
{
    // Baseline: EU_866 (LITE profile)
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_866;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST;
    initRegion();

    // Remote admin keeps the region but selects a NARROW preset (locked to EU_N_868)
    meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_866, true,
                                            meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST);

    testAdmin->handleSetConfig(c, true); // fromOthers = true

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868, config.lora.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST, config.lora.modem_preset);

    // Restore the region table pointer for subsequent tests
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    initRegion();
}

static void test_handleSetConfig_fromOthers_lockedPresetFromNonTrioRegionRejected()
{
    // Baseline: US is not one of the swappable trio, so a LITE preset must be rejected
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    meshtastic_Config c =
        makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_US, true, meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST);

    testAdmin->handleSetConfig(c, true); // fromOthers = true

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, config.lora.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset);
}

static void test_handleSetConfig_presetChosenBeforeRegionSurvives()
{
    // A fresh device: the user picks a preset in the app before choosing a region. The
    // unset region must not clamp that choice back to LONG_FAST.
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();

    meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_UNSET, true,
                                            meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);

    testAdmin->handleSetConfig(c, false);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, config.lora.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset);
}

static void test_handleSetConfig_unsettingRegionKeepsPreset()
{
    // Clearing the region is a valid request in its own right. It must take effect (and
    // disable tx) without discarding the config because the preset outlives the region.
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
    config.lora.tx_enabled = true;
    initRegion();

    meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_UNSET, true,
                                            meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
    c.payload_variant.lora.tx_enabled = true;

    testAdmin->handleSetConfig(c, false);

    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, config.lora.region);
    TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, config.lora.modem_preset);
    TEST_ASSERT_FALSE_MESSAGE(config.lora.tx_enabled, "unsetting the region must disable tx");

    // Restore the region table pointer for subsequent tests
    initRegion();
}

// -----------------------------------------------------------------------
// Channel-configuration warning + coalescing tests
//
// These exercise the real incoming-admin-message path (handleReceivedProtobuf):
// begin_edit_settings / set_channel / commit_edit_settings. Warnings raised while a
// transaction is open must be deferred and collapsed into a single notification at
// commit; outside a transaction each save emits its own single message immediately.
// -----------------------------------------------------------------------

static const uint8_t DEFAULT_KEY[] = {0x01};      // the well-known "default" PSK (AQ==)
static const uint8_t CUSTOM_KEY[] = {0x42, 0x17}; // any non-default key

// Count captured warnings whose text contains substr.
static int warningsContaining(const char *substr)
{
    int n = 0;
    for (const auto &w : capturedWarnings)
        if (w.find(substr) != std::string::npos)
            n++;
    return n;
}

static meshtastic_Channel makeChannel(int8_t index, meshtastic_Channel_Role role, const char *name, const uint8_t *psk,
                                      size_t pskLen)
{
    meshtastic_Channel ch = meshtastic_Channel_init_zero;
    ch.index = index;
    ch.role = role;
    ch.has_settings = true;
    strncpy(ch.settings.name, name, sizeof(ch.settings.name) - 1);
    ch.settings.psk.size = pskLen;
    for (size_t i = 0; i < pskLen; i++)
        ch.settings.psk.bytes[i] = psk[i];
    return ch;
}

// Dispatch one admin message as if it arrived from a local (from==0) client, which bypasses
// the passkey/authorization gates so the switch body runs.
static void sendAdmin(meshtastic_AdminMessage &m)
{
    meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
    mp.from = 0;
    mp.which_payload_variant = meshtastic_MeshPacket_decoded_tag; // required: handler drops non-decoded packets
    testAdmin->handleReceivedProtobuf(mp, &m);
}

static void sendSetChannel(const meshtastic_Channel &ch)
{
    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_set_channel_tag;
    m.set_channel = ch;
    sendAdmin(m);
}

static void sendBeginEdit()
{
    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_begin_edit_settings_tag;
    m.begin_edit_settings = true;
    sendAdmin(m);
}

static void sendCommitEdit()
{
    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_commit_edit_settings_tag;
    m.commit_edit_settings = true;
    sendAdmin(m);
}

// An admin message that changes nothing. It answers, so drain the reply or the packet pool leaks.
static void sendGetDeviceMetadata()
{
    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_request_tag;
    m.get_device_metadata_request = true;
    sendAdmin(m);
    testAdmin->drainReply();
}

// Preset = LongFast on US, unlicensed owner. "LongFast" is the display name we compare against.
static void usePresetLongFast()
{
    config.lora = meshtastic_Config_LoRaConfig_init_zero;
    config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
    config.lora.use_preset = true;
    config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
    initRegion();
    owner.is_licensed = false;
}

static void test_warn_singleChannel_variantName_oneSpecificMessage()
{
    usePresetLongFast();
    // Name is a case/space variant of the preset with the default key: a single name issue.
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
    TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
}

static void test_warn_singleChannel_nameAndPsk_collapsedToCatchAll()
{
    usePresetLongFast();
    // Variant name AND a non-default key: two issues on one channel collapse to one catch-all.
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", CUSTOM_KEY, 2));
    TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name and PSK issues on channel 0"));
}

static void test_warn_cleanChannel_noMessage()
{
    usePresetLongFast();
    // Exact preset name + default key: nothing to warn about.
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "LongFast", DEFAULT_KEY, 1));
    TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
}

static void test_warn_transaction_multipleChannels_singleCoalescedMessage()
{
    usePresetLongFast();
    sendBeginEdit();
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
    sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "long fast", DEFAULT_KEY, 1));
    // Nothing emitted yet - warnings are deferred until commit.
    TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());

    sendCommitEdit();
    // Exactly one message, naming both channels.
    TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name issues on channels 0, 1"));
}

static void test_warn_transaction_singleChannel_keepsSpecificMessage()
{
    usePresetLongFast();
    sendBeginEdit();
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
    TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());

    sendCommitEdit();
    // One flagged channel: the specific message verbatim, not the plural catch-all.
    TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
    TEST_ASSERT_EQUAL_INT(0, warningsContaining("on channels"));
}

// An idle transaction is retired by the next admin message, flushing the warnings it held.
static void test_editTransaction_abandoned_isRetiredOnNextAdminMessage()
{
    usePresetLongFast();
    sendBeginEdit();
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
    // Deferred, exactly as before: nothing emitted while the transaction looks alive.
    TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());
    TEST_ASSERT_TRUE(testAdmin->editTransactionOpen());

    testAdmin->ageEditTransaction();
    sendGetDeviceMetadata(); // any later admin message, from any client

    TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
}

// A write arriving after abandonment is saved, not deferred to a commit that never comes.
static void test_editTransaction_abandoned_laterWriteIsNoLongerDeferred()
{
    usePresetLongFast();
    sendBeginEdit();
    testAdmin->ageEditTransaction();

    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));

    // The write itself retired the stale transaction, so its own warning is emitted immediately.
    TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("looks like a mistype of 'LongFast'"));
}

// A transaction still in use is left alone: each write refreshes the window.
static void test_editTransaction_active_isNotRetired()
{
    usePresetLongFast();
    sendBeginEdit();
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1));
    sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "long fast", DEFAULT_KEY, 1));

    TEST_ASSERT_TRUE(testAdmin->editTransactionOpen());
    TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());

    sendCommitEdit();
    TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("There may be name issues on channels 0, 1"));
}

static void test_warn_license_noTransaction_emittedImmediately()
{
    usePresetLongFast();
    owner.is_licensed = true;
    // Setting a channel that still carries a key triggers ensureLicensedOperation() to strip it.
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2));
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("Licensed mode activated"));
}

static void test_warn_license_transaction_coalescedToSingleMessage()
{
    usePresetLongFast();
    owner.is_licensed = true;
    sendBeginEdit();
    // Two separate triggers within one transaction (two channels with keys to strip).
    sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2));
    sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "", CUSTOM_KEY, 2));
    TEST_ASSERT_EQUAL_INT(0, (int)capturedWarnings.size());

    sendCommitEdit();
    // Collapsed to a single licensed-mode notice (and no channel warning, since names are blank).
    TEST_ASSERT_EQUAL_INT(1, warningsContaining("Licensed mode activated"));
    TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
}

// -----------------------------------------------------------------------
// Node-DB admin metadata: favorite / ignore / mute
// -----------------------------------------------------------------------
//
// MeshService::reloadConfig() only re-derives the region and fires configChanged - which drives the
// live SX126x/RadioInterface reconfigure - when saveWhat includes SEGMENT_CONFIG or
// SEGMENT_CHANNELS. A pure node-DB metadata save must skip that reconfigure entirely. These watch
// service->configChanged directly, so widening the saveWhat mask or reordering the check is caught
// even though they run outside an edit transaction.
//
// Characterization: all three already hold on develop. They are worth pinning because that reload
// is the path implicated in the WisMesh Tag favourite-node crash, and nothing asserted it.

// Counts configChanged.notifyObservers() calls - the only externally visible signal that
// reloadConfig() took the radio-reconfigure branch.
class ConfigChangedCounter : public Observer<void *>
{
  public:
    int count = 0;

  protected:
    int onNotify(void *arg) override
    {
        count++;
        return 0;
    }
};

static const NodeNum TEST_NODE_NUM = 0x12345678;

static void test_setFavoriteNode_skipsRadioReload_butPersists()
{
    nodeDB->getOrCreateMeshNode(TEST_NODE_NUM);
    ConfigChangedCounter counter;
    counter.observe(&service->configChanged);

    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_set_favorite_node_tag;
    m.set_favorite_node = TEST_NODE_NUM;
    sendAdmin(m);

    TEST_ASSERT_EQUAL_INT(0, counter.count);
    TEST_ASSERT_TRUE(nodeInfoLiteIsFavorite(nodeDB->getMeshNode(TEST_NODE_NUM)));
}

static void test_setIgnoredNode_skipsRadioReload_butPersists()
{
    nodeDB->getOrCreateMeshNode(TEST_NODE_NUM);
    ConfigChangedCounter counter;
    counter.observe(&service->configChanged);

    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_set_ignored_node_tag;
    m.set_ignored_node = TEST_NODE_NUM;
    sendAdmin(m);

    TEST_ASSERT_EQUAL_INT(0, counter.count);
    TEST_ASSERT_TRUE(nodeInfoLiteIsIgnored(nodeDB->getMeshNode(TEST_NODE_NUM)));
}

static void test_toggleMutedNode_skipsRadioReload_butPersists()
{
    nodeDB->getOrCreateMeshNode(TEST_NODE_NUM);
    ConfigChangedCounter counter;
    counter.observe(&service->configChanged);

    meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
    m.which_payload_variant = meshtastic_AdminMessage_toggle_muted_node_tag;
    m.toggle_muted_node = TEST_NODE_NUM;
    sendAdmin(m);

    TEST_ASSERT_EQUAL_INT(0, counter.count);
    TEST_ASSERT_TRUE(nodeInfoLiteIsMuted(nodeDB->getMeshNode(TEST_NODE_NUM)));
}

// -----------------------------------------------------------------------
// Node menu mute toggle (graphics::menuHandler::toggleNodeMuted)
// -----------------------------------------------------------------------
//
// Reachable only since the mute branch was lifted out of its banner-callback lambda; the lambda
// runs via screen->showOverlayBanner(), so nothing in MenuHandler.cpp was testable before.

#if HAS_SCREEN
static void test_toggleNodeMuted_flipsBitAndSkipsRadioReload()
{
    nodeDB->getOrCreateMeshNode(TEST_NODE_NUM);
    ConfigChangedCounter counter;
    counter.observe(&service->configChanged);

    graphics::menuHandler::toggleNodeMuted(TEST_NODE_NUM);
    TEST_ASSERT_TRUE(nodeInfoLiteIsMuted(nodeDB->getMeshNode(TEST_NODE_NUM)));
    TEST_ASSERT_EQUAL_INT(0, counter.count);

    graphics::menuHandler::toggleNodeMuted(TEST_NODE_NUM);
    TEST_ASSERT_FALSE(nodeInfoLiteIsMuted(nodeDB->getMeshNode(TEST_NODE_NUM)));
    TEST_ASSERT_EQUAL_INT(0, counter.count);
}

static void test_toggleNodeMuted_unknownNodeDoesNothing()
{
    ConfigChangedCounter counter;
    counter.observe(&service->configChanged);

    graphics::menuHandler::toggleNodeMuted(0xDEADBEEF); // never added to the DB

    TEST_ASSERT_EQUAL_INT(0, counter.count);
    TEST_ASSERT_NULL(nodeDB->getMeshNode(0xDEADBEEF));
}

// CHARACTERIZATION OF A KNOWN DEFECT, not an endorsement. Flipping one NodeInfoLite bit currently
// calls bare nodeDB->saveToDisk(), which rewrites all five segments. saveToDisk() is not virtual,
// so the mask is observed through its effect: every prefs file reappears after being removed.
//
// A pending fix narrows this to SEGMENT_NODEDATABASE. When it lands, only nodes.proto should come
// back and this assertion is EXPECTED to change - that diff is the point, so the improvement is
// visible instead of silent.
static void test_toggleNodeMuted_currentlyRewritesEverySegment()
{
    nodeDB->getOrCreateMeshNode(TEST_NODE_NUM);

    const char *segmentFiles[] = {configFileName, moduleConfigFileName, deviceStateFileName, channelFileName,
                                  nodeDatabaseFileName};
    for (const char *f : segmentFiles)
        FSCom.remove(f);

    graphics::menuHandler::toggleNodeMuted(TEST_NODE_NUM);

    for (const char *f : segmentFiles)
        TEST_ASSERT_TRUE_MESSAGE(FSCom.exists(f), f);
}
#endif // HAS_SCREEN

// -----------------------------------------------------------------------
// Test runner
// -----------------------------------------------------------------------

void setUp(void)
{
    mockMeshService = new MockMeshService();
    service = mockMeshService;
    testAdmin = new AdminModuleTestShim();
    capturedWarnings.clear();
    // Every test gets its own NodeDB and its own copy of the globals the admin handlers write.
    replaceAdminRadioGlobals();
}
void tearDown(void)
{
    restoreAdminRadioGlobals();
    service = nullptr;
    delete mockMeshService;
    mockMeshService = nullptr;
    delete testAdmin;
    testAdmin = nullptr;
}

void setup()
{
    delay(10);
    delay(2000);

    initializeTestEnvironment();

    UNITY_BEGIN();

    // getRegion()
    RUN_TEST(test_handleSetOwner_persistsLicensedChannelSanitation);
    RUN_TEST(test_handleSetConfig_persistsLicensedFirstRegionIdentity);
    RUN_TEST(test_bootDefense_sanitizesStaleLicensedChannelsOnce);
    RUN_TEST(test_restorePreferences_sanitizesLicensedBackupBeforeReturn);
    RUN_TEST(test_getRegion_returnsCorrectRegion_US);
    RUN_TEST(test_getRegion_returnsCorrectRegion_EU868);
    RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24);
    RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry);
    RUN_TEST(test_getRegion_unknownCodeFallsToUnset);

    // validateConfigRegion()
    RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
    RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
    RUN_TEST(test_validateConfigRegion_unknownCodeReturnsFalse);
    RUN_TEST(test_validateConfigRegion_anotherUnknownCodeReturnsFalse);

    // Shadow table tests
    RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
    RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect);
    RUN_TEST(test_shadowTable_presetCountMatchesExpected);
    RUN_TEST(test_shadowTable_defaultPresetIsFirstInList);
    RUN_TEST(test_shadowTable_channelSpacingWithPadding);
    RUN_TEST(test_shadowTable_turboOnlyOnWideLora);
    RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel);
    RUN_TEST(test_shadowTable_presetHashProfileHasCorrectOverrideSlot);

    // validateConfigLora()
    RUN_TEST(test_validateConfigLora_validPresetForUS);
    RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS);
    RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868);
    RUN_TEST(test_validateConfigLora_validPresetsForEU868);
    RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868);
    RUN_TEST(test_validateConfigLora_customBandwidthFitsUS);
    RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868);
    RUN_TEST(test_validateConfigLora_bogusPresetRejected);
    RUN_TEST(test_validateConfigLora_unsetRegionAcceptsAnyRealPreset);
    RUN_TEST(test_isKnownModemPreset_matchesRegionTable);
    RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24);

    // clampConfigLora()
    RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
    RUN_TEST(test_clampConfigLora_validPresetUnchanged);
    RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw);
    RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged);
    RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast);
    RUN_TEST(test_clampConfigLora_unsetRegionKeepsRealPreset);
    RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault);

    // Region-locked preset swap
    RUN_TEST(test_clampConfigLora_narrowPresetOnEU866SwapsToEUN868);
    RUN_TEST(test_clampConfigLora_litePresetOnEU868SwapsToEU866);
    RUN_TEST(test_clampConfigLora_eu868PresetOnEUN868SwapsToEU868);
    RUN_TEST(test_clampConfigLora_litePresetOnUSDoesNotSwap);
    RUN_TEST(test_clampConfigLora_narrowPresetOnHam125cmDoesNotSwap);
    RUN_TEST(test_validateConfigLora_siblingLockedPresetStillFailsValidation);

    // RegionInfo preset list integrity
    RUN_TEST(test_presetsStd_hasTenEntries);
    RUN_TEST(test_presetsEU868_hasSevenEntries);
    RUN_TEST(test_presetsUndef_hasOneEntry);
    RUN_TEST(test_defaultPresetIsInAvailablePresets);
    RUN_TEST(test_regionFieldsAreSane);
    RUN_TEST(test_onlyLORA24HasWideLora);

    // OVERRIDE_SLOT_PRESET_HASH (-1) slot formula tests
    RUN_TEST(test_overrideSlotPresetHash_longFast_customChannelMatchesDefaultNameSlot);
    RUN_TEST(test_overrideSlotPresetHash_mediumFast_customChannelMatchesDefaultNameSlot);
    RUN_TEST(test_overrideSlotPresetHash_longFast_slotIsStableAcrossCustomNames);
    RUN_TEST(test_overrideSlotPresetHash_mediumFast_slotIsStableAcrossCustomNames);
    RUN_TEST(test_overrideSlotPresetHash_longFastAndMediumFast_slotsAreDifferentPresets);

    // Channel spacing (current + placeholder)
    RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST);
    RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST);
    RUN_TEST(test_channelSpacingCalculation_placeholder);

    // handleSetConfig fromOthers dispatch
    RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected);
    RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped);
    RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted);
    RUN_TEST(test_handleSetConfig_fromOthers_invalidChannelNumFullyRejected);
    RUN_TEST(test_clampBandwidthCode_zeroMapsToDefaultOthersUnchanged);
    RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthZeroClampedToDefault);
    RUN_TEST(test_handleSetConfig_fromOthers_customBandwidthZeroClampedToDefault);
    RUN_TEST(test_handleSetConfig_fromLocal_presetBandwidthZeroLeftUntouched);
    RUN_TEST(test_handleSetConfig_fromLocal_customBandwidthNonZeroPreserved);
    RUN_TEST(test_handleSetConfig_security_preservesKeypairWhenPrivateOmitted);
    RUN_TEST(test_handleSetConfig_security_acceptsSuppliedKeypair);
    RUN_TEST(test_handleSetConfig_security_rotationPreservesAdminKeys);
    RUN_TEST(test_handleSetConfig_security_clearsAdminKeysWhenKeypairUnchanged);
    RUN_TEST(test_regionInfo_supportsPreset);
    RUN_TEST(test_checkConfigRegion_quietCheckReportsReason);
    RUN_TEST(test_checkConfigRegion_allowsProspectiveLicensedOwner);
    RUN_TEST(test_handleSetConfig_fromOthers_siblingLockedPresetSwapsRegion);
    RUN_TEST(test_handleSetConfig_fromOthers_lockedPresetFromNonTrioRegionRejected);
    RUN_TEST(test_handleSetConfig_presetChosenBeforeRegionSurvives);
    RUN_TEST(test_handleSetConfig_unsettingRegionKeepsPreset);

    // Channel-configuration warning + coalescing
    RUN_TEST(test_warn_singleChannel_variantName_oneSpecificMessage);
    RUN_TEST(test_warn_singleChannel_nameAndPsk_collapsedToCatchAll);
    RUN_TEST(test_warn_cleanChannel_noMessage);
    RUN_TEST(test_warn_transaction_multipleChannels_singleCoalescedMessage);
    RUN_TEST(test_warn_transaction_singleChannel_keepsSpecificMessage);
    RUN_TEST(test_editTransaction_abandoned_isRetiredOnNextAdminMessage);
    RUN_TEST(test_editTransaction_abandoned_laterWriteIsNoLongerDeferred);
    RUN_TEST(test_editTransaction_active_isNotRetired);
    RUN_TEST(test_warn_license_noTransaction_emittedImmediately);
    RUN_TEST(test_warn_license_transaction_coalescedToSingleMessage);

    // Node-DB metadata saves must not reconfigure the radio
    RUN_TEST(test_setFavoriteNode_skipsRadioReload_butPersists);
    RUN_TEST(test_setIgnoredNode_skipsRadioReload_butPersists);
    RUN_TEST(test_toggleMutedNode_skipsRadioReload_butPersists);

#if HAS_SCREEN
    // Node menu mute toggle
    RUN_TEST(test_toggleNodeMuted_flipsBitAndSkipsRadioReload);
    RUN_TEST(test_toggleNodeMuted_unknownNodeDoesNothing);
    RUN_TEST(test_toggleNodeMuted_currentlyRewritesEverySegment);
#endif

    exit(UNITY_END());
}

void loop() {}
