#include "test_runner.h"

#include "tests/test_api.h"

#include <toolbox/cli/cli_command.h>
#include <toolbox/path.h>
#include <toolbox/pipe.h>
#include <loader/loader.h>
#include <storage/storage.h>
#include <dialogs/dialogs.h>
#include <notification/notification_messages.h>

#include <loader/firmware_api/firmware_api.h>
#include <flipper_application/flipper_application.h>
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/plugins/composite_resolver.h>

extern const ElfApiInterface* const unit_tests_api_interface;

#define TAG "TestRunner"

#define PLUGINS_PATH "/ext/apps_data/unit_tests/plugins"

struct TestRunner {
    Storage* storage;
    Loader* loader;
    NotificationApp* notification;

    // Temporary used things
    PipeSide* pipe;
    FuriString* args;

    // ELF related stuff
    CompositeApiResolver* composite_resolver;

    // Report data
    int minunit_run;
    int minunit_assert;
    int minunit_fail;
    int minunit_status;

    // Summary
    size_t total_failed;
};

TestRunner* test_runner_alloc(PipeSide* pipe, FuriString* args) {
    TestRunner* instance = malloc(sizeof(TestRunner));

    instance->storage = furi_record_open(RECORD_STORAGE);
    instance->loader = furi_record_open(RECORD_LOADER);
    instance->notification = furi_record_open(RECORD_NOTIFICATION);

    instance->pipe = pipe;
    instance->args = args;

    instance->composite_resolver = composite_api_resolver_alloc();
    composite_api_resolver_add(instance->composite_resolver, firmware_api_interface);
    composite_api_resolver_add(instance->composite_resolver, unit_tests_api_interface);

    return instance;
}

void test_runner_free(TestRunner* instance) {
    furi_assert(instance);

    composite_api_resolver_free(instance->composite_resolver);

    furi_record_close(RECORD_NOTIFICATION);
    instance->notification = NULL;

    furi_record_close(RECORD_LOADER);
    instance->loader = NULL;

    furi_record_close(RECORD_STORAGE);
    instance->storage = NULL;

    free(instance);
}

#define TEST_RUNNER_TMP_DIR            EXT_PATH(".tmp")
#define TEST_RUNNER_TMP_UNIT_TESTS_DIR TEST_RUNNER_TMP_DIR "/unit_tests"

static bool test_runner_run_plugin(TestRunner* instance, const char* path) {
    furi_assert(instance);

    FURI_LOG_D(TAG, "Loading %s", path);
    FlipperApplication* lib = flipper_application_alloc(
        instance->storage, composite_api_resolver_get(instance->composite_resolver));

    bool result = false;
    instance->minunit_fail = -1;
    do {
        FlipperApplicationPreloadStatus preload_res = flipper_application_preload(lib, path);

        if(preload_res != FlipperApplicationPreloadStatusSuccess) {
            FURI_LOG_E(TAG, "Failed to preload %s, %d", path, preload_res);
            break;
        }

        if(!flipper_application_is_plugin(lib)) {
            FURI_LOG_E(TAG, "Not a plugin %s", path);
            break;
        }

        FlipperApplicationLoadStatus load_status = flipper_application_map_to_memory(lib);
        if(load_status != FlipperApplicationLoadStatusSuccess) {
            FURI_LOG_E(TAG, "Failed to load %s", path);
            break;
        }

        const FlipperAppPluginDescriptor* app_descriptor =
            flipper_application_plugin_get_descriptor(lib);

        const TestApi* test = app_descriptor->entry_point;

        instance->minunit_fail = test->run();

        instance->minunit_run += test->get_minunit_run();
        instance->minunit_assert += test->get_minunit_assert();
        instance->minunit_status += test->get_minunit_status();

        result = (instance->minunit_fail == 0);

        instance->total_failed += instance->minunit_fail;
    } while(false);

    flipper_application_free(lib);

    return result;
}

static void test_runner_run_internal(TestRunner* instance) {
    furi_assert(instance);

    char file_name_buffer[256];
    FuriString* file_name = furi_string_alloc();
    FuriString* file_basename = furi_string_alloc();
    File* directory = storage_file_alloc(instance->storage);

    do {
        if(!storage_simply_mkdir(instance->storage, TEST_RUNNER_TMP_DIR)) {
            FURI_LOG_E(TAG, "Cannot create dir %s", TEST_RUNNER_TMP_DIR);
            break;
        }

        if(!storage_simply_mkdir(instance->storage, TEST_RUNNER_TMP_UNIT_TESTS_DIR)) {
            FURI_LOG_E(TAG, "Cannot create dir %s", TEST_RUNNER_TMP_UNIT_TESTS_DIR);
            break;
        }

        if(!storage_dir_open(directory, PLUGINS_PATH)) {
            FURI_LOG_E(TAG, "Failed to open directory %s", PLUGINS_PATH);
            break;
        }

        while(true) {
            if(instance->cli && cli_is_pipe_broken_or_is_etx_next_char(instance->pipe)) {
                break;
            }

            if(!storage_dir_read(directory, NULL, file_name_buffer, sizeof(file_name_buffer))) {
                break;
            }

            furi_string_set(file_name, file_name_buffer);
            if(!furi_string_end_with_str(file_name, ".fal")) {
                continue;
            }

            path_concat(PLUGINS_PATH, file_name_buffer, file_name);

            path_extract_filename(file_name, file_basename, true);
            const char* file_basename_cstr = furi_string_get_cstr(file_basename);

            bool result = true;
            if(furi_string_size(instance->args)) {
                if(furi_string_cmp_str(instance->args, file_basename_cstr) == 0) {
                    result = test_runner_run_plugin(instance, furi_string_get_cstr(file_name));
                } else {
                    printf("Skipping %s\r\n", file_basename_cstr);
                }
            } else {
                result = test_runner_run_plugin(instance, furi_string_get_cstr(file_name));
            }

            if(!result) {
                printf("Failed to execute test: %s\r\n", file_basename_cstr);

                if(!instance->cli) {
                    notification_message(instance->notification, &sequence_error);

                    DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
                    DialogMessage* message = dialog_message_alloc();
                    dialog_message_set_header(
                        message, "Failed Test:", 64, 30, AlignCenter, AlignBottom);
                    dialog_message_set_text(
                        message, file_basename_cstr, 64, 34, AlignCenter, AlignTop);
                    dialog_message_show(dialogs, message);
                    dialog_message_free(message);
                    furi_record_close(RECORD_DIALOGS);

                    notification_message_block(
                        instance->notification, &sequence_set_only_blue_255);
                }
            }
        }
    } while(false);

    storage_dir_close(directory);
    storage_file_free(directory);
    furi_string_free(file_name);
    furi_string_free(file_basename);
}

void test_runner_run(TestRunner* instance) {
    furi_assert(instance);

    // TODO FL-3491: lock device while test running
    if(loader_is_locked(instance->loader)) {
        printf("RPC: stop all applications to run tests\r\n");
        notification_message(instance->notification, &sequence_blink_magenta_100);
    } else {
        notification_message_block(instance->notification, &sequence_set_only_blue_255);

        uint32_t heap_before = memmgr_get_free_heap();
        uint32_t cycle_counter = furi_get_tick();

        test_runner_run_internal(instance);

        if(instance->minunit_run != 0) {
            printf("\r\nFailed tests: %d\r\n", instance->total_failed);

            // Time report
            cycle_counter = (furi_get_tick() - cycle_counter);
            printf("Consumed: %lu ms\r\n", cycle_counter);

            // Wait for tested services and apps to deallocate memory
            furi_delay_ms(200);
            uint32_t heap_after = memmgr_get_free_heap();
            printf("Leaked: %ld\r\n", heap_before - heap_after);

            // Final Report
            if(instance->total_failed == 0) {
                notification_message(instance->notification, &sequence_success);
                printf("Status: PASSED\r\n");
            } else {
                notification_message(instance->notification, &sequence_error);
                printf("Status: FAILED\r\n");
            }

            if(!instance->cli) {
                char text[70];
                snprintf(
                    text,
                    sizeof(text),
                    "Failed tests: %d\n"
                    "Consumed: %lu ms\n"
                    "Leaked: %ld",
                    instance->total_failed,
                    cycle_counter,
                    heap_before - heap_after);
                DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
                DialogMessage* message = dialog_message_alloc();
                dialog_message_set_header(
                    message,
                    instance->total_failed == 0 ? "PASSED" : "FAILED",
                    64,
                    8,
                    AlignCenter,
                    AlignTop);
                dialog_message_set_text(message, text, 64, 38, AlignCenter, AlignCenter);
                dialog_message_show(dialogs, message);
                dialog_message_free(message);
                furi_record_close(RECORD_DIALOGS);
            }
        }
    }
}
