/**
 * @file perf_tests.cpp
 * @brief Unity tests for CPU load, memory consumption, and stack usage.
 * 
 * This file implements comprehensive performance monitoring for ESP32 systems.
 * It tests three critical aspects of system health:
 * 1. Memory Usage - Ensures sufficient free heap memory
 * 2. Stack Usage - Monitors task stack consumption to prevent overflows
 * 3. CPU Load - Analyzes processor utilization across both cores
 * 
 * @author Mahmoud Elmohtady (improvements)
 * @company Nabd solutions - ASF
 * @copyright 2025
 */

// Standard C library includes for string operations and formatted output
#include <string.h>     // For strcasecmp(), strstr() - string comparison functions
#include <stdio.h>      // For printf-style functions (used by ESP_LOGI)
#include <inttypes.h>   // For portable integer type definitions

// FreeRTOS includes - the real-time operating system running on ESP32
#include "freertos/FreeRTOS.h"     // Core FreeRTOS definitions and types
#include "freertos/task.h"         // Task management functions (create, delay, etc.)
#include "freertos/portmacro.h"    // Platform-specific macros and definitions

// ESP-IDF (Espressif IoT Development Framework) includes
#include "esp_timer.h"      // High-resolution timer functions
#include "esp_system.h"     // System-level functions and utilities
#include "esp_heap_caps.h"  // Memory management with capability-based allocation
#include "esp_log.h"        // Logging system for debug output

// Conditional includes based on configuration
#if CONFIG_SPIRAM_SUPPORT
#include "esp_spiram.h"     // External SPIRAM (Serial Peripheral RAM) support
#endif

#if CONFIG_ENABLE_PERF_TESTS
#include "unity.h"          // Unity test framework for professional testing
#endif
 
// extern "C" tells the C++ compiler to use C-style function naming
// This is necessary because FreeRTOS and ESP-IDF are written in C
extern "C" {

// TAG is used by ESP_LOGI() for log message identification
// All log messages from this file will be prefixed with "perf_tests"
static const char *TAG = "perf_tests";

/* ========================================================================
 * CONFIGURABLE THRESHOLDS - Adjust these values for your specific application
 * ======================================================================== */

// Minimum free heap memory (in bytes) to consider the system "healthy"
// 1024 bytes = 1KB - very conservative threshold
// Increase this value for applications that need more memory headroom
#ifndef PERF_MIN_HEAP_BYTES
#define PERF_MIN_HEAP_BYTES    (1024U)
#endif

// Minimum unused stack space (in WORDS, not bytes) for the current task
// 100 words = 400 bytes on ESP32 (since each word is 4 bytes)
// This prevents stack overflow crashes in your application tasks
#ifndef PERF_MIN_STACK_WORDS_CURRENT
#define PERF_MIN_STACK_WORDS_CURRENT (100U)
#endif

// Minimum unused stack space (in WORDS) for system idle tasks
// 50 words = 200 bytes - idle tasks do minimal work so need less stack
// Critical for system stability - idle task crashes = system crash
#ifndef PERF_MIN_STACK_WORDS_IDLE
#define PERF_MIN_STACK_WORDS_IDLE    (50U)
#endif

// Tolerance for CPU usage percentage calculations (as a float)
// 20.0% tolerance accounts for multi-core timing complexities
// In dual-core systems, percentages don't always sum to exactly 100%
#ifndef PERF_RUNTIME_PERCENT_TOLERANCE
#define PERF_RUNTIME_PERCENT_TOLERANCE (20.0f)
#endif

/* ========================================================================
 * UTILITY FUNCTIONS
 * ======================================================================== */

/**
 * @brief Simple delay function that yields CPU to other tasks
 * @param ms Delay time in milliseconds
 * 
 * This is better than a busy-wait loop because it allows other tasks to run.
 * vTaskDelay() puts the current task to sleep and wakes it up after the specified time.
 * pdMS_TO_TICKS() converts milliseconds to FreeRTOS tick units.
 */
static void wait_ms(uint32_t ms)
{
    // Convert milliseconds to FreeRTOS ticks and delay the current task
    // This allows other tasks to run during the delay period
    vTaskDelay(pdMS_TO_TICKS(ms));
}
 
/* ========================================================================
 * PERFORMANCE TEST FUNCTIONS
 * These functions only compile when CONFIG_ENABLE_PERF_TESTS is enabled
 * ======================================================================== */
#if CONFIG_ENABLE_PERF_TESTS

/* ------------------------------------------------------------------------
 * MEMORY CONSUMPTION TEST
 * ------------------------------------------------------------------------ */
#if CONFIG_ENABLE_MEMORY_TEST
/**
 * @brief Tests system memory consumption and availability
 * 
 * This test checks different types of memory on the ESP32:
 * 1. 8-bit accessible memory (most common, used by malloc())
 * 2. DMA-capable memory (required for hardware peripherals)
 * 3. SPIRAM memory (external RAM, if configured)
 * 
 * The test ensures the system has sufficient free memory for stable operation.
 * Low memory can cause malloc() failures, task creation failures, or system crashes.
 */
void test_memory_consumption_basic(void)
{
    /* ---- CHECK 8-BIT ACCESSIBLE HEAP ---- */
    // This is the most commonly used memory type - where malloc() allocates from
    
    // Get current free memory in the 8-bit heap
    size_t free_now_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
    
    // Get the minimum free memory since boot (worst case scenario)
    // This shows if the system has been under memory pressure
    size_t free_min_8bit = heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT);
    
    // Log the results for debugging and monitoring
    ESP_LOGI(TAG, "8-bit heap: free_now=%u free_min=%u", (unsigned)free_now_8bit, (unsigned)free_min_8bit);

    // CRITICAL TEST: Ensure we have enough free memory
    // If this fails, your application may crash due to out-of-memory conditions
    TEST_ASSERT_MESSAGE(free_now_8bit >= PERF_MIN_HEAP_BYTES,
        "Not enough free 8-bit heap; increase RAM or lower threshold");

    /* ---- CHECK DMA-CAPABLE HEAP ---- */
    // DMA memory is required for hardware peripherals like SPI, I2C, WiFi, etc.
    // Some ESP32 configurations share this with 8-bit heap, others separate it
    
    // Get current free DMA-capable memory
    size_t free_now_dma = heap_caps_get_free_size(MALLOC_CAP_DMA);
    
    // Get minimum free DMA memory since boot
    size_t free_min_dma = heap_caps_get_minimum_free_size(MALLOC_CAP_DMA);
    
    // Log DMA memory status
    ESP_LOGI(TAG, "DMA heap: free_now=%u free_min=%u", (unsigned)free_now_dma, (unsigned)free_min_dma);

    // Basic sanity check - ensure the API call succeeded
    // Note: On some configurations, DMA heap is the same as 8-bit heap
    TEST_ASSERT_GREATER_OR_EQUAL_size_t(0, free_min_dma);

    /* ---- CHECK SPIRAM (EXTERNAL RAM) IF AVAILABLE ---- */
#if CONFIG_SPIRAM_SUPPORT
    // SPIRAM is external RAM that can expand memory from ~300KB to several MB
    // It's slower than internal RAM but great for large buffers
    
    // Check if SPIRAM was successfully initialized at boot
    if (esp_spiram_is_initialized()) {
        // Get free SPIRAM memory (combined with 8-bit capability)
        size_t free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        
        // Get minimum free SPIRAM since boot
        size_t min_spiram = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        
        // Log SPIRAM status
        ESP_LOGI(TAG, "SPIRAM: free_now=%u free_min=%u", (unsigned)free_spiram, (unsigned)min_spiram);
        
        // Ensure SPIRAM is functioning (basic sanity check)
        TEST_ASSERT_GREATER_OR_EQUAL_size_t(0, min_spiram);
    }
#endif
}
#endif // CONFIG_ENABLE_MEMORY_TEST
 
/* ------------------------------------------------------------------------
 * STACK USAGE MONITORING TESTS
 * ------------------------------------------------------------------------ */
#if CONFIG_ENABLE_STACK_USAGE_TEST

/**
 * @brief Helper function to convert stack words to bytes for human readability
 * @param words Stack size in words (FreeRTOS native unit)
 * @return Stack size in bytes
 * 
 * FreeRTOS measures stack in "words" which are platform-specific.
 * On ESP32, 1 word = 4 bytes (32-bit architecture).
 * This function converts to bytes for easier understanding.
 */
static inline size_t stack_words_to_bytes(UBaseType_t words)
{
    // sizeof(StackType_t) is 4 bytes on ESP32 (32-bit words)
    return (size_t)words * sizeof(StackType_t);
}

/**
 * @brief Tests stack usage of the currently running task
 * 
 * Stack overflow is one of the most common causes of embedded system crashes.
 * This test monitors how much stack space remains unused in the current task.
 * 
 * IMPORTANT: "High water mark" means UNUSED stack, not USED stack!
 * - High value = lots of unused stack = good
 * - Low value = little unused stack = danger of overflow
 */
void test_stack_usage_current_task(void)
{
    // Get the "high water mark" - minimum unused stack since task creation
    // NULL parameter means "check the currently running task"
    UBaseType_t high_water_words = uxTaskGetStackHighWaterMark(NULL);
    
    // Convert to bytes for easier understanding
    size_t high_water_bytes = stack_words_to_bytes(high_water_words);
    
    // Log the results for monitoring and debugging
    ESP_LOGI(TAG, "Current task high-water: %u words (%u bytes)",
        (unsigned)high_water_words, (unsigned)high_water_bytes);

    // CRITICAL TEST: Ensure sufficient unused stack remains
    // If this fails, the task is at risk of stack overflow crash
    TEST_ASSERT_MESSAGE(high_water_words >= PERF_MIN_STACK_WORDS_CURRENT,
        "Current task stack high-water too low; consider increasing stack size");
}

/* ---- MULTI-CORE vs SINGLE-CORE IDLE TASK MONITORING ---- */
#if (configNUM_CORES > 1)
/**
 * @brief Tests stack usage of idle tasks on multi-core ESP32
 * 
 * In multi-core systems, each CPU core has its own idle task:
 * - IDLE0: Runs on CPU core 0 when no other tasks need it
 * - IDLE1: Runs on CPU core 1 when no other tasks need it
 * 
 * Idle task stack overflow crashes the entire system!
 */
void test_stack_usage_idle_tasks(void)
{
    // Loop through each CPU core (ESP32 has 2 cores: 0 and 1)
    for (int cpu = 0; cpu < configNUM_CORES; ++cpu) {
        
        // Get the idle task handle for this specific CPU core
        TaskHandle_t idle = xTaskGetIdleTaskHandleForCore(cpu);
        
        // Ensure we got a valid handle (system sanity check)
        TEST_ASSERT_NOT_NULL_MESSAGE(idle, "Idle task handle is NULL for a CPU");

        // Get the high water mark for this idle task
        UBaseType_t hw = uxTaskGetStackHighWaterMark(idle);
        
        // Convert to bytes for logging
        size_t hw_bytes = stack_words_to_bytes(hw);
        
        // Log idle task stack status for each core
        ESP_LOGI(TAG, "Idle task (cpu %d) high-water: %u words (%u bytes)", 
                 cpu, (unsigned)hw, (unsigned)hw_bytes);

        // CRITICAL TEST: Ensure idle task has sufficient stack
        // Idle task crash = system crash, so this is very important
        TEST_ASSERT_MESSAGE(hw >= PERF_MIN_STACK_WORDS_IDLE,
            "Idle task stack high-water too low; increase idle task stack for CPU");
    }
}

#else
/**
 * @brief Tests stack usage of idle task on single-core ESP32
 * 
 * Single-core systems have only one idle task that runs when no other tasks are active.
 * This is a simpler version of the multi-core test above.
 */
void test_stack_usage_idle_task(void)
{
    // Get the single idle task handle (no core specification needed)
    TaskHandle_t idle = xTaskGetIdleTaskHandle();
    
    // Ensure we got a valid handle
    TEST_ASSERT_NOT_NULL_MESSAGE(idle, "Idle task handle is NULL");

    // Get high water mark for the idle task
    UBaseType_t hw = uxTaskGetStackHighWaterMark(idle);
    
    // Convert to bytes for logging
    size_t hw_bytes = stack_words_to_bytes(hw);
    
    // Log idle task stack status
    ESP_LOGI(TAG, "Idle task high-water: %u words (%u bytes)", (unsigned)hw, (unsigned)hw_bytes);

    // CRITICAL TEST: Ensure idle task has sufficient stack
    TEST_ASSERT_MESSAGE(hw >= PERF_MIN_STACK_WORDS_IDLE,
        "Idle task stack high-water too low; increase idle task stack");
}
#endif

#endif // CONFIG_ENABLE_STACK_USAGE_TEST
 
/* ------------------------------------------------------------------------
 * CPU LOAD ANALYSIS TEST
 * ------------------------------------------------------------------------ */
#if CONFIG_ENABLE_CPU_LOAD_TEST

/**
 * @brief Analyzes CPU utilization across all cores and tasks
 * 
 * This is the most complex test because it deals with multi-core timing statistics.
 * It measures how much CPU time each task consumes and identifies performance bottlenecks.
 * 
 * Key concepts:
 * - Runtime statistics track CPU time per task using a high-resolution timer
 * - Idle tasks run when no other tasks need CPU (high idle = low system load)
 * - Multi-core systems have separate idle tasks per core
 * - Percentages may not sum to exactly 100% due to timing complexities
 * 
 * Requirements:
 * - configGENERATE_RUN_TIME_STATS must be enabled in FreeRTOS config
 * - High-resolution timer must be configured (ESP_TIMER recommended)
 */
void test_cpu_load_estimation(void)
{
    /* ---- CHECK IF RUNTIME STATISTICS ARE ENABLED ---- */
#if ( configGENERATE_RUN_TIME_STATS == 1 )

    /* ---- STEP 1: STABILIZATION PERIOD ---- */
    // Allow the system to reach steady state before measurement
    // Longer period = more accurate measurements, but slower test
    wait_ms(1000);  // 1 second should be sufficient for most applications

    /* ---- STEP 2: GET TASK COUNT ---- */
    // Find out how many tasks are currently running in the system
    UBaseType_t num_tasks = uxTaskGetNumberOfTasks();
    
    // Sanity check - ensure FreeRTOS is reporting tasks correctly
    TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, num_tasks, "No tasks reported by uxTaskGetNumberOfTasks()");

    /* ---- STEP 3: ALLOCATE MEMORY FOR TASK DATA ---- */
    // We need an array to hold information about each task
    // Add a small margin in case new tasks are created during measurement
    const UBaseType_t margin = 4;
    
    // TaskStatus_t is a FreeRTOS structure containing task information
    TaskStatus_t *task_array = (TaskStatus_t *)malloc(sizeof(TaskStatus_t) * (num_tasks + margin));
    
    // Ensure memory allocation succeeded
    TEST_ASSERT_NOT_NULL_MESSAGE(task_array, "Failed to allocate task array for system state");

    /* ---- STEP 4: CAPTURE SYSTEM STATE SNAPSHOT ---- */
    // This is the critical measurement - atomic snapshot of all task states
    unsigned long total_run_time = 0;  // FreeRTOS will fill this with total CPU time
    
    // uxTaskGetSystemState() captures all task information atomically
    UBaseType_t fetched = uxTaskGetSystemState(task_array, num_tasks + margin, &total_run_time);
    
    // Log the raw data for debugging
    ESP_LOGI(TAG, "uxTaskGetSystemState fetched=%u total_run_time=%lu", (unsigned)fetched, total_run_time);

    /* ---- STEP 5: VALIDATE DATA QUALITY ---- */
    // Ensure the data capture was successful
    TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, fetched, "uxTaskGetSystemState returned no tasks");
    TEST_ASSERT_GREATER_THAN_MESSAGE(0UL, total_run_time, "Total run time is zero; ensure runtime stats timer is configured");

    /* ---- STEP 6: MULTI-CORE AWARENESS ---- */
    // Multi-core systems have complex timing behavior
    // Each core contributes independently to the total runtime
    (void)total_run_time; // Suppress unused variable warning for normalized_total_time
    
#if (configNUM_CORES > 1)
    // Log that we're handling multi-core complexity
    ESP_LOGI(TAG, "Multi-core system detected, using adjusted calculation");
#endif

    /* ---- STEP 7: INITIALIZE ANALYSIS VARIABLES ---- */
    double percent_sum = 0.0;           // Sum of non-idle task percentages
    double idle_percent_total = 0.0;    // Sum of all idle task percentages
    int idle_task_count = 0;            // Number of idle tasks found

    /* ---- STEP 8: ANALYZE EACH TASK ---- */
    for (UBaseType_t i = 0; i < fetched; ++i) {
        
        // Get the runtime counter for this task (CPU time consumed)
        unsigned long run = task_array[i].ulRunTimeCounter;
        
        // Calculate percentage: (task_time / total_time) * 100
        double pct = 0.0;
        if (total_run_time > 0) {
            pct = ((double)run * 100.0) / (double)total_run_time;
        }
        
        // Log detailed information for each task (useful for debugging)
        ESP_LOGI(TAG, "Task[%u] name='%s' handle=%p run=%lu pct=%.2f",
            (unsigned)i, task_array[i].pcTaskName, task_array[i].xHandle, run, pct);

        /* ---- IDENTIFY IDLE TASKS ---- */
        // Idle tasks have special names and behavior
        // In multi-core: "IDLE0" (core 0), "IDLE1" (core 1)
        // In single-core: usually just "IDLE"
        if (task_array[i].pcTaskName != NULL &&
            (strcasecmp(task_array[i].pcTaskName, "IDLE0") == 0 ||
             strcasecmp(task_array[i].pcTaskName, "IDLE1") == 0 ||
             strstr(task_array[i].pcTaskName, "IDLE") != NULL))
        {
            // This is an idle task - track it separately
            idle_percent_total += pct;
            idle_task_count++;
            ESP_LOGI(TAG, "Found idle task: %s with %.2f%% usage", task_array[i].pcTaskName, pct);
        }
        
        /* ---- SEPARATE ACTIVE vs IDLE TASKS ---- */
        // Only count non-idle tasks for system load calculation
        // Idle tasks run when nothing else needs CPU, so they don't represent "work"
        if (task_array[i].pcTaskName == NULL || 
            (strstr(task_array[i].pcTaskName, "IDLE") == NULL)) {
            percent_sum += pct;
        }
    }

    /* ---- STEP 9: CLEANUP MEMORY ---- */
    // Free the allocated task array
    free(task_array);

    /* ---- STEP 10: ANALYZE RESULTS ---- */
    // Log the analysis results
    ESP_LOGI(TAG, "Non-idle task runtime sum = %.2f%%", percent_sum);
    ESP_LOGI(TAG, "Total idle task runtime = %.2f%% (across %d idle tasks)", idle_percent_total, idle_task_count);
    ESP_LOGI(TAG, "System appears to be %.2f%% busy", percent_sum);

    /* ---- STEP 11: VALIDATE SYSTEM HEALTH ---- */
    // These are relaxed validations designed for multi-core systems
    // The goal is to ensure the system is reporting reasonable values
    
    // Ensure we found idle tasks (basic system sanity check)
    TEST_ASSERT_MESSAGE(idle_task_count > 0, "No idle tasks found; runtime stats may not be working");
    
    // Ensure idle percentage makes sense (not negative)
    TEST_ASSERT_MESSAGE(idle_percent_total >= 0.0, "Idle task percentage is negative");
    
    // Ensure active task percentage is reasonable (not impossibly high)
    TEST_ASSERT_MESSAGE(percent_sum >= 0.0 && percent_sum <= 200.0, "Non-idle task percentage sum is unreasonable");
    
    // Check for system overload (too much CPU usage)
    TEST_ASSERT_MESSAGE(percent_sum < 150.0, "System appears overloaded or runtime stats misconfigured");
    
    // Log successful completion
    ESP_LOGI(TAG, "CPU load test completed - system load appears reasonable");

#else
    /* ---- RUNTIME STATISTICS DISABLED ---- */
    // If FreeRTOS runtime statistics are not enabled, skip this test
    TEST_IGNORE_MESSAGE("configGENERATE_RUN_TIME_STATS is disabled; enable to run CPU load test");
#endif // configGENERATE_RUN_TIME_STATS
}
#endif // CONFIG_ENABLE_CPU_LOAD_TEST
 
 #endif // CONFIG_ENABLE_PERF_TESTS
 
 } // extern "C"

/* ========================================================================
 * UNITY TEST FRAMEWORK INTEGRATION
 * 
 * This section integrates our performance tests with the Unity test framework.
 * Unity provides professional test reporting, assertions, and test management.
 * ======================================================================== */
#if CONFIG_ENABLE_PERF_TESTS

// Forward declaration of app_main (defined in main.cpp)
extern "C" void app_main(void);

/**
 * @brief Test registration function (currently unused but kept for future expansion)
 * 
 * This function could be used for dynamic test registration if needed.
 * Currently, we use the simpler RUN_TEST() approach in unity_task().
 */
extern "C" void register_perf_tests(void)
{
    // This function demonstrates how to register tests by name
    // Currently not used, but available for future dynamic test loading
    
#if CONFIG_ENABLE_MEMORY_TEST
    unity_run_test_by_name("test_memory_consumption_basic");
#endif

#if CONFIG_ENABLE_STACK_USAGE_TEST
    unity_run_test_by_name("test_stack_usage_current_task");
#if (configNUM_CORES > 1)
    unity_run_test_by_name("test_stack_usage_idle_tasks");
#else
    unity_run_test_by_name("test_stack_usage_idle_task");
#endif
#endif

#if CONFIG_ENABLE_CPU_LOAD_TEST
    unity_run_test_by_name("test_cpu_load_estimation");
#endif
}

/**
 * @brief Main Unity test runner task
 * @param pvParameters Task parameters (unused, required by FreeRTOS)
 * 
 * This function runs as a FreeRTOS task and executes all performance tests.
 * It's called from main.cpp when performance testing is enabled.
 * 
 * Execution flow:
 * 1. Allow system to stabilize after boot
 * 2. Initialize Unity test framework
 * 3. Run each enabled test function
 * 4. Print final test results
 * 5. Keep task alive for system stability
 */
extern "C" void unity_task(void *pvParameters)
{
    // Suppress unused parameter warning (FreeRTOS requires this parameter)
    (void)pvParameters;
    
    /* ---- STEP 1: SYSTEM STABILIZATION ---- */
    // Give the system a moment to finish initialization
    // This ensures all components are ready before testing begins
    vTaskDelay(2); // 2 FreeRTOS ticks (usually 20ms)
    
    /* ---- STEP 2: INITIALIZE UNITY FRAMEWORK ---- */
    // UNITY_BEGIN() sets up the test framework and resets counters
    UNITY_BEGIN();
    
    /* ---- STEP 3: RUN PERFORMANCE TESTS ---- */
    // Each RUN_TEST() macro executes a test function and tracks results
    // Tests are run conditionally based on configuration flags
    
#if CONFIG_ENABLE_MEMORY_TEST
    // Test memory consumption and availability
    RUN_TEST(test_memory_consumption_basic);
#endif

#if CONFIG_ENABLE_STACK_USAGE_TEST
    // Test current task stack usage
    RUN_TEST(test_stack_usage_current_task);
    
    // Test idle task stack usage (multi-core vs single-core)
#if (configNUM_CORES > 1)
    RUN_TEST(test_stack_usage_idle_tasks);  // Multi-core version
#else
    RUN_TEST(test_stack_usage_idle_task);   // Single-core version
#endif
#endif

#if CONFIG_ENABLE_CPU_LOAD_TEST
    // Test CPU utilization across all cores and tasks
    RUN_TEST(test_cpu_load_estimation);
#endif
    
    /* ---- STEP 4: FINALIZE AND REPORT RESULTS ---- */
    // UNITY_END() prints the final test summary:
    // - Total tests run
    // - Number of failures
    // - Number of ignored tests
    // - Overall PASS/FAIL status
    UNITY_END();
    
    /* ---- STEP 5: KEEP TASK ALIVE ---- */
    // The task must remain alive for system stability
    // Deleting this task could cause issues with FreeRTOS scheduler
    while(1) {
        // Sleep for 1 second, then repeat
        // This keeps the task alive with minimal CPU usage
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

#endif // CONFIG_ENABLE_PERF_TESTS
 