#include "app_core/app_startup.h"
#include "test_harness.h"

#include <cmath>
#include <limits>
#include <string>

namespace {

class FakeAppStartupServices final
    : public pp::app::AppStartupServices
    , public pp::app::AppStartupResourceServices {
public:
    void store_run_counter(int value) override
    {
        stored_run_counter = value;
        call_order += "store-counter;";
    }

    void save_preferences() override
    {
        save_calls += 1;
        call_order += "save;";
    }

    void start_timelapse_recording() override
    {
        timelapse_starts += 1;
        call_order += "timelapse;";
    }

    void apply_vr_controllers_enabled(bool enabled) override
    {
        vr_controllers_enabled = enabled;
        call_order += "vr-controllers;";
    }

    void show_license_warning() override
    {
        license_warnings += 1;
        call_order += "license;";
    }

    void initialize_shaders() override
    {
        shader_initializations += 1;
        call_order += "shaders;";
    }

    void initialize_assets() override
    {
        asset_initializations += 1;
        call_order += "assets;";
    }

    void initialize_layout() override
    {
        layout_initializations += 1;
        call_order += "layout;";
    }

    void update_title() override
    {
        title_updates += 1;
        call_order += "title;";
    }

    void create_ui_render_target(int width, int height) override
    {
        ui_render_target_width = width;
        ui_render_target_height = height;
        ui_render_target_creations += 1;
        call_order += "ui-rtt;";
    }

    int stored_run_counter = 0;
    int save_calls = 0;
    int timelapse_starts = 0;
    bool vr_controllers_enabled = false;
    int license_warnings = 0;
    int shader_initializations = 0;
    int asset_initializations = 0;
    int layout_initializations = 0;
    int title_updates = 0;
    int ui_render_target_creations = 0;
    int ui_render_target_width = 0;
    int ui_render_target_height = 0;
    std::string call_order;
};

void startup_plan_increments_counter_and_enables_requested_side_effects(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_app_startup(7, true, false, false);

    PP_EXPECT(harness, plan);
    PP_EXPECT(harness, plan.value().previous_run_counter == 7);
    PP_EXPECT(harness, plan.value().next_run_counter == 8);
    PP_EXPECT(harness, plan.value().save_preferences);
    PP_EXPECT(harness, plan.value().start_timelapse);
    PP_EXPECT(harness, !plan.value().vr_controllers_enabled);
    PP_EXPECT(harness, plan.value().show_license_warning);
}

void startup_plan_preserves_disabled_optional_work(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_app_startup(0, false, true, true);

    PP_EXPECT(harness, plan);
    PP_EXPECT(harness, plan.value().next_run_counter == 1);
    PP_EXPECT(harness, !plan.value().start_timelapse);
    PP_EXPECT(harness, plan.value().vr_controllers_enabled);
    PP_EXPECT(harness, !plan.value().show_license_warning);
}

void startup_plan_rejects_invalid_counters(pp::tests::Harness& harness)
{
    const auto negative = pp::app::plan_app_startup(-1, true, true, true);
    const auto overflow = pp::app::plan_app_startup(std::numeric_limits<int>::max(), true, true, true);

    PP_EXPECT(harness, !negative);
    PP_EXPECT(harness, negative.status().code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, !overflow);
    PP_EXPECT(harness, overflow.status().code == pp::foundation::StatusCode::out_of_range);
}

void startup_executor_dispatches_in_stable_order(pp::tests::Harness& harness)
{
    FakeAppStartupServices services;
    const auto plan = pp::app::plan_app_startup(2, true, true, false);

    PP_EXPECT(harness, plan);
    PP_EXPECT(harness, pp::app::execute_app_startup_plan(plan.value(), services).ok());
    PP_EXPECT(harness, services.stored_run_counter == 3);
    PP_EXPECT(harness, services.save_calls == 1);
    PP_EXPECT(harness, services.timelapse_starts == 1);
    PP_EXPECT(harness, services.vr_controllers_enabled);
    PP_EXPECT(harness, services.license_warnings == 1);
    PP_EXPECT(harness, services.call_order == "store-counter;save;timelapse;vr-controllers;license;");
}

void startup_executor_preserves_no_op_side_effects(pp::tests::Harness& harness)
{
    FakeAppStartupServices services;
    auto plan = pp::app::plan_app_startup(4, false, false, true).value();

    PP_EXPECT(harness, pp::app::execute_app_startup_plan(plan, services).ok());
    PP_EXPECT(harness, services.stored_run_counter == 5);
    PP_EXPECT(harness, services.save_calls == 1);
    PP_EXPECT(harness, services.timelapse_starts == 0);
    PP_EXPECT(harness, !services.vr_controllers_enabled);
    PP_EXPECT(harness, services.license_warnings == 0);
    PP_EXPECT(harness, services.call_order == "store-counter;save;vr-controllers;");
}

void startup_split_executors_keep_persistence_and_runtime_separate(pp::tests::Harness& harness)
{
    FakeAppStartupServices persistence_services;
    FakeAppStartupServices runtime_services;
    const auto plan = pp::app::plan_app_startup(5, true, false, false);

    PP_EXPECT(harness, plan);
    PP_EXPECT(harness, pp::app::execute_app_startup_persistence_plan(plan.value(), persistence_services).ok());
    PP_EXPECT(harness, persistence_services.stored_run_counter == 6);
    PP_EXPECT(harness, persistence_services.save_calls == 1);
    PP_EXPECT(harness, persistence_services.timelapse_starts == 0);
    PP_EXPECT(harness, persistence_services.license_warnings == 0);
    PP_EXPECT(harness, persistence_services.call_order == "store-counter;save;");

    PP_EXPECT(harness, pp::app::execute_app_startup_runtime_plan(plan.value(), runtime_services).ok());
    PP_EXPECT(harness, runtime_services.stored_run_counter == 0);
    PP_EXPECT(harness, runtime_services.save_calls == 0);
    PP_EXPECT(harness, runtime_services.timelapse_starts == 1);
    PP_EXPECT(harness, !runtime_services.vr_controllers_enabled);
    PP_EXPECT(harness, runtime_services.license_warnings == 1);
    PP_EXPECT(harness, runtime_services.call_order == "timelapse;vr-controllers;license;");
}

void startup_executor_rejects_malformed_counter_state(pp::tests::Harness& harness)
{
    FakeAppStartupServices services;
    pp::app::AppStartupPlan plan;
    plan.previous_run_counter = 3;
    plan.next_run_counter = 3;

    PP_EXPECT(harness, !pp::app::execute_app_startup_plan(plan, services).ok());
    PP_EXPECT(harness, services.call_order.empty());
}

void startup_resource_plan_projects_ui_target_size(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_app_startup_resources(1280.9F, 720.1F);

    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().ui_render_target_width == 1280);
        PP_EXPECT(harness, plan.value().ui_render_target_height == 720);
        PP_EXPECT(harness, plan.value().initialize_shaders);
        PP_EXPECT(harness, plan.value().initialize_assets);
        PP_EXPECT(harness, plan.value().initialize_layout);
        PP_EXPECT(harness, plan.value().update_title);
        PP_EXPECT(harness, plan.value().create_ui_render_target);
    }
}

void startup_resource_plan_rejects_invalid_dimensions(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_app_startup_resources(0.0F, 720.0F));
    PP_EXPECT(harness, !pp::app::plan_app_startup_resources(1280.0F, -1.0F));
    PP_EXPECT(harness, !pp::app::plan_app_startup_resources(std::nanf(""), 720.0F));
}

void startup_resource_executor_dispatches_in_stable_order(pp::tests::Harness& harness)
{
    FakeAppStartupServices services;
    const auto plan = pp::app::plan_app_startup_resources(1024.0F, 512.0F);

    PP_EXPECT(harness, plan);
    PP_EXPECT(harness, pp::app::execute_app_startup_resources(plan.value(), services).ok());
    PP_EXPECT(harness, services.shader_initializations == 1);
    PP_EXPECT(harness, services.asset_initializations == 1);
    PP_EXPECT(harness, services.layout_initializations == 1);
    PP_EXPECT(harness, services.title_updates == 1);
    PP_EXPECT(harness, services.ui_render_target_creations == 1);
    PP_EXPECT(harness, services.ui_render_target_width == 1024);
    PP_EXPECT(harness, services.ui_render_target_height == 512);
    PP_EXPECT(harness, services.call_order == "shaders;assets;layout;title;ui-rtt;");
}

void startup_resource_executor_rejects_invalid_render_target_size(pp::tests::Harness& harness)
{
    FakeAppStartupServices services;
    pp::app::AppStartupResourcePlan plan;
    plan.ui_render_target_width = 0;
    plan.ui_render_target_height = 720;

    PP_EXPECT(harness, !pp::app::execute_app_startup_resources(plan, services).ok());
    PP_EXPECT(harness, services.call_order.empty());
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run(
        "startup plan increments counter and enables requested side effects",
        startup_plan_increments_counter_and_enables_requested_side_effects);
    harness.run("startup plan preserves disabled optional work", startup_plan_preserves_disabled_optional_work);
    harness.run("startup plan rejects invalid counters", startup_plan_rejects_invalid_counters);
    harness.run("startup executor dispatches in stable order", startup_executor_dispatches_in_stable_order);
    harness.run("startup executor preserves no-op side effects", startup_executor_preserves_no_op_side_effects);
    harness.run(
        "startup split executors keep persistence and runtime separate",
        startup_split_executors_keep_persistence_and_runtime_separate);
    harness.run("startup executor rejects malformed counter state", startup_executor_rejects_malformed_counter_state);
    harness.run("startup resource plan projects UI target size", startup_resource_plan_projects_ui_target_size);
    harness.run("startup resource plan rejects invalid dimensions", startup_resource_plan_rejects_invalid_dimensions);
    harness.run(
        "startup resource executor dispatches in stable order",
        startup_resource_executor_dispatches_in_stable_order);
    harness.run(
        "startup resource executor rejects invalid render target size",
        startup_resource_executor_rejects_invalid_render_target_size);
    return harness.finish();
}
