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

#include <limits>
#include <string>
#include <vector>

#define PP_REQUIRE(harness, expression) \
    do { \
        const bool pp_require_ok = static_cast<bool>(expression); \
        (harness).expect(pp_require_ok, #expression, __FILE__, __LINE__); \
        if (!pp_require_ok) { \
            return; \
        } \
    } while (false)

namespace {

class FakeDocumentAnimationServices final : public pp::app::DocumentAnimationServices {
public:
    void add_frame() override
    {
        adds += 1;
        call_order += "add;";
    }

    void duplicate_frame(int selected_frame) override
    {
        duplicates += 1;
        last_selected_frame = selected_frame;
        call_order += "duplicate;";
    }

    void remove_frame(int selected_frame, int target_frame) override
    {
        removes += 1;
        last_selected_frame = selected_frame;
        last_target_frame = target_frame;
        call_order += "remove;";
    }

    void set_frame_duration(int selected_frame, int duration) override
    {
        duration_sets += 1;
        last_selected_frame = selected_frame;
        last_duration = duration;
        call_order += "duration;";
    }

    int move_frame(int selected_frame, int move_offset) override
    {
        moves += 1;
        last_selected_frame = selected_frame;
        last_move_offset = move_offset;
        call_order += "move;";
        return move_result;
    }

    void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) override
    {
        frame_selects += 1;
        last_layer_id = layer_id;
        last_layer_index = layer_index;
        last_selected_frame = selected_frame;
        call_order += "select-frame;";
    }

    void select_layer(int layer_index) override
    {
        layer_selects += 1;
        last_layer_index = layer_index;
        call_order += "select-layer;";
    }

    void goto_frame(int target_frame) override
    {
        gotos += 1;
        last_target_frame = target_frame;
        call_order += "goto;";
    }

    void set_timeline_frame(int target_frame) override
    {
        timeline_sets += 1;
        last_timeline_frame = target_frame;
        call_order += "timeline;";
    }

    void set_onion_size(int onion_size) override
    {
        onion_sets += 1;
        last_onion_size = onion_size;
        call_order += "onion;";
    }

    void capture_playback_restore_mode() override
    {
        playback_restore_captures += 1;
        call_order += "capture-mode;";
    }

    void enter_playback_camera_mode() override
    {
        playback_camera_entries += 1;
        call_order += "camera-mode;";
    }

    void restore_playback_canvas_mode() override
    {
        playback_mode_restores += 1;
        call_order += "restore-mode;";
    }

    void set_playback_active(bool active) override
    {
        playback_active_sets += 1;
        last_playback_active = active;
        call_order += active ? "play-active;" : "play-inactive;";
    }

    void reset_playback_timer() override
    {
        playback_timer_resets += 1;
        call_order += "reset-timer;";
    }

    void set_playback_idle_ms(int idle_ms) override
    {
        playback_idle_sets += 1;
        last_playback_idle_ms = idle_ms;
        call_order += "idle;";
    }

    void update_canvas_animation() override
    {
        canvas_updates += 1;
        call_order += "update;";
    }

    void update_frame_status() override
    {
        frame_status_updates += 1;
        call_order += "frame-status;";
    }

    void reload_animation_layers() override
    {
        reloads += 1;
        call_order += "reload;";
    }

    void mark_unsaved() override
    {
        unsaved_marks += 1;
        call_order += "unsaved;";
    }

    int adds = 0;
    int duplicates = 0;
    int removes = 0;
    int duration_sets = 0;
    int moves = 0;
    int frame_selects = 0;
    int layer_selects = 0;
    int gotos = 0;
    int timeline_sets = 0;
    int onion_sets = 0;
    int playback_restore_captures = 0;
    int playback_camera_entries = 0;
    int playback_mode_restores = 0;
    int playback_active_sets = 0;
    int playback_timer_resets = 0;
    int playback_idle_sets = 0;
    int canvas_updates = 0;
    int frame_status_updates = 0;
    int reloads = 0;
    int unsaved_marks = 0;
    int last_selected_frame = -1;
    int last_target_frame = -1;
    int last_timeline_frame = -1;
    int last_duration = -1;
    int last_move_offset = 0;
    int last_onion_size = -1;
    int last_layer_index = -1;
    int last_playback_idle_ms = 0;
    std::uint32_t last_layer_id = 0;
    bool last_playback_active = false;
    int move_result = 0;
    std::string call_order;
};

void add_duplicate_and_remove_validate_frame_bounds(pp::tests::Harness& harness)
{
    const auto add = pp::app::plan_animation_add_frame(2, 0);
    PP_REQUIRE(harness, add);
    PP_EXPECT(harness, add.value().operation == pp::app::DocumentAnimationOperation::add_frame);
    PP_EXPECT(harness, add.value().selected_frame == 2);
    PP_EXPECT(harness, add.value().mutates_document);
    PP_EXPECT(harness, add.value().reloads_animation_layers);
    PP_EXPECT(harness, add.value().updates_canvas_animation);
    PP_EXPECT(harness, add.value().marks_unsaved);

    const auto duplicate = pp::app::plan_animation_duplicate_frame(3, 1);
    PP_REQUIRE(harness, duplicate);
    PP_EXPECT(harness, duplicate.value().operation == pp::app::DocumentAnimationOperation::duplicate_frame);
    PP_EXPECT(harness, duplicate.value().target_frame == 2);
    PP_EXPECT(harness, duplicate.value().requires_selected_frame);

    const auto remove = pp::app::plan_animation_remove_frame(3, 2);
    PP_REQUIRE(harness, remove);
    PP_EXPECT(harness, remove.value().operation == pp::app::DocumentAnimationOperation::remove_frame);
    PP_EXPECT(harness, remove.value().target_frame == 1);

    PP_EXPECT(harness, !pp::app::plan_animation_add_frame(0, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_add_frame(1, -1));
    PP_EXPECT(harness, !pp::app::plan_animation_duplicate_frame(2, 2));
    PP_EXPECT(harness, !pp::app::plan_animation_remove_frame(1, 0));
}

void duration_plans_clamp_floor_and_reject_overflow(pp::tests::Harness& harness)
{
    const auto up = pp::app::plan_animation_adjust_duration(2, 1, 4, 1);
    PP_REQUIRE(harness, up);
    PP_EXPECT(harness, up.value().operation == pp::app::DocumentAnimationOperation::adjust_duration);
    PP_EXPECT(harness, up.value().frame_duration == 5);
    PP_EXPECT(harness, up.value().mutates_document);

    const auto down = pp::app::plan_animation_adjust_duration(2, 1, 1, -1);
    PP_REQUIRE(harness, down);
    PP_EXPECT(harness, down.value().frame_duration == 1);
    PP_EXPECT(harness, !down.value().mutates_document);
    PP_EXPECT(harness, !down.value().marks_unsaved);

    PP_EXPECT(harness, !pp::app::plan_animation_adjust_duration(2, 1, 0, 1));
    PP_EXPECT(harness, !pp::app::plan_animation_adjust_duration(2, 1, 2, 0));
    PP_EXPECT(
        harness,
        !pp::app::plan_animation_adjust_duration(2, 1, std::numeric_limits<int>::max(), 1));
}

void move_and_timeline_plans_handle_edges(pp::tests::Harness& harness)
{
    const auto left_edge = pp::app::plan_animation_move_frame(3, 0, -1);
    PP_REQUIRE(harness, left_edge);
    PP_EXPECT(harness, left_edge.value().operation == pp::app::DocumentAnimationOperation::move_frame);
    PP_EXPECT(harness, left_edge.value().target_frame == 0);
    PP_EXPECT(harness, !left_edge.value().mutates_document);
    PP_EXPECT(harness, left_edge.value().reloads_animation_layers);

    const auto right = pp::app::plan_animation_move_frame(3, 1, 1);
    PP_REQUIRE(harness, right);
    PP_EXPECT(harness, right.value().target_frame == 2);
    PP_EXPECT(harness, right.value().mutates_document);

    const auto huge_left = pp::app::plan_animation_move_frame(3, 1, std::numeric_limits<int>::min());
    PP_REQUIRE(harness, huge_left);
    PP_EXPECT(harness, huge_left.value().target_frame == 0);

    const auto huge_right = pp::app::plan_animation_move_frame(3, 1, std::numeric_limits<int>::max());
    PP_REQUIRE(harness, huge_right);
    PP_EXPECT(harness, huge_right.value().target_frame == 2);

    const auto go = pp::app::plan_animation_goto_frame(5, 3);
    PP_REQUIRE(harness, go);
    PP_EXPECT(harness, go.value().operation == pp::app::DocumentAnimationOperation::goto_frame);
    PP_EXPECT(harness, go.value().target_frame == 3);
    PP_EXPECT(harness, !go.value().mutates_document);
    PP_EXPECT(harness, go.value().updates_canvas_animation);

    const auto next = pp::app::plan_animation_step_frame(5, 4, 1);
    PP_REQUIRE(harness, next);
    PP_EXPECT(harness, next.value().operation == pp::app::DocumentAnimationOperation::goto_next);
    PP_EXPECT(harness, next.value().target_frame == 0);

    const auto prev = pp::app::plan_animation_step_frame(5, 0, -1);
    PP_REQUIRE(harness, prev);
    PP_EXPECT(harness, prev.value().operation == pp::app::DocumentAnimationOperation::goto_previous);
    PP_EXPECT(harness, prev.value().target_frame == 4);

    PP_EXPECT(harness, !pp::app::plan_animation_move_frame(3, 1, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_goto_frame(5, 5));
    PP_EXPECT(harness, !pp::app::plan_animation_step_frame(0, 0, 1));
}

void frame_selection_and_playback_plans_keep_ui_refresh_scoped(pp::tests::Harness& harness)
{
    const auto select = pp::app::plan_animation_select_frame(3, 2, 42, 1);
    PP_REQUIRE(harness, select);
    PP_EXPECT(harness, select.value().operation == pp::app::DocumentAnimationOperation::select_frame);
    PP_EXPECT(harness, select.value().layer_index == 2);
    PP_EXPECT(harness, select.value().layer_id == 42);
    PP_EXPECT(harness, select.value().selected_frame == 1);
    PP_EXPECT(harness, select.value().target_frame == 1);
    PP_EXPECT(harness, select.value().updates_canvas_animation);
    PP_EXPECT(harness, !select.value().reloads_animation_layers);
    PP_EXPECT(harness, !select.value().mutates_document);

    const auto playback = pp::app::plan_animation_playback_step(4, 3, 1);
    PP_REQUIRE(harness, playback);
    PP_EXPECT(harness, playback.value().operation == pp::app::DocumentAnimationOperation::playback_step);
    PP_EXPECT(harness, playback.value().target_frame == 0);
    PP_EXPECT(harness, playback.value().move_offset == 1);
    PP_EXPECT(harness, playback.value().updates_canvas_animation);
    PP_EXPECT(harness, !playback.value().reloads_animation_layers);
    PP_EXPECT(harness, !playback.value().mutates_document);

    PP_EXPECT(harness, !pp::app::plan_animation_select_frame(2, -1, 42, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_select_frame(2, 0, 42, 2));
    PP_EXPECT(harness, !pp::app::plan_animation_playback_step(0, 0, 1));
    PP_EXPECT(harness, !pp::app::plan_animation_playback_step(3, 0, 0));
}

void playback_toggle_plans_capture_start_and_stop_intent(pp::tests::Harness& harness)
{
    const auto start = pp::app::plan_animation_playback_toggle(false);
    PP_REQUIRE(harness, start);
    PP_EXPECT(harness, start.value().operation == pp::app::DocumentAnimationOperation::toggle_playback);
    PP_EXPECT(harness, !start.value().playback_was_active);
    PP_EXPECT(harness, start.value().playback_active);
    PP_EXPECT(harness, start.value().resets_playback_timer);
    PP_EXPECT(harness, start.value().playback_idle_ms == 10);

    const auto stop = pp::app::plan_animation_playback_toggle(true);
    PP_REQUIRE(harness, stop);
    PP_EXPECT(harness, stop.value().operation == pp::app::DocumentAnimationOperation::toggle_playback);
    PP_EXPECT(harness, stop.value().playback_was_active);
    PP_EXPECT(harness, !stop.value().playback_active);
    PP_EXPECT(harness, !stop.value().resets_playback_timer);
    PP_EXPECT(harness, stop.value().playback_idle_ms == 100);
}

void panel_actions_plan_timeline_and_playback_intent(pp::tests::Harness& harness)
{
    const pp::app::DocumentAnimationPanelState state {
        .total_duration = 5,
        .current_frame = 4,
        .playback_active = false,
    };

    const auto goto_frame = pp::app::plan_animation_panel_action(
        pp::app::DocumentAnimationPanelAction::goto_frame,
        state,
        2);
    PP_REQUIRE(harness, goto_frame);
    PP_EXPECT(harness, goto_frame.value().operation == pp::app::DocumentAnimationOperation::goto_frame);
    PP_EXPECT(harness, goto_frame.value().target_frame == 2);

    const auto next = pp::app::plan_animation_panel_action(
        pp::app::DocumentAnimationPanelAction::next_frame,
        state);
    PP_REQUIRE(harness, next);
    PP_EXPECT(harness, next.value().operation == pp::app::DocumentAnimationOperation::goto_next);
    PP_EXPECT(harness, next.value().target_frame == 0);

    const auto previous = pp::app::plan_animation_panel_action(
        pp::app::DocumentAnimationPanelAction::previous_frame,
        state);
    PP_REQUIRE(harness, previous);
    PP_EXPECT(harness, previous.value().operation == pp::app::DocumentAnimationOperation::goto_previous);
    PP_EXPECT(harness, previous.value().target_frame == 3);

    const auto playback = pp::app::plan_animation_panel_action(
        pp::app::DocumentAnimationPanelAction::playback_step,
        state);
    PP_REQUIRE(harness, playback);
    PP_EXPECT(harness, playback.value().operation == pp::app::DocumentAnimationOperation::playback_step);
    PP_EXPECT(harness, playback.value().target_frame == 0);
    PP_EXPECT(harness, !playback.value().reloads_animation_layers);

    const auto play_toggle = pp::app::plan_animation_panel_action(
        pp::app::DocumentAnimationPanelAction::toggle_playback,
        state);
    PP_REQUIRE(harness, play_toggle);
    PP_EXPECT(harness, play_toggle.value().operation == pp::app::DocumentAnimationOperation::toggle_playback);
    PP_EXPECT(harness, play_toggle.value().playback_active);
    PP_EXPECT(harness, play_toggle.value().playback_idle_ms == 10);
}

void panel_actions_reject_invalid_timeline_state(pp::tests::Harness& harness)
{
    const pp::app::DocumentAnimationPanelState empty {
        .total_duration = 0,
        .current_frame = 0,
        .playback_active = false,
    };
    PP_EXPECT(
        harness,
        !pp::app::plan_animation_panel_action(pp::app::DocumentAnimationPanelAction::goto_frame, empty, 0));
    PP_EXPECT(
        harness,
        !pp::app::plan_animation_panel_action(pp::app::DocumentAnimationPanelAction::next_frame, empty));

    const pp::app::DocumentAnimationPanelState out_of_range {
        .total_duration = 3,
        .current_frame = 3,
        .playback_active = true,
    };
    PP_EXPECT(
        harness,
        !pp::app::plan_animation_panel_action(pp::app::DocumentAnimationPanelAction::playback_step, out_of_range));
    PP_EXPECT(
        harness,
        !pp::app::plan_animation_panel_action(
            pp::app::DocumentAnimationPanelAction::goto_frame,
            out_of_range,
            -1));

    const auto stop_playback = pp::app::plan_animation_panel_action(
        pp::app::DocumentAnimationPanelAction::toggle_playback,
        out_of_range);
    PP_REQUIRE(harness, stop_playback);
    PP_EXPECT(harness, stop_playback.value().operation == pp::app::DocumentAnimationOperation::toggle_playback);
    PP_EXPECT(harness, !stop_playback.value().playback_active);
    PP_EXPECT(harness, stop_playback.value().playback_idle_ms == 100);
}

void onion_size_updates_canvas_without_document_mutation(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_animation_onion_size(2);
    PP_REQUIRE(harness, plan);
    PP_EXPECT(harness, plan.value().operation == pp::app::DocumentAnimationOperation::set_onion_size);
    PP_EXPECT(harness, plan.value().onion_size == 2);
    PP_EXPECT(harness, plan.value().updates_canvas_animation);
    PP_EXPECT(harness, !plan.value().mutates_document);
    PP_EXPECT(harness, !plan.value().marks_unsaved);

    PP_EXPECT(harness, !pp::app::plan_animation_onion_size(-1));
}

void panel_view_projects_layers_frames_and_timeline_state(pp::tests::Harness& harness)
{
    std::vector<pp::app::DocumentAnimationLayerInput> layers;
    layers.push_back(pp::app::DocumentAnimationLayerInput {
        .layer_index = 0,
        .layer_id = 10,
        .name = "Base",
        .visible = true,
        .frame_durations = { 1, 2 },
    });
    layers.push_back(pp::app::DocumentAnimationLayerInput {
        .layer_index = 1,
        .layer_id = 20,
        .name = "Ink",
        .visible = false,
        .frame_durations = { 3 },
    });

    const auto view = pp::app::plan_animation_panel_view(
        layers,
        4,
        1,
        2,
        10,
        1,
        3);
    PP_REQUIRE(harness, view);
    PP_EXPECT(harness, view.value().total_duration == 4);
    PP_EXPECT(harness, view.value().current_frame == 2);
    PP_EXPECT(harness, view.value().onion_size == 3);
    PP_EXPECT(harness, view.value().has_selected_frame);
    PP_EXPECT(harness, view.value().layers.size() == 2);
    PP_EXPECT(harness, !view.value().layers[0].current);
    PP_EXPECT(harness, view.value().layers[1].current);
    PP_EXPECT(harness, view.value().layers[0].name == "Base");
    PP_EXPECT(harness, !view.value().layers[1].visible);
    PP_EXPECT(harness, view.value().layers[0].frames.size() == 2);
    PP_EXPECT(harness, view.value().layers[0].frames[0].duration == 1);
    PP_EXPECT(harness, view.value().layers[0].frames[1].duration == 2);
    PP_EXPECT(harness, !view.value().layers[0].frames[0].selected);
    PP_EXPECT(harness, view.value().layers[0].frames[1].selected);
    PP_EXPECT(harness, !view.value().layers[1].frames[0].selected);
}

void panel_view_preserves_stale_selection_as_no_active_frame(pp::tests::Harness& harness)
{
    const std::vector<pp::app::DocumentAnimationLayerInput> layers {
        pp::app::DocumentAnimationLayerInput {
            .layer_index = 0,
            .layer_id = 10,
            .name = "Base",
            .visible = true,
            .frame_durations = { 1 },
        },
    };

    const auto view = pp::app::plan_animation_panel_view(
        layers,
        1,
        0,
        0,
        999,
        42,
        0);
    PP_REQUIRE(harness, view);
    PP_EXPECT(harness, !view.value().has_selected_frame);
    PP_EXPECT(harness, !view.value().layers[0].frames[0].selected);
}

void panel_view_rejects_invalid_document_state(pp::tests::Harness& harness)
{
    const std::vector<pp::app::DocumentAnimationLayerInput> valid_layers {
        pp::app::DocumentAnimationLayerInput {
            .layer_index = 0,
            .layer_id = 10,
            .name = "Base",
            .visible = true,
            .frame_durations = { 1 },
        },
    };
    const std::vector<pp::app::DocumentAnimationLayerInput> empty_frames {
        pp::app::DocumentAnimationLayerInput {
            .layer_index = 0,
            .layer_id = 10,
            .name = "Base",
            .visible = true,
            .frame_durations = {},
        },
    };
    const std::vector<pp::app::DocumentAnimationLayerInput> bad_duration {
        pp::app::DocumentAnimationLayerInput {
            .layer_index = 0,
            .layer_id = 10,
            .name = "Base",
            .visible = true,
            .frame_durations = { 0 },
        },
    };

    PP_EXPECT(harness, !pp::app::plan_animation_panel_view({}, 1, 0, 0, 0, -1, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 0, 0, 0, 0, -1, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 1, 1, 0, 0, -1, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 1, 0, 1, 0, -1, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_panel_view(valid_layers, 1, 0, 0, 0, -1, -1));
    PP_EXPECT(harness, !pp::app::plan_animation_panel_view(empty_frames, 1, 0, 0, 0, -1, 0));
    PP_EXPECT(harness, !pp::app::plan_animation_panel_view(bad_duration, 1, 0, 0, 0, -1, 0));
}

void onion_frame_ranges_clamp_edges_and_alpha(pp::tests::Harness& harness)
{
    const auto center = pp::app::plan_animation_onion_frame_range(5, 2, 1);
    PP_REQUIRE(harness, center);
    PP_EXPECT(harness, center.value().first_frame == 1);
    PP_EXPECT(harness, center.value().last_frame == 3);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 2) == 1.0f);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 1) == 0.5f);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 3) == 0.5f);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(center.value(), 0) == 0.0f);

    const auto left = pp::app::plan_animation_onion_frame_range(5, 0, 3);
    PP_REQUIRE(harness, left);
    PP_EXPECT(harness, left.value().first_frame == 0);
    PP_EXPECT(harness, left.value().last_frame == 3);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(left.value(), 0) == 1.0f);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(left.value(), 3) == 0.25f);

    const auto right = pp::app::plan_animation_onion_frame_range(5, 4, 3);
    PP_REQUIRE(harness, right);
    PP_EXPECT(harness, right.value().first_frame == 1);
    PP_EXPECT(harness, right.value().last_frame == 4);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(right.value(), 1) == 0.25f);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(right.value(), 4) == 1.0f);

    const auto single = pp::app::plan_animation_onion_frame_range(1, 0, 8);
    PP_REQUIRE(harness, single);
    PP_EXPECT(harness, single.value().first_frame == 0);
    PP_EXPECT(harness, single.value().last_frame == 0);
    PP_EXPECT(harness, pp::app::animation_onion_frame_alpha(single.value(), 0) == 1.0f);
}

void onion_frame_ranges_reject_invalid_inputs(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(0, 0, 1));
    PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, -1, 1));
    PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, 3, 1));
    PP_EXPECT(harness, !pp::app::plan_animation_onion_frame_range(3, 1, -1));
}

void timeline_scrub_clamps_cursor_to_document_frame(pp::tests::Harness& harness)
{
    const auto left = pp::app::plan_animation_timeline_scrub(5, -0.01F);
    PP_REQUIRE(harness, left);
    PP_EXPECT(harness, left.value().target_frame == 0);

    const auto first = pp::app::plan_animation_timeline_scrub(5, 0.0F);
    PP_REQUIRE(harness, first);
    PP_EXPECT(harness, first.value().target_frame == 0);

    const auto second = pp::app::plan_animation_timeline_scrub(5, 35.0F);
    PP_REQUIRE(harness, second);
    PP_EXPECT(harness, second.value().target_frame == 1);

    const auto inside_last = pp::app::plan_animation_timeline_scrub(5, 174.99F);
    PP_REQUIRE(harness, inside_last);
    PP_EXPECT(harness, inside_last.value().target_frame == 4);

    const auto far_right = pp::app::plan_animation_timeline_scrub(5, 10000.0F);
    PP_REQUIRE(harness, far_right);
    PP_EXPECT(harness, far_right.value().target_frame == 4);

    const auto custom_width = pp::app::plan_animation_timeline_scrub(4, 21.0F, 10.0F);
    PP_REQUIRE(harness, custom_width);
    PP_EXPECT(harness, custom_width.value().target_frame == 2);
}

void timeline_scrub_rejects_invalid_inputs(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(0, 0.0F));
    PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, std::numeric_limits<float>::infinity()));
    PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, 0.0F, 0.0F));
    PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, 0.0F, -1.0F));
    PP_EXPECT(harness, !pp::app::plan_animation_timeline_scrub(3, 0.0F, std::numeric_limits<float>::infinity()));
}

void executor_dispatches_mutating_frame_operations(pp::tests::Harness& harness)
{
    FakeDocumentAnimationServices services;

    const auto add = pp::app::plan_animation_add_frame(2, 0);
    PP_REQUIRE(harness, add);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(add.value(), services).ok());

    const auto duplicate = pp::app::plan_animation_duplicate_frame(3, 1);
    PP_REQUIRE(harness, duplicate);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(duplicate.value(), services).ok());

    const auto remove = pp::app::plan_animation_remove_frame(3, 2);
    PP_REQUIRE(harness, remove);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(remove.value(), services).ok());

    PP_EXPECT(harness, services.adds == 1);
    PP_EXPECT(harness, services.duplicates == 1);
    PP_EXPECT(harness, services.removes == 1);
    PP_EXPECT(harness, services.unsaved_marks == 3);
    PP_EXPECT(harness, services.canvas_updates == 2);
    PP_EXPECT(harness, services.gotos == 1);
    PP_EXPECT(harness, services.reloads == 3);
    PP_EXPECT(harness, services.last_selected_frame == 2);
    PP_EXPECT(harness, services.last_target_frame == 1);
    PP_EXPECT(
        harness,
        services.call_order == "add;unsaved;update;reload;duplicate;unsaved;update;reload;remove;unsaved;goto;reload;");
}

void executor_dispatches_timeline_and_parameter_operations(pp::tests::Harness& harness)
{
    FakeDocumentAnimationServices services;
    services.move_result = 2;

    const auto duration = pp::app::plan_animation_adjust_duration(3, 1, 4, 1);
    PP_REQUIRE(harness, duration);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(duration.value(), services).ok());

    const auto duration_noop = pp::app::plan_animation_adjust_duration(3, 1, 1, -1);
    PP_REQUIRE(harness, duration_noop);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(duration_noop.value(), services).ok());

    const auto move = pp::app::plan_animation_move_frame(3, 1, 1);
    PP_REQUIRE(harness, move);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(move.value(), services).ok());

    const auto next = pp::app::plan_animation_step_frame(5, 4, 1);
    PP_REQUIRE(harness, next);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(next.value(), services).ok());

    const auto onion = pp::app::plan_animation_onion_size(3);
    PP_REQUIRE(harness, onion);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(onion.value(), services).ok());

    const auto select = pp::app::plan_animation_select_frame(3, 2, 42, 1);
    PP_REQUIRE(harness, select);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(select.value(), services).ok());

    const auto playback = pp::app::plan_animation_playback_step(5, 4, 1);
    PP_REQUIRE(harness, playback);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(playback.value(), services).ok());

    const auto play_start = pp::app::plan_animation_playback_toggle(false);
    PP_REQUIRE(harness, play_start);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(play_start.value(), services).ok());

    const auto play_stop = pp::app::plan_animation_playback_toggle(true);
    PP_REQUIRE(harness, play_stop);
    PP_EXPECT(harness, pp::app::execute_animation_operation_plan(play_stop.value(), services).ok());

    PP_EXPECT(harness, services.duration_sets == 1);
    PP_EXPECT(harness, services.last_duration == 5);
    PP_EXPECT(harness, services.moves == 1);
    PP_EXPECT(harness, services.last_move_offset == 1);
    PP_EXPECT(harness, services.frame_selects == 1);
    PP_EXPECT(harness, services.layer_selects == 1);
    PP_EXPECT(harness, services.gotos == 4);
    PP_EXPECT(harness, services.timeline_sets == 1);
    PP_EXPECT(harness, services.last_target_frame == 0);
    PP_EXPECT(harness, services.last_timeline_frame == 0);
    PP_EXPECT(harness, services.last_layer_index == 2);
    PP_EXPECT(harness, services.last_layer_id == 42);
    PP_EXPECT(harness, services.onion_sets == 1);
    PP_EXPECT(harness, services.last_onion_size == 3);
    PP_EXPECT(harness, services.unsaved_marks == 2);
    PP_EXPECT(harness, services.canvas_updates == 2);
    PP_EXPECT(harness, services.frame_status_updates == 1);
    PP_EXPECT(harness, services.reloads == 3);
    PP_EXPECT(harness, services.playback_restore_captures == 1);
    PP_EXPECT(harness, services.playback_camera_entries == 1);
    PP_EXPECT(harness, services.playback_mode_restores == 1);
    PP_EXPECT(harness, services.playback_active_sets == 2);
    PP_EXPECT(harness, services.playback_timer_resets == 1);
    PP_EXPECT(harness, services.playback_idle_sets == 2);
    PP_EXPECT(harness, !services.last_playback_active);
    PP_EXPECT(harness, services.last_playback_idle_ms == 100);
    PP_EXPECT(
        harness,
        services.call_order
            == "duration;unsaved;update;reload;move;unsaved;goto;reload;goto;reload;onion;update;"
               "select-frame;goto;select-layer;goto;timeline;frame-status;"
               "capture-mode;camera-mode;reset-timer;play-active;idle;restore-mode;play-inactive;idle;");
}

void executor_rejects_malformed_animation_plans(pp::tests::Harness& harness)
{
    FakeDocumentAnimationServices services;

    auto add = pp::app::plan_animation_add_frame(2, 0).value();
    add.marks_unsaved = false;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(add, services).ok());

    auto remove = pp::app::plan_animation_remove_frame(2, 1).value();
    remove.frame_count = 1;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(remove, services).ok());

    auto duration = pp::app::plan_animation_adjust_duration(2, 1, 4, 1).value();
    duration.frame_duration = 0;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(duration, services).ok());

    auto move = pp::app::plan_animation_move_frame(3, 1, 1).value();
    move.move_offset = 0;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(move, services).ok());

    auto go = pp::app::plan_animation_goto_frame(3, 1).value();
    go.target_frame = 3;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(go, services).ok());

    auto select = pp::app::plan_animation_select_frame(2, 0, 42, 1).value();
    select.layer_index = -1;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(select, services).ok());

    auto playback = pp::app::plan_animation_playback_step(3, 1, 1).value();
    playback.move_offset = 0;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(playback, services).ok());

    auto toggle = pp::app::plan_animation_playback_toggle(false).value();
    toggle.playback_active = false;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(toggle, services).ok());

    toggle = pp::app::plan_animation_playback_toggle(false).value();
    toggle.playback_idle_ms = 0;
    PP_EXPECT(harness, !pp::app::execute_animation_operation_plan(toggle, services).ok());

    PP_EXPECT(harness, services.adds == 0);
    PP_EXPECT(harness, services.duration_sets == 0);
    PP_EXPECT(harness, services.gotos == 0);
    PP_EXPECT(harness, services.call_order.empty());
}

} // namespace

int main()
{
    pp::tests::Harness harness;
    harness.run("add duplicate and remove validate frame bounds", add_duplicate_and_remove_validate_frame_bounds);
    harness.run("duration plans clamp floor and reject overflow", duration_plans_clamp_floor_and_reject_overflow);
    harness.run("move and timeline plans handle edges", move_and_timeline_plans_handle_edges);
    harness.run("frame selection and playback plans keep ui refresh scoped", frame_selection_and_playback_plans_keep_ui_refresh_scoped);
    harness.run("playback toggle plans capture start and stop intent", playback_toggle_plans_capture_start_and_stop_intent);
    harness.run("panel actions plan timeline and playback intent", panel_actions_plan_timeline_and_playback_intent);
    harness.run("panel actions reject invalid timeline state", panel_actions_reject_invalid_timeline_state);
    harness.run("onion size updates canvas without document mutation", onion_size_updates_canvas_without_document_mutation);
    harness.run("panel view projects layers frames and timeline state", panel_view_projects_layers_frames_and_timeline_state);
    harness.run("panel view preserves stale selection as no active frame", panel_view_preserves_stale_selection_as_no_active_frame);
    harness.run("panel view rejects invalid document state", panel_view_rejects_invalid_document_state);
    harness.run("onion frame ranges clamp edges and alpha", onion_frame_ranges_clamp_edges_and_alpha);
    harness.run("onion frame ranges reject invalid inputs", onion_frame_ranges_reject_invalid_inputs);
    harness.run("timeline scrub clamps cursor to document frame", timeline_scrub_clamps_cursor_to_document_frame);
    harness.run("timeline scrub rejects invalid inputs", timeline_scrub_rejects_invalid_inputs);
    harness.run("executor dispatches mutating frame operations", executor_dispatches_mutating_frame_operations);
    harness.run("executor dispatches timeline and parameter operations", executor_dispatches_timeline_and_parameter_operations);
    harness.run("executor rejects malformed animation plans", executor_rejects_malformed_animation_plans);
    return harness.finish();
}
