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

#include "fl/math/math.h"
#include "fl/stl/static_assert.h"


#include "fl/gfx/corkscrew.h"
#include "fl/math/grid.h"
#include "fl/math/screenmap.h"
#include "fl/gfx/tile2x2.h" // Ensure this header is included for Tile2x2_u8
#include "fl/stl/cstddef.h"
#include "fl/stl/stdint.h"
#include "fl/stl/new.h"
#include "test.h"
#include "fl/math/geometry.h"
#include "fl/log/log.h"
#include "fl/gfx/crgb.h"
#include "fl/stl/span.h"
#include "fl/stl/strstream.h"
#include "fl/stl/compiler_control.h"
#include "fl/stl/int.h"
#include "fl/stl/vector.h"
#include "hsv2rgb.h"

FL_TEST_FILE(FL_FILEPATH) {

#undef NUM_LEDS  // Avoid redefinition in unity builds
#define NUM_LEDS 288


FL_TEST_CASE("Corkscrew Circle10 test") {
    // Test basic dimensional calculations using Corkscrew objects directly
    
    // Test simple case: 1 turn, 10 LEDs
    fl::Corkscrew corkscrew_simple(1.0f, 10);
    FL_REQUIRE_EQ(corkscrew_simple.cylinderWidth(), 10);   // ceil(10 LEDs / 1 turn) = 10
    FL_REQUIRE_EQ(corkscrew_simple.cylinderHeight(), 1);   // ceil(1 turn) = 1
    
    // Test 2 turns with 20 LEDs (10 LEDs per turn)
    fl::Corkscrew corkscrew_example(2.0f, 20);
    FL_REQUIRE_EQ(corkscrew_example.cylinderWidth(), 10);  // LEDs per turn
    FL_REQUIRE_EQ(corkscrew_example.cylinderHeight(), 2);  // Number of turns
    
    // Test default case: 19 turns, 144 LEDs
    fl::Corkscrew corkscrew_default(19.0f, 144);
    FL_REQUIRE_EQ(corkscrew_default.cylinderWidth(), 8);   // ceil(144/19) = ceil(7.58) = 8
    FL_REQUIRE_EQ(corkscrew_default.cylinderHeight(), 18); // Optimized: ceil(144/8) = ceil(18) = 18
    
    // Test FestivalStick case: 19 turns, 288 LEDs 
    fl::Corkscrew corkscrew_festival(19.0f, 288);
    FL_REQUIRE_EQ(corkscrew_festival.cylinderWidth(), 16);  // ceil(288/19) = ceil(15.16) = 16  
    FL_REQUIRE_EQ(corkscrew_festival.cylinderHeight(), 18); // ceil(288/16) = ceil(18) = 18 (optimized!)
    
    // Verify grid size matches LED count
    FL_REQUIRE_EQ(corkscrew_festival.cylinderWidth() * corkscrew_festival.cylinderHeight(), 288);
    
    // Check LED distribution - find max height position actually used
    float max_height = 0.0f;
    float min_height = 999.0f;
    for (uint16_t i = 0; i < corkscrew_festival.size(); ++i) {
        fl::vec2f pos = corkscrew_festival.at_no_wrap(i);
        max_height = fl::max(max_height, pos.y);
        min_height = fl::min(min_height, pos.y);
    }
    
    // LEDs should span from 0 to height-1
    FL_REQUIRE(min_height >= 0.0f);
    FL_REQUIRE(max_height <= 18.0f); // height-1 = 18-1 = 17
}

FL_TEST_CASE("Corkscrew LED distribution test") {
    // Test if LEDs actually reach the top row
    fl::Corkscrew corkscrew(19.0f, 288); // FestivalStick case
    
    // Count how many LEDs map to each row
    fl::vector<int> row_counts(corkscrew.cylinderHeight(), 0);
    for (uint16_t i = 0; i < corkscrew.size(); ++i) {
        fl::vec2f pos = corkscrew.at_no_wrap(i);
        int row = static_cast<int>(pos.y);
        if (row >= 0 && row < corkscrew.cylinderHeight()) {
            row_counts[row]++;
        }
    }
    
    // Check if top row (now row 17) has LEDs
    FL_REQUIRE(row_counts[corkscrew.cylinderHeight() - 1] > 0); // Top row should have LEDs
    FL_REQUIRE(row_counts[0] > 0);  // Bottom row should have LEDs
}

FL_TEST_CASE("Corkscrew two turns test") {
    // Test 2 turns with 2 LEDs per turn (4 LEDs total)
    fl::Corkscrew corkscrew_two_turns(2.0f, 4); // 2 turns, 4 LEDs, defaults
    
    // Verify: 4 LEDs / 2 turns = 2 LEDs per turn
    FL_REQUIRE_EQ(corkscrew_two_turns.cylinderWidth(), 2);   // LEDs per turn
    FL_REQUIRE_EQ(corkscrew_two_turns.cylinderHeight(), 2);  // Number of turns
    
    // Verify grid size matches LED count
    FL_REQUIRE_EQ(corkscrew_two_turns.cylinderWidth() * corkscrew_two_turns.cylinderHeight(), 4);
    
    // Test LED positioning across both turns
    FL_REQUIRE_EQ(corkscrew_two_turns.size(), 4);
    
    // Check that LEDs are distributed across both rows
    fl::vector<int> row_counts(corkscrew_two_turns.cylinderHeight(), 0);
    
    // Unrolled loop for 4 LEDs
    fl::vec2f pos0 = corkscrew_two_turns.at_no_wrap(0);
    fl::vec2f pos1 = corkscrew_two_turns.at_no_wrap(1);
    fl::vec2f pos2 = corkscrew_two_turns.at_no_wrap(2);
    fl::vec2f pos3 = corkscrew_two_turns.at_no_wrap(3);

    FL_WARN("pos0: " << pos0);
    FL_WARN("pos1: " << pos1);
    FL_WARN("pos2: " << pos2);
    FL_WARN("pos3: " << pos3);

    int row0 = static_cast<int>(pos0.y);
    if (row0 >= 0 && row0 < corkscrew_two_turns.cylinderHeight()) {
        row_counts[row0]++;
    }
    
    
    int row1 = static_cast<int>(pos1.y);
    if (row1 >= 0 && row1 < corkscrew_two_turns.cylinderHeight()) {
        row_counts[row1]++;
    }
    

    int row2 = static_cast<int>(pos2.y);
    if (row2 >= 0 && row2 < corkscrew_two_turns.cylinderHeight()) {
        row_counts[row2]++;
    }
    

    int row3 = static_cast<int>(pos3.y);
    if (row3 >= 0 && row3 < corkscrew_two_turns.cylinderHeight()) {
        row_counts[row3]++;
    }
    
    // Both rows should have LEDs
    FL_REQUIRE(row_counts[0] > 0);  // First turn should have LEDs
    FL_REQUIRE(row_counts[1] > 0);  // Second turn should have LEDs
}

FL_TEST_CASE("Constexpr corkscrew dimension calculation") {
    // Test constexpr functions at compile time
    
    // FestivalStick case: 19 turns, 288 LEDs
    constexpr uint16_t festival_width = fl::calculateCorkscrewWidth(19.0f, 288);
    constexpr uint16_t festival_height = fl::calculateCorkscrewHeight(19.0f, 288);
    
    FL_STATIC_ASSERT(festival_width == 16, "FestivalStick width should be 16");
    FL_STATIC_ASSERT(festival_height == 18, "FestivalStick height should be 18");
    
    // Default case: 19 turns, 144 LEDs
    constexpr uint16_t default_width = fl::calculateCorkscrewWidth(19.0f, 144);
    constexpr uint16_t default_height = fl::calculateCorkscrewHeight(19.0f, 144);
    
    FL_STATIC_ASSERT(default_width == 8, "Default width should be 8");
    FL_STATIC_ASSERT(default_height == 18, "Default height should be 18");
    
    // Verify runtime and compile-time versions match
    fl::Corkscrew runtime_corkscrew(19.0f, 288);
    
    FL_REQUIRE_EQ(festival_width, runtime_corkscrew.cylinderWidth());
    FL_REQUIRE_EQ(festival_height, runtime_corkscrew.cylinderHeight());
    
    // Test simple perfect case: 100 LEDs, 10 turns = 10x10 grid
    constexpr uint16_t simple_width = fl::calculateCorkscrewWidth(10.0f, 100);
    constexpr uint16_t simple_height = fl::calculateCorkscrewHeight(10.0f, 100);
    
    FL_STATIC_ASSERT(simple_width == 10, "Simple width should be 10");
    FL_STATIC_ASSERT(simple_height == 10, "Simple height should be 10");
}

FL_TEST_CASE("TestCorkscrewBufferFunctionality") {
    // Create a Corkscrew with 16 LEDs and 4 turns for simple testing
    // Create a buffer for testing
    fl::vector<CRGB> led_buffer(16);
    fl::span<CRGB> led_span(led_buffer);
    fl::Corkscrew corkscrew(4.0f, led_span, false);
    
    // Get the rectangular buffer dimensions
    uint16_t width = corkscrew.cylinderWidth();
    uint16_t height = corkscrew.cylinderHeight();
    
    // Get the surface and verify it's properly initialized
    fl::Grid<CRGB>& surface = corkscrew.surface();
    FL_REQUIRE(surface.size() == width * height);
    
    // Fill the buffer with a simple pattern
    corkscrew.fillInputSurface(CRGB::Red);
    for (size_t i = 0; i < surface.size(); ++i) {
        FL_REQUIRE(surface.span()[i] == CRGB::Red);
    }
    
    // Clear the buffer
    corkscrew.clear();
    for (size_t i = 0; i < surface.size(); ++i) {
        FL_REQUIRE(surface.span()[i] == CRGB::Black);
    }
    
    // Create a source fl::Grid<CRGB> object with a checkerboard pattern
    fl::Grid<CRGB> source_grid(width, height);
    
    // Fill source with checkerboard pattern
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            if ((x + y) % 2 == 0) {
                source_grid(x, y) = CRGB::Blue;
            } else {
                source_grid(x, y) = CRGB::Green;
            }
        }
    }
    
    // First, copy the source grid to the corkscrew's surface, then draw
    auto& corkscrew_surface = corkscrew.surface();
    for (fl::u32 y = 0; y < height; ++y) {
        for (fl::u32 x = 0; x < width; ++x) {
            if (x < corkscrew_surface.width() && y < corkscrew_surface.height()) {
                corkscrew_surface(x, y) = source_grid(x, y);
            }
        }
    }
    corkscrew.draw();
    
    // Verify that the LED data has been populated with colors from the surface
    // Note: Not every LED may be written to by the corkscrew mapping
    bool found_blue = false;
    bool found_green = false;
    int non_black_count = 0;
    
    fl::span<CRGB> led_data = corkscrew.data();
    for (size_t i = 0; i < led_data.size(); ++i) {
        if (led_data[i] == CRGB::Blue) found_blue = true;
        if (led_data[i] == CRGB::Green) found_green = true;
        if (led_data[i] != CRGB::Black) non_black_count++;
    }
    
    // Should have found both colors in our checkerboard pattern
    FL_REQUIRE(found_blue);
    FL_REQUIRE(found_green);
    // Should have some non-black pixels since we read from a filled source
    FL_REQUIRE(non_black_count > 0);
}

FL_TEST_CASE("Corkscrew readFrom with bilinear interpolation") {
    // Create a small corkscrew for testing with LED buffer
    fl::vector<CRGB> led_buffer(12);
    fl::span<CRGB> led_span(led_buffer);
    fl::Corkscrew corkscrew(1.0f, led_span, false);
    
    // Create a source grid - simple 3x4 pattern
    const uint16_t width = 3;
    const uint16_t height = 4;
    fl::Grid<CRGB> source_grid(width, height);
    
    // Grid initializes to black by default, but let's be explicit
    source_grid.clear();
    
    // Set up a simple pattern: red in corners, blue in center
    source_grid(0, 0) = CRGB::Red;    // Bottom-left
    source_grid(2, 0) = CRGB::Red;    // Bottom-right  
    source_grid(0, 3) = CRGB::Red;    // Top-left
    source_grid(2, 3) = CRGB::Red;    // Top-right
    source_grid(1, 1) = CRGB::Blue;   // Center-ish
    source_grid(1, 2) = CRGB::Blue;   // Center-ish
    
    // Copy source to corkscrew surface and draw
    auto& corkscrew_surface2 = corkscrew.surface();
    for (fl::u32 y = 0; y < height; ++y) {
        for (fl::u32 x = 0; x < width; ++x) {
            if (x < corkscrew_surface2.width() && y < corkscrew_surface2.height()) {
                corkscrew_surface2(x, y) = source_grid(x, y);
            }
        }
    }
    corkscrew.draw();
    
    // Verify LED count
    FL_REQUIRE_EQ(corkscrew.size(), 12);
    
    // Check that some colors were captured in the LED data
    bool found_red_component = false;
    bool found_blue_component = false;
    int non_black_count = 0;
    
    fl::span<CRGB> led_data = corkscrew.data();
    for (size_t i = 0; i < led_data.size(); ++i) {
        const CRGB& color = led_data[i];
        if (color.r > 0 || color.g > 0 || color.b > 0) {
            non_black_count++;
        }
        if (color.r > 0) found_red_component = true;
        if (color.b > 0) found_blue_component = true;
    }
    
    // We should find some non-black LEDs and some red components
    FL_REQUIRE(non_black_count > 0);
    FL_REQUIRE(found_red_component);
    
    // Note: Blue components might not be found depending on the mapping, but we check anyway
    FL_UNUSED(found_blue_component);  // Suppress warning if not used in assertions
    
    // Test that coordinates mapping makes sense by checking a specific LED
    fl::vec2f pos0 = corkscrew.at_no_wrap(0);
    fl::vec2f pos5 = corkscrew.at_no_wrap(5);
    
    // Positions should be different
    bool positions_different = (pos0.x != pos5.x) || (pos0.y != pos5.y);
    FL_REQUIRE(positions_different);
    
    // Positions should be within reasonable bounds
    FL_REQUIRE(pos0.x >= 0.0f);
    FL_REQUIRE(pos0.y >= 0.0f);
    FL_REQUIRE(pos5.x >= 0.0f);
    FL_REQUIRE(pos5.y >= 0.0f);
}

FL_TEST_CASE("Corkscrew span data access") {
    // Create a corkscrew with LED buffer
    fl::vector<CRGB> led_buffer(6);
    fl::span<CRGB> led_span(led_buffer);
    fl::Corkscrew corkscrew(1.0f, led_span, false, fl::Gap());

    // Get span access - this should trigger lazy allocation
    fl::span<CRGB> data_ptr = corkscrew.data();
    FL_REQUIRE(!data_ptr.empty());
    
    // Verify buffer was allocated with correct size
    size_t expected_size = static_cast<size_t>(corkscrew.cylinderWidth()) * static_cast<size_t>(corkscrew.cylinderHeight());
    
    // All pixels should be initialized to black
    for (size_t i = 0; i < expected_size; ++i) {
        FL_REQUIRE_EQ(data_ptr[i].r, 0);
        FL_REQUIRE_EQ(data_ptr[i].g, 0);
        FL_REQUIRE_EQ(data_ptr[i].b, 0);
    }
    
    // Note: Removed const access test since we simplified the API to be non-const
    
    // Modify a pixel via the raw pointer
    if (expected_size > 0) {
        data_ptr[0] = CRGB::Red;
        
        // Verify the change is reflected
        FL_REQUIRE_EQ(data_ptr[0].r, 255);
        FL_REQUIRE_EQ(data_ptr[0].g, 0);
        FL_REQUIRE_EQ(data_ptr[0].b, 0);
        
        // Surface should remain separate from LED data
        const auto& surface = corkscrew.surface();
        FL_REQUIRE_EQ(surface.span()[0].r, 0); // Surface should still be black
        FL_REQUIRE_EQ(surface.span()[0].g, 0);
        FL_REQUIRE_EQ(surface.span()[0].b, 0);
    }
}

FL_TEST_CASE("Corkscrew ScreenMap functionality") {
    // Create a simple corkscrew for testing
    fl::Corkscrew corkscrew(2.0f, 8, false, fl::Gap()); // 2 turns, 8 LEDs
    
    // Test default diameter
    fl::ScreenMap screenMap = corkscrew.toScreenMap();
    
    // Verify the ScreenMap has the correct number of LEDs
    FL_REQUIRE_EQ(screenMap.getLength(), 8);
    
    // Verify default diameter
    FL_REQUIRE_EQ(screenMap.getDiameter(), 0.5f);
    
    // Test custom diameter
    fl::ScreenMap screenMapCustom = corkscrew.toScreenMap(1.2f);
    FL_REQUIRE_EQ(screenMapCustom.getDiameter(), 1.2f);
    
    // Verify that each LED index maps to the same position as at_exact() (wrapped)
    for (uint16_t i = 0; i < 8; ++i) {
        fl::vec2f corkscrewPos = corkscrew.at_exact(i);
        fl::vec2f screenMapPos = screenMap[i];
        
        // Positions should match exactly (both are wrapped)
        FL_REQUIRE(fl::almost_equal(corkscrewPos.x, screenMapPos.x));
        FL_REQUIRE(fl::almost_equal(corkscrewPos.y, screenMapPos.y));
    }
    
    // Test that different LED indices have different positions (at least some of them)
    bool positions_differ = false;
    for (uint16_t i = 1; i < 8; ++i) {
        fl::vec2f pos0 = screenMap[0];
        fl::vec2f posI = screenMap[i];
        if (!fl::almost_equal(pos0.x, posI.x) || !fl::almost_equal(pos0.y, posI.y)) {
            positions_differ = true;
            break;
        }
    }
    FL_REQUIRE(positions_differ); // At least some positions should be different
    
    // Test ScreenMap bounds
    fl::vec2f bounds = screenMap.getBounds();
    FL_REQUIRE(bounds.x > 0.0f); // Should have some width
    FL_REQUIRE(bounds.y >= 0.0f); // Should have some height (or 0 for single row)
    
    // Test a larger corkscrew to ensure it works with more complex cases
    fl::Corkscrew corkscrew_large(19.0f, 288, false, fl::Gap()); // FestivalStick case
    fl::ScreenMap screenMap_large = corkscrew_large.toScreenMap(0.8f);
    
    FL_REQUIRE_EQ(screenMap_large.getLength(), 288);
    FL_REQUIRE_EQ(screenMap_large.getDiameter(), 0.8f);
    
    // Verify all positions are valid (non-negative)
    for (uint16_t i = 0; i < 288; ++i) {
        fl::vec2f pos = screenMap_large[i];
        FL_REQUIRE(pos.x >= 0.0f);
        FL_REQUIRE(pos.y >= 0.0f);
    }
}

FL_TEST_CASE("Corkscrew Gap struct functionality") {
    // Test default Gap construction
    fl::Gap defaultGap;
    FL_REQUIRE_EQ(defaultGap.gap, 0.0f);
    
    // Test Gap construction with value
    fl::Gap halfGap(0.5f);
    FL_REQUIRE_EQ(halfGap.gap, 0.5f);
    
    fl::Gap fullGap(1.0f);
    FL_REQUIRE_EQ(fullGap.gap, 1.0f);
    
    // Test Corkscrew construction with Gap struct
    fl::Gap customGap(0.3f);
    fl::Corkscrew corkscrewWithGap(19.0f, 144, false, customGap);
    FL_REQUIRE_EQ(corkscrewWithGap.size(), 144);
    
    // Test that different gap values still produce valid corkscrews
    fl::Gap noGap(0.0f);
    fl::Gap smallGap(0.1f);
    fl::Gap largeGap(0.9f);
    
    fl::Corkscrew corkscrewNoGap(2.0f, 8, false, noGap);
    fl::Corkscrew corkscrewSmallGap(2.0f, 8, false, smallGap);
    fl::Corkscrew corkscrewLargeGap(2.0f, 8, false, largeGap);
    
    // All should have the same basic properties since gap computation is not implemented yet
    FL_REQUIRE_EQ(corkscrewNoGap.size(), 8);
    FL_REQUIRE_EQ(corkscrewSmallGap.size(), 8);
    FL_REQUIRE_EQ(corkscrewLargeGap.size(), 8);
    
    // Test that Gap struct supports copy construction and assignment
    fl::Gap originalGap(0.7f);
    fl::Gap copiedGap(originalGap);
    FL_REQUIRE_EQ(copiedGap.gap, 0.7f);
    
    fl::Gap assignedGap;
    assignedGap = originalGap;
    FL_REQUIRE_EQ(assignedGap.gap, 0.7f);
    
    // Test Gap struct in corkscrew construction
    fl::Corkscrew corkscrewWithGap2(19.0f, 144, false, customGap);
    FL_REQUIRE(corkscrewWithGap2.cylinderWidth() > 0);
    FL_REQUIRE(corkscrewWithGap2.cylinderHeight() > 0);
}

FL_TEST_CASE("Corkscrew Enhanced Gap - Specific user test: 2 LEDs, 1 turn, 1.0f gap every 1 LED") {
    // Test case: 2 LEDs, 1 turn, gap of 1.0f every 1 LED
    fl::Gap gapEvery1(1, 1.0f); // Gap after every 1 LED, adds full 1.0 width unit
    fl::Corkscrew corkscrew(1.0f, 2, false, gapEvery1); // 1 turn, 2 LEDs
    
    // Get dimensions - accept whatever height the algorithm produces
    uint16_t width = corkscrew.cylinderWidth();
    
    FL_WARN("User test dimensions: width=" << width << " height=" << corkscrew.cylinderHeight());
    
    // Total turns should still be exactly 1
    FL_REQUIRE_EQ(1.0f, 1.0f);
    
    // Get positions for both LEDs (unwrapped and wrapped)
    fl::vec2f pos0_unwrapped = corkscrew.at_no_wrap(0); // First LED, no gap yet
    fl::vec2f pos1_unwrapped = corkscrew.at_no_wrap(1); // Second LED, after gap trigger
    fl::vec2f pos0_wrapped = corkscrew.at_exact(0); // First LED, wrapped
    fl::vec2f pos1_wrapped = corkscrew.at_exact(1); // Second LED, wrapped
    
    FL_WARN("LED0 unwrapped: " << pos0_unwrapped << ", wrapped: " << pos0_wrapped);
    FL_WARN("LED1 unwrapped: " << pos1_unwrapped << ", wrapped: " << pos1_wrapped);
    
    // Both unwrapped positions should be valid
    FL_REQUIRE(pos0_unwrapped.x >= 0.0f);
    FL_REQUIRE(pos0_unwrapped.y >= 0.0f);
    FL_REQUIRE(pos1_unwrapped.x >= 0.0f);
    FL_REQUIRE(pos1_unwrapped.y >= 0.0f);
    
    // Both wrapped positions should be valid
    FL_REQUIRE(pos0_wrapped.x >= 0.0f);
    FL_REQUIRE(pos0_wrapped.y >= 0.0f);
    FL_REQUIRE(pos1_wrapped.x >= 0.0f);
    FL_REQUIRE(pos1_wrapped.y >= 0.0f);
    
    // First LED should be at or near starting position
    FL_REQUIRE(pos0_unwrapped.x <= static_cast<float>(width));
    FL_REQUIRE(pos0_unwrapped.y <= 1.0f);
    
    // Wrapped positions should be within the cylinder width
    FL_REQUIRE(pos0_wrapped.x < static_cast<float>(width));
    FL_REQUIRE(pos1_wrapped.x < static_cast<float>(width));
    
    // The key test: total height should not exceed the specified turns
    float maxHeight = fl::max(pos0_unwrapped.y, pos1_unwrapped.y);
    FL_REQUIRE(maxHeight <= 1.0f + 0.1f); // Small tolerance for floating point
    
    // LEDs should have different unwrapped positions due to gap
    bool unwrapped_different = (pos0_unwrapped.x != pos1_unwrapped.x) || (pos0_unwrapped.y != pos1_unwrapped.y);
    FL_REQUIRE(unwrapped_different);
    
    // Test the expected gap behavior: LED1 should be "back at starting position" when wrapped
    // This means LED1 wrapped x-coordinate should be close to LED0's x-coordinate
    float x_diff = (pos1_wrapped.x > pos0_wrapped.x) ? 
                   (pos1_wrapped.x - pos0_wrapped.x) : 
                   (pos0_wrapped.x - pos1_wrapped.x);
    FL_REQUIRE(x_diff < 0.1f); // LED1 should wrap back to near the starting x position
}

FL_TEST_CASE("Corkscrew gap test with 3 LEDs") {
    // User's specific test case:
    // 3 LEDs, gap of 0.5 every 1 LED, one turn
    // Expected: LED0 at w=0, LED1 at w=3.0, LED2 at w=0 (back to start)
    
    fl::Gap gapParams(1, 0.5f); // gap of 0.5 every 1 LED
    
    fl::Corkscrew corkscrew_gap(1.0f, 3, false, gapParams);
    FL_REQUIRE_EQ(corkscrew_gap.size(), 3);
    
    // Get LED positions
    fl::vec2f pos0 = corkscrew_gap.at_exact(0);  // wrapped position
    fl::vec2f pos1 = corkscrew_gap.at_exact(1);  // wrapped position
    fl::vec2f pos2 = corkscrew_gap.at_exact(2);  // wrapped position
    
    FL_WARN("LED0 wrapped pos: (" << pos0.x << "," << pos0.y << ")");
    FL_WARN("LED1 wrapped pos: (" << pos1.x << "," << pos1.y << ")");
    FL_WARN("LED2 wrapped pos: (" << pos2.x << "," << pos2.y << ")");
    
    // Also get unwrapped positions to understand the math
    fl::vec2f pos0_unwrap = corkscrew_gap.at_no_wrap(0);
    fl::vec2f pos1_unwrap = corkscrew_gap.at_no_wrap(1);
    fl::vec2f pos2_unwrap = corkscrew_gap.at_no_wrap(2);
    
    FL_WARN("LED0 unwrapped pos: (" << pos0_unwrap.x << "," << pos0_unwrap.y << ")");
    FL_WARN("LED1 unwrapped pos: (" << pos1_unwrap.x << "," << pos1_unwrap.y << ")");
    FL_WARN("LED2 unwrapped pos: (" << pos2_unwrap.x << "," << pos2_unwrap.y << ")");
    
    FL_WARN("Calculated width: " << corkscrew_gap.cylinderWidth());
    
    // Verify the corrected gap calculation produces expected results:
    // - LED0: unwrapped (0,0), wrapped (0,0)
    // - LED1: unwrapped (3,1), wrapped (0,1) - 3.0 exactly as user wanted!
    // - LED2: unwrapped (6,2), wrapped (0,2) - wraps back to 0 as expected
    
    // Test unwrapped positions (what the user specified)
    FL_REQUIRE(fl::almost_equal(pos0_unwrap.x, 0.0f)); // LED0 at unwrapped w=0
    FL_REQUIRE(fl::almost_equal(pos1_unwrap.x, 3.0f)); // LED1 at unwrapped w=3.0 ✓
    FL_REQUIRE(fl::almost_equal(pos2_unwrap.x, 6.0f)); // LED2 at unwrapped w=6.0
    
    // Test wrapped positions - all LEDs wrap back to w=0 due to width=3
    FL_REQUIRE(fl::almost_equal(pos0.x, 0.0f)); // LED0 wrapped w=0
    FL_REQUIRE(fl::almost_equal(pos1.x, 0.0f)); // LED1 wrapped w=0 (3.0 % 3 = 0) 
    FL_REQUIRE(fl::almost_equal(pos2.x, 0.0f)); // LED2 wrapped w=0 (6.0 % 3 = 0)
    
    // Height positions should increase by 1 for each LED (one turn per 3 width units)
    FL_REQUIRE(fl::almost_equal(pos0_unwrap.y, 0.0f)); // LED0 height
    FL_REQUIRE(fl::almost_equal(pos1_unwrap.y, 1.0f)); // LED1 height  
    FL_REQUIRE(fl::almost_equal(pos2_unwrap.y, 2.0f)); // LED2 height
}

FL_TEST_CASE("Corkscrew caching functionality") {
    // Create a small corkscrew for testing
    fl::Corkscrew corkscrew(2.0f, 10); // 2 turns, 10 LEDs
    
    // Test that caching is enabled by default
    fl::Tile2x2_u8_wrap tile1 = corkscrew.at_wrap(1.0f);
    fl::Tile2x2_u8_wrap tile1_again = corkscrew.at_wrap(1.0f);
    
    // Values should be identical (from cache)
    for (int x = 0; x < 2; x++) {
        for (int y = 0; y < 2; y++) {
            auto data1 = tile1.at(x, y);
            auto data1_again = tile1_again.at(x, y);
            FL_REQUIRE_EQ(data1.first.x, data1_again.first.x);
            FL_REQUIRE_EQ(data1.first.y, data1_again.first.y);
            FL_REQUIRE_EQ(data1.second, data1_again.second);
        }
    }
}

FL_TEST_CASE("Corkscrew caching disable functionality") {
    // Create a small corkscrew for testing
    fl::Corkscrew corkscrew(2.0f, 10); // 2 turns, 10 LEDs
    
    // Get a tile with caching enabled
    fl::Tile2x2_u8_wrap tile_cached = corkscrew.at_wrap(1.0f);
    
    // Disable caching
    corkscrew.setCachingEnabled(false);
    
    // Get the same tile with caching disabled
    fl::Tile2x2_u8_wrap tile_uncached = corkscrew.at_wrap(1.0f);
    
    // Values should still be identical (same calculation)
    for (int x = 0; x < 2; x++) {
        for (int y = 0; y < 2; y++) {
            auto data_cached = tile_cached.at(x, y);
            auto data_uncached = tile_uncached.at(x, y);
            FL_REQUIRE_EQ(data_cached.first.x, data_uncached.first.x);
            FL_REQUIRE_EQ(data_cached.first.y, data_uncached.first.y);
            FL_REQUIRE_EQ(data_cached.second, data_uncached.second);
        }
    }
    
    // Re-enable caching
    corkscrew.setCachingEnabled(true);
    
    // Get a tile again - should work with caching re-enabled
    fl::Tile2x2_u8_wrap tile_recached = corkscrew.at_wrap(1.0f);
    
    // Values should still be identical
    for (int x = 0; x < 2; x++) {
        for (int y = 0; y < 2; y++) {
            auto data_cached = tile_cached.at(x, y);
            auto data_recached = tile_recached.at(x, y);
            FL_REQUIRE_EQ(data_cached.first.x, data_recached.first.x);
            FL_REQUIRE_EQ(data_cached.first.y, data_recached.first.y);
            FL_REQUIRE_EQ(data_cached.second, data_recached.second);
        }
    }
}

FL_TEST_CASE("Corkscrew caching with edge cases") {
    // Create a small corkscrew for testing
    fl::Corkscrew corkscrew(1.5f, 5); // 1.5 turns, 5 LEDs
    
    // Test caching with various float indices
    fl::Tile2x2_u8_wrap tile0 = corkscrew.at_wrap(0.0f);
    fl::Tile2x2_u8_wrap tile4 = corkscrew.at_wrap(4.0f);
    
    // Test that different indices produce different tiles
    bool tiles_different = false;
    for (int x = 0; x < 2 && !tiles_different; x++) {
        for (int y = 0; y < 2 && !tiles_different; y++) {
            auto data0 = tile0.at(x, y);
            auto data4 = tile4.at(x, y);
            if (data0.first.x != data4.first.x || 
                data0.first.y != data4.first.y || 
                data0.second != data4.second) {
                tiles_different = true;
            }
        }
    }
    FL_REQUIRE(tiles_different); // Tiles at different positions should be different
    
    // Test that same index produces same tile (cache consistency)
    fl::Tile2x2_u8_wrap tile0_again = corkscrew.at_wrap(0.0f);
    for (int x = 0; x < 2; x++) {
        for (int y = 0; y < 2; y++) {
            auto data0 = tile0.at(x, y);
            auto data0_again = tile0_again.at(x, y);
            FL_REQUIRE_EQ(data0.first.x, data0_again.first.x);
            FL_REQUIRE_EQ(data0.first.y, data0_again.first.y);
            FL_REQUIRE_EQ(data0.second, data0_again.second);
        }
    }
}

} // FL_TEST_FILE
