// ok standalone

// g++ --std=c++11 test.cpp

// NOTE: Arduino macros (min, max, abs, round, radians, degrees, map) are defined
// in tests/test_pch.h BEFORE FastLED.h is included to test that fl/undef.h works correctly.
// This simulates what happens on real Arduino hardware where Arduino.h defines these macros
// before user code includes FastLED.h.

#include "FastLED.h"

// Verify that fl/undef.h successfully undefined the Arduino macros
// The macros are defined in tests/test_pch.h BEFORE FastLED.h is included
// fl/undef.h includes stdlib.h first (so stdlib can use macro versions), then undefs them
#ifdef min
#error "min macro still defined after FastLED.h"
#endif
#ifdef max
#error "max macro still defined after FastLED.h"
#endif
#ifdef abs
#error "abs macro still defined after FastLED.h"
#endif
#ifdef round
#error "round macro still defined after FastLED.h"
#endif
#ifdef radians
#error "radians macro still defined after FastLED.h"
#endif
#ifdef degrees
#error "degrees macro still defined after FastLED.h"
#endif
#ifdef map
#error "map macro still defined after FastLED.h"
#endif
#include "colorutils.h"
#include "test.h"
#include "eorder.h"
#include "fl/gfx/colorutils_misc.h"
#include "fl/gfx/eorder.h"
#include "fl/gfx/fill.h"
#include "hsv2rgb.h"
#include "fl/channels/channel.h"
#include "fl/channels/config.h"
#include "fl/channels/driver.h"
#include "fl/channels/data.h"
#include "fl/channels/manager.h"
#include "fl/channels/options.h"
#include "fl/chipsets/chipset_timing_config.h"
#include "fl/chipsets/led_timing.h"
#include "fl/stl/vector.h"
#include "fl/stl/move.h"
#include "fl/stl/shared_ptr.h"
#include "fl/stl/weak_ptr.h"
#include "fl/stl/new.h"
#include "fl/channels/cled_controller.h"

FL_TEST_FILE(FL_FILEPATH) {

#undef NUM_LEDS  // Avoid redefinition in unity builds
#define NUM_LEDS 1000
#define DATA_PIN 2
#define CLOCK_PIN 3

FL_TEST_CASE("Simple") {
    static CRGB leds[NUM_LEDS];  // Use static to avoid global constructor warning
    FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, BGR>(leds, NUM_LEDS);
}

FL_TEST_CASE("Fill Gradient SHORTEST_HUES") {
    static CRGB leds[NUM_LEDS];
    fill_gradient(leds, 0, CHSV(0, 255, 255), NUM_LEDS - 1, CHSV(96, 255, 255), SHORTEST_HUES);
}

FL_TEST_CASE("Legacy aliases resolve to FastLED instance") {
    // Verify that all legacy aliases point to the same object
    // These aliases provide backward compatibility for code written for
    // FastSPI_LED and FastSPI_LED2 (the original library names)

    FL_SUBCASE("FastSPI_LED alias") {
        CFastLED* pFastLED = &FastLED;
        CFastLED* pFastSPI_LED = &FastSPI_LED;
        FL_CHECK(pFastLED == pFastSPI_LED);
    }

    FL_SUBCASE("FastSPI_LED2 alias") {
        CFastLED* pFastLED = &FastLED;
        CFastLED* pFastSPI_LED2 = &FastSPI_LED2;
        FL_CHECK(pFastLED == pFastSPI_LED2);
    }

    FL_SUBCASE("LEDS alias") {
        CFastLED* pFastLED = &FastLED;
        CFastLED* pLEDS = &LEDS;
        FL_CHECK(pFastLED == pLEDS);
    }

    FL_SUBCASE("All aliases access same brightness setting") {
        static CRGB leds[NUM_LEDS];
        FastLED.clear();
        FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, BGR>(leds, NUM_LEDS);

        // Set brightness using FastLED
        FastLED.setBrightness(128);

        // Verify all aliases see the same brightness
        FL_CHECK(FastLED.getBrightness() == 128);
        FL_CHECK(FastSPI_LED.getBrightness() == 128);
        FL_CHECK(FastSPI_LED2.getBrightness() == 128);
        FL_CHECK(LEDS.getBrightness() == 128);

        // Change brightness using legacy alias
        FastSPI_LED.setBrightness(64);

        // Verify all aliases see the new brightness
        FL_CHECK(FastLED.getBrightness() == 64);
        FL_CHECK(FastSPI_LED.getBrightness() == 64);
        FL_CHECK(FastSPI_LED2.getBrightness() == 64);
        FL_CHECK(LEDS.getBrightness() == 64);
    }
}

// Channel API tests - validates new channels API (addresses GitHub issue #2167)
namespace {

/// @brief Mock channel driver for testing the channels API
///
/// This mock driver validates that:
/// - enqueue() is called when channel data is submitted
/// - show() triggers transmission
/// - getName() returns "MOCK" for affinity binding
class ChannelEngineMock : public fl::IChannelDriver {
public:
    int mEnqueueCount = 0;
    int mShowCount = 0;
    fl::vector<fl::ChannelDataPtr> mEnqueuedChannels;

    explicit ChannelEngineMock(const char* name = "MOCK") : mName(fl::string::from_literal(name)) {}

    bool canHandle(const fl::ChannelDataPtr& data) const override {
        (void)data;
        return true;  // Test driver accepts all channel types
    }

    void enqueue(fl::ChannelDataPtr channelData) override {
        if (channelData) {
            mEnqueueCount++;
            mEnqueuedChannels.push_back(channelData);
        }
    }

    void show() override {
        mShowCount++;
        mEnqueuedChannels.clear();
    }

    DriverState poll() override {
        return DriverState(DriverState::READY);
    }

    fl::string getName() const override { return mName; }

    Capabilities getCapabilities() const override {
        return Capabilities(true, true);  // Mock accepts both clockless and SPI
    }

    void reset() {
        mEnqueueCount = 0;
        mShowCount = 0;
        mEnqueuedChannels.clear();
    }

private:
    fl::string mName;
};

} // anonymous namespace

FL_TEST_CASE("Channel API: Mock driver workflow (GitHub issue #2167)") {
    // This test validates the complete workflow reported in issue #2167:
    // 1. Create ChannelEngineMock with string name "MOCK"
    // 2. Inject it into ChannelManager
    // 3. Construct ChannelConfig with affinity string "MOCK"
    // 4. Add channel to FastLED
    // 5. Call FastLED.show()
    // 6. Verify driver received data via enqueue()

    auto mockEngine = fl::make_shared<ChannelEngineMock>("MOCK");
    mockEngine->reset();

    // Step 1 & 2: Register mock driver
    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(1000, mockEngine);  // High priority

    // Verify registration
    auto registeredEngine = manager.getDriverByName("MOCK");
    FL_REQUIRE(registeredEngine != nullptr);
    FL_CHECK(registeredEngine.get() == mockEngine.get());

    // Step 3: Create channel with affinity "MOCK"
    static CRGB leds[10];
    fl::fill_solid(leds, 10, CRGB::Red);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions options;

    fl::ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB, options);

    // Create channel
    auto channel = fl::Channel::create(config);
    FL_REQUIRE(channel != nullptr);

    // Verify channel is NOT in controller list yet (deferred registration)
    FL_CHECK(!channel->isInDrawList());

    // Step 4: Add to FastLED
    FastLED.add(channel);

    // Verify channel IS NOW in controller list (explicit registration)
    FL_CHECK(channel->isInDrawList());

    // Double-check by walking the list
    bool found = false;
    CLEDController* pCur = CLEDController::head();
    while (pCur) {
        if (pCur == channel->asController()) {
            found = true;
            break;
        }
        pCur = pCur->next();
    }
    FL_CHECK(found);

    // Step 5 & 6: Call FastLED.show() and verify enqueue()
    int enqueueBefore = mockEngine->mEnqueueCount;
    FastLED.show();

    // Validate: driver received data via enqueue()
    FL_CHECK(mockEngine->mEnqueueCount > enqueueBefore);

    // Clean up
    channel->removeFromDrawList();
    manager.setDriverEnabled("MOCK", false);
}

FL_TEST_CASE("Channel API: Double add protection") {
    // Verify that calling add() multiple times doesn't create duplicates
    auto mockEngine = fl::make_shared<ChannelEngineMock>("MOCK_DOUBLE");
    mockEngine->reset();

    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(1000, mockEngine);

    static CRGB leds[5];
    fl::fill_solid(leds, 5, CRGB::Green);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions options;

    fl::ChannelConfig config(10, timing, fl::span<CRGB>(leds, 5), GRB, options);
    auto channel = fl::Channel::create(config);

    FL_REQUIRE(channel != nullptr);

    // Before adding: not in list
    FL_CHECK(!channel->isInDrawList());

    // First add
    FastLED.add(channel);
    FL_CHECK(channel->isInDrawList());

    // Second add (should be safe, no duplicate)
    FastLED.add(channel);
    FL_CHECK(channel->isInDrawList());

    // Third add (should still be safe)
    FastLED.add(channel);
    FL_CHECK(channel->isInDrawList());

    // Walk the list and count occurrences of this channel
    int occurrenceCount = 0;
    CLEDController* pCur = CLEDController::head();
    while (pCur) {
        if (pCur == channel->asController()) {
            occurrenceCount++;
        }
        pCur = pCur->next();
    }

    // Should appear exactly once, not multiple times
    FL_CHECK(occurrenceCount == 1);

    // Clean up
    channel->removeFromDrawList();
    manager.setDriverEnabled("MOCK_DOUBLE", false);
}

FL_TEST_CASE("Channel API: Add and remove symmetry") {
    // Verify that add() and remove() work symmetrically
    auto mockEngine = fl::make_shared<ChannelEngineMock>("MOCK_REMOVE");
    mockEngine->reset();

    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(1000, mockEngine);

    static CRGB leds[8];
    fl::fill_solid(leds, 8, CRGB::Blue);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions options;

    fl::ChannelConfig config(12, timing, fl::span<CRGB>(leds, 8), GRB, options);
    auto channel = fl::Channel::create(config);

    FL_REQUIRE(channel != nullptr);

    // Initial state: not in list
    FL_CHECK(!channel->isInDrawList());

    // Add to list
    FastLED.add(channel);
    FL_CHECK(channel->isInDrawList());

    // Remove from list
    FastLED.remove(channel);
    FL_CHECK(!channel->isInDrawList());

    // Verify channel object is still valid (not destroyed)
    FL_CHECK(channel->size() == 8);
    FL_CHECK(channel->getPin() == 12);

    // Can re-add if needed
    FastLED.add(channel);
    FL_CHECK(channel->isInDrawList());

    // Remove again
    FastLED.remove(channel);
    FL_CHECK(!channel->isInDrawList());

    // Safe to call remove multiple times
    FastLED.remove(channel);
    FastLED.remove(channel);
    FL_CHECK(!channel->isInDrawList());

    // Clean up
    manager.setDriverEnabled("MOCK_REMOVE", false);
}

FL_TEST_CASE("Channel API: Internal ChannelPtr storage prevents dangling") {
    // Verify that CFastLED stores ChannelPtrs internally so channels
    // survive even if the caller drops their reference.
    auto mockEngine = fl::make_shared<ChannelEngineMock>("MOCK_STORAGE");
    mockEngine->reset();

    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(1000, mockEngine);

    static CRGB leds[4];
    fl::fill_solid(leds, 4, CRGB::White);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions options;

    fl::ChannelConfig config(7, timing, fl::span<CRGB>(leds, 4), GRB, options);
    auto channel = fl::Channel::create(config);
    FL_REQUIRE(channel != nullptr);

    // After add, CFastLED holds an internal reference
    FastLED.add(channel);
    FL_CHECK(channel->isInDrawList());
    FL_CHECK(channel.use_count() >= 2);  // caller + CFastLED internal

    // Drop local reference - channel should survive via CFastLED's storage
    fl::Channel* raw = channel.get();
    channel.reset();

    // Channel should still be in the linked list (not destroyed)
    bool found = false;
    CLEDController* pCur = CLEDController::head();
    while (pCur) {
        if (pCur == raw->asController()) {
            found = true;
            break;
        }
        pCur = pCur->next();
    }
    FL_CHECK(found);

    // After remove, the internal reference is released too
    // Re-acquire a ChannelPtr to call remove (we need to find it)
    // Use a fresh channel to test remove releases the internal ref
    auto channel2 = fl::Channel::create(config);
    FL_REQUIRE(channel2 != nullptr);
    FastLED.add(channel2);
    FL_CHECK(channel2.use_count() >= 2);

    FastLED.remove(channel2);
    FL_CHECK(!channel2->isInDrawList());
    FL_CHECK(channel2.use_count() == 1);  // only local ref remains

    // Clean up the first channel that's still in the list
    // Walk list and remove the raw pointer's entry
    pCur = CLEDController::head();
    while (pCur) {
        if (pCur == raw->asController()) {
            raw->removeFromDrawList();
            break;
        }
        pCur = pCur->next();
    }
    manager.setDriverEnabled("MOCK_STORAGE", false);
}

FL_TEST_CASE("Channel API: 4 parallel strips via FastLED.add() routes through mock driver") {
    // Post-#2428 architecture: legacy `FastLED.addLeds<>()` pre-binds to the
    // platform-default driver (Phase 5b) and bypasses `ChannelManager`, so the
    // way to route runtime traffic through a custom mock driver is the
    // explicit Channel API: `FastLED.add(cfg)`. Each `Channel` registered via
    // that path consults the manager on every show() and picks the highest-
    // priority driver that `canHandle` the channel data.
    //
    // This test validates that workflow with 4 parallel strips:
    // - Register a mock driver with the manager at high priority
    // - Create 4 channels via `FastLED.add(ChannelConfig)`
    // - Call `FastLED.show()`
    // - Verify the mock received all 4 strips

    auto mockEngine = fl::make_shared<ChannelEngineMock>("MOCK_LEGACY");
    mockEngine->reset();

    // Register mock driver with high priority
    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(1000, mockEngine);

    // Verify registration
    auto registeredEngine = manager.getDriverByName("MOCK_LEGACY");
    FL_REQUIRE(registeredEngine != nullptr);
    FL_CHECK(registeredEngine.get() == mockEngine.get());

    #define NUM_LEDS 60
    #define PIN1 16
    #define PIN2 17
    #define PIN3 18
    #define PIN4 19

    static CRGB strip1[NUM_LEDS];
    static CRGB strip2[NUM_LEDS];
    static CRGB strip3[NUM_LEDS];
    static CRGB strip4[NUM_LEDS];

    // Channel API: 4 channels routed through the manager.
    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    auto ch1 = FastLED.add(fl::ChannelConfig(PIN1, timing, fl::span<CRGB>(strip1, NUM_LEDS), RGB));
    auto ch2 = FastLED.add(fl::ChannelConfig(PIN2, timing, fl::span<CRGB>(strip2, NUM_LEDS), RGB));
    auto ch3 = FastLED.add(fl::ChannelConfig(PIN3, timing, fl::span<CRGB>(strip3, NUM_LEDS), RGB));
    auto ch4 = FastLED.add(fl::ChannelConfig(PIN4, timing, fl::span<CRGB>(strip4, NUM_LEDS), RGB));

    fl::fill_solid(strip1, NUM_LEDS, CRGB::Red);
    fl::fill_solid(strip2, NUM_LEDS, CRGB::Green);
    fl::fill_solid(strip3, NUM_LEDS, CRGB::Blue);
    fl::fill_solid(strip4, NUM_LEDS, CRGB::Yellow);

    mockEngine->reset();
    FastLED.show();

    FL_CHECK(mockEngine->mEnqueueCount == 4);
    FL_CHECK(mockEngine->mShowCount == 1);
    FL_CHECK(mockEngine->mEnqueuedChannels.size() == 0);  // Cleared by show()

    FL_CHECK(strip1[0] == CRGB::Red);
    FL_CHECK(strip2[0] == CRGB::Green);
    FL_CHECK(strip3[0] == CRGB::Blue);
    FL_CHECK(strip4[0] == CRGB::Yellow);

    for (int i = 0; i < NUM_LEDS; i++) {
        FL_CHECK(strip1[i] == CRGB::Red);
    }

    mockEngine->reset();
    static uint8_t hue = 0;
    for(int i = 0; i < NUM_LEDS; i++) {
        strip1[i] = CHSV(hue + (i * 4), 255, 255);
        strip2[i] = CHSV(hue + (i * 4) + 64, 255, 255);
        strip3[i] = CHSV(hue + (i * 4) + 128, 255, 255);
        strip4[i] = CHSV(hue + (i * 4) + 192, 255, 255);
    }
    FastLED.show();

    FL_CHECK(mockEngine->mEnqueueCount == 4);
    FL_CHECK(mockEngine->mShowCount == 1);

    FastLED.remove(ch1);
    FastLED.remove(ch2);
    FastLED.remove(ch3);
    FastLED.remove(ch4);
    manager.removeDriver(mockEngine);

    #undef NUM_LEDS
    #undef PIN1
    #undef PIN2
    #undef PIN3
    #undef PIN4
}

// --- Tests moved from tests/fl/channels/channel_add_remove.cpp ---

namespace channel_add_remove_test {

using namespace fl;

/// Minimal driver for testing - always READY
class StubEngine : public IChannelDriver {
public:
    bool canHandle(const ChannelDataPtr&) const override { return true; }
    void enqueue(ChannelDataPtr) override {}
    void show() override {}
    DriverState poll() override { return DriverState::READY; }
    fl::string getName() const override { return fl::string::from_literal("STUB_ADD_REMOVE"); }
    Capabilities getCapabilities() const override {
        return Capabilities(true, true);
    }
};

static ChannelPtr makeChannel(CRGB* leds, int n) {
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelOptions opts;
    ChannelConfig config(1, timing, fl::span<CRGB>(leds, n), RGB, opts);
    return Channel::create(config);
}

static bool controllerInList(CLEDController* ctrl) {
    CLEDController* cur = CLEDController::head();
    while (cur) {
        if (cur == ctrl) return true;
        cur = cur->next();
    }
    return false;
}

static bool controllerInList(fl::Channel* channel) {
    return controllerInList(channel->asController());
}

FL_TEST_CASE("FastLED.add stores ChannelPtr - survives caller scope") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2000, driver);

    CRGB leds[4];
    fl::weak_ptr<Channel> weakRef;

    {
        auto ch = makeChannel(leds, 4);
        weakRef = ch;
        // Before add: refcount is 1 (only 'ch')
        FL_CHECK(ch.use_count() == 1);
        FL_CHECK(!weakRef.expired());

        FastLED.add(ch);

        // After add: refcount is 2 ('ch' + internal storage)
        FL_CHECK(ch.use_count() == 2);
        FL_CHECK(controllerInList(ch.get()));
        // 'ch' goes out of scope here, dropping refcount to 1
    }

    // The weak_ptr must NOT be expired because FastLED's internal
    // mChannels keeps the ChannelPtr alive
    FL_CHECK(!weakRef.expired());
    FL_CHECK(weakRef.use_count() == 1);

    // Lock the weak_ptr to get a shared_ptr for verification
    auto locked = weakRef.lock();
    FL_REQUIRE(locked != nullptr);
    FL_CHECK(controllerInList(locked.get()));

    // Clean up
    FastLED.remove(locked);
    FL_CHECK(!controllerInList(locked.get()));

    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.add double-add is safe") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2001, driver);

    CRGB leds[4];
    auto ch = makeChannel(leds, 4);

    FastLED.add(ch);
    FL_CHECK(ch.use_count() == 2); // ch + internal

    FastLED.add(ch); // double add - should be no-op
    FL_CHECK(ch.use_count() == 2); // still only 2, not 3

    // Controller should appear exactly once in the linked list
    int count = 0;
    CLEDController* cur = CLEDController::head();
    while (cur) {
        if (cur == ch->asController()) count++;
        cur = cur->next();
    }
    FL_CHECK(count == 1);

    // Clean up
    FastLED.remove(ch);
    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.remove double-remove is safe") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2002, driver);

    CRGB leds[4];
    auto ch = makeChannel(leds, 4);

    FastLED.add(ch);
    FL_CHECK(controllerInList(ch.get()));

    FastLED.remove(ch);
    FL_CHECK(!controllerInList(ch.get()));

    // Double remove - should not crash or change refcount
    long rc = ch.use_count();
    FastLED.remove(ch);
    FL_CHECK(!controllerInList(ch.get()));
    FL_CHECK(ch.use_count() == rc);

    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.remove nullptr is safe") {
    FastLED.remove(ChannelPtr());
}

FL_TEST_CASE("FastLED.add nullptr is safe") {
    FastLED.add(ChannelPtr());
}

FL_TEST_CASE("FastLED.reset() removes all channels and drops refcount to 1") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2003, driver);

    CRGB leds1[4];
    CRGB leds2[4];
    CRGB leds3[4];

    // Create three channels
    auto ch1 = makeChannel(leds1, 4);
    auto ch2 = makeChannel(leds2, 4);
    auto ch3 = makeChannel(leds3, 4);

    // Before add: refcount is 1 (only stack reference)
    FL_CHECK(ch1.use_count() == 1);
    FL_CHECK(ch2.use_count() == 1);
    FL_CHECK(ch3.use_count() == 1);

    // Add all channels
    FastLED.add(ch1);
    FastLED.add(ch2);
    FastLED.add(ch3);

    // After add: refcount is 2 (stack + internal storage)
    FL_CHECK(ch1.use_count() == 2);
    FL_CHECK(ch2.use_count() == 2);
    FL_CHECK(ch3.use_count() == 2);

    // Verify all channels are in the draw list
    FL_CHECK(controllerInList(ch1.get()));
    FL_CHECK(controllerInList(ch2.get()));
    FL_CHECK(controllerInList(ch3.get()));

    // Call reset() - should wait for transmissions and remove all channels
    FastLED.clear(ClearFlags::CHANNELS);

    // After reset: refcount should be 1 (only stack reference remains)
    FL_CHECK(ch1.use_count() == 1);
    FL_CHECK(ch2.use_count() == 1);
    FL_CHECK(ch3.use_count() == 1);

    // Verify no channels are in the draw list
    FL_CHECK(!controllerInList(ch1.get()));
    FL_CHECK(!controllerInList(ch2.get()));
    FL_CHECK(!controllerInList(ch3.get()));

    // Verify channels are still valid (not destroyed)
    FL_CHECK(ch1->size() == 4);
    FL_CHECK(ch2->size() == 4);
    FL_CHECK(ch3->size() == 4);

    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.clear() when no channels exist is safe") {
    // Should be safe to call reset when no channels are registered
    FastLED.clear(ClearFlags::CHANNELS);  // Should not crash
    FastLED.clear(ClearFlags::POWER_SETTINGS);  // Should not crash
    FastLED.clear(ClearFlags::BRIGHTNESS);  // Should not crash
    FastLED.clear(ClearFlags::CHANNEL_ENGINES);  // Should not crash
}

FL_TEST_CASE("FastLED.clear() with POWER_SETTINGS flag resets power management") {
    // Set power management and brightness separately
    FastLED.setBrightness(128);
    FL_CHECK(FastLED.getBrightness() == 128);

    FastLED.setMaxPowerInMilliWatts(5000);

    // Reset only power settings (should NOT affect brightness)
    FastLED.clear(ClearFlags::POWER_SETTINGS);

    // Brightness should remain unchanged
    FL_CHECK(FastLED.getBrightness() == 128);

    // Power settings should be reset to defaults (tested indirectly via show())
    // After reset, power limiting should be disabled
}

FL_TEST_CASE("FastLED.clear() with BRIGHTNESS flag resets brightness to 255") {
    // Set custom brightness
    FastLED.setBrightness(64);
    FL_CHECK(FastLED.getBrightness() == 64);

    // Reset only brightness
    FastLED.clear(ClearFlags::BRIGHTNESS);

    // Brightness should be back to default (255)
    FL_CHECK(FastLED.getBrightness() == 255);
}

FL_TEST_CASE("FastLED.clear() with REFRESH_RATE flag resets refresh rate") {
    // Set a custom refresh rate (30 FPS = 33333 microseconds per frame)
    FastLED.setMaxRefreshRate(30, false);

    // Reset only refresh rate
    FastLED.clear(ClearFlags::REFRESH_RATE);

    // Refresh rate should be unlimited (0 microseconds minimum)
    // Can't directly test mNMinMicros, but reset should work without error
}

FL_TEST_CASE("FastLED.clear() with FPS_COUNTER flag resets FPS tracking") {
    // Note: FPS counter is internal state, we can only verify reset doesn't crash
    FastLED.clear(ClearFlags::FPS_COUNTER);

    // After reset, FPS should be 0
    FL_CHECK(FastLED.getFPS() == 0);
}

FL_TEST_CASE("FastLED.reset() with multiple flags using OR operator") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2004, driver);

    CRGB leds[4];
    auto ch = makeChannel(leds, 4);

    // Set up non-default state
    FastLED.add(ch);
    FastLED.setBrightness(100);
    FastLED.setMaxPowerInMilliWatts(5000);
    FastLED.setMaxRefreshRate(60, false);

    // Verify non-default state
    FL_CHECK(ch.use_count() == 2);
    FL_CHECK(FastLED.getBrightness() == 100);

    // Reset multiple settings at once
    FastLED.clear(ClearFlags::CHANNELS | ClearFlags::BRIGHTNESS | ClearFlags::POWER_SETTINGS);

    // Verify channels were removed
    FL_CHECK(ch.use_count() == 1);
    FL_CHECK(!controllerInList(ch.get()));

    // Verify brightness was reset
    FL_CHECK(FastLED.getBrightness() == 255);

    // Refresh rate should NOT be reset (we didn't include that flag)
    // Power settings should be reset (included in flags)

    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.reset() with all flags resets everything") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2005, driver);

    CRGB leds[4];
    auto ch = makeChannel(leds, 4);

    // Set up all non-default state
    FastLED.add(ch);
    FastLED.setBrightness(50);
    FastLED.setMaxPowerInMilliWatts(3000);
    FastLED.setMaxRefreshRate(30, false);

    // Verify non-default state
    FL_CHECK(ch.use_count() == 2);
    FL_CHECK(FastLED.getBrightness() == 50);

    // Reset EVERYTHING by OR'ing all flags together
    FastLED.clear(ClearFlags::CHANNELS | ClearFlags::POWER_SETTINGS |
                  ClearFlags::BRIGHTNESS | ClearFlags::REFRESH_RATE |
                  ClearFlags::FPS_COUNTER | ClearFlags::CHANNEL_ENGINES);

    // Verify all state was reset to defaults
    FL_CHECK(ch.use_count() == 1);           // Channels removed
    FL_CHECK(!controllerInList(ch.get()));   // Not in draw list
    FL_CHECK(FastLED.getBrightness() == 255); // Brightness reset
    FL_CHECK(FastLED.getFPS() == 0);          // FPS counter reset
    // Power settings and refresh rate also reset (can't directly test)
    // Channel drivers also cleared (tested separately)

    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.reset() with CHANNELS flag resets only channels") {
    auto driver = fl::make_shared<StubEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(2006, driver);

    CRGB leds[4];
    auto ch = makeChannel(leds, 4);

    // Set up mixed state
    FastLED.add(ch);
    FastLED.setBrightness(75);
    FL_CHECK(FastLED.getBrightness() == 75);

    // Call reset with CHANNELS flag only
    FastLED.clear(ClearFlags::CHANNELS);

    // Verify channels were removed
    FL_CHECK(ch.use_count() == 1);
    FL_CHECK(!controllerInList(ch.get()));

    // Verify brightness was NOT reset (only CHANNELS was specified)
    FL_CHECK(FastLED.getBrightness() == 75);

    mgr.setDriverEnabled("STUB_ADD_REMOVE", false);
}

FL_TEST_CASE("FastLED.reset() with CHANNEL_ENGINES flag clears all drivers") {
    ChannelManager& mgr = ChannelManager::instance();

    // Add a test driver to the manager
    auto driver = fl::make_shared<StubEngine>();
    mgr.addDriver(3000, driver);

    // Verify driver was registered
    FL_CHECK(mgr.getDriverCount() > 0);

    // Reset only channel drivers
    FastLED.clear(ClearFlags::CHANNEL_ENGINES);

    // Verify all drivers were cleared
    FL_CHECK(mgr.getDriverCount() == 0);
}

} // namespace channel_add_remove_test

FL_TEST_CASE("Channel::applyConfig updates reconfigurable fields") {
    // Create initial channel with known settings
    static CRGB leds1[8];
    fl::fill_solid(leds1, 8, CRGB::Red);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions opts;
    opts.mCorrection = TypicalSMD5050;
    opts.mDitherMode = BINARY_DITHER;
    // (#2558) mWhiteCfg default-constructs to fl::Empty (plain RGB), same
    // semantics as the legacy `mRgbw = RgbwInvalid::value()` sentinel.
    opts.mWhiteCfg.reset();

    fl::ChannelConfig config1(5, timing, fl::span<CRGB>(leds1, 8), GRB, opts);
    auto channel = fl::Channel::create(config1);
    FL_REQUIRE(channel != nullptr);

    fl::i32 originalId = channel->id();
    int originalPin = channel->getPin();

    // Verify initial state
    FL_CHECK(channel->getRgbOrder() == GRB);
    FL_CHECK(channel->size() == 8);
    FL_CHECK(channel->getCorrection() == CRGB(TypicalSMD5050));
    FL_CHECK(channel->getDither() == BINARY_DITHER);

    // Build new config with different values
    static CRGB leds2[16];
    fl::fill_solid(leds2, 16, CRGB::Blue);

    fl::ChannelOptions opts2;
    opts2.mCorrection = Typical8mmPixel;
    opts2.mTemperature = CRGB(200, 180, 160);
    opts2.mDitherMode = DISABLE_DITHER;
    opts2.mWhiteCfg = fl::Rgbw(fl::kRGBWDefaultColorTemp, fl::RGBW_MODE::kRGBWExactColors);

    fl::ChannelConfig config2(99, timing, fl::span<CRGB>(leds2, 16), BGR, opts2);

    // Apply new config
    channel->applyConfig(config2);

    // Verify reconfigurable fields changed
    FL_CHECK(channel->getRgbOrder() == BGR);
    FL_CHECK(channel->size() == 16);
    FL_CHECK(channel->leds() == leds2);
    FL_CHECK(channel->getCorrection() == CRGB(Typical8mmPixel));
    FL_CHECK(channel->getTemperature() == CRGB(200, 180, 160));
    FL_CHECK(channel->getDither() == DISABLE_DITHER);
    FL_CHECK(channel->getRgbw().active());

    // Verify structural members are unchanged
    FL_CHECK(channel->id() == originalId);
    FL_CHECK(channel->getPin() == originalPin);
}

FL_TEST_CASE("Channel LED span tracks underlying array correctly") {
    // Channel stores a span (non-owning view) into an LED array.
    // Verify that:
    //  1) Writes through channel->leds() modify the original array
    //  2) applyConfig with a new array disconnects from the old one
    //  3) The old array is unaffected after switching

    static CRGB leds1[4] = {CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black};
    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelConfig config1(5, timing, fl::span<CRGB>(leds1, 4), GRB);
    auto channel = fl::Channel::create(config1);
    FL_REQUIRE(channel != nullptr);

    // Write through channel — should modify leds1 directly (span is a view)
    channel->leds()[0] = CRGB::Red;
    channel->leds()[1] = CRGB::Green;
    FL_CHECK(leds1[0] == CRGB::Red);
    FL_CHECK(leds1[1] == CRGB::Green);

    // Switch to a different LED array via applyConfig
    static CRGB leds2[6] = {CRGB::Black, CRGB::Black, CRGB::Black,
                            CRGB::Black, CRGB::Black, CRGB::Black};
    fl::ChannelConfig config2(5, timing, fl::span<CRGB>(leds2, 6), GRB);
    channel->applyConfig(config2);

    FL_CHECK(channel->size() == 6);
    FL_CHECK(channel->leds() == leds2);

    // Writes now go to leds2, not leds1
    channel->leds()[0] = CRGB::Blue;
    FL_CHECK(leds2[0] == CRGB::Blue);
    // leds1 retains its last state — channel no longer points to it
    FL_CHECK(leds1[0] == CRGB::Red);
    FL_CHECK(leds1[1] == CRGB::Green);
}

// --- Channel Events Tests ---

namespace channel_events_test {

using namespace fl;

/// @brief Event tracker for channel event testing
struct EventTracker {
    int mCreatedCount = 0;
    int mBeginDestroyCount = 0;
    int mAddedCount = 0;
    int mRemovedCount = 0;
    int mConfiguredCount = 0;
    int mEnqueuedCount = 0;
    fl::string mLastEngineName;
    const IChannel* mLastChannel = nullptr;

    void reset() {
        mCreatedCount = 0;
        mBeginDestroyCount = 0;
        mAddedCount = 0;
        mRemovedCount = 0;
        mConfiguredCount = 0;
        mEnqueuedCount = 0;
        mLastEngineName.clear();
        mLastChannel = nullptr;
    }

    void onCreated(const IChannel& ch) {
        mCreatedCount++;
        mLastChannel = &ch;
    }

    void onBeginDestroy(const IChannel& ch) {
        mBeginDestroyCount++;
        mLastChannel = &ch;
    }

    void onAdded(const IChannel& ch) {
        mAddedCount++;
        mLastChannel = &ch;
    }

    void onRemoved(const IChannel& ch) {
        mRemovedCount++;
        mLastChannel = &ch;
    }

    void onConfigured(const IChannel& ch, const ChannelConfig&) {
        mConfiguredCount++;
        mLastChannel = &ch;
    }

    void onEnqueued(const IChannel& ch, const fl::string& engineName) {
        mEnqueuedCount++;
        mLastChannel = &ch;
        mLastEngineName = engineName;
    }
};

/// Minimal driver for event testing
class EventTestEngine : public IChannelDriver {
public:
    bool canHandle(const ChannelDataPtr&) const override { return true; }
    void enqueue(ChannelDataPtr) override {}
    void show() override {}
    DriverState poll() override { return DriverState::READY; }
    fl::string getName() const override { return fl::string::from_literal("EVENT_TEST"); }
    Capabilities getCapabilities() const override {
        return Capabilities(true, true);
    }
};

FL_TEST_CASE("Channel Events: onChannelCreated fires on Channel::create()") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();

    // Add listener
    int listenerId = events.onChannelCreated.add([&tracker](const IChannel& ch) {
        tracker.onCreated(ch);
    });

    // Create channel
    static CRGB leds[10];
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB);

    int countBefore = tracker.mCreatedCount;
    auto channel = Channel::create(config);

    // Verify event was called
    FL_CHECK(tracker.mCreatedCount == countBefore + 1);
    FL_CHECK(tracker.mLastChannel == channel.get());

    // Cleanup
    events.onChannelCreated.remove(listenerId);
}

FL_TEST_CASE("Channel Events: onChannelBeginDestroy fires on channel destruction") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();

    // Add listener
    int listenerId = events.onChannelBeginDestroy.add([&tracker](const IChannel& ch) {
        tracker.onBeginDestroy(ch);
    });

    // Create and destroy channel
    static CRGB leds[10];
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB);

    int countBefore = tracker.mBeginDestroyCount;
    {
        auto channel = Channel::create(config);
        // channel goes out of scope and is destroyed here
    }

    // Verify event was called
    FL_CHECK(tracker.mBeginDestroyCount == countBefore + 1);

    // Cleanup
    events.onChannelBeginDestroy.remove(listenerId);
}

FL_TEST_CASE("Channel Events: onChannelAdded fires on FastLED.add()") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();
    auto driver = fl::make_shared<EventTestEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(3000, driver);

    // Add listener
    int listenerId = events.onChannelAdded.add([&tracker](const IChannel& ch) {
        tracker.onAdded(ch);
    });

    // Create channel
    static CRGB leds[10];
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelOptions opts;
    ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB, opts);
    auto channel = Channel::create(config);

    int countBefore = tracker.mAddedCount;

    // Add to FastLED
    FastLED.add(channel);

    // Verify event was called
    FL_CHECK(tracker.mAddedCount == countBefore + 1);
    FL_CHECK(tracker.mLastChannel == channel.get());

    // Cleanup
    FastLED.remove(channel);
    events.onChannelAdded.remove(listenerId);
    mgr.setDriverEnabled("EVENT_TEST", false);
}

FL_TEST_CASE("Channel Events: onChannelRemoved fires on FastLED.remove()") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();
    auto driver = fl::make_shared<EventTestEngine>();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(3001, driver);

    // Add listener
    int listenerId = events.onChannelRemoved.add([&tracker](const IChannel& ch) {
        tracker.onRemoved(ch);
    });

    // Create and add channel
    static CRGB leds[10];
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelOptions opts;
    ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB, opts);
    auto channel = Channel::create(config);
    FastLED.add(channel);

    int countBefore = tracker.mRemovedCount;

    // Remove from FastLED
    FastLED.remove(channel);

    // Verify event was called
    FL_CHECK(tracker.mRemovedCount == countBefore + 1);
    FL_CHECK(tracker.mLastChannel == channel.get());

    // Cleanup
    events.onChannelRemoved.remove(listenerId);
    mgr.setDriverEnabled("EVENT_TEST", false);
}

FL_TEST_CASE("Channel Events: onChannelConfigured fires on applyConfig()") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();

    // Add listener
    int listenerId = events.onChannelConfigured.add([&tracker](const IChannel& ch, const ChannelConfig& cfg) {
        tracker.onConfigured(ch, cfg);
    });

    // Create channel
    static CRGB leds1[10];
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelConfig config1(5, timing, fl::span<CRGB>(leds1, 10), GRB);
    auto channel = Channel::create(config1);

    int countBefore = tracker.mConfiguredCount;

    // Apply new config
    static CRGB leds2[20];
    ChannelConfig config2(5, timing, fl::span<CRGB>(leds2, 20), BGR);
    channel->applyConfig(config2);

    // Verify event was called
    FL_CHECK(tracker.mConfiguredCount == countBefore + 1);
    FL_CHECK(tracker.mLastChannel == channel.get());

    // Cleanup
    events.onChannelConfigured.remove(listenerId);
}

FL_TEST_CASE("Channel Events: onChannelEnqueued fires when data is enqueued to driver") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();
    auto mockEngine = fl::make_shared<ChannelEngineMock>("EVENT_ENQUEUE_TEST");
    mockEngine->reset();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(3003, mockEngine);

    // Add listener
    int listenerId = events.onChannelEnqueued.add([&tracker](const IChannel& ch, const fl::string& engineName) {
        tracker.onEnqueued(ch, engineName);
    });

    // Create and add channel
    static CRGB leds[10];
    fl::fill_solid(leds, 10, CRGB::Green);
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelOptions opts;
    ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB, opts);
    auto channel = Channel::create(config);
    FastLED.add(channel);

    int countBefore = tracker.mEnqueuedCount;

    // Trigger show (which calls enqueue)
    FastLED.show();

    // Verify event was called with correct driver name
    FL_CHECK(tracker.mEnqueuedCount == countBefore + 1);
    FL_CHECK(tracker.mLastChannel == channel.get());
    FL_CHECK(tracker.mLastEngineName == "EVENT_ENQUEUE_TEST");  // ChannelEngineMock returns configured name

    // Cleanup
    FastLED.remove(channel);
    events.onChannelEnqueued.remove(listenerId);
    mgr.setDriverEnabled("EVENT_ENQUEUE_TEST", false);
}

FL_TEST_CASE("Channel Events: Multiple listeners with priority ordering") {
    auto& events = ChannelEvents::instance();

    fl::vector<int> callOrder;

    // Add listeners with different priorities (higher priority = called first)
    int id1 = events.onChannelCreated.add([&callOrder](const IChannel&) {
        callOrder.push_back(1);
    }, 10);  // Low priority

    int id2 = events.onChannelCreated.add([&callOrder](const IChannel&) {
        callOrder.push_back(2);
    }, 100);  // High priority

    int id3 = events.onChannelCreated.add([&callOrder](const IChannel&) {
        callOrder.push_back(3);
    }, 50);  // Medium priority

    // Create channel (triggers event)
    static CRGB leds[5];
    auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
    ChannelConfig config(5, timing, fl::span<CRGB>(leds, 5), GRB);
    auto channel = Channel::create(config);

    // Verify listeners were called in priority order (high to low)
    FL_REQUIRE(callOrder.size() == 3);
    FL_CHECK(callOrder[0] == 2);  // Priority 100
    FL_CHECK(callOrder[1] == 3);  // Priority 50
    FL_CHECK(callOrder[2] == 1);  // Priority 10

    // Cleanup
    events.onChannelCreated.remove(id1);
    events.onChannelCreated.remove(id2);
    events.onChannelCreated.remove(id3);
}

FL_TEST_CASE("Channel Events: Complete lifecycle event sequence") {
    EventTracker tracker;
    auto& events = ChannelEvents::instance();
    auto mockEngine = fl::make_shared<ChannelEngineMock>("EVENT_LIFECYCLE_TEST");
    mockEngine->reset();
    ChannelManager& mgr = ChannelManager::instance();
    mgr.addDriver(3004, mockEngine);

    // Add all listeners
    int createdId = events.onChannelCreated.add([&tracker](const IChannel& ch) {
        tracker.onCreated(ch);
    });
    int addedId = events.onChannelAdded.add([&tracker](const IChannel& ch) {
        tracker.onAdded(ch);
    });
    int configuredId = events.onChannelConfigured.add([&tracker](const IChannel& ch, const ChannelConfig& cfg) {
        tracker.onConfigured(ch, cfg);
    });
    int enqueuedId = events.onChannelEnqueued.add([&tracker](const IChannel& ch, const fl::string& engineName) {
        tracker.onEnqueued(ch, engineName);
    });
    int removedId = events.onChannelRemoved.add([&tracker](const IChannel& ch) {
        tracker.onRemoved(ch);
    });
    int destroyId = events.onChannelBeginDestroy.add([&tracker](const IChannel& ch) {
        tracker.onBeginDestroy(ch);
    });

    tracker.reset();

    // Complete lifecycle
    {
        // 1. Create channel
        static CRGB leds1[10];
        fl::fill_solid(leds1, 10, CRGB::Red);
        auto timing = makeTimingConfig<TIMING_WS2812_800KHZ>();
        ChannelOptions opts;
        ChannelConfig config1(5, timing, fl::span<CRGB>(leds1, 10), GRB, opts);
        auto channel = Channel::create(config1);
        FL_CHECK(tracker.mCreatedCount == 1);

        // 2. Add to FastLED
        FastLED.add(channel);
        FL_CHECK(tracker.mAddedCount == 1);

        // 3. Apply new config
        static CRGB leds2[20];
        ChannelConfig config2(5, timing, fl::span<CRGB>(leds2, 20), BGR, opts);
        channel->applyConfig(config2);
        FL_CHECK(tracker.mConfiguredCount == 1);

        // 4. Show (triggers Enqueued)
        FastLED.show();
        FL_CHECK(tracker.mEnqueuedCount == 1);

        // 5. Remove from FastLED
        FastLED.remove(channel);
        FL_CHECK(tracker.mRemovedCount == 1);

        // 6. Channel destroyed at end of scope
    }
    FL_CHECK(tracker.mBeginDestroyCount == 1);

    // Verify complete sequence
    FL_CHECK(tracker.mCreatedCount == 1);
    FL_CHECK(tracker.mAddedCount == 1);
    FL_CHECK(tracker.mConfiguredCount == 1);
    FL_CHECK(tracker.mEnqueuedCount == 1);
    FL_CHECK(tracker.mRemovedCount == 1);
    FL_CHECK(tracker.mBeginDestroyCount == 1);

    // Cleanup
    events.onChannelCreated.remove(createdId);
    events.onChannelAdded.remove(addedId);
    events.onChannelConfigured.remove(configuredId);
    events.onChannelEnqueued.remove(enqueuedId);
    events.onChannelRemoved.remove(removedId);
    events.onChannelBeginDestroy.remove(destroyId);
    mgr.setDriverEnabled("EVENT_LIFECYCLE_TEST", false);
}

} // namespace channel_events_test

// --- Arduino Macro Undefinition Tests ---
// These tests verify that fl/undef.h successfully undefines Arduino macros
// so that FastLED's type-safe fl:: functions work correctly

FL_TEST_CASE("Arduino macro undefs: fl::min works correctly") {
    // Test fl::min with various types
    FL_SUBCASE("fl::min with integers") {
        FL_CHECK(fl::min(5, 10) == 5);
        FL_CHECK(fl::min(10, 5) == 5);
        FL_CHECK(fl::min(-5, -10) == -10);
        FL_CHECK(fl::min(0, 0) == 0);
    }

    FL_SUBCASE("fl::min with floats") {
        FL_CHECK(fl::min(3.14f, 2.71f) == 2.71f);
        FL_CHECK(fl::min(-1.5f, -2.5f) == -2.5f);
        FL_CHECK(fl::min(0.0f, 0.0f) == 0.0f);
    }

    FL_SUBCASE("fl::min with doubles") {
        FL_CHECK(fl::min(3.14, 2.71) == 2.71);
        FL_CHECK(fl::min(-1.5, -2.5) == -2.5);
    }

    FL_SUBCASE("fl::min with unsigned") {
        FL_CHECK(fl::min(5u, 10u) == 5u);
        FL_CHECK(fl::min(10u, 5u) == 5u);
    }
}

FL_TEST_CASE("Arduino macro undefs: fl::max works correctly") {
    // Test fl::max with various types
    FL_SUBCASE("fl::max with integers") {
        FL_CHECK(fl::max(5, 10) == 10);
        FL_CHECK(fl::max(10, 5) == 10);
        FL_CHECK(fl::max(-5, -10) == -5);
        FL_CHECK(fl::max(0, 0) == 0);
    }

    FL_SUBCASE("fl::max with floats") {
        FL_CHECK(fl::max(3.14f, 2.71f) == 3.14f);
        FL_CHECK(fl::max(-1.5f, -2.5f) == -1.5f);
        FL_CHECK(fl::max(0.0f, 0.0f) == 0.0f);
    }

    FL_SUBCASE("fl::max with doubles") {
        FL_CHECK(fl::max(3.14, 2.71) == 3.14);
        FL_CHECK(fl::max(-1.5, -2.5) == -1.5);
    }

    FL_SUBCASE("fl::max with unsigned") {
        FL_CHECK(fl::max(5u, 10u) == 10u);
        FL_CHECK(fl::max(10u, 5u) == 10u);
    }
}

FL_TEST_CASE("Arduino macro undefs: fl::abs works correctly") {
    // Test fl::abs with various types
    FL_SUBCASE("fl::abs with integers") {
        FL_CHECK(fl::abs(5) == 5);
        FL_CHECK(fl::abs(-5) == 5);
        FL_CHECK(fl::abs(0) == 0);
        FL_CHECK(fl::abs(-100) == 100);
    }

    FL_SUBCASE("fl::abs with floats") {
        FL_CHECK(fl::abs(3.14f) == 3.14f);
        FL_CHECK(fl::abs(-3.14f) == 3.14f);
        FL_CHECK(fl::abs(0.0f) == 0.0f);
    }

    FL_SUBCASE("fl::abs with doubles") {
        FL_CHECK(fl::abs(3.14) == 3.14);
        FL_CHECK(fl::abs(-3.14) == 3.14);
    }
}

FL_TEST_CASE("Arduino macro undefs: fl::round works correctly") {
    // Test fl::round with various values
    FL_SUBCASE("fl::round with positive values") {
        FL_CHECK(fl::round(3.14f) == 3.0f);
        FL_CHECK(fl::round(3.5f) == 4.0f);
        FL_CHECK(fl::round(3.9f) == 4.0f);
        FL_CHECK(fl::round(4.0f) == 4.0f);
    }

    FL_SUBCASE("fl::round with negative values") {
        FL_CHECK(fl::round(-3.14f) == -3.0f);
        FL_CHECK(fl::round(-3.5f) == -4.0f);
        FL_CHECK(fl::round(-3.9f) == -4.0f);
        FL_CHECK(fl::round(-4.0f) == -4.0f);
    }

    FL_SUBCASE("fl::round with zero") {
        FL_CHECK(fl::round(0.0f) == 0.0f);
        FL_CHECK(fl::round(-0.0f) == 0.0f);
    }

    FL_SUBCASE("fl::round with doubles") {
        FL_CHECK(fl::round(3.14) == 3.0);
        FL_CHECK(fl::round(3.5) == 4.0);
    }
}

FL_TEST_CASE("Arduino macro undefs: fl::radians works correctly") {
    // Test fl::radians function (degrees to radians conversion)
    FL_SUBCASE("fl::radians with common angles") {
        // 180 degrees = π radians ≈ 3.14159
        float rad180 = fl::radians(180.0f);
        FL_CHECK(rad180 > 3.14f);
        FL_CHECK(rad180 < 3.15f);

        // 90 degrees = π/2 radians ≈ 1.5708
        float rad90 = fl::radians(90.0f);
        FL_CHECK(rad90 > 1.57f);
        FL_CHECK(rad90 < 1.58f);

        // 0 degrees = 0 radians
        FL_CHECK(fl::radians(0.0f) == 0.0f);

        // 360 degrees = 2π radians ≈ 6.28318
        float rad360 = fl::radians(360.0f);
        FL_CHECK(rad360 > 6.28f);
        FL_CHECK(rad360 < 6.29f);
    }

    FL_SUBCASE("fl::radians with negative angles") {
        // -90 degrees = -π/2 radians ≈ -1.5708
        float radNeg90 = fl::radians(-90.0f);
        FL_CHECK(radNeg90 < -1.57f);
        FL_CHECK(radNeg90 > -1.58f);
    }
}

FL_TEST_CASE("Arduino macro undefs: fl::degrees works correctly") {
    // Test fl::degrees function (radians to degrees conversion)
    FL_SUBCASE("fl::degrees with common angles") {
        // π radians ≈ 3.14159 = 180 degrees
        float deg_pi = fl::degrees(3.14159f);
        FL_CHECK(deg_pi > 179.9f);
        FL_CHECK(deg_pi < 180.1f);

        // π/2 radians ≈ 1.5708 = 90 degrees
        float deg_pi_2 = fl::degrees(1.5708f);
        FL_CHECK(deg_pi_2 > 89.9f);
        FL_CHECK(deg_pi_2 < 90.1f);

        // 0 radians = 0 degrees
        FL_CHECK(fl::degrees(0.0f) == 0.0f);

        // 2π radians ≈ 6.28318 = 360 degrees
        float deg_2pi = fl::degrees(6.28318f);
        FL_CHECK(deg_2pi > 359.9f);
        FL_CHECK(deg_2pi < 360.1f);
    }

    FL_SUBCASE("fl::degrees with negative angles") {
        // -π/2 radians ≈ -1.5708 = -90 degrees
        float deg_neg_pi_2 = fl::degrees(-1.5708f);
        FL_CHECK(deg_neg_pi_2 < -89.9f);
        FL_CHECK(deg_neg_pi_2 > -90.1f);
    }
}

FL_TEST_CASE("Arduino macro undefs: fl::map container works correctly") {
    // Test fl::map (the dictionary/container) to ensure it wasn't broken by Arduino's map macro
    FL_SUBCASE("fl::map basic operations") {
        fl::map<int, fl::string> myMap;

        // Insert elements
        myMap[1] = fl::string::from_literal("one");
        myMap[2] = fl::string::from_literal("two");
        myMap[3] = fl::string::from_literal("three");

        // Verify size
        FL_CHECK(myMap.size() == 3);

        // Verify values
        FL_CHECK(myMap[1] == fl::string::from_literal("one"));
        FL_CHECK(myMap[2] == fl::string::from_literal("two"));
        FL_CHECK(myMap[3] == fl::string::from_literal("three"));

        // Test find
        auto it = myMap.find(2);
        FL_CHECK(it != myMap.end());
        FL_CHECK(it->second == fl::string::from_literal("two"));

        // Test erase
        myMap.erase(2);
        FL_CHECK(myMap.size() == 2);
        FL_CHECK(myMap.find(2) == myMap.end());
    }

    FL_SUBCASE("fl::map with different types") {
        fl::map<fl::string, int> stringToInt;

        stringToInt[fl::string::from_literal("red")] = 255;
        stringToInt[fl::string::from_literal("green")] = 128;
        stringToInt[fl::string::from_literal("blue")] = 64;

        FL_CHECK(stringToInt.size() == 3);
        FL_CHECK(stringToInt[fl::string::from_literal("red")] == 255);
        FL_CHECK(stringToInt[fl::string::from_literal("green")] == 128);
        FL_CHECK(stringToInt[fl::string::from_literal("blue")] == 64);
    }

    FL_SUBCASE("fl::map iteration") {
        fl::map<int, int> squares;
        squares[1] = 1;
        squares[2] = 4;
        squares[3] = 9;
        squares[4] = 16;

        int count = 0;
        for (auto& pair : squares) {
            FL_CHECK(pair.second == pair.first * pair.first);
            count++;
        }
        FL_CHECK(count == 4);
    }

    FL_SUBCASE("fl::map clear and empty") {
        fl::map<int, int> testMap;
        FL_CHECK(testMap.empty());

        testMap[1] = 100;
        testMap[2] = 200;
        FL_CHECK(!testMap.empty());
        FL_CHECK(testMap.size() == 2);

        testMap.clear();
        FL_CHECK(testMap.empty());
        FL_CHECK(testMap.size() == 0);
    }
}

FL_TEST_CASE("Arduino macro undefs: Global using declarations work") {
    // FastLED.h brings fl::min, fl::max, etc. into global namespace via using declarations
    // Verify these work correctly (these should resolve to fl:: versions, not Arduino macros)

    FL_SUBCASE("Global min/max from using declarations") {
        // These resolve to the using declarations in FastLED.h:
        // using fl::min; using fl::max;
        FL_CHECK(min(5, 10) == 5);
        FL_CHECK(max(5, 10) == 10);
        FL_CHECK(abs(-42) == 42);
        FL_CHECK(radians(180.0f) > 3.14f);
        FL_CHECK(degrees(3.14159f) > 179.9f);
    }

    FL_SUBCASE("Type safety - no double evaluation bug") {
        // Arduino macros have double-evaluation bugs:
        // #define min(a,b) ((a)<(b)?(a):(b))
        // min(x++, y++) would evaluate x++ or y++ TWICE!
        //
        // FastLED's fl::min is a proper template function with no such bug
        int x = 5;
        int y = 10;
        int result = min(x++, y++);  // Uses fl::min from using declaration

        // With Arduino macro, x would be 6 or 7 (double increment)
        // With fl::min template, x is exactly 6 (single increment)
        FL_CHECK(result == 5);
        FL_CHECK(x == 6);  // Incremented exactly once
        FL_CHECK(y == 11); // Incremented exactly once
    }
}

FL_TEST_CASE("Arduino macro undefs: Comprehensive round-trip test") {
    // Test all math functions in a realistic usage scenario
    FL_SUBCASE("Angle conversions round-trip") {
        float degrees_in = 45.0f;
        float radians_out = fl::radians(degrees_in);
        float degrees_back = fl::degrees(radians_out);

        // Should round-trip with minimal error
        FL_CHECK(degrees_back > 44.99f);
        FL_CHECK(degrees_back < 45.01f);
    }

    FL_SUBCASE("Min/max/abs combination") {
        int values[] = {-10, 5, -3, 8, -15, 12};
        int minVal = values[0];
        int maxVal = values[0];
        int sumAbs = 0;

        for (int val : values) {
            minVal = fl::min(minVal, val);
            maxVal = fl::max(maxVal, val);
            sumAbs += fl::abs(val);
        }

        FL_CHECK(minVal == -15);
        FL_CHECK(maxVal == 12);
        FL_CHECK(sumAbs == 53); // |-10| + |5| + |-3| + |8| + |-15| + |12|
    }

    FL_SUBCASE("Round with min/max clamping") {
        float values[] = {1.2f, 5.7f, 3.4f, 9.9f, 2.1f};
        float clampMin = 2.0f;
        float clampMax = 8.0f;

        for (float val : values) {
            float rounded = fl::round(val);
            float clamped = fl::min(fl::max(rounded, clampMin), clampMax);
            FL_CHECK(clamped >= clampMin);
            FL_CHECK(clamped <= clampMax);
        }
    }
}

FL_TEST_CASE("Channel API: Empty affinity binds to high priority driver") {
    // Create two mock drivers with different priorities. The pre-#2459 version
    // of this test also covered "mAffinity = LOW_PRIORITY pins to the lower-
    // priority driver", but with `mAffinity` removed the only way to override
    // priority dispatch is `setExclusiveDriver`, which leaves persistent state
    // that pollutes downstream tests. The priority-dispatch half of the
    // original test is preserved here verbatim.
    auto lowPriorityEngine = fl::make_shared<ChannelEngineMock>("LOW_PRIORITY");
    auto highPriorityEngine = fl::make_shared<ChannelEngineMock>("HIGH_PRIORITY");
    lowPriorityEngine->reset();
    highPriorityEngine->reset();

    // Register both drivers with ChannelManager
    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(10, lowPriorityEngine);   // Low priority
    manager.addDriver(100, highPriorityEngine); // High priority

    // Verify both drivers are registered
    auto lowEngine = manager.getDriverByName("LOW_PRIORITY");
    auto highEngine = manager.getDriverByName("HIGH_PRIORITY");
    FL_REQUIRE(lowEngine != nullptr);
    FL_REQUIRE(highEngine != nullptr);

    // Channel WITHOUT affinity should bind to high priority driver
    {
        static CRGB leds2[10];
        fl::fill_solid(leds2, 10, CRGB::Green);

        auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
        fl::ChannelOptions opts;
        // No affinity set

        fl::ChannelConfig config(6, timing, fl::span<CRGB>(leds2, 10), GRB, opts);
        auto channel2 = fl::Channel::create(config);
        FL_REQUIRE(channel2 != nullptr);

        // Add to FastLED and trigger show
        FastLED.add(channel2);

        // Reset counters before show
        lowPriorityEngine->reset();
        highPriorityEngine->reset();

        FastLED.show();

        // Verify: HIGH priority driver should receive data (no affinity, highest priority wins)
        FL_CHECK(highPriorityEngine->mEnqueueCount == 1);
        FL_CHECK(lowPriorityEngine->mEnqueueCount == 0);  // Should NOT receive data

        // Cleanup
        FastLED.remove(channel2);
    }

    // Cleanup drivers
    manager.setDriverEnabled("LOW_PRIORITY", false);
    manager.setDriverEnabled("HIGH_PRIORITY", false);
}

FL_TEST_CASE("Channel API: Non-affinity channel re-binds when driver priorities swap") {
    // Create two mock drivers with different priorities
    auto engine1 = fl::make_shared<ChannelEngineMock>("ENGINE_1");
    auto engine2 = fl::make_shared<ChannelEngineMock>("ENGINE_2");
    engine1->reset();
    engine2->reset();

    // Register both drivers with ChannelManager
    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(100, engine1);  // ENGINE_1 starts with high priority
    manager.addDriver(10, engine2);   // ENGINE_2 starts with low priority

    // Verify both drivers are registered
    auto eng1 = manager.getDriverByName("ENGINE_1");
    auto eng2 = manager.getDriverByName("ENGINE_2");
    FL_REQUIRE(eng1 != nullptr);
    FL_REQUIRE(eng2 != nullptr);

    // Create channel WITHOUT affinity (should bind to highest priority)
    static CRGB leds[10];
    fl::fill_solid(leds, 10, CRGB::Red);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions opts;
    // No affinity set - should dynamically select highest priority driver

    fl::ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB, opts);
    auto channel = fl::Channel::create(config);
    FL_REQUIRE(channel != nullptr);

    // Add to FastLED
    FastLED.add(channel);

    // Reset counters before first show
    engine1->reset();
    engine2->reset();

    // First show() - should bind to ENGINE_1 (priority 100)
    FastLED.show();

    // Verify: ENGINE_1 should receive data (highest priority)
    FL_CHECK(engine1->mEnqueueCount == 1);
    FL_CHECK(engine2->mEnqueueCount == 0);
    FL_CHECK(channel->getEngineName() == "ENGINE_1");

    // Now swap priorities: ENGINE_2 becomes highest priority
    manager.setDriverPriority("ENGINE_1", 10);   // Lower ENGINE_1's priority
    manager.setDriverPriority("ENGINE_2", 100);  // Raise ENGINE_2's priority

    // Reset counters before second show
    engine1->reset();
    engine2->reset();

    // Second show() - channel should re-select and bind to ENGINE_2 (new highest priority)
    FastLED.show();

    // Verify: ENGINE_2 should now receive data (new highest priority)
    FL_CHECK(engine1->mEnqueueCount == 0);  // ENGINE_1 should NOT receive data
    FL_CHECK(engine2->mEnqueueCount == 1);  // ENGINE_2 should receive data
    FL_CHECK(channel->getEngineName() == "ENGINE_2");

    // Swap priorities back
    manager.setDriverPriority("ENGINE_1", 100);  // Raise ENGINE_1's priority again
    manager.setDriverPriority("ENGINE_2", 10);   // Lower ENGINE_2's priority

    // Reset counters before third show
    engine1->reset();
    engine2->reset();

    // Third show() - channel should re-select and bind back to ENGINE_1
    FastLED.show();

    // Verify: ENGINE_1 should receive data again (back to highest priority)
    FL_CHECK(engine1->mEnqueueCount == 1);
    FL_CHECK(engine2->mEnqueueCount == 0);
    FL_CHECK(channel->getEngineName() == "ENGINE_1");

    // Cleanup
    FastLED.remove(channel);
    manager.setDriverEnabled("ENGINE_1", false);
    manager.setDriverEnabled("ENGINE_2", false);
}

FL_TEST_CASE("Channel API: removeFromDrawList() clears driver weak_ptr") {
    // Create mock driver
    auto driver = fl::make_shared<ChannelEngineMock>("CLEAR_TEST");
    driver->reset();

    // Register driver with ChannelManager
    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(50, driver);

    // Create channel with affinity (so it caches the driver)
    static CRGB leds[10];
    fl::fill_solid(leds, 10, CRGB::Blue);

    auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
    fl::ChannelOptions opts;

    fl::ChannelConfig config(5, timing, fl::span<CRGB>(leds, 10), GRB, opts);
    auto channel = fl::Channel::create(config);
    FL_REQUIRE(channel != nullptr);

    // Add to FastLED
    FastLED.add(channel);

    // Call show() to bind the driver
    FastLED.show();

    // Verify driver is bound (getEngineName() should return "CLEAR_TEST")
    FL_CHECK(channel->getEngineName() == "CLEAR_TEST");

    // Remove from draw list
    FastLED.remove(channel);

    // Verify driver weak_ptr was cleared (getEngineName() should return empty string)
    FL_CHECK(channel->getEngineName().empty());

    // Cleanup
    manager.setDriverEnabled("CLEAR_TEST", false);
}

FL_TEST_CASE("Channel API: Late binding - driver name empty after construction") {
    // Create mock drivers
    auto engine1 = fl::make_shared<ChannelEngineMock>("LATE_BIND_AFFINITY");
    auto engine2 = fl::make_shared<ChannelEngineMock>("LATE_BIND_NO_AFFINITY");
    engine1->reset();
    engine2->reset();

    // Register drivers
    fl::ChannelManager& manager = fl::ChannelManager::instance();
    manager.addDriver(100, engine1);
    manager.addDriver(50, engine2);

    // Test 1: Affinity channel - late binding
    {
        static CRGB leds1[10];
        fl::fill_solid(leds1, 10, CRGB::Red);

        auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
        fl::ChannelOptions opts;

        fl::ChannelConfig config(5, timing, fl::span<CRGB>(leds1, 10), GRB, opts);
        auto channel = fl::Channel::create(config);
        FL_REQUIRE(channel != nullptr);

        // VERIFY: Immediately after construction, driver name is empty (late binding)
        FL_CHECK(channel->getEngineName().empty());

        // Add to FastLED and show
        FastLED.add(channel);
        FastLED.show();

        // VERIFY: After show(), driver is bound
        FL_CHECK(channel->getEngineName() == "LATE_BIND_AFFINITY");

        // Cleanup
        FastLED.remove(channel);
    }

    // Test 2: Non-affinity channel - late binding
    {
        static CRGB leds2[10];
        fl::fill_solid(leds2, 10, CRGB::Green);

        auto timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
        fl::ChannelOptions opts;
        // No affinity set

        fl::ChannelConfig config(5, timing, fl::span<CRGB>(leds2, 10), GRB, opts);
        auto channel = fl::Channel::create(config);
        FL_REQUIRE(channel != nullptr);

        // VERIFY: Immediately after construction, driver name is empty (late binding)
        FL_CHECK(channel->getEngineName().empty());

        // Add to FastLED and show
        FastLED.add(channel);
        FastLED.show();

        // VERIFY: After show(), driver is bound (should select highest priority)
        FL_CHECK(channel->getEngineName() == "LATE_BIND_AFFINITY");  // Priority 100

        // Cleanup
        FastLED.remove(channel);
    }

    // Cleanup
    manager.setDriverEnabled("LATE_BIND_AFFINITY", false);
    manager.setDriverEnabled("LATE_BIND_NO_AFFINITY", false);
}

} // FL_TEST_FILE
