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

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

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;
};

class FakeBrushTextureListServices final : public pp::app::BrushTextureListServices {
public:
    pp::foundation::Status add_texture_from_source(
        std::string_view source_path,
        std::string_view high_path,
        std::string_view thumbnail_path,
        std::string_view brush_name,
        bool converts_brush_alpha) override
    {
        if (fail_add) {
            call_order += "add-failed;";
            return pp::foundation::Status::invalid_argument("fake add failure");
        }

        adds += 1;
        last_source_path = std::string(source_path);
        last_high_path = std::string(high_path);
        last_thumbnail_path = std::string(thumbnail_path);
        last_brush_name = std::string(brush_name);
        last_converts_brush_alpha = converts_brush_alpha;
        call_order += "add;";
        return pp::foundation::Status::success();
    }

    void remove_texture(int index, bool delete_texture_files) override
    {
        removes += 1;
        last_index = index;
        last_deletes_texture_files = delete_texture_files;
        call_order += "remove;";
    }

    void move_texture(int from_index, int to_index) override
    {
        moves += 1;
        last_index = from_index;
        last_target_index = to_index;
        call_order += "move;";
    }

    void select_texture(int index) override
    {
        selections += 1;
        last_target_index = index;
        call_order += "select;";
    }

    void save_texture_list() override
    {
        saves += 1;
        call_order += "save;";
    }

    int adds = 0;
    int removes = 0;
    int moves = 0;
    int selections = 0;
    int saves = 0;
    int last_index = -1;
    int last_target_index = -1;
    std::string last_source_path;
    std::string last_high_path;
    std::string last_thumbnail_path;
    std::string last_brush_name;
    bool last_converts_brush_alpha = false;
    bool last_deletes_texture_files = false;
    bool fail_add = false;
    std::string call_order;
};

class FakeBrushPresetListServices final : public pp::app::BrushPresetListServices {
public:
    pp::foundation::Status add_current_brush_preset(int target_index) override
    {
        if (fail_add) {
            call_order += "add-failed;";
            return pp::foundation::Status::invalid_argument("fake preset add failure");
        }

        adds += 1;
        last_target_index = target_index;
        call_order += "add;";
        return pp::foundation::Status::success();
    }

    void remove_brush_preset(
        int current_index,
        int target_index,
        bool selects_target,
        bool clears_selection) override
    {
        removes += 1;
        last_index = current_index;
        last_target_index = target_index;
        last_selects_target = selects_target;
        last_remove_clears_selection = clears_selection;
        call_order += "remove;";
    }

    void move_brush_preset(int from_index, int to_index) override
    {
        moves += 1;
        last_index = from_index;
        last_target_index = to_index;
        call_order += "move;";
    }

    void select_brush_preset(int index, bool notify_brush_changed) override
    {
        selections += 1;
        last_target_index = index;
        last_notifies_brush_changed = notify_brush_changed;
        call_order += "select;";
    }

    void clear_brush_presets(bool clears_selection) override
    {
        clears += 1;
        last_clear_clears_selection = clears_selection;
        call_order += "clear;";
    }

    void update_preset_empty_notification() override
    {
        notification_updates += 1;
        call_order += "notification;";
    }

    void save_preset_list() override
    {
        saves += 1;
        call_order += "save;";
    }

    int adds = 0;
    int removes = 0;
    int moves = 0;
    int selections = 0;
    int clears = 0;
    int notification_updates = 0;
    int saves = 0;
    int last_index = -1;
    int last_target_index = -1;
    bool last_selects_target = false;
    bool last_remove_clears_selection = false;
    bool last_clear_clears_selection = false;
    bool last_notifies_brush_changed = false;
    bool fail_add = false;
    std::string call_order;
};

class FakeBrushStrokeControlServices final : public pp::app::BrushStrokeControlServices {
public:
    void set_float_setting(pp::app::BrushStrokeFloatSetting setting, float value) override
    {
        float_sets += 1;
        last_float_setting = setting;
        last_float_value = value;
        call_order += "float;";
    }

    void set_bool_setting(pp::app::BrushStrokeBoolSetting setting, bool value) override
    {
        bool_sets += 1;
        last_bool_setting = setting;
        last_bool_value = value;
        call_order += "bool;";
    }

    void set_blend_mode(pp::app::BrushStrokeBlendSetting setting, int blend_mode) override
    {
        blend_sets += 1;
        last_blend_setting = setting;
        last_blend_mode = blend_mode;
        call_order += "blend;";
    }

    void reset_tip_aspect(float value) override
    {
        tip_aspect_resets += 1;
        last_float_value = value;
        call_order += "tip-aspect;";
    }

    void reset_default_brush() override
    {
        default_resets += 1;
        call_order += "default;";
    }

    void update_stroke_controls() override
    {
        control_updates += 1;
        call_order += "controls;";
    }

    void refresh_stroke_preview() override
    {
        preview_refreshes += 1;
        call_order += "preview;";
    }

    void notify_stroke_changed() override
    {
        stroke_notifications += 1;
        call_order += "notify;";
    }

    int float_sets = 0;
    int bool_sets = 0;
    int blend_sets = 0;
    int tip_aspect_resets = 0;
    int default_resets = 0;
    int control_updates = 0;
    int preview_refreshes = 0;
    int stroke_notifications = 0;
    pp::app::BrushStrokeFloatSetting last_float_setting = pp::app::BrushStrokeFloatSetting::tip_size;
    pp::app::BrushStrokeBoolSetting last_bool_setting = pp::app::BrushStrokeBoolSetting::tip_angle_init;
    pp::app::BrushStrokeBlendSetting last_blend_setting = pp::app::BrushStrokeBlendSetting::tip;
    float last_float_value = 0.0F;
    bool last_bool_value = false;
    int last_blend_mode = 0;
    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 brush_ui_refresh_projects_requested_surfaces(pp::tests::Harness& harness)
{
    const auto view = pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput {
        .update_color = true,
        .update_brush = true,
        .has_current_brush = true,
        .has_floating_picker = true,
        .has_floating_color_panel = false,
        .tip_flow = 0.9F,
        .tip_size = 64.0F,
        .r = 0.25F,
        .g = 0.5F,
        .b = 0.75F,
        .a = 1.0F,
    });

    PP_EXPECT(harness, view);
    if (view) {
        PP_EXPECT(harness, view.value().updates_stroke_controls);
        PP_EXPECT(harness, view.value().updates_quick_flow);
        PP_EXPECT(harness, view.value().updates_quick_size);
        PP_EXPECT(harness, view.value().updates_quick_brush_preview);
        PP_EXPECT(harness, view.value().updates_quick_color);
        PP_EXPECT(harness, view.value().updates_floating_picker);
        PP_EXPECT(harness, !view.value().updates_floating_color_panel);
        PP_EXPECT(harness, !view.value().no_op);
        PP_EXPECT(harness, view.value().tip_flow == 0.9F);
        PP_EXPECT(harness, view.value().tip_size == 64.0F);
        PP_EXPECT(harness, view.value().r == 0.25F);
        PP_EXPECT(harness, view.value().g == 0.5F);
        PP_EXPECT(harness, view.value().b == 0.75F);
        PP_EXPECT(harness, view.value().a == 1.0F);
    }

    const auto color_only = pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput {
        .update_color = true,
        .update_brush = false,
        .has_current_brush = true,
        .has_floating_picker = false,
        .has_floating_color_panel = true,
        .r = 0.1F,
        .g = 0.2F,
        .b = 0.3F,
        .a = 1.0F,
    });
    PP_EXPECT(harness, color_only);
    if (color_only) {
        PP_EXPECT(harness, !color_only.value().updates_stroke_controls);
        PP_EXPECT(harness, !color_only.value().updates_quick_flow);
        PP_EXPECT(harness, color_only.value().updates_quick_color);
        PP_EXPECT(harness, !color_only.value().updates_floating_picker);
        PP_EXPECT(harness, color_only.value().updates_floating_color_panel);
    }

    const auto no_op = pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput {
        .update_color = false,
        .update_brush = false,
        .has_current_brush = false,
    });
    PP_EXPECT(harness, no_op);
    if (no_op) {
        PP_EXPECT(harness, no_op.value().no_op);
        PP_EXPECT(harness, !no_op.value().updates_quick_color);
        PP_EXPECT(harness, !no_op.value().updates_quick_brush_preview);
    }
}

void brush_ui_refresh_rejects_invalid_state(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput {
        .update_color = true,
        .update_brush = false,
        .has_current_brush = false,
    }));
    PP_EXPECT(harness, !pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput {
        .update_color = true,
        .update_brush = false,
        .has_current_brush = true,
        .r = 1.25F,
        .g = 0.0F,
        .b = 0.0F,
        .a = 1.0F,
    }));
    PP_EXPECT(harness, !pp::app::plan_brush_ui_refresh(pp::app::BrushUiRefreshInput {
        .update_color = false,
        .update_brush = true,
        .has_current_brush = true,
        .tip_flow = std::nanf(""),
        .tip_size = 64.0F,
    }));
}

void stroke_control_plans_validate_values_and_reject_breaking_points(pp::tests::Harness& harness)
{
    const auto slider = pp::app::plan_brush_stroke_float_setting(
        pp::app::BrushStrokeFloatSetting::tip_size,
        42.5F);
    PP_EXPECT(harness, slider);
    if (slider) {
        PP_EXPECT(harness, slider.value().operation == pp::app::BrushStrokeControlOperation::set_float);
        PP_EXPECT(harness, slider.value().float_setting == pp::app::BrushStrokeFloatSetting::tip_size);
        PP_EXPECT(harness, slider.value().float_value == 42.5F);
        PP_EXPECT(harness, slider.value().mutates_brush);
        PP_EXPECT(harness, slider.value().refreshes_preview);
        PP_EXPECT(harness, slider.value().notifies_stroke_change);
    }

    const auto checkbox = pp::app::plan_brush_stroke_bool_setting(
        pp::app::BrushStrokeBoolSetting::pattern_enabled,
        true);
    PP_EXPECT(harness, checkbox.operation == pp::app::BrushStrokeControlOperation::set_bool);
    PP_EXPECT(harness, checkbox.bool_setting == pp::app::BrushStrokeBoolSetting::pattern_enabled);
    PP_EXPECT(harness, checkbox.bool_value);

    const auto blend = pp::app::plan_brush_stroke_blend_mode(
        pp::app::BrushStrokeBlendSetting::dual,
        7);
    PP_EXPECT(harness, blend);
    if (blend) {
        PP_EXPECT(harness, blend.value().operation == pp::app::BrushStrokeControlOperation::set_blend_mode);
        PP_EXPECT(harness, blend.value().blend_setting == pp::app::BrushStrokeBlendSetting::dual);
        PP_EXPECT(harness, blend.value().blend_mode == 7);
    }

    const auto tip_aspect = pp::app::plan_brush_tip_aspect_reset();
    PP_EXPECT(harness, tip_aspect.operation == pp::app::BrushStrokeControlOperation::reset_tip_aspect);
    PP_EXPECT(harness, tip_aspect.float_setting == pp::app::BrushStrokeFloatSetting::tip_aspect);
    PP_EXPECT(harness, tip_aspect.float_value == 0.5F);

    const auto defaults = pp::app::plan_brush_default_settings_reset();
    PP_EXPECT(harness, defaults.operation == pp::app::BrushStrokeControlOperation::reset_default_brush);
    PP_EXPECT(harness, defaults.updates_controls);
    PP_EXPECT(harness, defaults.refreshes_preview);
    PP_EXPECT(harness, defaults.notifies_stroke_change);

    PP_EXPECT(
        harness,
        !pp::app::plan_brush_stroke_float_setting(
            pp::app::BrushStrokeFloatSetting::tip_flow,
            std::nanf("")));
    PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::tip, -1));
    PP_EXPECT(harness, !pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 64));
}

void stroke_panel_view_projects_brush_state(pp::tests::Harness& harness)
{
    pp::app::BrushStrokePanelInput input;
    input.float_values.push_back(pp::app::BrushStrokeFloatValue {
        .setting = pp::app::BrushStrokeFloatSetting::tip_size,
        .value = 42.5F,
    });
    input.float_values.push_back(pp::app::BrushStrokeFloatValue {
        .setting = pp::app::BrushStrokeFloatSetting::jitter_scatter,
        .value = 0.75F,
    });
    input.bool_values.push_back(pp::app::BrushStrokeBoolValue {
        .setting = pp::app::BrushStrokeBoolSetting::dual_enabled,
        .value = true,
    });
    input.bool_values.push_back(pp::app::BrushStrokeBoolValue {
        .setting = pp::app::BrushStrokeBoolSetting::pattern_random_offset,
        .value = false,
    });
    input.blend_values.push_back(pp::app::BrushStrokeBlendValue {
        .setting = pp::app::BrushStrokeBlendSetting::tip,
        .blend_mode = 4,
    });
    input.blend_values.push_back(pp::app::BrushStrokeBlendValue {
        .setting = pp::app::BrushStrokeBlendSetting::pattern,
        .blend_mode = 8,
    });
    input.tip_thumbnail_path = "data/brushes/thumbs/soft.png";
    input.dual_thumbnail_path = "data/brushes/thumbs/hard.png";
    input.pattern_thumbnail_path = "data/patterns/thumbs/noise.png";

    const auto view = pp::app::plan_brush_stroke_panel_view(std::move(input));
    PP_EXPECT(harness, view);
    if (view) {
        PP_EXPECT(harness, view.value().float_values.size() == 2);
        PP_EXPECT(harness, view.value().bool_values.size() == 2);
        PP_EXPECT(harness, view.value().blend_values.size() == 2);
        PP_EXPECT(harness, view.value().float_values[0].setting == pp::app::BrushStrokeFloatSetting::tip_size);
        PP_EXPECT(harness, view.value().float_values[0].value == 42.5F);
        PP_EXPECT(harness, view.value().bool_values[0].setting == pp::app::BrushStrokeBoolSetting::dual_enabled);
        PP_EXPECT(harness, view.value().bool_values[0].value);
        PP_EXPECT(harness, view.value().blend_values[1].setting == pp::app::BrushStrokeBlendSetting::pattern);
        PP_EXPECT(harness, view.value().blend_values[1].blend_mode == 8);
        PP_EXPECT(harness, view.value().tip_thumbnail_path == "data/brushes/thumbs/soft.png");
        PP_EXPECT(harness, view.value().dual_thumbnail_path == "data/brushes/thumbs/hard.png");
        PP_EXPECT(harness, view.value().pattern_thumbnail_path == "data/patterns/thumbs/noise.png");
        PP_EXPECT(harness, view.value().updates_preview);
        PP_EXPECT(harness, view.value().updates_thumbnails);
    }
}

void stroke_panel_view_rejects_invalid_brush_state(pp::tests::Harness& harness)
{
    pp::app::BrushStrokePanelInput bad_float;
    bad_float.float_values.push_back(pp::app::BrushStrokeFloatValue {
        .setting = pp::app::BrushStrokeFloatSetting::tip_flow,
        .value = std::nanf(""),
    });
    PP_EXPECT(harness, !pp::app::plan_brush_stroke_panel_view(std::move(bad_float)));

    pp::app::BrushStrokePanelInput bad_blend;
    bad_blend.blend_values.push_back(pp::app::BrushStrokeBlendValue {
        .setting = pp::app::BrushStrokeBlendSetting::dual,
        .blend_mode = 64,
    });
    PP_EXPECT(harness, !pp::app::plan_brush_stroke_panel_view(std::move(bad_blend)));
}

void texture_list_add_plans_target_paths_and_rejects_bad_input(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_brush_texture_list_add(
        "brushes",
        "D:/Paint/data",
        "C:/Users/artist/My Brush.JPG");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::BrushTextureListOperation::add_texture);
        PP_EXPECT(harness, plan.value().source_path == "C:/Users/artist/My Brush.JPG");
        PP_EXPECT(harness, plan.value().brush_name == "My Brush");
        PP_EXPECT(harness, plan.value().high_path == "D:/Paint/data/brushes/My Brush.png");
        PP_EXPECT(harness, plan.value().thumbnail_path == "D:/Paint/data/brushes/thumbs/My Brush.png");
        PP_EXPECT(harness, plan.value().user_texture);
        PP_EXPECT(harness, plan.value().saves_list);
        PP_EXPECT(harness, plan.value().converts_brush_alpha);
    }

    const auto pattern = pp::app::plan_brush_texture_list_add(
        "patterns",
        "D:/Paint/data",
        R"(C:\Textures\noise.png)");
    PP_EXPECT(harness, pattern);
    if (pattern) {
        PP_EXPECT(harness, !pattern.value().converts_brush_alpha);
        PP_EXPECT(harness, pattern.value().brush_name == "noise");
    }

    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("", "D:/Paint/data", "a.png"));
    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "", "a.png"));
    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "no-extension"));
    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/dir/"));
}

void texture_list_remove_and_move_plans_handle_edges(pp::tests::Harness& harness)
{
    const auto remove_middle = pp::app::plan_brush_texture_list_remove(3, 1, true);
    PP_EXPECT(harness, remove_middle);
    if (remove_middle) {
        PP_EXPECT(harness, remove_middle.value().operation == pp::app::BrushTextureListOperation::remove_texture);
        PP_EXPECT(harness, remove_middle.value().current_index == 1);
        PP_EXPECT(harness, remove_middle.value().target_index == 1);
        PP_EXPECT(harness, remove_middle.value().deletes_texture_files);
        PP_EXPECT(harness, remove_middle.value().notifies_selection);
        PP_EXPECT(harness, remove_middle.value().saves_list);
    }

    const auto remove_last = pp::app::plan_brush_texture_list_remove(1, 0, false);
    PP_EXPECT(harness, remove_last);
    if (remove_last) {
        PP_EXPECT(harness, remove_last.value().target_index == -1);
        PP_EXPECT(harness, !remove_last.value().deletes_texture_files);
        PP_EXPECT(harness, !remove_last.value().notifies_selection);
    }

    const auto move_up_edge = pp::app::plan_brush_texture_list_move(3, 0, -1);
    PP_EXPECT(harness, move_up_edge);
    if (move_up_edge) {
        PP_EXPECT(harness, move_up_edge.value().operation == pp::app::BrushTextureListOperation::move_texture);
        PP_EXPECT(harness, move_up_edge.value().target_index == 0);
        PP_EXPECT(harness, move_up_edge.value().no_op);
    }

    const auto move_down = pp::app::plan_brush_texture_list_move(3, 1, 1);
    PP_EXPECT(harness, move_down);
    if (move_down) {
        PP_EXPECT(harness, move_down.value().target_index == 2);
        PP_EXPECT(harness, !move_down.value().no_op);
    }

    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(0, 0, true));
    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_remove(2, 2, true));
    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, 0, 0));
    PP_EXPECT(harness, !pp::app::plan_brush_texture_list_move(2, -1, 1));
}

void preset_list_plans_add_select_move_remove_and_clear(pp::tests::Harness& harness)
{
    const auto add = pp::app::plan_brush_preset_list_add(2, true);
    PP_EXPECT(harness, add);
    if (add) {
        PP_EXPECT(harness, add.value().operation == pp::app::BrushPresetListOperation::add_current_brush);
        PP_EXPECT(harness, add.value().target_index == 2);
        PP_EXPECT(harness, add.value().saves_list);
        PP_EXPECT(harness, add.value().updates_empty_notification);
    }

    const auto select = pp::app::plan_brush_preset_list_select(3, 1);
    PP_EXPECT(harness, select);
    if (select) {
        PP_EXPECT(harness, select.value().operation == pp::app::BrushPresetListOperation::select_preset);
        PP_EXPECT(harness, select.value().target_index == 1);
        PP_EXPECT(harness, select.value().selects_target);
        PP_EXPECT(harness, select.value().notifies_brush_changed);
        PP_EXPECT(harness, !select.value().saves_list);
    }

    const auto move_up_edge = pp::app::plan_brush_preset_list_move(3, 0, -1);
    PP_EXPECT(harness, move_up_edge);
    if (move_up_edge) {
        PP_EXPECT(harness, move_up_edge.value().target_index == 0);
        PP_EXPECT(harness, move_up_edge.value().no_op);
        PP_EXPECT(harness, move_up_edge.value().saves_list);
    }

    const auto move_down = pp::app::plan_brush_preset_list_move(3, 1, 1);
    PP_EXPECT(harness, move_down);
    if (move_down) {
        PP_EXPECT(harness, move_down.value().operation == pp::app::BrushPresetListOperation::move_preset);
        PP_EXPECT(harness, move_down.value().target_index == 2);
        PP_EXPECT(harness, !move_down.value().no_op);
    }

    const auto remove_middle = pp::app::plan_brush_preset_list_remove(3, 1);
    PP_EXPECT(harness, remove_middle);
    if (remove_middle) {
        PP_EXPECT(harness, remove_middle.value().operation == pp::app::BrushPresetListOperation::remove_preset);
        PP_EXPECT(harness, remove_middle.value().target_index == 1);
        PP_EXPECT(harness, remove_middle.value().selects_target);
        PP_EXPECT(harness, !remove_middle.value().clears_selection);
        PP_EXPECT(harness, remove_middle.value().saves_list);
    }

    const auto remove_only = pp::app::plan_brush_preset_list_remove(1, 0);
    PP_EXPECT(harness, remove_only);
    if (remove_only) {
        PP_EXPECT(harness, remove_only.value().target_index == -1);
        PP_EXPECT(harness, !remove_only.value().selects_target);
        PP_EXPECT(harness, remove_only.value().clears_selection);
    }

    const auto clear_empty = pp::app::plan_brush_preset_list_clear(0);
    PP_EXPECT(harness, clear_empty);
    if (clear_empty) {
        PP_EXPECT(harness, clear_empty.value().operation == pp::app::BrushPresetListOperation::clear_presets);
        PP_EXPECT(harness, clear_empty.value().no_op);
        PP_EXPECT(harness, clear_empty.value().saves_list);
        PP_EXPECT(harness, clear_empty.value().clears_selection);
    }
}

void preset_list_plans_reject_breaking_points(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_add(-1, true));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_add(0, false));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_select(0, 0));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_select(2, 2));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_remove(0, 0));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_remove(2, -1));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_move(2, 0, 0));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_move(2, 3, 1));
    PP_EXPECT(harness, !pp::app::plan_brush_preset_list_clear(-1));
}

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 texture_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& harness)
{
    FakeBrushTextureListServices services;

    const auto add = pp::app::plan_brush_texture_list_add("brushes", "D:/Paint/data", "C:/Temp/soft.png");
    PP_EXPECT(harness, add);
    if (add) {
        PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(add.value(), services).ok());
    }

    const auto remove = pp::app::plan_brush_texture_list_remove(3, 2, true);
    PP_EXPECT(harness, remove);
    if (remove) {
        PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(remove.value(), services).ok());
    }

    const auto move = pp::app::plan_brush_texture_list_move(3, 1, -1);
    PP_EXPECT(harness, move);
    if (move) {
        PP_EXPECT(harness, pp::app::execute_brush_texture_list_plan(move.value(), services).ok());
    }

    PP_EXPECT(harness, services.adds == 1);
    PP_EXPECT(harness, services.last_source_path == "C:/Temp/soft.png");
    PP_EXPECT(harness, services.last_high_path == "D:/Paint/data/brushes/soft.png");
    PP_EXPECT(harness, services.last_thumbnail_path == "D:/Paint/data/brushes/thumbs/soft.png");
    PP_EXPECT(harness, services.last_brush_name == "soft");
    PP_EXPECT(harness, services.last_converts_brush_alpha);
    PP_EXPECT(harness, services.removes == 1);
    PP_EXPECT(harness, services.moves == 1);
    PP_EXPECT(harness, services.selections == 1);
    PP_EXPECT(harness, services.saves == 3);
    PP_EXPECT(harness, services.last_deletes_texture_files);
    PP_EXPECT(harness, services.call_order == "add;save;remove;select;save;move;save;");

    FakeBrushTextureListServices failing_services;
    failing_services.fail_add = true;
    PP_EXPECT(harness, add);
    if (add) {
        PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add.value(), failing_services).ok());
    }
    PP_EXPECT(harness, failing_services.saves == 0);
    PP_EXPECT(harness, failing_services.call_order == "add-failed;");
}

void preset_list_executor_dispatches_and_preserves_failure(pp::tests::Harness& harness)
{
    FakeBrushPresetListServices services;

    const auto add = pp::app::plan_brush_preset_list_add(2, true);
    PP_EXPECT(harness, add);
    if (add) {
        PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(add.value(), services).ok());
    }

    const auto select = pp::app::plan_brush_preset_list_select(3, 1);
    PP_EXPECT(harness, select);
    if (select) {
        PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(select.value(), services).ok());
    }

    const auto move = pp::app::plan_brush_preset_list_move(3, 2, -1);
    PP_EXPECT(harness, move);
    if (move) {
        PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(move.value(), services).ok());
    }

    const auto remove = pp::app::plan_brush_preset_list_remove(3, 1);
    PP_EXPECT(harness, remove);
    if (remove) {
        PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(remove.value(), services).ok());
    }

    const auto clear = pp::app::plan_brush_preset_list_clear(0);
    PP_EXPECT(harness, clear);
    if (clear) {
        PP_EXPECT(harness, pp::app::execute_brush_preset_list_plan(clear.value(), services).ok());
    }

    PP_EXPECT(harness, services.adds == 1);
    PP_EXPECT(harness, services.selections == 1);
    PP_EXPECT(harness, services.moves == 1);
    PP_EXPECT(harness, services.removes == 1);
    PP_EXPECT(harness, services.clears == 1);
    PP_EXPECT(harness, services.notification_updates == 3);
    PP_EXPECT(harness, services.saves == 4);
    PP_EXPECT(harness, services.last_index == 1);
    PP_EXPECT(harness, services.last_target_index == 1);
    PP_EXPECT(harness, services.last_selects_target);
    PP_EXPECT(harness, !services.last_remove_clears_selection);
    PP_EXPECT(harness, services.last_clear_clears_selection);
    PP_EXPECT(harness, services.last_notifies_brush_changed);
    PP_EXPECT(
        harness,
        services.call_order == "add;notification;save;select;move;save;remove;notification;save;clear;"
            "notification;save;");

    FakeBrushPresetListServices failing_services;
    failing_services.fail_add = true;
    PP_EXPECT(harness, add);
    if (add) {
        PP_EXPECT(harness, !pp::app::execute_brush_preset_list_plan(add.value(), failing_services).ok());
    }
    PP_EXPECT(harness, failing_services.saves == 0);
    PP_EXPECT(harness, failing_services.call_order == "add-failed;");
}

void stroke_control_executor_dispatches_and_rejects_bad_payloads(pp::tests::Harness& harness)
{
    FakeBrushStrokeControlServices services;

    const auto slider = pp::app::plan_brush_stroke_float_setting(
        pp::app::BrushStrokeFloatSetting::pattern_depth,
        0.75F);
    PP_EXPECT(harness, slider);
    if (slider) {
        PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(slider.value(), services).ok());
    }

    const auto checkbox = pp::app::plan_brush_stroke_bool_setting(
        pp::app::BrushStrokeBoolSetting::dual_enabled,
        true);
    PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(checkbox, services).ok());

    const auto blend = pp::app::plan_brush_stroke_blend_mode(pp::app::BrushStrokeBlendSetting::pattern, 3);
    PP_EXPECT(harness, blend);
    if (blend) {
        PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(blend.value(), services).ok());
    }

    PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_tip_aspect_reset(), services).ok());
    PP_EXPECT(harness, pp::app::execute_brush_stroke_control_plan(pp::app::plan_brush_default_settings_reset(), services).ok());

    PP_EXPECT(harness, services.float_sets == 1);
    PP_EXPECT(harness, services.last_float_setting == pp::app::BrushStrokeFloatSetting::pattern_depth);
    PP_EXPECT(harness, services.last_bool_setting == pp::app::BrushStrokeBoolSetting::dual_enabled);
    PP_EXPECT(harness, services.last_blend_setting == pp::app::BrushStrokeBlendSetting::pattern);
    PP_EXPECT(harness, services.last_blend_mode == 3);
    PP_EXPECT(harness, services.tip_aspect_resets == 1);
    PP_EXPECT(harness, services.default_resets == 1);
    PP_EXPECT(harness, services.preview_refreshes == 5);
    PP_EXPECT(harness, services.control_updates == 1);
    PP_EXPECT(harness, services.stroke_notifications == 5);
    PP_EXPECT(
        harness,
        services.call_order == "float;preview;notify;bool;preview;notify;blend;preview;notify;tip-aspect;"
            "preview;notify;default;controls;preview;notify;");

    FakeBrushStrokeControlServices bad_services;
    pp::app::BrushStrokeControlPlan bad_float;
    bad_float.operation = pp::app::BrushStrokeControlOperation::set_float;
    bad_float.float_value = std::nanf("");
    PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_float, bad_services).ok());

    pp::app::BrushStrokeControlPlan bad_blend;
    bad_blend.operation = pp::app::BrushStrokeControlOperation::set_blend_mode;
    bad_blend.blend_mode = 99;
    PP_EXPECT(harness, !pp::app::execute_brush_stroke_control_plan(bad_blend, bad_services).ok());
    PP_EXPECT(harness, bad_services.call_order.empty());
}

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);

    FakeBrushTextureListServices list_services;
    pp::app::BrushTextureListPlan add;
    add.operation = pp::app::BrushTextureListOperation::add_texture;
    add.source_path = "source.png";
    PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(add, list_services).ok());

    pp::app::BrushTextureListPlan move;
    move.operation = pp::app::BrushTextureListOperation::move_texture;
    move.item_count = 1;
    move.current_index = 0;
    move.target_index = 1;
    PP_EXPECT(harness, !pp::app::execute_brush_texture_list_plan(move, list_services).ok());
    PP_EXPECT(harness, list_services.call_order.empty());

    FakeBrushPresetListServices preset_services;
    pp::app::BrushPresetListPlan remove_preset;
    remove_preset.operation = pp::app::BrushPresetListOperation::remove_preset;
    remove_preset.item_count = 1;
    remove_preset.current_index = 0;
    remove_preset.target_index = 1;
    remove_preset.selects_target = true;
    PP_EXPECT(harness, !pp::app::execute_brush_preset_list_plan(remove_preset, preset_services).ok());

    pp::app::BrushPresetListPlan select_preset;
    select_preset.operation = pp::app::BrushPresetListOperation::select_preset;
    select_preset.item_count = 1;
    select_preset.target_index = 1;
    PP_EXPECT(harness, !pp::app::execute_brush_preset_list_plan(select_preset, preset_services).ok());
    PP_EXPECT(harness, preset_services.call_order.empty());
}

} // 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("brush UI refresh projects requested surfaces", brush_ui_refresh_projects_requested_surfaces);
    harness.run("brush UI refresh rejects invalid state", brush_ui_refresh_rejects_invalid_state);
    harness.run("stroke control plans validate values and reject breaking points", stroke_control_plans_validate_values_and_reject_breaking_points);
    harness.run("stroke panel view projects brush state", stroke_panel_view_projects_brush_state);
    harness.run("stroke panel view rejects invalid brush state", stroke_panel_view_rejects_invalid_brush_state);
    harness.run("texture list add plans target paths and rejects bad input", texture_list_add_plans_target_paths_and_rejects_bad_input);
    harness.run("texture list remove and move plans handle edges", texture_list_remove_and_move_plans_handle_edges);
    harness.run("preset list plans add select move remove and clear", preset_list_plans_add_select_move_remove_and_clear);
    harness.run("preset list plans reject breaking points", preset_list_plans_reject_breaking_points);
    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("texture list executor dispatches and preserves failure", texture_list_executor_dispatches_and_preserves_failure);
    harness.run("preset list executor dispatches and preserves failure", preset_list_executor_dispatches_and_preserves_failure);
    harness.run("stroke control executor dispatches and rejects bad payloads", stroke_control_executor_dispatches_and_rejects_bad_payloads);
    harness.run("executor rejects invalid plan payloads", executor_rejects_invalid_plan_payloads);
    return harness.finish();
}
