esphome:
  name: scheduler-removed-item-race

host:

api:
  services:
    - service: run_test
      then:
        - script.execute: run_test_script

logger:
  level: DEBUG

globals:
  - id: test_passed
    type: bool
    initial_value: 'true'
  - id: removed_item_executed
    type: int
    initial_value: '0'
  - id: normal_item_executed
    type: int
    initial_value: '0'

sensor:
  - platform: template
    id: test_sensor
    name: "Test Sensor"
    update_interval: never
    lambda: return 0.0;

script:
  - id: run_test_script
    then:
      - logger.log: "=== Starting Removed Item Race Test ==="

      # This test creates a scenario where:
      # 1. First item in heap is NOT cancelled (cleanup stops immediately)
      # 2. Items behind it ARE cancelled (remain in heap after cleanup)
      # 3. All items execute at the same time, including cancelled ones

      - lambda: |-
          // The key to hitting the race:
          // 1. Add items in a specific order to control heap structure
          // 2. Cancel ONLY items that won't be at the front
          // 3. Ensure the first item stays non-cancelled so cleanup_() stops immediately

          // Schedule all items to execute at the SAME time (1ms from now)
          // Using 1ms instead of 0 to avoid defer queue on multi-core platforms
          // This ensures they'll all be ready together and go through the heap
          const uint32_t exec_time = 1;

          // CRITICAL: Add a non-cancellable item FIRST
          // This will be at the front of the heap and block cleanup_()
          App.scheduler.set_timeout(id(test_sensor), "blocker", exec_time, []() {
            ESP_LOGD("test", "Blocker timeout executed (expected) - was at front of heap");
            id(normal_item_executed)++;
          });

          // Now add items that we WILL cancel
          // These will be behind the blocker in the heap
          App.scheduler.set_timeout(id(test_sensor), "cancel_1", exec_time, []() {
            ESP_LOGE("test", "RACE: Cancelled timeout 1 executed after being cancelled!");
            id(removed_item_executed)++;
            id(test_passed) = false;
          });

          App.scheduler.set_timeout(id(test_sensor), "cancel_2", exec_time, []() {
            ESP_LOGE("test", "RACE: Cancelled timeout 2 executed after being cancelled!");
            id(removed_item_executed)++;
            id(test_passed) = false;
          });

          App.scheduler.set_timeout(id(test_sensor), "cancel_3", exec_time, []() {
            ESP_LOGE("test", "RACE: Cancelled timeout 3 executed after being cancelled!");
            id(removed_item_executed)++;
            id(test_passed) = false;
          });

          // Add some more normal items
          App.scheduler.set_timeout(id(test_sensor), "normal_1", exec_time, []() {
            ESP_LOGD("test", "Normal timeout 1 executed (expected)");
            id(normal_item_executed)++;
          });

          App.scheduler.set_timeout(id(test_sensor), "normal_2", exec_time, []() {
            ESP_LOGD("test", "Normal timeout 2 executed (expected)");
            id(normal_item_executed)++;
          });

          App.scheduler.set_timeout(id(test_sensor), "normal_3", exec_time, []() {
            ESP_LOGD("test", "Normal timeout 3 executed (expected)");
            id(normal_item_executed)++;
          });

          // Force items into the heap before cancelling
          App.scheduler.process_to_add();

          // NOW cancel the items - they're behind "blocker" in the heap
          // When cleanup_() runs, it will see "blocker" (not removed) at the front
          // and stop immediately, leaving cancel_1, cancel_2, cancel_3 in the heap
          bool c1 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_1");
          bool c2 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_2");
          bool c3 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_3");

          ESP_LOGD("test", "Cancelled items (behind blocker): %s, %s, %s",
                   c1 ? "true" : "false",
                   c2 ? "true" : "false",
                   c3 ? "true" : "false");

          // The heap now has:
          // - "blocker" at front (not cancelled)
          // - cancelled items behind it (marked remove=true but still in heap)
          // - When all execute at once, cleanup_() stops at "blocker"
          // - The loop then executes ALL ready items including cancelled ones

          ESP_LOGD("test", "Setup complete. Blocker at front prevents cleanup of cancelled items behind it");

      # Wait for all timeouts to execute (or not)
      - delay: 20ms

      # Check results
      - lambda: |-
          ESP_LOGI("test", "=== Test Results ===");
          ESP_LOGI("test", "Normal items executed: %d (expected 4)", id(normal_item_executed));
          ESP_LOGI("test", "Removed items executed: %d (expected 0)", id(removed_item_executed));

          if (id(removed_item_executed) > 0) {
            ESP_LOGE("test", "TEST FAILED: %d cancelled items were executed!", id(removed_item_executed));
            id(test_passed) = false;
          } else if (id(normal_item_executed) != 4) {
            ESP_LOGE("test", "TEST FAILED: Expected 4 normal items, got %d", id(normal_item_executed));
            id(test_passed) = false;
          } else {
            ESP_LOGI("test", "TEST PASSED: No cancelled items were executed");
          }

          ESP_LOGI("test", "=== Test Complete ===");
