#include "app_core/document_canvas.h"
#include "assets/ppi_header.h"
#include "test_harness.h"

#include <cstdint>
#include <limits>
#include <span>
#include <string>

namespace {

class FakeDocumentCanvasClearServices final : public pp::app::DocumentCanvasClearServices {
public:
    void clear_current_canvas(float r, float g, float b, float a) override
    {
        clear_calls += 1;
        last_r = r;
        last_g = g;
        last_b = b;
        last_a = a;
        call_order += "clear;";
    }

    int clear_calls = 0;
    float last_r = 0.0F;
    float last_g = 0.0F;
    float last_b = 0.0F;
    float last_a = 0.0F;
    std::string call_order;
};

void snapshot_plan_projects_canvas_metadata(pp::tests::Harness& harness)
{
    const std::uint32_t base_frames[] { 120U, 240U };
    const std::uint32_t paint_frames[] { 180U };
    const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
        {
            .name = "Base",
            .visible = false,
            .alpha_locked = true,
            .opacity = 0.5F,
            .blend_mode = 2,
            .frame_durations_ms = std::span<const std::uint32_t>(base_frames),
            .pending_face_payloads = 6U,
        },
        {
            .name = "Paint",
            .visible = true,
            .alpha_locked = false,
            .opacity = 0.75F,
            .blend_mode = 4,
            .frame_durations_ms = std::span<const std::uint32_t>(paint_frames),
            .pending_face_payloads = 3U,
        },
    };

    const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 64U,
        .height = 32U,
        .active_layer_index = 1U,
        .active_frame_index = 1U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });

    PP_EXPECT(harness, result);
    if (!result) {
        return;
    }

    const auto& value = result.value();
    PP_EXPECT(harness, value.layer_count == 2U);
    PP_EXPECT(harness, value.frame_count == 2U);
    PP_EXPECT(harness, value.pending_face_payloads == 9U);
    PP_EXPECT(harness, value.metadata_only);
    PP_EXPECT(harness, value.requires_renderer_payload_readback);
    PP_EXPECT(harness, value.document.width() == 64U);
    PP_EXPECT(harness, value.document.height() == 32U);
    PP_EXPECT(harness, value.document.active_layer_index() == 1U);
    PP_EXPECT(harness, value.document.active_frame_index() == 1U);
    PP_EXPECT(harness, value.document.layers().size() == 2U);
    PP_EXPECT(harness, value.document.frames().size() == 2U);
    PP_EXPECT(harness, value.document.layers()[0].name == "Base");
    PP_EXPECT(harness, !value.document.layers()[0].visible);
    PP_EXPECT(harness, value.document.layers()[0].alpha_locked);
    PP_EXPECT(harness, value.document.layers()[0].opacity == 0.5F);
    PP_EXPECT(harness, value.document.layers()[0].blend_mode == pp::paint::BlendMode::screen);
    PP_EXPECT(harness, value.document.layers()[0].frames[1].duration_ms == 240U);
    PP_EXPECT(harness, value.document.layers()[1].name == "Paint");
    PP_EXPECT(harness, value.document.layers()[1].blend_mode == pp::paint::BlendMode::overlay);
    PP_EXPECT(harness, value.document.layers()[1].frames.size() == 1U);
    PP_EXPECT(harness, value.document.face_pixel_payload_count() == 0U);

    const auto export_result = pp::app::export_document_canvas_save_snapshot_to_ppi(value);
    PP_EXPECT(harness, !export_result);
    PP_EXPECT(harness, export_result.status().code == pp::foundation::StatusCode::invalid_argument);
}

void snapshot_plan_defaults_empty_names_and_frames(pp::tests::Harness& harness)
{
    const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
        {
            .name = "",
            .frame_durations_ms = {},
        },
    };

    const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 16U,
        .height = 8U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });

    PP_EXPECT(harness, result);
    if (!result) {
        return;
    }

    PP_EXPECT(harness, result.value().frame_count == 1U);
    PP_EXPECT(harness, result.value().pending_face_payloads == 0U);
    PP_EXPECT(harness, !result.value().requires_renderer_payload_readback);
    PP_EXPECT(harness, result.value().document.layers()[0].name == "Layer 1");
    PP_EXPECT(harness, result.value().document.frames()[0].duration_ms == 100U);

    const auto export_result = pp::app::export_document_canvas_save_snapshot_to_ppi(result.value());
    PP_EXPECT(harness, export_result);
    if (export_result) {
        const auto decoded = pp::assets::decode_ppi_project_images(export_result.value().bytes);
        PP_EXPECT(harness, decoded);
        if (decoded) {
            PP_EXPECT(harness, decoded.value().project.body.summary.dirty_face_count == 0U);
        }
    }
}

void snapshot_plan_attaches_captured_face_payloads(pp::tests::Harness& harness)
{
    const std::uint32_t frames[] { 100U };
    const std::uint8_t rgba[] {
        255U, 0U, 0U, 255U,
        0U, 255U, 0U, 255U,
    };
    const pp::app::DocumentCanvasFacePayloadInput payloads[] {
        {
            .frame_index = 0U,
            .face_index = 2U,
            .x = 3U,
            .y = 4U,
            .width = 2U,
            .height = 1U,
            .rgba8 = std::span<const std::uint8_t>(rgba),
        },
    };
    const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
        {
            .name = "Paint",
            .frame_durations_ms = std::span<const std::uint32_t>(frames),
            .pending_face_payloads = 1U,
            .captured_face_payloads = std::span<const pp::app::DocumentCanvasFacePayloadInput>(payloads),
        },
    };

    const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 16U,
        .height = 8U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });

    PP_EXPECT(harness, result);
    if (!result) {
        return;
    }

    PP_EXPECT(harness, result.value().pending_face_payloads == 1U);
    PP_EXPECT(harness, result.value().captured_face_payloads == 1U);
    PP_EXPECT(harness, !result.value().metadata_only);
    PP_EXPECT(harness, !result.value().requires_renderer_payload_readback);
    PP_EXPECT(harness, result.value().document.face_pixel_payload_count() == 1U);
    PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].face_index == 2U);
    PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].x == 3U);
    PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].rgba8[4] == 0U);
    PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].rgba8[5] == 255U);

    const auto report = pp::app::make_document_canvas_save_snapshot_report(result.value());
    PP_EXPECT(harness, report.width == 16U);
    PP_EXPECT(harness, report.height == 8U);
    PP_EXPECT(harness, report.layer_count == 1U);
    PP_EXPECT(harness, report.frame_count == 1U);
    PP_EXPECT(harness, report.pending_face_payloads == 1U);
    PP_EXPECT(harness, report.captured_face_payloads == 1U);
    PP_EXPECT(harness, report.payload_complete);
    PP_EXPECT(harness, report.can_export_ppi);

    const auto export_result = pp::app::export_document_canvas_save_snapshot_to_ppi(result.value());
    PP_EXPECT(harness, export_result);
    if (export_result) {
        PP_EXPECT(harness, export_result.value().report.can_export_ppi);
        PP_EXPECT(harness, export_result.value().bytes.size() > 0U);

        const auto decoded = pp::assets::decode_ppi_project_images(export_result.value().bytes);
        PP_EXPECT(harness, decoded);
        if (decoded) {
            PP_EXPECT(harness, decoded.value().project.body.summary.dirty_face_count == 1U);
            PP_EXPECT(harness, decoded.value().project.body.summary.rgba_face_payload_count == 1U);
        }
    }
}

void save_writer_route_uses_ppi_writer_for_complete_payloads(pp::tests::Harness& harness)
{
    pp::app::DocumentCanvasSaveSnapshotReport report;
    report.payload_complete = true;
    report.can_export_ppi = true;

    const auto plan = pp::app::plan_document_canvas_save_writer_route(report);

    PP_EXPECT(harness, plan.action == pp::app::DocumentCanvasSaveWriterAction::use_document_ppi_writer);
    PP_EXPECT(harness, plan.uses_document_ppi_writer);
    PP_EXPECT(harness, plan.payload_complete);
    PP_EXPECT(harness, plan.can_export_ppi);
    PP_EXPECT(harness, plan.fallback_reason.empty());
}

void save_writer_route_falls_back_for_pending_payloads(pp::tests::Harness& harness)
{
    pp::app::DocumentCanvasSaveSnapshotReport report;
    report.payload_complete = false;
    report.can_export_ppi = false;

    const auto plan = pp::app::plan_document_canvas_save_writer_route(report);

    PP_EXPECT(harness, plan.action == pp::app::DocumentCanvasSaveWriterAction::use_legacy_project_save);
    PP_EXPECT(harness, !plan.uses_document_ppi_writer);
    PP_EXPECT(harness, !plan.payload_complete);
    PP_EXPECT(harness, !plan.can_export_ppi);
    PP_EXPECT(
        harness,
        plan.fallback_reason == "canvas document snapshot still requires renderer payload readback");
}

void project_save_target_plan_preserves_legacy_paths(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_target(
        "D:/Paint/data",
        "D:/Paint/projects/demo.ppi");

    PP_EXPECT(harness, plan);
    if (!plan) {
        return;
    }

    PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi");
    PP_EXPECT(harness, plan.value().file_name == "demo");
    PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi");
    PP_EXPECT(harness, plan.value().timelapse_path == "D:/Paint/data/demo.pptl");
}

void project_save_target_plan_accepts_windows_backslashes(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_target(
        "D:/Paint/data",
        "D:\\Paint\\projects\\demo.ppi");

    PP_EXPECT(harness, plan);
    if (!plan) {
        return;
    }

    PP_EXPECT(harness, plan.value().file_name == "demo");
    PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi");
    PP_EXPECT(harness, plan.value().timelapse_path == "D:/Paint/data/demo.pptl");
}

void project_save_target_plan_rejects_empty_inputs(pp::tests::Harness& harness)
{
    const auto no_data = pp::app::plan_document_canvas_project_save_target("", "D:/Paint/demo.ppi");
    const auto no_target = pp::app::plan_document_canvas_project_save_target("D:/Paint/data", "");
    const auto no_name = pp::app::plan_document_canvas_project_save_target("D:/Paint/data", "D:/Paint/");

    PP_EXPECT(harness, !no_data);
    PP_EXPECT(harness, !no_target);
    PP_EXPECT(harness, !no_name);
    PP_EXPECT(harness, no_data.status().code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, no_target.status().code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, no_name.status().code == pp::foundation::StatusCode::invalid_argument);
}

void project_save_write_plan_writes_direct_for_new_targets(pp::tests::Harness& harness)
{
    const auto target = pp::app::plan_document_canvas_project_save_target(
        "D:/Paint/data",
        "D:/Paint/projects/demo.ppi");
    PP_EXPECT(harness, target);
    if (!target) {
        return;
    }

    const auto plan = pp::app::plan_document_canvas_project_save_write(target.value(), false);

    PP_EXPECT(harness, plan);
    if (!plan) {
        return;
    }

    PP_EXPECT(harness, plan.value().action == pp::app::DocumentCanvasProjectSaveWriteAction::write_direct_to_target);
    PP_EXPECT(harness, plan.value().write_path == "D:/Paint/projects/demo.ppi");
    PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi");
    PP_EXPECT(harness, !plan.value().target_exists);
    PP_EXPECT(harness, !plan.value().uses_temporary);
    PP_EXPECT(harness, !plan.value().falls_back_to_direct_on_temporary_open_failure);
}

void project_save_write_plan_prefers_temporary_for_existing_targets(pp::tests::Harness& harness)
{
    const auto target = pp::app::plan_document_canvas_project_save_target(
        "D:/Paint/data",
        "D:/Paint/projects/demo.ppi");
    PP_EXPECT(harness, target);
    if (!target) {
        return;
    }

    const auto plan = pp::app::plan_document_canvas_project_save_write(target.value(), true);

    PP_EXPECT(harness, plan);
    if (!plan) {
        return;
    }

    PP_EXPECT(harness, plan.value().action == pp::app::DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap);
    PP_EXPECT(harness, plan.value().write_path == "D:/Paint/data/demo.tmp.ppi");
    PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi");
    PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi");
    PP_EXPECT(harness, plan.value().target_exists);
    PP_EXPECT(harness, plan.value().uses_temporary);
    PP_EXPECT(harness, plan.value().falls_back_to_direct_on_temporary_open_failure);
}

void project_save_write_plan_rejects_missing_paths(pp::tests::Harness& harness)
{
    pp::app::DocumentCanvasProjectSaveTargetPlan empty_target;
    const auto no_target = pp::app::plan_document_canvas_project_save_write(empty_target, false);

    pp::app::DocumentCanvasProjectSaveTargetPlan no_temporary;
    no_temporary.target_path = "D:/Paint/projects/demo.ppi";
    const auto missing_temporary = pp::app::plan_document_canvas_project_save_write(no_temporary, true);

    PP_EXPECT(harness, !no_target);
    PP_EXPECT(harness, !missing_temporary);
    PP_EXPECT(harness, no_target.status().code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, missing_temporary.status().code == pp::foundation::StatusCode::invalid_argument);
}

void project_save_commit_plan_succeeds_for_direct_write(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_commit(
        pp::app::DocumentCanvasProjectSaveCommitInput {
            .used_temporary = false,
        });

    PP_EXPECT(harness, plan.saved);
    PP_EXPECT(harness, !plan.used_temporary);
    PP_EXPECT(harness, !plan.target_removed);
    PP_EXPECT(harness, !plan.temporary_renamed);
    PP_EXPECT(harness, !plan.target_may_be_missing);
    PP_EXPECT(harness, plan.log_message == "project saved to target");
}

void project_save_commit_plan_succeeds_for_temporary_swap(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_commit(
        pp::app::DocumentCanvasProjectSaveCommitInput {
            .used_temporary = true,
            .target_remove_attempted = true,
            .target_remove_succeeded = true,
            .temporary_rename_attempted = true,
            .temporary_rename_succeeded = true,
        });

    PP_EXPECT(harness, plan.saved);
    PP_EXPECT(harness, plan.used_temporary);
    PP_EXPECT(harness, plan.target_removed);
    PP_EXPECT(harness, plan.temporary_renamed);
    PP_EXPECT(harness, !plan.target_may_be_missing);
    PP_EXPECT(harness, plan.log_message == "temporary project swapped successfully");
}

void project_save_commit_plan_fails_when_target_remove_fails(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_commit(
        pp::app::DocumentCanvasProjectSaveCommitInput {
            .used_temporary = true,
            .target_remove_attempted = true,
            .target_remove_succeeded = false,
            .temporary_rename_attempted = false,
            .temporary_rename_succeeded = false,
        });

    PP_EXPECT(harness, !plan.saved);
    PP_EXPECT(harness, plan.used_temporary);
    PP_EXPECT(harness, !plan.target_removed);
    PP_EXPECT(harness, !plan.temporary_renamed);
    PP_EXPECT(harness, !plan.target_may_be_missing);
    PP_EXPECT(harness, plan.log_message == "could not remove target project before temporary swap");
}

void project_save_commit_plan_flags_missing_target_after_rename_failure(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_commit(
        pp::app::DocumentCanvasProjectSaveCommitInput {
            .used_temporary = true,
            .target_remove_attempted = true,
            .target_remove_succeeded = true,
            .temporary_rename_attempted = true,
            .temporary_rename_succeeded = false,
        });

    PP_EXPECT(harness, !plan.saved);
    PP_EXPECT(harness, plan.used_temporary);
    PP_EXPECT(harness, plan.target_removed);
    PP_EXPECT(harness, !plan.temporary_renamed);
    PP_EXPECT(harness, plan.target_may_be_missing);
    PP_EXPECT(harness, plan.log_message == "temporary project not swapped after original removal");
}

void project_save_post_commit_plan_records_success_side_effects(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_post_commit(
        pp::app::DocumentCanvasProjectSavePostCommitInput {
            .save_succeeded = true,
            .timelapse_encoder_available = true,
            .progress_ui_visible = true,
        });

    PP_EXPECT(harness, plan.marks_document_clean);
    PP_EXPECT(harness, plan.marks_new_document_committed);
    PP_EXPECT(harness, plan.saves_timelapse_sidecar);
    PP_EXPECT(harness, plan.flushes_platform_storage);
    PP_EXPECT(harness, plan.dismisses_progress_ui);
    PP_EXPECT(harness, plan.updates_title);
}

void project_save_post_commit_plan_skips_timelapse_without_encoder(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_post_commit(
        pp::app::DocumentCanvasProjectSavePostCommitInput {
            .save_succeeded = true,
            .timelapse_encoder_available = false,
            .progress_ui_visible = false,
        });

    PP_EXPECT(harness, plan.marks_document_clean);
    PP_EXPECT(harness, plan.marks_new_document_committed);
    PP_EXPECT(harness, !plan.saves_timelapse_sidecar);
    PP_EXPECT(harness, plan.flushes_platform_storage);
    PP_EXPECT(harness, !plan.dismisses_progress_ui);
    PP_EXPECT(harness, plan.updates_title);
}

void project_save_post_commit_plan_preserves_state_after_failure(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_project_save_post_commit(
        pp::app::DocumentCanvasProjectSavePostCommitInput {
            .save_succeeded = false,
            .timelapse_encoder_available = true,
            .progress_ui_visible = true,
        });

    PP_EXPECT(harness, !plan.marks_document_clean);
    PP_EXPECT(harness, !plan.marks_new_document_committed);
    PP_EXPECT(harness, !plan.saves_timelapse_sidecar);
    PP_EXPECT(harness, !plan.flushes_platform_storage);
    PP_EXPECT(harness, plan.dismisses_progress_ui);
    PP_EXPECT(harness, plan.updates_title);
}

void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness)
{
    const std::uint32_t frames[] { 100U };
    const pp::app::DocumentCanvasLayerSnapshotInput layers[] {
        {
            .name = "Layer",
            .frame_durations_ms = std::span<const std::uint32_t>(frames),
        },
    };

    const auto no_canvas = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = false,
        .width = 16U,
        .height = 8U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });
    const auto bad_layer = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 16U,
        .height = 8U,
        .active_layer_index = 1U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });
    const pp::app::DocumentCanvasLayerSnapshotInput bad_blend_layers[] {
        {
            .name = "Layer",
            .blend_mode = 64,
            .frame_durations_ms = std::span<const std::uint32_t>(frames),
        },
    };
    const auto bad_blend = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 16U,
        .height = 8U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(bad_blend_layers),
    });
    const std::uint32_t bad_frames[] { 0U };
    const pp::app::DocumentCanvasLayerSnapshotInput bad_duration_layers[] {
        {
            .name = "Layer",
            .frame_durations_ms = std::span<const std::uint32_t>(bad_frames),
        },
    };
    const auto bad_duration = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 16U,
        .height = 8U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(bad_duration_layers),
    });
    const std::uint8_t bad_rgba[] { 1U, 2U, 3U };
    const pp::app::DocumentCanvasFacePayloadInput bad_payloads[] {
        {
            .frame_index = 0U,
            .face_index = 0U,
            .x = 0U,
            .y = 0U,
            .width = 1U,
            .height = 1U,
            .rgba8 = std::span<const std::uint8_t>(bad_rgba),
        },
    };
    const pp::app::DocumentCanvasLayerSnapshotInput bad_payload_layers[] {
        {
            .name = "Layer",
            .frame_durations_ms = std::span<const std::uint32_t>(frames),
            .pending_face_payloads = 1U,
            .captured_face_payloads = std::span<const pp::app::DocumentCanvasFacePayloadInput>(bad_payloads),
        },
    };
    const auto bad_payload = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = 16U,
        .height = 8U,
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(bad_payload_layers),
    });

    PP_EXPECT(harness, !no_canvas);
    PP_EXPECT(harness, !bad_layer);
    PP_EXPECT(harness, !bad_blend);
    PP_EXPECT(harness, !bad_duration);
    PP_EXPECT(harness, !bad_payload);
}

void clear_plan_records_legacy_canvas_effects(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_clear(true, 0.0F, 0.1F, 0.2F, 0.3F);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, plan.value().clears_canvas);
        PP_EXPECT(harness, plan.value().records_undo);
        PP_EXPECT(harness, plan.value().marks_unsaved);
        PP_EXPECT(harness, !plan.value().no_op);
        PP_EXPECT(harness, plan.value().r == 0.0F);
        PP_EXPECT(harness, plan.value().g == 0.1F);
        PP_EXPECT(harness, plan.value().b == 0.2F);
        PP_EXPECT(harness, plan.value().a == 0.3F);
    }
}

void clear_plan_noops_without_canvas(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_canvas_clear(false);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, !plan.value().clears_canvas);
        PP_EXPECT(harness, !plan.value().records_undo);
        PP_EXPECT(harness, !plan.value().marks_unsaved);
        PP_EXPECT(harness, plan.value().no_op);
    }
}

void clear_plan_rejects_bad_color_channels(pp::tests::Harness& harness)
{
    PP_EXPECT(harness, !pp::app::plan_document_canvas_clear(true, -0.01F, 0.0F, 0.0F, 0.0F));
    PP_EXPECT(harness, !pp::app::plan_document_canvas_clear(true, 0.0F, 1.01F, 0.0F, 0.0F));
    PP_EXPECT(harness, !pp::app::plan_document_canvas_clear(
        true,
        0.0F,
        0.0F,
        std::numeric_limits<float>::infinity(),
        0.0F));
}

void clear_executor_dispatches_color_to_service(pp::tests::Harness& harness)
{
    FakeDocumentCanvasClearServices services;
    const auto plan = pp::app::plan_document_canvas_clear(true, 0.25F, 0.5F, 0.75F, 1.0F);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, pp::app::execute_document_canvas_clear_plan(plan.value(), services).ok());
    }

    PP_EXPECT(harness, services.clear_calls == 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.call_order == "clear;");
}

void clear_executor_preserves_noop_without_canvas(pp::tests::Harness& harness)
{
    FakeDocumentCanvasClearServices services;
    const auto plan = pp::app::plan_document_canvas_clear(false);
    PP_EXPECT(harness, plan);
    if (plan) {
        PP_EXPECT(harness, pp::app::execute_document_canvas_clear_plan(plan.value(), services).ok());
    }

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

void clear_executor_rejects_invalid_color(pp::tests::Harness& harness)
{
    FakeDocumentCanvasClearServices services;
    pp::app::DocumentCanvasClearPlan plan;
    plan.clears_canvas = true;
    plan.records_undo = true;
    plan.marks_unsaved = true;
    plan.no_op = false;
    plan.r = 0.0F;
    plan.g = 0.0F;
    plan.b = 1.5F;
    plan.a = 0.0F;

    const auto status = pp::app::execute_document_canvas_clear_plan(plan, services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, status.code == pp::foundation::StatusCode::out_of_range);
    PP_EXPECT(harness, services.clear_calls == 0);
}

} // namespace

int main()
{
    pp::tests::Harness harness;
    harness.run("snapshot plan projects canvas metadata", snapshot_plan_projects_canvas_metadata);
    harness.run("snapshot plan defaults empty names and frames", snapshot_plan_defaults_empty_names_and_frames);
    harness.run("snapshot plan attaches captured face payloads", snapshot_plan_attaches_captured_face_payloads);
    harness.run("save writer route uses ppi writer for complete payloads", save_writer_route_uses_ppi_writer_for_complete_payloads);
    harness.run("save writer route falls back for pending payloads", save_writer_route_falls_back_for_pending_payloads);
    harness.run("project save target plan preserves legacy paths", project_save_target_plan_preserves_legacy_paths);
    harness.run("project save target plan accepts windows backslashes", project_save_target_plan_accepts_windows_backslashes);
    harness.run("project save target plan rejects empty inputs", project_save_target_plan_rejects_empty_inputs);
    harness.run("project save write plan writes direct for new targets", project_save_write_plan_writes_direct_for_new_targets);
    harness.run("project save write plan prefers temporary for existing targets", project_save_write_plan_prefers_temporary_for_existing_targets);
    harness.run("project save write plan rejects missing paths", project_save_write_plan_rejects_missing_paths);
    harness.run("project save commit plan succeeds for direct write", project_save_commit_plan_succeeds_for_direct_write);
    harness.run("project save commit plan succeeds for temporary swap", project_save_commit_plan_succeeds_for_temporary_swap);
    harness.run("project save commit plan fails when target remove fails", project_save_commit_plan_fails_when_target_remove_fails);
    harness.run("project save commit plan flags missing target after rename failure", project_save_commit_plan_flags_missing_target_after_rename_failure);
    harness.run("project save post commit plan records success side effects", project_save_post_commit_plan_records_success_side_effects);
    harness.run("project save post commit plan skips timelapse without encoder", project_save_post_commit_plan_skips_timelapse_without_encoder);
    harness.run("project save post commit plan preserves state after failure", project_save_post_commit_plan_preserves_state_after_failure);
    harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state);
    harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects);
    harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas);
    harness.run("clear plan rejects bad color channels", clear_plan_rejects_bad_color_channels);
    harness.run("clear executor dispatches color to service", clear_executor_dispatches_color_to_service);
    harness.run("clear executor preserves noop without canvas", clear_executor_preserves_noop_without_canvas);
    harness.run("clear executor rejects invalid color", clear_executor_rejects_invalid_color);
    return harness.finish();
}
