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

#include <cmath>
#include <string>
#include <string_view>

namespace {

class FakeBrushUiServices final : public pp::app::BrushUiServices {
public:
    void set_tip_color(float r, float g, float b, float a) override
    {
        color_sets += 1;
        last_r = r;
        last_g = g;
        last_b = b;
        last_a = a;
        call_order += "color;";
    }

    void set_texture(
        pp::app::BrushUiTextureSlot slot,
        std::string_view path,
        std::string_view thumbnail_path) override
    {
        texture_sets += 1;
        last_slot = slot;
        last_path = std::string(path);
        last_thumbnail_path = std::string(thumbnail_path);
        call_order += "texture;";
    }

    void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) override
    {
        preset_replacements += 1;
        preserved_color = preserve_existing_color;
        loaded_resources = load_resources;
        call_order += "preset;";
    }

    void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) override
    {
        refreshes += 1;
        last_update_color_ui = update_color_ui;
        last_update_brush_ui = update_brush_ui;
        call_order += "refresh;";
    }

    int color_sets = 0;
    int texture_sets = 0;
    int preset_replacements = 0;
    int refreshes = 0;
    float last_r = 0.0F;
    float last_g = 0.0F;
    float last_b = 0.0F;
    float last_a = 0.0F;
    pp::app::BrushUiTextureSlot last_slot = pp::app::BrushUiTextureSlot::tip;
    std::string last_path;
    std::string last_thumbnail_path;
    bool preserved_color = false;
    bool loaded_resources = false;
    bool last_update_color_ui = false;
    bool last_update_brush_ui = false;
    std::string call_order;
};

void color_plan_validates_all_channels(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::set_tip_color);
        PP_EXPECT(harness, plan.value().r == 0.25F);
        PP_EXPECT(harness, plan.value().g == 0.5F);
        PP_EXPECT(harness, plan.value().b == 0.75F);
        PP_EXPECT(harness, plan.value().a == 1.0F);
        PP_EXPECT(harness, plan.value().mutates_brush);
        PP_EXPECT(harness, plan.value().update_color_ui);
        PP_EXPECT(harness, !plan.value().update_brush_ui);
    }

    PP_EXPECT(harness, !pp::app::plan_brush_ui_color(-0.01F, 0.5F, 0.5F, 1.0F));
    PP_EXPECT(harness, !pp::app::plan_brush_ui_color(0.5F, 1.01F, 0.5F, 1.0F));
    PP_EXPECT(harness, !pp::app::plan_brush_ui_color(0.5F, 0.5F, std::nanf(""), 1.0F));
}

void texture_plan_validates_path_and_slot(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_brush_ui_texture(
        pp::app::BrushUiTextureSlot::pattern,
        "data/patterns/noise.png",
        "data/patterns/thumbs/noise.png");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::set_texture);
        PP_EXPECT(harness, plan.value().texture_slot == pp::app::BrushUiTextureSlot::pattern);
        PP_EXPECT(harness, plan.value().path == "data/patterns/noise.png");
        PP_EXPECT(harness, plan.value().thumbnail_path == "data/patterns/thumbs/noise.png");
        PP_EXPECT(harness, plan.value().loads_brush_resources);
        PP_EXPECT(harness, plan.value().update_color_ui);
        PP_EXPECT(harness, plan.value().update_brush_ui);
    }

    PP_EXPECT(
        harness,
        !pp::app::plan_brush_ui_texture(pp::app::BrushUiTextureSlot::tip, "", "thumb.png"));
}

void preset_plan_preserves_color_and_requires_brush(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_brush_ui_preset_replace(true);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::BrushUiOperation::replace_brush_from_preset);
        PP_EXPECT(harness, plan.value().preserves_existing_color);
        PP_EXPECT(harness, plan.value().loads_brush_resources);
        PP_EXPECT(harness, plan.value().update_color_ui);
        PP_EXPECT(harness, plan.value().update_brush_ui);
    }

    PP_EXPECT(harness, !pp::app::plan_brush_ui_preset_replace(false));
}

void stroke_settings_plan_updates_brush_preview(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_brush_ui_stroke_settings_changed();
    PP_EXPECT(harness, plan.operation == pp::app::BrushUiOperation::stroke_settings_changed);
    PP_EXPECT(harness, plan.mutates_brush);
    PP_EXPECT(harness, plan.update_color_ui);
    PP_EXPECT(harness, plan.update_brush_ui);
    PP_EXPECT(harness, !plan.loads_brush_resources);
}

void executor_dispatches_color_and_refresh(pp::tests::Harness& harness)
{
    FakeBrushUiServices services;
    const auto plan = pp::app::plan_brush_ui_color(0.25F, 0.5F, 0.75F, 1.0F);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, pp::app::execute_brush_ui_plan(plan.value(), services).ok());
    }

    PP_EXPECT(harness, services.color_sets == 1);
    PP_EXPECT(harness, services.last_r == 0.25F);
    PP_EXPECT(harness, services.last_g == 0.5F);
    PP_EXPECT(harness, services.last_b == 0.75F);
    PP_EXPECT(harness, services.last_a == 1.0F);
    PP_EXPECT(harness, services.refreshes == 1);
    PP_EXPECT(harness, services.last_update_color_ui);
    PP_EXPECT(harness, !services.last_update_brush_ui);
    PP_EXPECT(harness, services.call_order == "color;refresh;");
}

void executor_dispatches_texture_and_preset(pp::tests::Harness& harness)
{
    FakeBrushUiServices services;

    const auto texture = pp::app::plan_brush_ui_texture(
        pp::app::BrushUiTextureSlot::dual,
        "data/brushes/dual.png",
        "data/brushes/thumbs/dual.png");
    PP_EXPECT(harness, texture);
    if (texture) {
        PP_EXPECT(harness, pp::app::execute_brush_ui_plan(texture.value(), services).ok());
    }

    const auto preset = pp::app::plan_brush_ui_preset_replace(true);
    PP_EXPECT(harness, preset);
    if (preset) {
        PP_EXPECT(harness, pp::app::execute_brush_ui_plan(preset.value(), services).ok());
    }

    PP_EXPECT(harness, services.texture_sets == 1);
    PP_EXPECT(harness, services.last_slot == pp::app::BrushUiTextureSlot::dual);
    PP_EXPECT(harness, services.last_path == "data/brushes/dual.png");
    PP_EXPECT(harness, services.last_thumbnail_path == "data/brushes/thumbs/dual.png");
    PP_EXPECT(harness, services.preset_replacements == 1);
    PP_EXPECT(harness, services.preserved_color);
    PP_EXPECT(harness, services.loaded_resources);
    PP_EXPECT(harness, services.refreshes == 2);
    PP_EXPECT(harness, services.call_order == "texture;refresh;preset;refresh;");
}

void executor_dispatches_stroke_refresh_only(pp::tests::Harness& harness)
{
    FakeBrushUiServices services;
    const auto plan = pp::app::plan_brush_ui_stroke_settings_changed();
    PP_EXPECT(harness, pp::app::execute_brush_ui_plan(plan, services).ok());

    PP_EXPECT(harness, services.color_sets == 0);
    PP_EXPECT(harness, services.texture_sets == 0);
    PP_EXPECT(harness, services.preset_replacements == 0);
    PP_EXPECT(harness, services.refreshes == 1);
    PP_EXPECT(harness, services.last_update_color_ui);
    PP_EXPECT(harness, services.last_update_brush_ui);
    PP_EXPECT(harness, services.call_order == "refresh;");
}

void executor_rejects_invalid_plan_payloads(pp::tests::Harness& harness)
{
    FakeBrushUiServices services;

    pp::app::BrushUiPlan color;
    color.operation = pp::app::BrushUiOperation::set_tip_color;
    color.r = std::nanf("");
    color.g = 0.0F;
    color.b = 0.0F;
    color.a = 1.0F;
    PP_EXPECT(harness, !pp::app::execute_brush_ui_plan(color, services).ok());

    pp::app::BrushUiPlan texture;
    texture.operation = pp::app::BrushUiOperation::set_texture;
    texture.texture_slot = pp::app::BrushUiTextureSlot::tip;
    texture.path.clear();
    PP_EXPECT(harness, !pp::app::execute_brush_ui_plan(texture, services).ok());

    PP_EXPECT(harness, services.color_sets == 0);
    PP_EXPECT(harness, services.texture_sets == 0);
    PP_EXPECT(harness, services.refreshes == 0);
}

} // namespace

int main()
{
    pp::tests::Harness harness;
    harness.run("color plan validates all channels", color_plan_validates_all_channels);
    harness.run("texture plan validates path and slot", texture_plan_validates_path_and_slot);
    harness.run("preset plan preserves color and requires brush", preset_plan_preserves_color_and_requires_brush);
    harness.run("stroke settings plan updates brush preview", stroke_settings_plan_updates_brush_preview);
    harness.run("executor dispatches color and refresh", executor_dispatches_color_and_refresh);
    harness.run("executor dispatches texture and preset", executor_dispatches_texture_and_preset);
    harness.run("executor dispatches stroke refresh only", executor_dispatches_stroke_refresh_only);
    harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
    return harness.finish();
}
