#include <furi.h>
#include <flipper_format.h>
#include <infrared.h>
#include <common/infrared_common_i.h>
#include <lib/infrared/signal/infrared_brute_force.h>
#include "../test.h" // IWYU pragma: keep

#define IR_TEST_FILES_DIR   EXT_PATH("unit_tests/infrared/")
#define IR_TEST_FILE_PREFIX "test_"
#define IR_TEST_FILE_SUFFIX ".irtest"

typedef struct {
    InfraredDecoderHandler* decoder_handler;
    InfraredEncoderHandler* encoder_handler;
    FuriString* file_path;
    FlipperFormat* ff;
    InfraredBruteForce* brutedb;
} InfraredTest;

static InfraredTest* test;

static void infrared_test_alloc(void) {
    Storage* storage = furi_record_open(RECORD_STORAGE);
    test = malloc(sizeof(InfraredTest));
    test->decoder_handler = infrared_alloc_decoder();
    test->encoder_handler = infrared_alloc_encoder();
    test->ff = flipper_format_buffered_file_alloc(storage);
    test->file_path = furi_string_alloc();
    test->brutedb = infrared_brute_force_alloc();
}

static void infrared_test_free(void) {
    furi_check(test);
    infrared_free_decoder(test->decoder_handler);
    infrared_free_encoder(test->encoder_handler);
    infrared_brute_force_free(test->brutedb);
    flipper_format_free(test->ff);
    furi_string_free(test->file_path);
    furi_record_close(RECORD_STORAGE);
    free(test);
    test = NULL;
}

static bool infrared_test_prepare_file(const char* protocol_name) {
    FuriString* file_type;
    file_type = furi_string_alloc();
    bool success = false;

    furi_string_printf(
        test->file_path,
        "%s%s%s%s",
        IR_TEST_FILES_DIR,
        IR_TEST_FILE_PREFIX,
        protocol_name,
        IR_TEST_FILE_SUFFIX);

    do {
        uint32_t format_version;
        if(!flipper_format_buffered_file_open_existing(
               test->ff, furi_string_get_cstr(test->file_path)))
            break;
        if(!flipper_format_read_header(test->ff, file_type, &format_version)) break;
        if(furi_string_cmp_str(file_type, "IR tests file") || format_version != 1) break;
        success = true;
    } while(false);

    furi_string_free(file_type);
    return success;
}

static bool infrared_test_load_raw_signal(
    FlipperFormat* ff,
    const char* signal_name,
    uint32_t** timings,
    uint32_t* timings_count) {
    FuriString* buf;
    buf = furi_string_alloc();
    bool success = false;

    do {
        bool is_name_found = false;
        for(; !is_name_found && flipper_format_read_string(ff, "name", buf);
            is_name_found = !furi_string_cmp(buf, signal_name))
            ;

        if(!is_name_found) break;
        if(!flipper_format_read_string(ff, "type", buf) || furi_string_cmp_str(buf, "raw")) break;
        if(!flipper_format_get_value_count(ff, "data", timings_count)) break;
        if(!*timings_count) break;

        *timings = malloc(*timings_count * sizeof(uint32_t*));
        if(!flipper_format_read_uint32(ff, "data", *timings, *timings_count)) {
            free(*timings);
            break;
        }
        success = true;
    } while(false);

    furi_string_free(buf);
    return success;
}

static bool infrared_test_read_message(FlipperFormat* ff, InfraredMessage* message) {
    FuriString* buf;
    buf = furi_string_alloc();
    bool success = false;

    do {
        if(!flipper_format_read_string(ff, "protocol", buf)) break;
        message->protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
        if(!infrared_is_protocol_valid(message->protocol)) break;
        if(!flipper_format_read_hex(ff, "address", (uint8_t*)&message->address, sizeof(uint32_t)))
            break;
        if(!flipper_format_read_hex(ff, "command", (uint8_t*)&message->command, sizeof(uint32_t)))
            break;
        if(!flipper_format_read_bool(ff, "repeat", &message->repeat, 1)) break;
        success = true;
    } while(false);

    furi_string_free(buf);
    return success;
}

static bool infrared_test_load_messages(
    FlipperFormat* ff,
    const char* signal_name,
    InfraredMessage** messages,
    uint32_t* messages_count) {
    FuriString* buf;
    buf = furi_string_alloc();
    bool success = false;

    do {
        bool is_name_found = false;
        for(; !is_name_found && flipper_format_read_string(ff, "name", buf);
            is_name_found = !furi_string_cmp(buf, signal_name))
            ;

        if(!is_name_found) break;
        if(!flipper_format_read_string(ff, "type", buf) ||
           furi_string_cmp_str(buf, "parsed_array"))
            break;
        if(!flipper_format_read_uint32(ff, "count", messages_count, 1)) break;
        if(!*messages_count) break;

        *messages = malloc(*messages_count * sizeof(InfraredMessage));
        uint32_t i;
        for(i = 0; i < *messages_count; ++i) {
            if(!infrared_test_read_message(ff, (*messages) + i)) {
                break;
            }
        }
        if(*messages_count != i) {
            free(*messages);
            break;
        }
        success = true;
    } while(false);

    furi_string_free(buf);
    return success;
}

static void infrared_test_compare_message_results(
    const InfraredMessage* message_decoded,
    const InfraredMessage* message_expected) {
    mu_check(message_decoded->protocol == message_expected->protocol);
    mu_check(message_decoded->command == message_expected->command);
    mu_check(message_decoded->address == message_expected->address);
    if((message_expected->protocol == InfraredProtocolSIRC) ||
       (message_expected->protocol == InfraredProtocolSIRC15) ||
       (message_expected->protocol == InfraredProtocolSIRC20)) {
        mu_check(message_decoded->repeat == false);
    } else {
        mu_check(message_decoded->repeat == message_expected->repeat);
    }
}

/* Encodes signal and merges same levels (high+high, low+low) */
static void infrared_test_run_encoder_fill_array(
    InfraredEncoderHandler* handler,
    uint32_t* timings,
    uint32_t* timings_len,
    bool* start_level) {
    uint32_t duration = 0;
    bool level = false;
    bool level_read;
    InfraredStatus status = InfraredStatusError;
    size_t i = 0;
    bool first = true;

    while(1) {
        status = infrared_encode(handler, &duration, &level_read);
        if(first) {
            if(start_level) *start_level = level_read;
            first = false;
            timings[0] = 0;
        } else if(level_read != level) {
            ++i;
            furi_check(i < *timings_len);
            timings[i] = 0;
        }
        level = level_read;
        timings[i] += duration;

        furi_check((status == InfraredStatusOk) || (status == InfraredStatusDone));
        if(status == InfraredStatusDone) break;
    }

    *timings_len = i + 1;
}

// messages in input array for encoder should have one protocol
static void infrared_test_run_encoder(InfraredProtocol protocol, uint32_t test_index) {
    uint32_t* timings;
    uint32_t timings_count = 200;
    uint32_t* expected_timings;
    uint32_t expected_timings_count;
    InfraredMessage* input_messages;
    uint32_t input_messages_count;

    FuriString* buf;
    buf = furi_string_alloc();

    const char* protocol_name = infrared_get_protocol_name(protocol);
    mu_assert(infrared_test_prepare_file(protocol_name), "Failed to prepare test file");

    furi_string_printf(buf, "encoder_input%ld", test_index);
    mu_assert(
        infrared_test_load_messages(
            test->ff, furi_string_get_cstr(buf), &input_messages, &input_messages_count),
        "Failed to load messages from file");

    furi_string_printf(buf, "encoder_expected%ld", test_index);
    mu_assert(
        infrared_test_load_raw_signal(
            test->ff, furi_string_get_cstr(buf), &expected_timings, &expected_timings_count),
        "Failed to load raw signal from file");

    flipper_format_buffered_file_close(test->ff);
    furi_string_free(buf);

    uint32_t j = 0;
    timings = malloc(sizeof(uint32_t) * timings_count);

    for(uint32_t message_counter = 0; message_counter < input_messages_count; ++message_counter) {
        const InfraredMessage* message = &input_messages[message_counter];
        if(!message->repeat) {
            infrared_reset_encoder(test->encoder_handler, message);
        }

        timings_count = 200;
        infrared_test_run_encoder_fill_array(test->encoder_handler, timings, &timings_count, NULL);
        furi_check(timings_count <= 200);

        for(size_t i = 0; i < timings_count; ++i, ++j) {
            mu_check(MATCH_TIMING(timings[i], expected_timings[j], 120));
            mu_assert(j < expected_timings_count, "encoded more timings than expected");
        }
    }

    free(input_messages);
    free(expected_timings);
    free(timings);

    mu_assert(j == expected_timings_count, "encoded less timings than expected");
}

static void infrared_test_run_encoder_decoder(InfraredProtocol protocol, uint32_t test_index) {
    uint32_t* timings = 0;
    uint32_t timings_count = 200;
    InfraredMessage* input_messages;
    uint32_t input_messages_count;
    bool level = false;

    FuriString* buf;
    buf = furi_string_alloc();

    timings = malloc(sizeof(uint32_t) * timings_count);

    const char* protocol_name = infrared_get_protocol_name(protocol);
    mu_assert(infrared_test_prepare_file(protocol_name), "Failed to prepare test file");

    furi_string_printf(buf, "encoder_decoder_input%ld", test_index);
    mu_assert(
        infrared_test_load_messages(
            test->ff, furi_string_get_cstr(buf), &input_messages, &input_messages_count),
        "Failed to load messages from file");

    flipper_format_buffered_file_close(test->ff);
    furi_string_free(buf);

    for(uint32_t message_counter = 0; message_counter < input_messages_count; ++message_counter) {
        const InfraredMessage* message_encoded = &input_messages[message_counter];
        if(!message_encoded->repeat) {
            infrared_reset_encoder(test->encoder_handler, message_encoded);
        }

        timings_count = 200;
        infrared_test_run_encoder_fill_array(
            test->encoder_handler, timings, &timings_count, &level);
        furi_check(timings_count <= 200);

        const InfraredMessage* message_decoded = 0;
        for(size_t i = 0; i < timings_count; ++i) {
            message_decoded = infrared_decode(test->decoder_handler, level, timings[i]);
            if((i == timings_count - 2) && level && message_decoded) {
                /* In case we end with space timing - message can be decoded at last mark */
                break;
            } else if(i < timings_count - 1) {
                mu_check(!message_decoded);
            } else {
                if(!message_decoded) {
                    message_decoded = infrared_check_decoder_ready(test->decoder_handler);
                }
                mu_check(message_decoded);
            }
            level = !level;
        }
        if(message_decoded) {
            infrared_test_compare_message_results(message_decoded, message_encoded);
        } else {
            mu_check(0);
        }
    }
    free(input_messages);
    free(timings);
}

static void infrared_test_run_decoder(InfraredProtocol protocol, uint32_t test_index) {
    uint32_t* timings;
    uint32_t timings_count;
    InfraredMessage* messages;
    uint32_t messages_count;

    FuriString* buf;
    buf = furi_string_alloc();

    mu_assert(
        infrared_test_prepare_file(infrared_get_protocol_name(protocol)),
        "Failed to prepare test file");

    furi_string_printf(buf, "decoder_input%ld", test_index);
    mu_assert(
        infrared_test_load_raw_signal(
            test->ff, furi_string_get_cstr(buf), &timings, &timings_count),
        "Failed to load raw signal from file");

    furi_string_printf(buf, "decoder_expected%ld", test_index);
    mu_assert(
        infrared_test_load_messages(
            test->ff, furi_string_get_cstr(buf), &messages, &messages_count),
        "Failed to load messages from file");

    flipper_format_buffered_file_close(test->ff);
    furi_string_free(buf);

    InfraredMessage message_decoded_check_local;
    bool level = 0;
    uint32_t message_counter = 0;
    const InfraredMessage* message_decoded = 0;

    for(uint32_t i = 0; i < timings_count; ++i) {
        const InfraredMessage* message_decoded_check = 0;

        if(timings[i] > INFRARED_RAW_RX_TIMING_DELAY_US) {
            message_decoded_check = infrared_check_decoder_ready(test->decoder_handler);
            if(message_decoded_check) {
                /* infrared_decode() can reset message, but we have to call infrared_decode() to perform real
                 * simulation: infrared_check() by timeout, then infrared_decode() when meet edge */
                message_decoded_check_local = *message_decoded_check;
                message_decoded_check = &message_decoded_check_local;
            }
        }

        message_decoded = infrared_decode(test->decoder_handler, level, timings[i]);

        if(message_decoded_check || message_decoded) {
            mu_assert(
                !(message_decoded_check && message_decoded),
                "both messages decoded: check_ready() and infrared_decode()");

            if(message_decoded_check) {
                message_decoded = message_decoded_check;
            }

            mu_assert(message_counter < messages_count, "decoded more than expected");
            infrared_test_compare_message_results(message_decoded, &messages[message_counter]);

            ++message_counter;
        }
        level = !level;
    }

    message_decoded = infrared_check_decoder_ready(test->decoder_handler);
    if(message_decoded) {
        infrared_test_compare_message_results(message_decoded, &messages[message_counter]);
        ++message_counter;
    }

    free(timings);
    free(messages);

    mu_assert(message_counter == messages_count, "decoded less than expected");
}

MU_TEST(infrared_test_decoder_samsung32) {
    infrared_test_run_decoder(InfraredProtocolSamsung32, 1);
}

MU_TEST(infrared_test_decoder_mixed) {
    infrared_test_run_decoder(InfraredProtocolRC5, 2);
    infrared_test_run_decoder(InfraredProtocolSIRC, 1);
    infrared_test_run_decoder(InfraredProtocolNECext, 1);
    infrared_test_run_decoder(InfraredProtocolRC6, 2);
    infrared_test_run_decoder(InfraredProtocolSamsung32, 1);
    infrared_test_run_decoder(InfraredProtocolRC6, 1);
    infrared_test_run_decoder(InfraredProtocolSamsung32, 1);
    infrared_test_run_decoder(InfraredProtocolRC5, 1);
    infrared_test_run_decoder(InfraredProtocolSIRC, 2);
    infrared_test_run_decoder(InfraredProtocolNECext, 1);
    infrared_test_run_decoder(InfraredProtocolSIRC, 4);
    infrared_test_run_decoder(InfraredProtocolNEC, 2);
    infrared_test_run_decoder(InfraredProtocolRC6, 1);
    infrared_test_run_decoder(InfraredProtocolNECext, 1);
    infrared_test_run_decoder(InfraredProtocolSIRC, 5);
    infrared_test_run_decoder(InfraredProtocolNEC, 3);
    infrared_test_run_decoder(InfraredProtocolRC5, 5);
    infrared_test_run_decoder(InfraredProtocolSamsung32, 1);
    infrared_test_run_decoder(InfraredProtocolSIRC, 3);
    infrared_test_run_decoder(InfraredProtocolKaseikyo, 1);
    infrared_test_run_decoder(InfraredProtocolRCA, 1);
    infrared_test_run_decoder(InfraredProtocolPioneer, 6);
}

MU_TEST(infrared_test_decoder_nec) {
    for(uint32_t i = 1; i <= 3; ++i) {
        infrared_test_run_decoder(InfraredProtocolNEC, i);
    }
}

MU_TEST(infrared_test_decoder_unexpected_end_in_sequence) {
    for(uint32_t i = 1; i <= 2; ++i) {
        infrared_test_run_decoder(InfraredProtocolNEC, i);
        infrared_test_run_decoder(InfraredProtocolNEC, i);
    }
}

MU_TEST(infrared_test_decoder_necext1) {
    for(uint32_t i = 0; i < 2; ++i) {
        UNUSED(i);
        infrared_test_run_decoder(InfraredProtocolNECext, 1);
    }
}

MU_TEST(infrared_test_decoder_long_packets_with_nec_start) {
    for(uint32_t i = 1; i <= 2; ++i) {
        infrared_test_run_decoder(InfraredProtocolNEC42ext, i);
    }
}

MU_TEST(infrared_test_encoder_sirc) {
    for(uint32_t i = 1; i <= 2; ++i) {
        infrared_test_run_encoder(InfraredProtocolSIRC, i);
    }
}

MU_TEST(infrared_test_decoder_sirc) {
    for(uint32_t i = 1; i <= 5; ++i) {
        infrared_test_run_decoder(InfraredProtocolSIRC, 5);
    }
}

MU_TEST(infrared_test_decoder_rc5) {
    infrared_test_run_decoder(InfraredProtocolRC5X, 1);

    for(uint32_t i = 1; i <= 7; ++i) {
        infrared_test_run_decoder(InfraredProtocolRC5, i);
    }
}

MU_TEST(infrared_test_encoder_rc5x) {
    infrared_test_run_encoder(InfraredProtocolRC5X, 1);
}

MU_TEST(infrared_test_encoder_rc5) {
    infrared_test_run_encoder(InfraredProtocolRC5, 1);
}

MU_TEST(infrared_test_decoder_rc6) {
    infrared_test_run_decoder(InfraredProtocolRC6, 1);
}

MU_TEST(infrared_test_encoder_rc6) {
    infrared_test_run_encoder(InfraredProtocolRC6, 1);
}

MU_TEST(infrared_test_decoder_kaseikyo) {
    for(uint32_t i = 1; i <= 6; ++i) {
        infrared_test_run_decoder(InfraredProtocolKaseikyo, i);
    }
}

MU_TEST(infrared_test_decoder_rca) {
    for(uint32_t i = 1; i <= 6; ++i) {
        infrared_test_run_decoder(InfraredProtocolRCA, i);
    }
}

MU_TEST(infrared_test_decoder_pioneer) {
    for(uint32_t i = 1; i <= 11; ++i) {
        infrared_test_run_decoder(InfraredProtocolPioneer, i);
    }
}

MU_TEST(infrared_test_encoder_decoder_all) {
    infrared_test_run_encoder_decoder(InfraredProtocolNEC, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolNECext, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolNEC42, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolNEC42ext, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolSamsung32, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolRC6, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolRC5, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolSIRC, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolKaseikyo, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolRCA, 1);
    infrared_test_run_encoder_decoder(InfraredProtocolPioneer, 1);
}

MU_TEST(infrared_test_ac_database) {
    infrared_brute_force_set_db_filename(test->brutedb, EXT_PATH("infrared/assets/ac.ir"));
    uint32_t i = 0;
    infrared_brute_force_add_record(test->brutedb, i++, "Off");
    infrared_brute_force_add_record(test->brutedb, i++, "Dh");
    infrared_brute_force_add_record(test->brutedb, i++, "Cool_hi");
    infrared_brute_force_add_record(test->brutedb, i++, "Heat_hi");
    infrared_brute_force_add_record(test->brutedb, i++, "Cool_lo");
    infrared_brute_force_add_record(test->brutedb, i++, "Heat_lo");

    mu_assert(
        infrared_brute_force_calculate_messages(test->brutedb) == InfraredErrorCodeNone,
        "universal ac database is invalid");

    infrared_brute_force_reset(test->brutedb);
}

MU_TEST(infrared_test_audio_database) {
    infrared_brute_force_set_db_filename(test->brutedb, EXT_PATH("infrared/assets/audio.ir"));
    uint32_t i = 0;
    infrared_brute_force_add_record(test->brutedb, i++, "Power");
    infrared_brute_force_add_record(test->brutedb, i++, "Mute");
    infrared_brute_force_add_record(test->brutedb, i++, "Play");
    infrared_brute_force_add_record(test->brutedb, i++, "Pause");
    infrared_brute_force_add_record(test->brutedb, i++, "Prev");
    infrared_brute_force_add_record(test->brutedb, i++, "Next");
    infrared_brute_force_add_record(test->brutedb, i++, "Vol_dn");
    infrared_brute_force_add_record(test->brutedb, i++, "Vol_up");

    mu_assert(
        infrared_brute_force_calculate_messages(test->brutedb) == InfraredErrorCodeNone,
        "universal audio database is invalid");

    infrared_brute_force_reset(test->brutedb);
}

MU_TEST(infrared_test_projector_database) {
    infrared_brute_force_set_db_filename(test->brutedb, EXT_PATH("infrared/assets/projector.ir"));
    uint32_t i = 0;
    infrared_brute_force_add_record(test->brutedb, i++, "Power");
    infrared_brute_force_add_record(test->brutedb, i++, "Mute");
    infrared_brute_force_add_record(test->brutedb, i++, "Vol_up");
    infrared_brute_force_add_record(test->brutedb, i++, "Vol_dn");

    mu_assert(
        infrared_brute_force_calculate_messages(test->brutedb) == InfraredErrorCodeNone,
        "universal projector database is invalid");

    infrared_brute_force_reset(test->brutedb);
}

MU_TEST(infrared_test_tv_database) {
    infrared_brute_force_set_db_filename(test->brutedb, EXT_PATH("infrared/assets/tv.ir"));
    uint32_t i = 0;
    infrared_brute_force_add_record(test->brutedb, i++, "Power");
    infrared_brute_force_add_record(test->brutedb, i++, "Mute");
    infrared_brute_force_add_record(test->brutedb, i++, "Vol_up");
    infrared_brute_force_add_record(test->brutedb, i++, "Ch_next");
    infrared_brute_force_add_record(test->brutedb, i++, "Vol_dn");
    infrared_brute_force_add_record(test->brutedb, i++, "Ch_prev");

    mu_assert(
        infrared_brute_force_calculate_messages(test->brutedb) == InfraredErrorCodeNone,
        "universal tv database is invalid");

    infrared_brute_force_reset(test->brutedb);
}

MU_TEST_SUITE(infrared_test) {
    MU_SUITE_CONFIGURE(&infrared_test_alloc, &infrared_test_free);

    MU_RUN_TEST(infrared_test_encoder_sirc);
    MU_RUN_TEST(infrared_test_decoder_sirc);
    MU_RUN_TEST(infrared_test_encoder_rc5x);
    MU_RUN_TEST(infrared_test_encoder_rc5);
    MU_RUN_TEST(infrared_test_decoder_rc5);
    MU_RUN_TEST(infrared_test_decoder_rc6);
    MU_RUN_TEST(infrared_test_encoder_rc6);
    MU_RUN_TEST(infrared_test_decoder_unexpected_end_in_sequence);
    MU_RUN_TEST(infrared_test_decoder_long_packets_with_nec_start);
    MU_RUN_TEST(infrared_test_decoder_nec);
    MU_RUN_TEST(infrared_test_decoder_samsung32);
    MU_RUN_TEST(infrared_test_decoder_necext1);
    MU_RUN_TEST(infrared_test_decoder_kaseikyo);
    MU_RUN_TEST(infrared_test_decoder_rca);
    MU_RUN_TEST(infrared_test_decoder_pioneer);
    MU_RUN_TEST(infrared_test_decoder_mixed);
    MU_RUN_TEST(infrared_test_encoder_decoder_all);
    MU_RUN_TEST(infrared_test_ac_database);
    MU_RUN_TEST(infrared_test_audio_database);
    MU_RUN_TEST(infrared_test_projector_database);
    MU_RUN_TEST(infrared_test_tv_database);
}

int run_minunit_test_infrared(void) {
    MU_RUN_SUITE(infrared_test);
    return MU_EXIT_CODE;
}

TEST_API_DEFINE(run_minunit_test_infrared)
