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

#include <cmath>
#include <string>

namespace {

class FakeCanvasToolServices final : public pp::app::CanvasToolServices {
public:
    void select_toolbar_button(pp::app::CanvasToolMode mode) override
    {
        toolbar_selections += 1;
        last_mode = mode;
        call_order += "select;";
    }

    void set_transform_action(pp::app::CanvasToolTransformAction action) override
    {
        transform_sets += 1;
        last_transform_action = action;
        call_order += "transform;";
    }

    void set_canvas_mode(pp::app::CanvasToolMode mode) override
    {
        mode_sets += 1;
        last_mode = mode;
        call_order += "mode;";
    }

    void toggle_picking() override
    {
        picking_toggles += 1;
        call_order += "pick;";
    }

    void toggle_touch_lock() override
    {
        touch_lock_toggles += 1;
        call_order += "touch;";
    }

    int toolbar_selections = 0;
    int transform_sets = 0;
    int mode_sets = 0;
    int picking_toggles = 0;
    int touch_lock_toggles = 0;
    pp::app::CanvasToolMode last_mode = pp::app::CanvasToolMode::draw;
    pp::app::CanvasToolTransformAction last_transform_action = pp::app::CanvasToolTransformAction::none;
    std::string call_order;
};

void selection_plans_canvas_modes(pp::tests::Harness& harness)
{
    const auto draw = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw);
    PP_EXPECT(harness, draw.operation == pp::app::CanvasToolOperation::select_mode);
    PP_EXPECT(harness, draw.mode == pp::app::CanvasToolMode::draw);
    PP_EXPECT(harness, draw.transform_action == pp::app::CanvasToolTransformAction::none);
    PP_EXPECT(harness, draw.selects_toolbar_button);
    PP_EXPECT(harness, draw.updates_canvas_mode);
    PP_EXPECT(harness, !draw.no_op);

    const auto mask_line = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::mask_line);
    PP_EXPECT(harness, mask_line.mode == pp::app::CanvasToolMode::mask_line);
    PP_EXPECT(harness, mask_line.updates_canvas_mode);
}

void transform_tools_plan_copy_and_cut_actions(pp::tests::Harness& harness)
{
    const auto copy = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy);
    PP_EXPECT(harness, copy.mode == pp::app::CanvasToolMode::copy);
    PP_EXPECT(harness, copy.transform_action == pp::app::CanvasToolTransformAction::copy);
    PP_EXPECT(harness, copy.updates_canvas_mode);

    const auto cut = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::cut);
    PP_EXPECT(harness, cut.mode == pp::app::CanvasToolMode::cut);
    PP_EXPECT(harness, cut.transform_action == pp::app::CanvasToolTransformAction::cut);
    PP_EXPECT(harness, cut.updates_canvas_mode);
}

void pick_and_touch_lock_toggle_state(pp::tests::Harness& harness)
{
    const auto pick_active = pp::app::plan_canvas_tool_pick_toggle(true);
    PP_EXPECT(harness, pick_active.operation == pp::app::CanvasToolOperation::toggle_picking);
    PP_EXPECT(harness, pick_active.requires_draw_mode);
    PP_EXPECT(harness, pick_active.toggles_picking);
    PP_EXPECT(harness, !pick_active.no_op);

    const auto pick_noop = pp::app::plan_canvas_tool_pick_toggle(false);
    PP_EXPECT(harness, pick_noop.requires_draw_mode);
    PP_EXPECT(harness, !pick_noop.toggles_picking);
    PP_EXPECT(harness, pick_noop.no_op);

    const auto touch_lock = pp::app::plan_canvas_tool_touch_lock_toggle();
    PP_EXPECT(harness, touch_lock.operation == pp::app::CanvasToolOperation::toggle_touch_lock);
    PP_EXPECT(harness, touch_lock.toggles_touch_lock);
    PP_EXPECT(harness, !touch_lock.no_op);
}

void toolbar_plan_exposes_full_legacy_button_set(pp::tests::Harness& harness)
{
    const auto toolbar = pp::app::plan_canvas_tool_toolbar();
    PP_EXPECT(harness, toolbar.bindings.size() == 13);
    PP_EXPECT(harness, toolbar.default_mode == pp::app::CanvasToolMode::draw);
    PP_EXPECT(harness, toolbar.bindings[0].button_id == "btn-pen");
    PP_EXPECT(harness, toolbar.bindings[0].mode == pp::app::CanvasToolMode::draw);
    PP_EXPECT(harness, toolbar.bindings[0].applies_default_on_init);
    PP_EXPECT(harness, toolbar.bindings[1].button_id == "btn-pick");
    PP_EXPECT(harness, toolbar.bindings[1].action == pp::app::CanvasToolToolbarAction::toggle_picking);
    PP_EXPECT(harness, toolbar.bindings[2].button_id == "btn-touchlock");
    PP_EXPECT(harness, toolbar.bindings[2].action == pp::app::CanvasToolToolbarAction::toggle_touch_lock);
    PP_EXPECT(harness, toolbar.bindings[5].button_id == "btn-cam");
    PP_EXPECT(harness, !toolbar.bindings[5].custom_button);
    PP_EXPECT(harness, toolbar.bindings[12].button_id == "btn-bucket");
    PP_EXPECT(harness, toolbar.bindings[12].mode == pp::app::CanvasToolMode::flood_fill);
}

void toolbar_binding_actions_match_canvas_tool_plans(pp::tests::Harness& harness)
{
    const auto toolbar = pp::app::plan_canvas_tool_toolbar();
    const auto draw = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[0], true);
    const auto pick_from_draw = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[1], true);
    const auto pick_from_camera = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[1], false);
    const auto touch_lock = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[2], false);
    const auto copy = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[7], false);
    const auto cut = pp::app::plan_canvas_tool_toolbar_binding_action(toolbar.bindings[8], false);

    PP_EXPECT(harness, draw.operation == pp::app::CanvasToolOperation::select_mode);
    PP_EXPECT(harness, draw.mode == pp::app::CanvasToolMode::draw);
    PP_EXPECT(harness, pick_from_draw.operation == pp::app::CanvasToolOperation::toggle_picking);
    PP_EXPECT(harness, pick_from_draw.toggles_picking);
    PP_EXPECT(harness, !pick_from_draw.no_op);
    PP_EXPECT(harness, pick_from_camera.no_op);
    PP_EXPECT(harness, !pick_from_camera.toggles_picking);
    PP_EXPECT(harness, touch_lock.operation == pp::app::CanvasToolOperation::toggle_touch_lock);
    PP_EXPECT(harness, touch_lock.toggles_touch_lock);
    PP_EXPECT(harness, copy.transform_action == pp::app::CanvasToolTransformAction::copy);
    PP_EXPECT(harness, cut.transform_action == pp::app::CanvasToolTransformAction::cut);
}

void button_state_tracks_active_mode_and_toggles(pp::tests::Harness& harness)
{
    const auto draw = pp::app::plan_canvas_tool_button_state(
        pp::app::CanvasToolMode::draw,
        true,
        true);
    PP_EXPECT(harness, draw.mode == pp::app::CanvasToolMode::draw);
    PP_EXPECT(harness, draw.pick_active);
    PP_EXPECT(harness, draw.touch_lock_active);
    PP_EXPECT(harness, draw.pen_active);
    PP_EXPECT(harness, !draw.erase_active);
    PP_EXPECT(harness, !draw.copy_active);
    PP_EXPECT(harness, !draw.flood_fill_active);

    const auto copy = pp::app::plan_canvas_tool_button_state(
        pp::app::CanvasToolMode::copy,
        true,
        false);
    PP_EXPECT(harness, copy.copy_active);
    PP_EXPECT(harness, !copy.pick_active);
    PP_EXPECT(harness, !copy.touch_lock_active);
    PP_EXPECT(harness, !copy.pen_active);

    const auto bucket = pp::app::plan_canvas_tool_button_state(
        pp::app::CanvasToolMode::flood_fill,
        false,
        false);
    PP_EXPECT(harness, bucket.flood_fill_active);
    PP_EXPECT(harness, !bucket.mask_line_active);
}

void cursor_visibility_projects_canvas_state(pp::tests::Harness& harness)
{
    const auto non_paint = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::camera,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::never,
    });
    PP_EXPECT(harness, non_paint);
    if (non_paint) {
        PP_EXPECT(harness, non_paint.value().visible);
        PP_EXPECT(harness, !non_paint.value().paint_mode);
    }

    const auto small = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::draw,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
        .has_current_brush = true,
        .brush_tip_size = 9.5F,
    });
    PP_EXPECT(harness, small);
    if (small) {
        PP_EXPECT(harness, small.value().visible);
        PP_EXPECT(harness, small.value().paint_mode);
        PP_EXPECT(harness, small.value().uses_brush_size);
    }

    const auto large = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::erase,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
        .has_current_brush = true,
        .brush_tip_size = 10.0F,
    });
    PP_EXPECT(harness, large);
    if (large) {
        PP_EXPECT(harness, !large.value().visible);
        PP_EXPECT(harness, large.value().uses_brush_size);
    }

    const auto painting = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::draw,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::not_painting,
        .pen_is_drawing = true,
    });
    PP_EXPECT(harness, painting);
    if (painting) {
        PP_EXPECT(harness, !painting.value().visible);
        PP_EXPECT(harness, painting.value().uses_pen_state);
    }
}

void cursor_visibility_forces_visible_for_modifier_or_tool(pp::tests::Harness& harness)
{
    const auto alt = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::draw,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::never,
        .alt_down = true,
    });
    PP_EXPECT(harness, alt);
    if (alt) {
        PP_EXPECT(harness, alt.value().visible);
        PP_EXPECT(harness, alt.value().forced_visible_by_modifier_or_tool);
    }

    const auto picking = pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::erase,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::not_painting,
        .pen_is_drawing = true,
        .pen_is_picking = true,
    });
    PP_EXPECT(harness, picking);
    if (picking) {
        PP_EXPECT(harness, picking.value().visible);
        PP_EXPECT(harness, picking.value().forced_visible_by_modifier_or_tool);
    }
}

void cursor_visibility_rejects_invalid_brush_state(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::draw,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
        .has_current_brush = false,
    }));
    PP_EXPECT(harness, !pp::app::plan_canvas_cursor_visibility(pp::app::CanvasCursorVisibilityInput {
        .mode = pp::app::CanvasToolMode::draw,
        .visibility_mode = pp::app::CanvasCursorVisibilityMode::small_brush,
        .has_current_brush = true,
        .brush_tip_size = std::nanf(""),
    }));
}

void executor_dispatches_tool_actions(pp::tests::Harness& harness)
{
    FakeCanvasToolServices services;

    PP_EXPECT(harness, pp::app::execute_canvas_tool_plan(
        pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy),
        services).ok());
    PP_EXPECT(harness, pp::app::execute_canvas_tool_plan(
        pp::app::plan_canvas_tool_pick_toggle(true),
        services).ok());
    PP_EXPECT(harness, pp::app::execute_canvas_tool_plan(
        pp::app::plan_canvas_tool_touch_lock_toggle(),
        services).ok());

    PP_EXPECT(harness, services.toolbar_selections == 1);
    PP_EXPECT(harness, services.transform_sets == 1);
    PP_EXPECT(harness, services.mode_sets == 1);
    PP_EXPECT(harness, services.picking_toggles == 1);
    PP_EXPECT(harness, services.touch_lock_toggles == 1);
    PP_EXPECT(harness, services.last_mode == pp::app::CanvasToolMode::copy);
    PP_EXPECT(harness, services.last_transform_action == pp::app::CanvasToolTransformAction::copy);
    PP_EXPECT(harness, services.call_order == "select;transform;mode;pick;touch;");
}

void executor_no_ops_pick_when_not_in_draw_mode(pp::tests::Harness& harness)
{
    FakeCanvasToolServices services;

    PP_EXPECT(harness, pp::app::execute_canvas_tool_plan(
        pp::app::plan_canvas_tool_pick_toggle(false),
        services).ok());

    PP_EXPECT(harness, services.toolbar_selections == 0);
    PP_EXPECT(harness, services.mode_sets == 0);
    PP_EXPECT(harness, services.picking_toggles == 0);
    PP_EXPECT(harness, services.call_order.empty());
}

void executor_rejects_malformed_plans(pp::tests::Harness& harness)
{
    FakeCanvasToolServices services;

    auto select = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::copy);
    select.transform_action = pp::app::CanvasToolTransformAction::cut;
    PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(select, services).ok());

    auto missing_update = pp::app::plan_canvas_tool_select(pp::app::CanvasToolMode::draw);
    missing_update.updates_canvas_mode = false;
    PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(missing_update, services).ok());

    pp::app::CanvasToolPlan pick;
    pick.operation = pp::app::CanvasToolOperation::toggle_picking;
    pick.requires_draw_mode = false;
    pick.toggles_picking = true;
    PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(pick, services).ok());

    pp::app::CanvasToolPlan touch;
    touch.operation = pp::app::CanvasToolOperation::toggle_touch_lock;
    touch.toggles_touch_lock = false;
    PP_EXPECT(harness, !pp::app::execute_canvas_tool_plan(touch, services).ok());

    PP_EXPECT(harness, services.toolbar_selections == 0);
    PP_EXPECT(harness, services.transform_sets == 0);
    PP_EXPECT(harness, services.mode_sets == 0);
    PP_EXPECT(harness, services.picking_toggles == 0);
    PP_EXPECT(harness, services.touch_lock_toggles == 0);
}

} // namespace

int main()
{
    pp::tests::Harness harness;
    harness.run("selection plans canvas modes", selection_plans_canvas_modes);
    harness.run("transform tools plan copy and cut actions", transform_tools_plan_copy_and_cut_actions);
    harness.run("pick and touch lock toggle state", pick_and_touch_lock_toggle_state);
    harness.run("toolbar plan exposes full legacy button set", toolbar_plan_exposes_full_legacy_button_set);
    harness.run("toolbar binding actions match canvas tool plans", toolbar_binding_actions_match_canvas_tool_plans);
    harness.run("button state tracks active mode and toggles", button_state_tracks_active_mode_and_toggles);
    harness.run("cursor visibility projects canvas state", cursor_visibility_projects_canvas_state);
    harness.run("cursor visibility forces visible for modifier or tool", cursor_visibility_forces_visible_for_modifier_or_tool);
    harness.run("cursor visibility rejects invalid brush state", cursor_visibility_rejects_invalid_brush_state);
    harness.run("executor dispatches tool actions", executor_dispatches_tool_actions);
    harness.run("executor no-ops pick when not in draw mode", executor_no_ops_pick_when_not_in_draw_mode);
    harness.run("executor rejects malformed plans", executor_rejects_malformed_plans);
    return harness.finish();
}
