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

#include <cmath>
#include <string>
#include <vector>

namespace {

class FakeDocumentLayerMenuServices final : public pp::app::DocumentLayerMenuServices {
public:
    void clear_current_layer() override { clear_calls += 1; }
    void show_rename_dialog() override { rename_dialogs += 1; }
    void merge_with_lower_layer(int from_index, int to_index) override
    {
        merge_calls += 1;
        last_from_index = from_index;
        last_to_index = to_index;
    }
    void show_merge_animated_not_supported() override { merge_blocked_messages += 1; }

    [[nodiscard]] int total_calls() const noexcept
    {
        return clear_calls + rename_dialogs + merge_calls + merge_blocked_messages;
    }

    int clear_calls = 0;
    int rename_dialogs = 0;
    int merge_calls = 0;
    int merge_blocked_messages = 0;
    int last_from_index = -1;
    int last_to_index = -1;
};

class FakeDocumentLayerRenameServices final : public pp::app::DocumentLayerRenameServices {
public:
    void record_layer_rename_undo(std::string_view old_name, std::string_view new_name) override
    {
        history_calls += 1;
        last_old_name = std::string(old_name);
        last_new_name = std::string(new_name);
        call_order += "history;";
    }

    void set_current_layer_name(std::string_view new_name) override
    {
        rename_calls += 1;
        last_new_name = std::string(new_name);
        call_order += "rename;";
    }

    void finish_layer_rename() override
    {
        finish_calls += 1;
        call_order += "finish;";
    }

    int history_calls = 0;
    int rename_calls = 0;
    int finish_calls = 0;
    std::string last_old_name;
    std::string last_new_name;
    std::string call_order;
};

class FakeDocumentLayerOperationServices final : public pp::app::DocumentLayerOperationServices {
public:
    void add_layer(std::string_view name, int insert_index) override
    {
        add_calls += 1;
        last_name = std::string(name);
        last_insert_index = insert_index;
    }

    void duplicate_layer(int source_index, int insert_index) override
    {
        duplicate_calls += 1;
        last_source_index = source_index;
        last_insert_index = insert_index;
    }

    void select_layer(int index) override
    {
        select_calls += 1;
        last_index = index;
    }

    void reorder_layer(int from_index, int to_index) override
    {
        reorder_calls += 1;
        last_from_index = from_index;
        last_to_index = to_index;
    }

    void remove_layer(int index) override
    {
        remove_calls += 1;
        last_index = index;
    }

    void set_layer_opacity(int index, float opacity) override
    {
        opacity_calls += 1;
        last_index = index;
        last_opacity = opacity;
    }

    void set_layer_visibility(int index, bool visible) override
    {
        visibility_calls += 1;
        last_index = index;
        last_flag = visible;
    }

    void set_layer_alpha_lock(int index, bool locked) override
    {
        alpha_lock_calls += 1;
        last_index = index;
        last_flag = locked;
    }

    void set_layer_blend_mode(int index, int blend_mode) override
    {
        blend_mode_calls += 1;
        last_index = index;
        last_blend_mode = blend_mode;
    }

    void set_layer_highlight(int index, bool highlighted) override
    {
        highlight_calls += 1;
        last_index = index;
        last_flag = highlighted;
    }

    void mark_unsaved() override { unsaved_marks += 1; }
    void reload_animation_layers() override { animation_reloads += 1; }
    void update_title() override { title_updates += 1; }

    [[nodiscard]] int mutation_calls() const noexcept
    {
        return add_calls + duplicate_calls + reorder_calls + remove_calls + opacity_calls
            + visibility_calls + alpha_lock_calls + blend_mode_calls;
    }

    int add_calls = 0;
    int duplicate_calls = 0;
    int select_calls = 0;
    int reorder_calls = 0;
    int remove_calls = 0;
    int opacity_calls = 0;
    int visibility_calls = 0;
    int alpha_lock_calls = 0;
    int blend_mode_calls = 0;
    int highlight_calls = 0;
    int unsaved_marks = 0;
    int animation_reloads = 0;
    int title_updates = 0;
    int last_index = -1;
    int last_from_index = -1;
    int last_to_index = -1;
    int last_insert_index = -1;
    int last_source_index = -1;
    int last_blend_mode = -1;
    float last_opacity = -1.0F;
    bool last_flag = false;
    std::string last_name;
};

class FakeDocumentLayerMergeServices final : public pp::app::DocumentLayerMergeServices {
public:
    void merge_layers(int from_index, int to_index, bool create_history) override
    {
        merge_calls += 1;
        last_from_index = from_index;
        last_to_index = to_index;
        last_create_history = create_history;
    }

    int merge_calls = 0;
    int last_from_index = -1;
    int last_to_index = -1;
    bool last_create_history = false;
};

void layer_rename_records_changed_name(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_rename("Base", "Paint");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().old_name == "Base");
        PP_EXPECT(harness, plan.value().new_name == "Paint");
        PP_EXPECT(
            harness,
            plan.value().action == pp::app::DocumentLayerRenameAction::rename_and_record_undo);
    }
}

void layer_rename_ignores_unchanged_name(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_rename("Ink", "Ink");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().old_name == "Ink");
        PP_EXPECT(harness, plan.value().new_name == "Ink");
        PP_EXPECT(
            harness,
            plan.value().action == pp::app::DocumentLayerRenameAction::no_op_same_name);
    }
}

void layer_rename_rejects_empty_name(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_rename("Ink", "");
    PP_EXPECT(harness, !plan);
    if (!plan) {
        PP_EXPECT(harness, plan.status().code == pp::foundation::StatusCode::invalid_argument);
    }
}

void layer_rename_rejects_overlong_name(pp::tests::Harness& harness)
{
    const std::string too_long(pp::app::document_layer_name_max_length + 1U, 'x');
    const auto plan = pp::app::plan_document_layer_rename("Ink", too_long);
    PP_EXPECT(harness, !plan);
    if (!plan) {
        PP_EXPECT(harness, plan.status().code == pp::foundation::StatusCode::out_of_range);
    }
}

void layer_rename_executor_dispatches_changed_name(pp::tests::Harness& harness)
{
    FakeDocumentLayerRenameServices services;

    const auto plan = pp::app::plan_document_layer_rename("Base", "Paint");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, pp::app::execute_document_layer_rename_plan(plan.value(), services).ok());
        PP_EXPECT(harness, services.history_calls == 1);
        PP_EXPECT(harness, services.rename_calls == 1);
        PP_EXPECT(harness, services.finish_calls == 1);
        PP_EXPECT(harness, services.last_old_name == "Base");
        PP_EXPECT(harness, services.last_new_name == "Paint");
        PP_EXPECT(harness, services.call_order == "history;rename;finish;");
    }
}

void layer_rename_executor_finishes_no_op(pp::tests::Harness& harness)
{
    FakeDocumentLayerRenameServices services;

    const auto plan = pp::app::plan_document_layer_rename("Ink", "Ink");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, pp::app::execute_document_layer_rename_plan(plan.value(), services).ok());
        PP_EXPECT(harness, services.history_calls == 0);
        PP_EXPECT(harness, services.rename_calls == 0);
        PP_EXPECT(harness, services.finish_calls == 1);
        PP_EXPECT(harness, services.call_order == "finish;");
    }
}

void layer_rename_executor_rejects_malformed_changed_plan(pp::tests::Harness& harness)
{
    FakeDocumentLayerRenameServices services;

    pp::app::DocumentLayerRenamePlan malformed;
    malformed.old_name = "Ink";
    malformed.action = pp::app::DocumentLayerRenameAction::rename_and_record_undo;

    const auto status = pp::app::execute_document_layer_rename_plan(malformed, services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, services.history_calls == 0);
    PP_EXPECT(harness, services.rename_calls == 0);
    PP_EXPECT(harness, services.finish_calls == 0);
}

void layer_rename_history_requirement_matches_action(pp::tests::Harness& harness)
{
    const auto rename = pp::app::plan_document_layer_rename("Base", "Paint");
    const auto no_op = pp::app::plan_document_layer_rename("Ink", "Ink");

    PP_EXPECT(harness, rename);
    PP_EXPECT(harness, no_op);
    if (rename) {
        PP_EXPECT(harness, pp::app::document_layer_rename_records_history(rename.value()));
    }
    if (no_op) {
        PP_EXPECT(harness, !pp::app::document_layer_rename_records_history(no_op.value()));
    }
}

void layer_add_validates_insert_index_and_name(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_add(2, 1, "Paint");
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::DocumentLayerOperation::add);
        PP_EXPECT(harness, plan.value().insert_index == 1);
        PP_EXPECT(harness, plan.value().name == "Paint");
        PP_EXPECT(harness, plan.value().marks_unsaved);
        PP_EXPECT(harness, plan.value().reloads_animation_layers);
    }

    PP_EXPECT(harness, !pp::app::plan_document_layer_add(2, -1, "Paint"));
    PP_EXPECT(harness, !pp::app::plan_document_layer_add(2, 3, "Paint"));
    PP_EXPECT(harness, !pp::app::plan_document_layer_add(2, 1, ""));
}

void layer_duplicate_select_and_reorder_validate_indices(pp::tests::Harness& harness)
{
    const auto duplicate = pp::app::plan_document_layer_duplicate(3, 1);
    PP_EXPECT(harness, duplicate);
    if (duplicate) {
        PP_EXPECT(harness, duplicate.value().source_index == 1);
        PP_EXPECT(harness, duplicate.value().insert_index == 2);
        PP_EXPECT(harness, duplicate.value().marks_unsaved);
    }

    const auto select = pp::app::plan_document_layer_select(3, 2);
    PP_EXPECT(harness, select);
    if (select) {
        PP_EXPECT(harness, select.value().index == 2);
        PP_EXPECT(harness, !select.value().marks_unsaved);
        PP_EXPECT(harness, select.value().reloads_animation_layers);
    }

    const auto reorder = pp::app::plan_document_layer_reorder(3, 2, 0);
    PP_EXPECT(harness, reorder);
    if (reorder) {
        PP_EXPECT(harness, reorder.value().from_index == 2);
        PP_EXPECT(harness, reorder.value().to_index == 0);
        PP_EXPECT(harness, reorder.value().marks_unsaved);
    }

    const auto no_op_reorder = pp::app::plan_document_layer_reorder(3, 1, 1);
    PP_EXPECT(harness, no_op_reorder);
    if (no_op_reorder) {
        PP_EXPECT(harness, !no_op_reorder.value().mutates_document);
        PP_EXPECT(harness, !no_op_reorder.value().marks_unsaved);
    }

    PP_EXPECT(harness, !pp::app::plan_document_layer_duplicate(3, 3));
    PP_EXPECT(harness, !pp::app::plan_document_layer_select(3, -1));
    PP_EXPECT(harness, !pp::app::plan_document_layer_reorder(3, 0, 3));
}

void layer_remove_keeps_at_least_one_layer(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_remove(2, 0);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::DocumentLayerOperation::remove);
        PP_EXPECT(harness, plan.value().index == 0);
        PP_EXPECT(harness, plan.value().marks_unsaved);
        PP_EXPECT(harness, plan.value().reloads_animation_layers);
    }

    PP_EXPECT(harness, !pp::app::plan_document_layer_remove(1, 0));
    PP_EXPECT(harness, !pp::app::plan_document_layer_remove(2, 2));
}

void layer_metadata_plans_validate_values(pp::tests::Harness& harness)
{
    const auto opacity = pp::app::plan_document_layer_opacity(2, 1, 0.25F);
    PP_EXPECT(harness, opacity);
    if (opacity) {
        PP_EXPECT(harness, opacity.value().operation == pp::app::DocumentLayerOperation::set_opacity);
        PP_EXPECT(harness, opacity.value().opacity == 0.25F);
        PP_EXPECT(harness, opacity.value().marks_unsaved);
        PP_EXPECT(harness, !opacity.value().reloads_animation_layers);
    }

    const auto visibility = pp::app::plan_document_layer_visibility(2, 1, false);
    PP_EXPECT(harness, visibility);
    if (visibility) {
        PP_EXPECT(harness, visibility.value().operation == pp::app::DocumentLayerOperation::set_visibility);
        PP_EXPECT(harness, !visibility.value().flag);
        PP_EXPECT(harness, visibility.value().reloads_animation_layers);
    }

    const auto alpha_lock = pp::app::plan_document_layer_alpha_lock(2, 1, true);
    PP_EXPECT(harness, alpha_lock);
    if (alpha_lock) {
        PP_EXPECT(harness, alpha_lock.value().operation == pp::app::DocumentLayerOperation::set_alpha_lock);
        PP_EXPECT(harness, alpha_lock.value().flag);
    }

    const auto blend = pp::app::plan_document_layer_blend_mode(2, 1, 4);
    PP_EXPECT(harness, blend);
    if (blend) {
        PP_EXPECT(harness, blend.value().operation == pp::app::DocumentLayerOperation::set_blend_mode);
        PP_EXPECT(harness, blend.value().blend_mode == 4);
    }

    PP_EXPECT(harness, !pp::app::plan_document_layer_opacity(2, 1, -0.1F));
    PP_EXPECT(harness, !pp::app::plan_document_layer_opacity(2, 1, 1.1F));
    PP_EXPECT(harness, !pp::app::plan_document_layer_opacity(2, 1, std::nanf("")));
    PP_EXPECT(harness, !pp::app::plan_document_layer_blend_mode(2, 1, -1));
    PP_EXPECT(harness, !pp::app::plan_document_layer_blend_mode(2, 1, 5));
    PP_EXPECT(harness, !pp::app::plan_document_layer_visibility(2, 2, true));
    PP_EXPECT(harness, !pp::app::plan_document_layer_alpha_lock(2, 2, true));
}

void layer_highlight_is_transient(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_highlight(2, 1, true);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().operation == pp::app::DocumentLayerOperation::set_highlight);
        PP_EXPECT(harness, plan.value().flag);
        PP_EXPECT(harness, !plan.value().mutates_document);
        PP_EXPECT(harness, !plan.value().marks_unsaved);
        PP_EXPECT(harness, !plan.value().updates_title);
    }

    PP_EXPECT(harness, !pp::app::plan_document_layer_highlight(2, 2, true));
}

void layer_panel_view_projects_current_controls_and_visibility(pp::tests::Harness& harness)
{
    const std::vector<pp::app::DocumentLayerPanelInput> layers {
        pp::app::DocumentLayerPanelInput {
            .layer_index = 0,
            .name = "Base",
            .opacity = 1.0F,
            .visible = true,
            .alpha_locked = false,
            .blend_mode = 0,
        },
        pp::app::DocumentLayerPanelInput {
            .layer_index = 1,
            .name = "Ink",
            .opacity = 0.25F,
            .visible = false,
            .alpha_locked = true,
            .blend_mode = 4,
        },
    };

    const auto view = pp::app::plan_document_layer_panel_view(layers, 1);
    PP_EXPECT(harness, view);
    if (view) {
        PP_EXPECT(harness, view.value().current_index == 1);
        PP_EXPECT(harness, view.value().current_opacity == 0.25F);
        PP_EXPECT(harness, view.value().current_alpha_locked);
        PP_EXPECT(harness, view.value().current_blend_mode == 4);
        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].name == "Ink");
        PP_EXPECT(harness, view.value().layers[0].visible);
        PP_EXPECT(harness, !view.value().layers[1].visible);
        PP_EXPECT(harness, view.value().layers[1].alpha_locked);
    }
}

void layer_panel_view_rejects_invalid_document_state(pp::tests::Harness& harness)
{
    const std::vector<pp::app::DocumentLayerPanelInput> valid_layers {
        pp::app::DocumentLayerPanelInput {
            .layer_index = 0,
            .name = "Base",
            .opacity = 1.0F,
            .visible = true,
            .alpha_locked = false,
            .blend_mode = 0,
        },
    };
    const std::vector<pp::app::DocumentLayerPanelInput> bad_index {
        pp::app::DocumentLayerPanelInput {
            .layer_index = 2,
            .name = "Base",
            .opacity = 1.0F,
            .visible = true,
            .alpha_locked = false,
            .blend_mode = 0,
        },
    };
    const std::vector<pp::app::DocumentLayerPanelInput> bad_opacity {
        pp::app::DocumentLayerPanelInput {
            .layer_index = 0,
            .name = "Base",
            .opacity = std::nanf(""),
            .visible = true,
            .alpha_locked = false,
            .blend_mode = 0,
        },
    };
    const std::vector<pp::app::DocumentLayerPanelInput> bad_blend {
        pp::app::DocumentLayerPanelInput {
            .layer_index = 0,
            .name = "Base",
            .opacity = 1.0F,
            .visible = true,
            .alpha_locked = false,
            .blend_mode = pp::app::document_layer_legacy_blend_mode_count,
        },
    };

    PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view({}, 0));
    PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(valid_layers, -1));
    PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(valid_layers, 1));
    PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(bad_index, 0));
    PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(bad_opacity, 0));
    PP_EXPECT(harness, !pp::app::plan_document_layer_panel_view(bad_blend, 0));
}

void layer_operation_executor_dispatches_document_mutations(pp::tests::Harness& harness)
{
    FakeDocumentLayerOperationServices services;

    const auto add = pp::app::plan_document_layer_add(2, 1, "Paint");
    PP_EXPECT(harness, add);
    if (add) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(add.value(), services).ok());
        PP_EXPECT(harness, services.add_calls == 1);
        PP_EXPECT(harness, services.last_name == "Paint");
        PP_EXPECT(harness, services.last_insert_index == 1);
        PP_EXPECT(harness, services.unsaved_marks == 1);
        PP_EXPECT(harness, services.animation_reloads == 1);
        PP_EXPECT(harness, services.title_updates == 1);
    }

    const auto duplicate = pp::app::plan_document_layer_duplicate(3, 1);
    PP_EXPECT(harness, duplicate);
    if (duplicate) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(duplicate.value(), services).ok());
        PP_EXPECT(harness, services.duplicate_calls == 1);
        PP_EXPECT(harness, services.last_source_index == 1);
        PP_EXPECT(harness, services.last_insert_index == 2);
        PP_EXPECT(harness, services.unsaved_marks == 2);
        PP_EXPECT(harness, services.animation_reloads == 2);
        PP_EXPECT(harness, services.title_updates == 2);
    }

    const auto reorder = pp::app::plan_document_layer_reorder(3, 2, 0);
    PP_EXPECT(harness, reorder);
    if (reorder) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(reorder.value(), services).ok());
        PP_EXPECT(harness, services.reorder_calls == 1);
        PP_EXPECT(harness, services.last_from_index == 2);
        PP_EXPECT(harness, services.last_to_index == 0);
        PP_EXPECT(harness, services.unsaved_marks == 3);
        PP_EXPECT(harness, services.animation_reloads == 3);
        PP_EXPECT(harness, services.title_updates == 3);
    }

    const auto remove = pp::app::plan_document_layer_remove(3, 1);
    PP_EXPECT(harness, remove);
    if (remove) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(remove.value(), services).ok());
        PP_EXPECT(harness, services.remove_calls == 1);
        PP_EXPECT(harness, services.last_index == 1);
        PP_EXPECT(harness, services.unsaved_marks == 4);
        PP_EXPECT(harness, services.animation_reloads == 4);
        PP_EXPECT(harness, services.title_updates == 4);
    }
}

void layer_operation_history_requirement_matches_operation(pp::tests::Harness& harness)
{
    const auto add = pp::app::plan_document_layer_add(2, 1, "Paint");
    const auto duplicate = pp::app::plan_document_layer_duplicate(3, 1);
    const auto select = pp::app::plan_document_layer_select(3, 2);
    const auto reorder = pp::app::plan_document_layer_reorder(3, 2, 0);
    const auto no_op_reorder = pp::app::plan_document_layer_reorder(3, 1, 1);
    const auto remove = pp::app::plan_document_layer_remove(3, 1);
    const auto opacity = pp::app::plan_document_layer_opacity(3, 1, 0.25F);
    const auto visibility = pp::app::plan_document_layer_visibility(3, 1, false);
    const auto alpha_lock = pp::app::plan_document_layer_alpha_lock(3, 1, true);
    const auto blend = pp::app::plan_document_layer_blend_mode(3, 1, 4);
    const auto highlight = pp::app::plan_document_layer_highlight(3, 1, true);

    PP_EXPECT(harness, add);
    PP_EXPECT(harness, duplicate);
    PP_EXPECT(harness, select);
    PP_EXPECT(harness, reorder);
    PP_EXPECT(harness, no_op_reorder);
    PP_EXPECT(harness, remove);
    PP_EXPECT(harness, opacity);
    PP_EXPECT(harness, visibility);
    PP_EXPECT(harness, alpha_lock);
    PP_EXPECT(harness, blend);
    PP_EXPECT(harness, highlight);

    if (add) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(add.value()));
    }
    if (duplicate) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(duplicate.value()));
    }
    if (select) {
        PP_EXPECT(harness, !pp::app::document_layer_operation_records_history(select.value()));
    }
    if (reorder) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(reorder.value()));
    }
    if (no_op_reorder) {
        PP_EXPECT(harness, !pp::app::document_layer_operation_records_history(no_op_reorder.value()));
    }
    if (remove) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(remove.value()));
    }
    if (opacity) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(opacity.value()));
    }
    if (visibility) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(visibility.value()));
    }
    if (alpha_lock) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(alpha_lock.value()));
    }
    if (blend) {
        PP_EXPECT(harness, pp::app::document_layer_operation_records_history(blend.value()));
    }
    if (highlight) {
        PP_EXPECT(harness, !pp::app::document_layer_operation_records_history(highlight.value()));
    }
}

void layer_operation_executor_dispatches_selection_and_metadata(pp::tests::Harness& harness)
{
    FakeDocumentLayerOperationServices services;

    const auto select = pp::app::plan_document_layer_select(3, 2);
    PP_EXPECT(harness, select);
    if (select) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(select.value(), services).ok());
        PP_EXPECT(harness, services.select_calls == 1);
        PP_EXPECT(harness, services.last_index == 2);
        PP_EXPECT(harness, services.unsaved_marks == 0);
        PP_EXPECT(harness, services.animation_reloads == 1);
        PP_EXPECT(harness, services.title_updates == 0);
    }

    const auto opacity = pp::app::plan_document_layer_opacity(3, 1, 0.25F);
    PP_EXPECT(harness, opacity);
    if (opacity) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(opacity.value(), services).ok());
        PP_EXPECT(harness, services.opacity_calls == 1);
        PP_EXPECT(harness, services.last_index == 1);
        PP_EXPECT(harness, services.last_opacity == 0.25F);
        PP_EXPECT(harness, services.unsaved_marks == 1);
        PP_EXPECT(harness, services.animation_reloads == 1);
        PP_EXPECT(harness, services.title_updates == 1);
    }

    const auto visibility = pp::app::plan_document_layer_visibility(3, 1, false);
    PP_EXPECT(harness, visibility);
    if (visibility) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(visibility.value(), services).ok());
        PP_EXPECT(harness, services.visibility_calls == 1);
        PP_EXPECT(harness, !services.last_flag);
        PP_EXPECT(harness, services.unsaved_marks == 2);
        PP_EXPECT(harness, services.animation_reloads == 2);
        PP_EXPECT(harness, services.title_updates == 2);
    }

    const auto alpha_lock = pp::app::plan_document_layer_alpha_lock(3, 1, true);
    PP_EXPECT(harness, alpha_lock);
    if (alpha_lock) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(alpha_lock.value(), services).ok());
        PP_EXPECT(harness, services.alpha_lock_calls == 1);
        PP_EXPECT(harness, services.last_flag);
        PP_EXPECT(harness, services.unsaved_marks == 3);
        PP_EXPECT(harness, services.animation_reloads == 2);
        PP_EXPECT(harness, services.title_updates == 3);
    }

    const auto blend = pp::app::plan_document_layer_blend_mode(3, 1, 4);
    PP_EXPECT(harness, blend);
    if (blend) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(blend.value(), services).ok());
        PP_EXPECT(harness, services.blend_mode_calls == 1);
        PP_EXPECT(harness, services.last_blend_mode == 4);
        PP_EXPECT(harness, services.unsaved_marks == 4);
        PP_EXPECT(harness, services.title_updates == 4);
    }
}

void layer_operation_executor_preserves_no_op_and_transient_actions(pp::tests::Harness& harness)
{
    FakeDocumentLayerOperationServices services;

    const auto no_op_reorder = pp::app::plan_document_layer_reorder(3, 1, 1);
    PP_EXPECT(harness, no_op_reorder);
    if (no_op_reorder) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(no_op_reorder.value(), services).ok());
        PP_EXPECT(harness, services.reorder_calls == 0);
        PP_EXPECT(harness, services.mutation_calls() == 0);
        PP_EXPECT(harness, services.unsaved_marks == 0);
        PP_EXPECT(harness, services.animation_reloads == 0);
        PP_EXPECT(harness, services.title_updates == 0);
    }

    const auto highlight = pp::app::plan_document_layer_highlight(3, 1, true);
    PP_EXPECT(harness, highlight);
    if (highlight) {
        PP_EXPECT(harness, pp::app::execute_document_layer_operation_plan(highlight.value(), services).ok());
        PP_EXPECT(harness, services.highlight_calls == 1);
        PP_EXPECT(harness, services.last_index == 1);
        PP_EXPECT(harness, services.last_flag);
        PP_EXPECT(harness, services.unsaved_marks == 0);
        PP_EXPECT(harness, services.animation_reloads == 0);
        PP_EXPECT(harness, services.title_updates == 0);
    }
}

void layer_operation_executor_rejects_malformed_mutation_plans(pp::tests::Harness& harness)
{
    FakeDocumentLayerOperationServices services;

    pp::app::DocumentLayerOperationPlan malformed;
    malformed.operation = pp::app::DocumentLayerOperation::add;
    malformed.insert_index = 1;
    malformed.mutates_document = true;

    const auto status = pp::app::execute_document_layer_operation_plan(malformed, services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, services.add_calls == 0);
    PP_EXPECT(harness, services.unsaved_marks == 0);
}

void layer_menu_labels_selected_layer_commands(pp::tests::Harness& harness)
{
    const auto clear = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::clear,
        true,
        1,
        1,
        "Paint",
        "Base");
    const auto rename = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::rename,
        true,
        1,
        1,
        "Paint",
        "Base");

    PP_EXPECT(harness, clear);
    PP_EXPECT(harness, rename);
    if (clear) {
        PP_EXPECT(harness, clear.value().label == "Clear Layer Paint");
        PP_EXPECT(harness, clear.value().action == pp::app::DocumentLayerMenuAction::clear_current_layer);
    }
    if (rename) {
        PP_EXPECT(harness, rename.value().label == "Rename Layer Paint");
        PP_EXPECT(harness, rename.value().action == pp::app::DocumentLayerMenuAction::show_rename_dialog);
    }
}

void layer_menu_plans_merge_down_or_blocks_it(pp::tests::Harness& harness)
{
    const auto merge = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::merge_down,
        true,
        2,
        1,
        "Ink",
        "Paint");
    const auto base_layer = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::merge_down,
        true,
        0,
        1,
        "Base",
        "");
    const auto animated = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::merge_down,
        true,
        2,
        3,
        "Ink",
        "Paint");

    PP_EXPECT(harness, merge);
    PP_EXPECT(harness, base_layer);
    PP_EXPECT(harness, animated);
    if (merge) {
        PP_EXPECT(harness, merge.value().label == "Merge with Paint");
        PP_EXPECT(harness, merge.value().from_index == 2);
        PP_EXPECT(harness, merge.value().to_index == 1);
        PP_EXPECT(harness, merge.value().action == pp::app::DocumentLayerMenuAction::merge_with_lower_layer);
    }
    if (base_layer) {
        PP_EXPECT(harness, base_layer.value().action == pp::app::DocumentLayerMenuAction::no_op_select_upper_layer);
        PP_EXPECT(harness, base_layer.value().label == "Merge Layer (Select upper layers)");
    }
    if (animated) {
        PP_EXPECT(
            harness,
            animated.value().action == pp::app::DocumentLayerMenuAction::show_merge_animated_not_supported);
    }
}

void layer_menu_handles_missing_selection_and_bad_state(pp::tests::Harness& harness)
{
    const auto missing = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::rename,
        false,
        0,
        1,
        "",
        "");

    PP_EXPECT(harness, missing);
    if (missing) {
        PP_EXPECT(harness, missing.value().action == pp::app::DocumentLayerMenuAction::no_op_select_layer);
        PP_EXPECT(harness, missing.value().label == "Rename Layer (Select a layer)");
    }

    PP_EXPECT(
        harness,
        !pp::app::plan_document_layer_menu(
            pp::app::DocumentLayerMenuCommand::merge_down,
            true,
            -1,
            1,
            "Ink",
            "Paint"));
    PP_EXPECT(
        harness,
        !pp::app::plan_document_layer_menu(
            pp::app::DocumentLayerMenuCommand::merge_down,
            true,
            1,
            -1,
            "Ink",
            "Paint"));
}

void layer_menu_executor_dispatches_menu_actions(pp::tests::Harness& harness)
{
    FakeDocumentLayerMenuServices services;

    const auto clear = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::clear,
        true,
        1,
        1,
        "Paint",
        "Base");
    PP_EXPECT(harness, clear);
    if (clear) {
        PP_EXPECT(harness, pp::app::execute_document_layer_menu_plan(clear.value(), services).ok());
        PP_EXPECT(harness, services.clear_calls == 1);
    }

    const auto rename = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::rename,
        true,
        1,
        1,
        "Paint",
        "Base");
    PP_EXPECT(harness, rename);
    if (rename) {
        PP_EXPECT(harness, pp::app::execute_document_layer_menu_plan(rename.value(), services).ok());
        PP_EXPECT(harness, services.rename_dialogs == 1);
    }

    const auto merge = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::merge_down,
        true,
        2,
        1,
        "Ink",
        "Paint");
    PP_EXPECT(harness, merge);
    if (merge) {
        PP_EXPECT(harness, pp::app::execute_document_layer_menu_plan(merge.value(), services).ok());
        PP_EXPECT(harness, services.merge_calls == 1);
        PP_EXPECT(harness, services.last_from_index == 2);
        PP_EXPECT(harness, services.last_to_index == 1);
    }

    const auto animated = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::merge_down,
        true,
        2,
        3,
        "Ink",
        "Paint");
    PP_EXPECT(harness, animated);
    if (animated) {
        PP_EXPECT(harness, pp::app::execute_document_layer_menu_plan(animated.value(), services).ok());
        PP_EXPECT(harness, services.merge_blocked_messages == 1);
    }
}

void layer_menu_executor_preserves_no_op_actions(pp::tests::Harness& harness)
{
    FakeDocumentLayerMenuServices services;

    const auto missing = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::clear,
        false,
        0,
        1,
        "",
        "");
    PP_EXPECT(harness, missing);
    if (missing) {
        PP_EXPECT(harness, missing.value().action == pp::app::DocumentLayerMenuAction::no_op_select_layer);
        PP_EXPECT(harness, pp::app::execute_document_layer_menu_plan(missing.value(), services).ok());
    }

    const auto base_layer = pp::app::plan_document_layer_menu(
        pp::app::DocumentLayerMenuCommand::merge_down,
        true,
        0,
        1,
        "Base",
        "");
    PP_EXPECT(harness, base_layer);
    if (base_layer) {
        PP_EXPECT(harness, base_layer.value().action == pp::app::DocumentLayerMenuAction::no_op_select_upper_layer);
        PP_EXPECT(harness, pp::app::execute_document_layer_menu_plan(base_layer.value(), services).ok());
    }

    PP_EXPECT(harness, services.total_calls() == 0);
}

void layer_merge_plan_validates_supported_merge(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_layer_merge(3, 2, 1, 1);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().from_index == 2);
        PP_EXPECT(harness, plan.value().to_index == 1);
        PP_EXPECT(harness, plan.value().create_history);
    }

    const auto no_history = pp::app::plan_document_layer_merge(3, 2, 0, 1, false);
    PP_EXPECT(harness, no_history);
    if (no_history) {
        PP_EXPECT(harness, !no_history.value().create_history);
    }
}

void layer_merge_history_requirement_matches_flag(pp::tests::Harness& harness)
{
    const auto merge = pp::app::plan_document_layer_merge(3, 2, 1, 1);
    const auto no_history = pp::app::plan_document_layer_merge(3, 2, 0, 1, false);

    PP_EXPECT(harness, merge);
    PP_EXPECT(harness, no_history);
    if (merge) {
        PP_EXPECT(harness, pp::app::document_layer_merge_records_history(merge.value()));
    }
    if (no_history) {
        PP_EXPECT(harness, !pp::app::document_layer_merge_records_history(no_history.value()));
    }
}

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

void layer_merge_executor_dispatches_merge(pp::tests::Harness& harness)
{
    FakeDocumentLayerMergeServices services;

    const auto plan = pp::app::plan_document_layer_merge(3, 2, 1, 1);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, pp::app::execute_document_layer_merge_plan(plan.value(), services).ok());
        PP_EXPECT(harness, services.merge_calls == 1);
        PP_EXPECT(harness, services.last_from_index == 2);
        PP_EXPECT(harness, services.last_to_index == 1);
        PP_EXPECT(harness, services.last_create_history);
    }
}

void layer_merge_executor_rejects_malformed_plan(pp::tests::Harness& harness)
{
    FakeDocumentLayerMergeServices services;

    pp::app::DocumentLayerMergePlan malformed;
    malformed.from_index = 1;
    malformed.to_index = 1;

    const auto status = pp::app::execute_document_layer_merge_plan(malformed, services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, services.merge_calls == 0);
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("layer rename records changed name", layer_rename_records_changed_name);
    harness.run("layer rename ignores unchanged name", layer_rename_ignores_unchanged_name);
    harness.run("layer rename rejects empty name", layer_rename_rejects_empty_name);
    harness.run("layer rename rejects overlong name", layer_rename_rejects_overlong_name);
    harness.run("layer rename executor dispatches changed name", layer_rename_executor_dispatches_changed_name);
    harness.run("layer rename executor finishes no op", layer_rename_executor_finishes_no_op);
    harness.run("layer rename executor rejects malformed changed plan", layer_rename_executor_rejects_malformed_changed_plan);
    harness.run("layer rename history requirement matches action", layer_rename_history_requirement_matches_action);
    harness.run("layer add validates insert index and name", layer_add_validates_insert_index_and_name);
    harness.run("layer duplicate select and reorder validate indices", layer_duplicate_select_and_reorder_validate_indices);
    harness.run("layer remove keeps at least one layer", layer_remove_keeps_at_least_one_layer);
    harness.run("layer metadata plans validate values", layer_metadata_plans_validate_values);
    harness.run("layer highlight is transient", layer_highlight_is_transient);
    harness.run("layer panel view projects current controls and visibility", layer_panel_view_projects_current_controls_and_visibility);
    harness.run("layer panel view rejects invalid document state", layer_panel_view_rejects_invalid_document_state);
    harness.run("layer operation executor dispatches document mutations", layer_operation_executor_dispatches_document_mutations);
    harness.run("layer operation history requirement matches operation", layer_operation_history_requirement_matches_operation);
    harness.run("layer operation executor dispatches selection and metadata", layer_operation_executor_dispatches_selection_and_metadata);
    harness.run("layer operation executor preserves no op and transient actions", layer_operation_executor_preserves_no_op_and_transient_actions);
    harness.run("layer operation executor rejects malformed mutation plans", layer_operation_executor_rejects_malformed_mutation_plans);
    harness.run("layer menu labels selected layer commands", layer_menu_labels_selected_layer_commands);
    harness.run("layer menu plans merge down or blocks it", layer_menu_plans_merge_down_or_blocks_it);
    harness.run("layer menu handles missing selection and bad state", layer_menu_handles_missing_selection_and_bad_state);
    harness.run("layer menu executor dispatches menu actions", layer_menu_executor_dispatches_menu_actions);
    harness.run("layer menu executor preserves no op actions", layer_menu_executor_preserves_no_op_actions);
    harness.run("layer merge plan validates supported merge", layer_merge_plan_validates_supported_merge);
    harness.run("layer merge history requirement matches flag", layer_merge_history_requirement_matches_flag);
    harness.run("layer merge plan rejects bad or unsupported state", layer_merge_plan_rejects_bad_or_unsupported_state);
    harness.run("layer merge executor dispatches merge", layer_merge_executor_dispatches_merge);
    harness.run("layer merge executor rejects malformed plan", layer_merge_executor_rejects_malformed_plan);
    return harness.finish();
}
