#include "unity.h"
#include "app_controller.h"
#include "midea_protocol.h"
#include "zigbee_zcl.h"
#include <string.h>

// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------

// Build a valid MideaUART status response frame (same layout the decoder and
// status_monitor tests expect).
static size_t build_status_frame(uint8_t *buf, uint8_t mode, uint8_t power,
                                 int16_t indoor, int16_t target,
                                 uint8_t fan, uint8_t error, uint16_t alarm) {
    size_t o = 0;
    buf[o++] = 0xAA;
    buf[o++] = 0x55;
    buf[o++] = 8;      // length
    buf[o++] = 0x07;   // status command
    buf[o++] = (mode & 0x0F) | ((power & 0x01) << 4);
    buf[o++] = indoor & 0xFF;
    buf[o++] = (indoor >> 8) & 0xFF;
    buf[o++] = target & 0xFF;
    buf[o++] = (target >> 8) & 0xFF;
    buf[o++] = (fan & 0x0F) | ((error & 0x0F) << 4);
    buf[o++] = alarm & 0xFF;
    buf[o++] = (alarm >> 8) & 0xFF;
    uint8_t chk = 0;
    for (size_t i = 2; i < o; i++) chk ^= buf[i];
    buf[o++] = chk;
    return o;
}

static app_controller_t app;

void setUp(void) {
    memset(&app, 0, sizeof(app));
}

void tearDown(void) {
    app_controller_deinit(&app);
}

// -------------------------------------------------------------------------
// Init / deinit
// -------------------------------------------------------------------------

void test_app_init_defaults(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));
    TEST_ASSERT_TRUE(app.initialized);
    TEST_ASSERT_TRUE(uart_driver_is_initialized(&app.uart));
    TEST_ASSERT_TRUE(app.integration.initialized);
    TEST_ASSERT_TRUE(app.status_monitor.initialized);
    // Defaults: 9600 baud, 50ms spacing.
    TEST_ASSERT_EQUAL_INT(9600, app.config.uart_config.baud_rate);
    TEST_ASSERT_EQUAL_UINT32(50, app.integration.command_spacing_ms);
}

void test_app_init_null(void) {
    TEST_ASSERT_FALSE(app_controller_init(NULL, NULL));
}

void test_app_init_custom_config(void) {
    app_controller_config_t cfg;
    memset(&cfg, 0, sizeof(cfg));
    cfg.uart_config.baud_rate = 9600;
    cfg.uart_config.data_bits = 8;
    cfg.uart_config.parity = 0;
    cfg.uart_config.stop_bits = 1;
    cfg.status_config.poll_interval_ms = 2000;
    cfg.status_config.timeout_ms = 500;
    cfg.status_config.max_retries = 2;
    cfg.command_spacing_ms = 75;

    TEST_ASSERT_TRUE(app_controller_init(&app, &cfg));
    TEST_ASSERT_EQUAL_UINT32(2000, app.status_monitor.config.poll_interval_ms);
    TEST_ASSERT_EQUAL_UINT8(2, app.status_monitor.config.max_retries);
    TEST_ASSERT_EQUAL_UINT32(75, app.integration.command_spacing_ms);
}

void test_app_deinit(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));
    app_controller_deinit(&app);
    TEST_ASSERT_FALSE(app.initialized);
    TEST_ASSERT_FALSE(uart_driver_is_initialized(&app.uart));
}

// -------------------------------------------------------------------------
// Command path: HA -> Zigbee -> UART -> AC
// -------------------------------------------------------------------------

void test_command_path_produces_uart_frame(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));

    // Zigbee setpoint command: [mode=Cool(3), temp_lsb, temp_msb] -> 2500 (25.00C)
    uint8_t payload[] = {0x03, (uint8_t)(2500 & 0xFF), (uint8_t)((2500 >> 8) & 0xFF)};
    uint8_t uart_buf[64];
    size_t uart_len = sizeof(uart_buf);

    bool ok = app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02,
                                                    payload, sizeof(payload),
                                                    uart_buf, &uart_len);
    TEST_ASSERT_TRUE(ok);
    TEST_ASSERT_GREATER_THAN(0, uart_len);
    TEST_ASSERT_EQUAL_UINT8(0xAA, uart_buf[0]);
    TEST_ASSERT_EQUAL_UINT8(0x55, uart_buf[1]);
    TEST_ASSERT_EQUAL_UINT32(1, app.commands_sent);

    // The encoded frame must decode back to the requested cool @ 25.00C.
    midea_status_t decoded;
    TEST_ASSERT_TRUE(midea_protocol_decode(uart_buf, uart_len, &decoded));
    TEST_ASSERT_EQUAL_INT16(2500, decoded.target_temp);
    TEST_ASSERT_EQUAL_UINT8(MODE_COOL, decoded.mode);
}

void test_command_path_rejects_when_uninitialized(void) {
    // Not initialized (setUp zeroed it).
    uint8_t payload[] = {0x03, 0x00, 0x00};
    uint8_t uart_buf[64];
    size_t uart_len = sizeof(uart_buf);
    bool ok = app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02,
                                                    payload, sizeof(payload),
                                                    uart_buf, &uart_len);
    TEST_ASSERT_FALSE(ok);
    TEST_ASSERT_EQUAL_UINT32(0, uart_len);
}

// -------------------------------------------------------------------------
// Feedback path: AC -> Status -> Zigbee -> HA
// -------------------------------------------------------------------------

void test_feedback_path_updates_zigbee_attrs(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));

    // Simulated AC status: cooling, on, indoor 24.00C, target 22.00C.
    uint8_t frame[32];
    size_t len = build_status_frame(frame, MODE_COOL, 1, 2400, 2200, 3, 0, 0);

    zcl_thermostat_attrs_t attrs;
    bool ok = app_controller_process_ac_status(&app, frame, len, 1000, &attrs);
    TEST_ASSERT_TRUE(ok);
    TEST_ASSERT_EQUAL_INT16(2400, attrs.local_temperature);
    TEST_ASSERT_EQUAL_UINT8(0x03, attrs.system_mode); // Cooling

    // Verify the ZCL cluster (what HA reads) was actually updated.
    TEST_ASSERT_EQUAL_INT16(2400, zigbee_zcl_get_local_temperature());
    TEST_ASSERT_EQUAL_UINT8(0x03, zigbee_zcl_get_system_mode());
    TEST_ASSERT_EQUAL_UINT32(1, app.status_updates);
}

void test_feedback_path_rejects_bad_frame(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));
    uint8_t junk[] = {0x00, 0x11, 0x22, 0x33};
    bool ok = app_controller_process_ac_status(&app, junk, sizeof(junk), 1000, NULL);
    TEST_ASSERT_FALSE(ok);
    TEST_ASSERT_EQUAL_UINT32(0, app.status_updates);
}

void test_feedback_path_detects_fault(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));
    // error code set -> fault.
    uint8_t frame[32];
    size_t len = build_status_frame(frame, MODE_COOL, 1, 2400, 2200, 3, 0x5, 0);
    TEST_ASSERT_TRUE(app_controller_process_ac_status(&app, frame, len, 1000, NULL));
    TEST_ASSERT_TRUE(status_monitor_has_fault(&app.status_monitor));
    TEST_ASSERT_FALSE(app_controller_is_healthy(&app, 1000));
}

// -------------------------------------------------------------------------
// Full end-to-end round trip: HA -> ... -> AC -> ... -> HA
// -------------------------------------------------------------------------

void test_end_to_end_round_trip(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));

    // 1. HA requests Heat @ 21.00C.
    uint8_t payload[] = {0x04, (uint8_t)(2100 & 0xFF), (uint8_t)((2100 >> 8) & 0xFF)};
    uint8_t uart_buf[64];
    size_t uart_len = sizeof(uart_buf);
    TEST_ASSERT_TRUE(app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02,
                                                          payload, sizeof(payload),
                                                          uart_buf, &uart_len));
    TEST_ASSERT_GREATER_THAN(0, uart_len);

    // 2. AC responds with a status frame reflecting the new state.
    //    (heating mode maps to MODE_HEAT, indoor 20.50C, target 21.00C)
    uint8_t status_frame[32];
    size_t status_len = build_status_frame(status_frame, MODE_HEAT, 1, 2050, 2100, 2, 0, 0);

    // 3. Feedback flows back to the Zigbee cluster.
    zcl_thermostat_attrs_t attrs;
    TEST_ASSERT_TRUE(app_controller_process_ac_status(&app, status_frame, status_len,
                                                     2000, &attrs));
    TEST_ASSERT_EQUAL_INT16(2050, zigbee_zcl_get_local_temperature());
    TEST_ASSERT_EQUAL_UINT8(0x04, zigbee_zcl_get_system_mode()); // Heating
    TEST_ASSERT_TRUE(app_controller_is_healthy(&app, 2000));
}

// -------------------------------------------------------------------------
// Timing / load
// -------------------------------------------------------------------------

void test_timing_under_load(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));

    // Fire a burst of commands; every one must encode cleanly and be counted.
    const int N = 100;
    for (int i = 0; i < N; i++) {
        int16_t temp = (int16_t)(1600 + (i % 16) * 100); // 16.00C .. 31.00C
        uint8_t mode = (i % 2) ? 0x03 : 0x04;            // alternate cool/heat
        uint8_t payload[] = {mode, (uint8_t)(temp & 0xFF), (uint8_t)((temp >> 8) & 0xFF)};
        uint8_t uart_buf[64];
        size_t uart_len = sizeof(uart_buf);

        bool ok = app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02,
                                                        payload, sizeof(payload),
                                                        uart_buf, &uart_len);
        TEST_ASSERT_TRUE(ok);
        TEST_ASSERT_GREATER_THAN(0, uart_len);

        midea_status_t decoded;
        TEST_ASSERT_TRUE(midea_protocol_decode(uart_buf, uart_len, &decoded));
        TEST_ASSERT_EQUAL_INT16(temp, decoded.target_temp);
    }
    TEST_ASSERT_EQUAL_UINT32((uint32_t)N, app.commands_sent);
    // Rate-limit spacing constraint preserved throughout.
    TEST_ASSERT_EQUAL_UINT32(50, app.integration.command_spacing_ms);
}

// -------------------------------------------------------------------------
// Reset / recovery
// -------------------------------------------------------------------------

void test_reset_restores_state(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));

    // Dirty some state.
    uint8_t frame[32];
    size_t len = build_status_frame(frame, MODE_COOL, 1, 2400, 2200, 3, 0x5, 0);
    app_controller_process_ac_status(&app, frame, len, 1000, NULL);
    TEST_ASSERT_TRUE(status_monitor_has_fault(&app.status_monitor));

    // Reset clears the fault and re-initializes subsystems.
    TEST_ASSERT_TRUE(app_controller_reset(&app));
    TEST_ASSERT_TRUE(app.initialized);
    TEST_ASSERT_FALSE(status_monitor_has_fault(&app.status_monitor));
    TEST_ASSERT_TRUE(uart_driver_is_initialized(&app.uart));
    TEST_ASSERT_TRUE(app.integration.initialized);
}

void test_recovery_on_comm_timeout(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));

    // Force the watchdog to trip by exhausting retries.
    for (int i = 0; i < 5; i++) {
        status_monitor_handle_error(&app.status_monitor);
    }
    TEST_ASSERT_TRUE(app.status_monitor.comm_timeout);

    bool recovered = app_controller_recover_if_needed(&app, 100000);
    TEST_ASSERT_TRUE(recovered);
    TEST_ASSERT_EQUAL_UINT32(1, app.recovery_count);
    TEST_ASSERT_FALSE(app.status_monitor.comm_timeout); // cleared by reset
}

void test_no_recovery_at_startup(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));
    // Fresh init, never polled, no errors -> must not churn recovery.
    bool recovered = app_controller_recover_if_needed(&app, 100000);
    TEST_ASSERT_FALSE(recovered);
    TEST_ASSERT_EQUAL_UINT32(0, app.recovery_count);
}

void test_healthy_after_good_poll(void) {
    TEST_ASSERT_TRUE(app_controller_init(&app, NULL));
    uint8_t frame[32];
    size_t len = build_status_frame(frame, MODE_AUTO, 1, 2300, 2300, 2, 0, 0);
    TEST_ASSERT_TRUE(app_controller_process_ac_status(&app, frame, len, 1000, NULL));
    // Within the watchdog window immediately after a good status.
    TEST_ASSERT_TRUE(app_controller_is_healthy(&app, 1000));
}

// -------------------------------------------------------------------------

int main(void) {
    UNITY_BEGIN();

    RUN_TEST(test_app_init_defaults);
    RUN_TEST(test_app_init_null);
    RUN_TEST(test_app_init_custom_config);
    RUN_TEST(test_app_deinit);

    RUN_TEST(test_command_path_produces_uart_frame);
    RUN_TEST(test_command_path_rejects_when_uninitialized);

    RUN_TEST(test_feedback_path_updates_zigbee_attrs);
    RUN_TEST(test_feedback_path_rejects_bad_frame);
    RUN_TEST(test_feedback_path_detects_fault);

    RUN_TEST(test_end_to_end_round_trip);

    RUN_TEST(test_timing_under_load);

    RUN_TEST(test_reset_restores_state);
    RUN_TEST(test_recovery_on_comm_timeout);
    RUN_TEST(test_no_recovery_at_startup);
    RUN_TEST(test_healthy_after_good_poll);

    return UNITY_END();
}
