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

#include <array>
#include <cstddef>
#include <span>
#include <string>
#include <string_view>

namespace {

class FakeDocumentExportMenuServices final : public pp::app::DocumentExportMenuServices {
public:
    void show_jpeg_dialog() override { jpeg_dialogs += 1; }
    void show_png_dialog() override { png_dialogs += 1; }
    void show_layers_dialog() override { layer_dialogs += 1; }
    void show_cube_faces_dialog() override { cube_dialogs += 1; }
    void show_depth_dialog() override { depth_dialogs += 1; }
    void show_animation_frames_dialog() override { animation_frame_dialogs += 1; }
    void show_animation_mp4_dialog() override { animation_mp4_dialogs += 1; }
    void show_timelapse_dialog() override { timelapse_dialogs += 1; }
    void show_license_disabled() override { license_messages += 1; }

    [[nodiscard]] int total_calls() const noexcept
    {
        return jpeg_dialogs + png_dialogs + layer_dialogs + cube_dialogs + depth_dialogs
            + animation_frame_dialogs + animation_mp4_dialogs + timelapse_dialogs
            + license_messages;
    }

    int jpeg_dialogs = 0;
    int png_dialogs = 0;
    int layer_dialogs = 0;
    int cube_dialogs = 0;
    int depth_dialogs = 0;
    int animation_frame_dialogs = 0;
    int animation_mp4_dialogs = 0;
    int timelapse_dialogs = 0;
    int license_messages = 0;
};

class FakeDocumentExportServices final : public pp::app::DocumentExportServices {
public:
    bool create_directory(std::string_view directory) override
    {
        create_dir_calls += 1;
        last_directory = std::string(directory);
        return create_directory_result;
    }

    void export_equirectangular(const pp::app::DocumentExportFileTarget& target) override
    {
        equirectangular_exports += 1;
        last_path = target.path;
        call_order += "equirect;";
    }

    void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override
    {
        layer_stem_exports += 1;
        last_stem = target.stem_path;
        call_order += "layers-stem;";
    }

    void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
    {
        layer_collection_exports += 1;
        last_stem = target.stem_path;
        call_order += "layers-collection;";
    }

    void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override
    {
        frame_stem_exports += 1;
        last_stem = target.stem_path;
        call_order += "frames-stem;";
    }

    void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
    {
        frame_collection_exports += 1;
        last_stem = target.stem_path;
        call_order += "frames-collection;";
    }

    void export_depth(std::string_view document_name) override
    {
        depth_exports += 1;
        last_name = std::string(document_name);
        call_order += "depth;";
    }

    void export_cube_faces(std::string_view document_name) override
    {
        cube_exports += 1;
        last_name = std::string(document_name);
        call_order += "cube;";
    }

    bool create_directory_result = true;
    int create_dir_calls = 0;
    int equirectangular_exports = 0;
    int layer_stem_exports = 0;
    int layer_collection_exports = 0;
    int frame_stem_exports = 0;
    int frame_collection_exports = 0;
    int depth_exports = 0;
    int cube_exports = 0;
    std::string last_directory;
    std::string last_path;
    std::string last_stem;
    std::string last_name;
    std::string call_order;
};

class FakeDocumentVideoExportServices final : public pp::app::DocumentVideoExportServices {
public:
    void export_animation_mp4(std::string_view path) override
    {
        animation_exports += 1;
        last_path = std::string(path);
        call_order += "animation-export;";
    }

    void export_timelapse_mp4(std::string_view path) override
    {
        timelapse_exports += 1;
        last_path = std::string(path);
        call_order += "timelapse-export;";
    }

    void show_animation_export_success(std::string_view path) override
    {
        animation_messages += 1;
        last_message_path = std::string(path);
        call_order += "animation-message;";
    }

    void show_timelapse_export_success(std::string_view path) override
    {
        timelapse_messages += 1;
        last_message_path = std::string(path);
        call_order += "timelapse-message;";
    }

    int animation_exports = 0;
    int timelapse_exports = 0;
    int animation_messages = 0;
    int timelapse_messages = 0;
    std::string last_path;
    std::string last_message_path;
    std::string call_order;
};

class FakeDocumentCubeFaceExportWriteServices final
    : public pp::app::DocumentCubeFaceExportWriteServices
    , public pp::app::DocumentDepthExportWriteServices
    , public pp::app::DocumentExportFileWriteServices
    , public pp::app::DocumentExportCollectionWriteServices {
public:
    pp::foundation::Status write_binary_file(
        std::string_view path,
        std::span<const std::byte> bytes) override
    {
        write_calls += 1;
        last_path = std::string(path);
        total_bytes += bytes.size();
        call_order += "write:";
        call_order += path;
        call_order += ";";
        if (fail_on_write_call == write_calls) {
            return pp::foundation::Status::invalid_argument("write failed");
        }
        return pp::foundation::Status::success();
    }

    void publish_exported_image(std::string_view path) override
    {
        publish_calls += 1;
        last_published_path = std::string(path);
        call_order += "publish:";
        call_order += path;
        call_order += ";";
    }

    int write_calls = 0;
    int publish_calls = 0;
    int fail_on_write_call = -1;
    std::size_t total_bytes = 0;
    std::string last_path;
    std::string last_published_path;
    std::string call_order;
};

std::array<pp::app::DocumentCubeFaceExportPayload, 6> make_test_cube_face_payloads(
    const std::array<std::array<std::byte, 4>, 6>& bytes)
{
    return {
        pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[0]) },
        pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[1]) },
        pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[2]) },
        pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[3]) },
        pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[4]) },
        pp::app::DocumentCubeFaceExportPayload { .bytes = std::span<const std::byte>(bytes[5]) },
    };
}

void equirectangular_export_builds_file_target(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
    PP_EXPECT(harness, target);
    PP_EXPECT(harness, target.value().path == "D:/Paint/demo.png");
    PP_EXPECT(harness, target.value().suggested_name == "demo.png");
}

void export_file_target_rejects_invalid_input(pp::tests::Harness& harness)
{
    const auto missing_name = pp::app::make_document_export_file_target("D:/Paint", "", ".jpg");
    const auto missing_extension = pp::app::make_document_export_file_target("D:/Paint", "demo", "");
    PP_EXPECT(harness, !missing_name);
    PP_EXPECT(harness, missing_name.status().code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, !missing_extension);
    PP_EXPECT(harness, missing_extension.status().code == pp::foundation::StatusCode::invalid_argument);
}

void collection_export_builds_directory_and_stem(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers");
    PP_EXPECT(harness, target);
    PP_EXPECT(harness, target.value().directory == "D:/Paint/demo_layers");
    PP_EXPECT(harness, target.value().stem_path == "D:/Paint/demo_layers/demo");
}

void picked_directory_export_builds_stem(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_stem_target("D:/Exports", "demo");
    PP_EXPECT(harness, target);
    PP_EXPECT(harness, target.value().stem_path == "D:/Exports/demo");
}

void cube_face_export_builds_legacy_work_paths(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
    PP_EXPECT(harness, target);
    PP_EXPECT(harness, target.value().face_count == 6U);
    PP_EXPECT(harness, target.value().faces[0].face_name == "front");
    PP_EXPECT(harness, target.value().faces[0].path == "D:/Paint/demo-front.png");
    PP_EXPECT(harness, target.value().faces[1].face_name == "right");
    PP_EXPECT(harness, target.value().faces[1].path == "D:/Paint/demo-right.png");
    PP_EXPECT(harness, target.value().faces[2].face_name == "back");
    PP_EXPECT(harness, target.value().faces[2].path == "D:/Paint/demo-back.png");
    PP_EXPECT(harness, target.value().faces[3].face_name == "left");
    PP_EXPECT(harness, target.value().faces[3].path == "D:/Paint/demo-left.png");
    PP_EXPECT(harness, target.value().faces[4].face_name == "top");
    PP_EXPECT(harness, target.value().faces[4].path == "D:/Paint/demo-top.png");
    PP_EXPECT(harness, target.value().faces[5].face_name == "bottom");
    PP_EXPECT(harness, target.value().faces[5].path == "D:/Paint/demo-bottom.png");
}

void cube_face_export_rejects_invalid_target_inputs(pp::tests::Harness& harness)
{
    const auto missing_work_directory = pp::app::make_document_cube_face_export_target("", "demo");
    const auto missing_name = pp::app::make_document_cube_face_export_target("D:/Paint", "");

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

void depth_export_builds_legacy_work_paths(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_depth_export_target("D:/Paint", "demo");

    PP_EXPECT(harness, target);
    PP_EXPECT(harness, target.value().image_path == "D:/Paint/demo.png");
    PP_EXPECT(harness, target.value().depth_path == "D:/Paint/demo_depth.png");
}

void depth_export_rejects_invalid_target_inputs(pp::tests::Harness& harness)
{
    const auto missing_work_directory = pp::app::make_document_depth_export_target("", "demo");
    const auto missing_name = pp::app::make_document_depth_export_target("D:/Paint", "");

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

void cube_face_export_writer_writes_and_publishes_faces_in_order(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
    const std::array<std::array<std::byte, 4>, 6> bytes {};
    const auto payloads = make_test_cube_face_payloads(bytes);
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    PP_EXPECT(
        harness,
        pp::app::execute_document_cube_face_export_write(target.value(), payloads, services).ok());
    PP_EXPECT(harness, services.write_calls == 6);
    PP_EXPECT(harness, services.publish_calls == 6);
    PP_EXPECT(harness, services.total_bytes == 24U);
    PP_EXPECT(harness, services.last_path == "D:/Paint/demo-bottom.png");
    PP_EXPECT(harness, services.last_published_path == "D:/Paint/demo-bottom.png");
    PP_EXPECT(
        harness,
        services.call_order
            == "write:D:/Paint/demo-front.png;publish:D:/Paint/demo-front.png;"
               "write:D:/Paint/demo-right.png;publish:D:/Paint/demo-right.png;"
               "write:D:/Paint/demo-back.png;publish:D:/Paint/demo-back.png;"
               "write:D:/Paint/demo-left.png;publish:D:/Paint/demo-left.png;"
               "write:D:/Paint/demo-top.png;publish:D:/Paint/demo-top.png;"
               "write:D:/Paint/demo-bottom.png;publish:D:/Paint/demo-bottom.png;");
}

void cube_face_export_writer_stops_before_publish_on_write_failure(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
    const std::array<std::array<std::byte, 4>, 6> bytes {};
    const auto payloads = make_test_cube_face_payloads(bytes);
    FakeDocumentCubeFaceExportWriteServices services;
    services.fail_on_write_call = 2;

    PP_EXPECT(harness, target);
    const auto status = pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, services.write_calls == 2);
    PP_EXPECT(harness, services.publish_calls == 1);
    PP_EXPECT(
        harness,
        services.call_order
            == "write:D:/Paint/demo-front.png;publish:D:/Paint/demo-front.png;"
               "write:D:/Paint/demo-right.png;");
}

void cube_face_export_writer_rejects_malformed_inputs(pp::tests::Harness& harness)
{
    auto target = pp::app::make_document_cube_face_export_target("D:/Paint", "demo");
    const std::array<std::array<std::byte, 4>, 6> bytes {};
    auto payloads = make_test_cube_face_payloads(bytes);
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    auto malformed_target = target.value();
    malformed_target.face_count = 5U;
    PP_EXPECT(
        harness,
        !pp::app::execute_document_cube_face_export_write(malformed_target, payloads, services).ok());

    PP_EXPECT(
        harness,
        !pp::app::execute_document_cube_face_export_write(
            target.value(),
            std::span<const pp::app::DocumentCubeFaceExportPayload>(payloads.data(), payloads.size() - 1U),
            services)
             .ok());

    auto empty_path_target = target.value();
    empty_path_target.faces[0].path.clear();
    PP_EXPECT(
        harness,
        !pp::app::execute_document_cube_face_export_write(empty_path_target, payloads, services).ok());

    payloads[0].bytes = {};
    PP_EXPECT(
        harness,
        !pp::app::execute_document_cube_face_export_write(target.value(), payloads, services).ok());
    PP_EXPECT(harness, services.write_calls == 0);
    PP_EXPECT(harness, services.publish_calls == 0);
}

void depth_export_writer_writes_and_publishes_image_then_depth(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_depth_export_target("D:/Paint", "demo");
    const std::array<std::byte, 2> image_bytes {};
    const std::array<std::byte, 3> depth_bytes {};
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    PP_EXPECT(
        harness,
        pp::app::execute_document_depth_export_write(
            target.value(),
            pp::app::DocumentDepthExportPayload {
                .image_bytes = std::span<const std::byte>(image_bytes),
                .depth_bytes = std::span<const std::byte>(depth_bytes),
            },
            services)
            .ok());
    PP_EXPECT(harness, services.write_calls == 2);
    PP_EXPECT(harness, services.publish_calls == 2);
    PP_EXPECT(harness, services.total_bytes == 5U);
    PP_EXPECT(harness, services.last_path == "D:/Paint/demo_depth.png");
    PP_EXPECT(
        harness,
        services.call_order
            == "write:D:/Paint/demo.png;publish:D:/Paint/demo.png;"
               "write:D:/Paint/demo_depth.png;publish:D:/Paint/demo_depth.png;");
}

void depth_export_writer_stops_before_depth_publish_on_write_failure(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_depth_export_target("D:/Paint", "demo");
    const std::array<std::byte, 2> image_bytes {};
    const std::array<std::byte, 3> depth_bytes {};
    FakeDocumentCubeFaceExportWriteServices services;
    services.fail_on_write_call = 2;

    PP_EXPECT(harness, target);
    const auto status = pp::app::execute_document_depth_export_write(
        target.value(),
        pp::app::DocumentDepthExportPayload {
            .image_bytes = std::span<const std::byte>(image_bytes),
            .depth_bytes = std::span<const std::byte>(depth_bytes),
        },
        services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, services.write_calls == 2);
    PP_EXPECT(harness, services.publish_calls == 1);
    PP_EXPECT(
        harness,
        services.call_order
            == "write:D:/Paint/demo.png;publish:D:/Paint/demo.png;"
               "write:D:/Paint/demo_depth.png;");
}

void depth_export_writer_rejects_malformed_inputs(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_depth_export_target("D:/Paint", "demo");
    const std::array<std::byte, 2> image_bytes {};
    const std::array<std::byte, 3> depth_bytes {};
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    auto empty_image_path = target.value();
    empty_image_path.image_path.clear();
    PP_EXPECT(
        harness,
        !pp::app::execute_document_depth_export_write(
            empty_image_path,
            pp::app::DocumentDepthExportPayload {
                .image_bytes = std::span<const std::byte>(image_bytes),
                .depth_bytes = std::span<const std::byte>(depth_bytes),
            },
            services)
             .ok());

    PP_EXPECT(
        harness,
        !pp::app::execute_document_depth_export_write(
            target.value(),
            pp::app::DocumentDepthExportPayload {
                .image_bytes = {},
                .depth_bytes = std::span<const std::byte>(depth_bytes),
            },
            services)
             .ok());

    PP_EXPECT(harness, services.write_calls == 0);
    PP_EXPECT(harness, services.publish_calls == 0);
}

void file_export_writer_writes_and_publishes_target(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
    const std::array<std::byte, 4> bytes {};
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_file_write(
            target.value(),
            pp::app::DocumentExportFilePayload {
                .bytes = std::span<const std::byte>(bytes),
            },
            services)
            .ok());
    PP_EXPECT(harness, services.write_calls == 1);
    PP_EXPECT(harness, services.publish_calls == 1);
    PP_EXPECT(harness, services.total_bytes == 4U);
    PP_EXPECT(harness, services.last_path == "D:/Paint/demo.png");
    PP_EXPECT(harness, services.last_published_path == "D:/Paint/demo.png");
    PP_EXPECT(harness, services.call_order == "write:D:/Paint/demo.png;publish:D:/Paint/demo.png;");
}

void file_export_writer_stops_before_publish_on_write_failure(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".jpg");
    const std::array<std::byte, 4> bytes {};
    FakeDocumentCubeFaceExportWriteServices services;
    services.fail_on_write_call = 1;

    PP_EXPECT(harness, target);
    const auto status = pp::app::execute_document_export_file_write(
        target.value(),
        pp::app::DocumentExportFilePayload {
            .bytes = std::span<const std::byte>(bytes),
        },
        services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, services.write_calls == 1);
    PP_EXPECT(harness, services.publish_calls == 0);
    PP_EXPECT(harness, services.call_order == "write:D:/Paint/demo.jpg;");
}

void file_export_writer_rejects_malformed_inputs(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
    const std::array<std::byte, 4> bytes {};
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    auto empty_path = target.value();
    empty_path.path.clear();
    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_file_write(
            empty_path,
            pp::app::DocumentExportFilePayload {
                .bytes = std::span<const std::byte>(bytes),
            },
            services)
             .ok());

    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_file_write(
            target.value(),
            pp::app::DocumentExportFilePayload {
                .bytes = {},
            },
            services)
             .ok());

    PP_EXPECT(harness, services.write_calls == 0);
    PP_EXPECT(harness, services.publish_calls == 0);
}

void collection_export_builds_legacy_path_suffixes(pp::tests::Harness& harness)
{
    PP_EXPECT(
        harness,
        pp::app::make_document_layer_export_path_suffix(0, "Base") == "-layer00-Base.png");
    PP_EXPECT(
        harness,
        pp::app::make_document_layer_export_path_suffix(12, "Paint") == "-layer12-Paint.png");
    PP_EXPECT(
        harness,
        pp::app::make_document_animation_frame_export_path_suffix(0) == "-00.png");
    PP_EXPECT(
        harness,
        pp::app::make_document_animation_frame_export_path_suffix(125) == "-125.png");
}

void collection_export_writer_writes_and_publishes_payloads_in_order(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers");
    const std::array<std::byte, 2> base_bytes {};
    const std::array<std::byte, 3> paint_bytes {};
    const pp::app::DocumentExportCollectionPngPayload payloads[] {
        {
            .path_suffix = pp::app::make_document_layer_export_path_suffix(0, "Base"),
            .bytes = std::span<const std::byte>(base_bytes),
        },
        {
            .path_suffix = pp::app::make_document_layer_export_path_suffix(1, "Paint"),
            .bytes = std::span<const std::byte>(paint_bytes),
        },
    };
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_collection_write(target.value(), payloads, services).ok());
    PP_EXPECT(harness, services.write_calls == 2);
    PP_EXPECT(harness, services.publish_calls == 2);
    PP_EXPECT(harness, services.total_bytes == 5U);
    PP_EXPECT(harness, services.last_path == "D:/Paint/demo_layers/demo-layer01-Paint.png");
    PP_EXPECT(
        harness,
        services.call_order
            == "write:D:/Paint/demo_layers/demo-layer00-Base.png;"
               "publish:D:/Paint/demo_layers/demo-layer00-Base.png;"
               "write:D:/Paint/demo_layers/demo-layer01-Paint.png;"
               "publish:D:/Paint/demo_layers/demo-layer01-Paint.png;");
}

void collection_export_writer_rejects_malformed_inputs(pp::tests::Harness& harness)
{
    const auto target = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_frames");
    const std::array<std::byte, 2> bytes {};
    pp::app::DocumentExportCollectionPngPayload payloads[] {
        {
            .path_suffix = pp::app::make_document_animation_frame_export_path_suffix(0),
            .bytes = std::span<const std::byte>(bytes),
        },
    };
    FakeDocumentCubeFaceExportWriteServices services;

    PP_EXPECT(harness, target);
    auto empty_stem = target.value();
    empty_stem.stem_path.clear();
    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_collection_write(empty_stem, payloads, services).ok());

    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_collection_write(
            target.value(),
            std::span<const pp::app::DocumentExportCollectionPngPayload>(),
            services)
             .ok());

    payloads[0].path_suffix.clear();
    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_collection_write(target.value(), payloads, services).ok());
    payloads[0].path_suffix = pp::app::make_document_animation_frame_export_path_suffix(0);
    payloads[0].bytes = {};
    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_collection_write(target.value(), payloads, services).ok());
    PP_EXPECT(harness, services.write_calls == 0);
    PP_EXPECT(harness, services.publish_calls == 0);
}

void video_export_builds_suggested_name(pp::tests::Harness& harness)
{
    const auto timelapse = pp::app::make_document_export_suggested_name("demo", "-timelapse");
    const auto animation = pp::app::make_document_export_suggested_name("demo", "-animation");
    PP_EXPECT(harness, timelapse);
    PP_EXPECT(harness, animation);
    PP_EXPECT(harness, timelapse.value().name == "demo-timelapse");
    PP_EXPECT(harness, animation.value().name == "demo-animation");
}

void collection_export_target_plan_selects_platform_destination(pp::tests::Harness& harness)
{
    const auto layer_collection = pp::app::plan_document_export_collection_target(
        pp::app::DocumentExportCollectionKind::layers,
        true);
    const auto frame_collection = pp::app::plan_document_export_collection_target(
        pp::app::DocumentExportCollectionKind::animation_frames,
        true);
    const auto picked_layers = pp::app::plan_document_export_collection_target(
        pp::app::DocumentExportCollectionKind::layers,
        false);

    PP_EXPECT(
        harness,
        layer_collection.destination == pp::app::DocumentExportCollectionDestination::work_directory_collection);
    PP_EXPECT(
        harness,
        frame_collection.destination == pp::app::DocumentExportCollectionDestination::work_directory_collection);
    PP_EXPECT(
        harness,
        picked_layers.destination == pp::app::DocumentExportCollectionDestination::picked_directory_stem);
    PP_EXPECT(harness, layer_collection.suffix == "_layers");
    PP_EXPECT(harness, frame_collection.suffix == "_frames");
    PP_EXPECT(harness, picked_layers.suffix == "_layers");
}

void export_success_dialog_plans_image_destinations(pp::tests::Harness& harness)
{
    const auto photos = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::equirectangular,
        pp::app::DocumentExportSuccessDestination::photos);
    const auto pictures = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::equirectangular,
        pp::app::DocumentExportSuccessDestination::pictures_panopainter);
    const auto work = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::equirectangular,
        pp::app::DocumentExportSuccessDestination::work_directory,
        "D:/Paint");

    PP_EXPECT(harness, photos.show_dialog);
    PP_EXPECT(harness, photos.dialog.title == "Export Equirectangular");
    PP_EXPECT(harness, photos.dialog.message == "Image exported to Photos");
    PP_EXPECT(harness, pictures.dialog.message == "Image exported to Pictures/PanoPainter folder");
    PP_EXPECT(harness, work.dialog.message == "Image exported to D:/Paint");
}

void export_success_dialog_plans_collection_destinations(pp::tests::Harness& harness)
{
    const auto layers_path = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::layers,
        pp::app::DocumentExportSuccessDestination::path,
        "D:/Paint/demo_layers/demo");
    const auto frames_path = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::animation_frames,
        pp::app::DocumentExportSuccessDestination::path,
        "D:/Paint/demo_frames/demo");
    const auto layers_collection = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::layers,
        pp::app::DocumentExportSuccessDestination::files_panopainter);
    const auto frames_collection = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::animation_frames,
        pp::app::DocumentExportSuccessDestination::files_panopainter);

    PP_EXPECT(harness, layers_path.dialog.title == "Export Layers");
    PP_EXPECT(harness, layers_path.dialog.message == "Layers exported to: D:/Paint/demo_layers/demo");
    PP_EXPECT(harness, frames_path.dialog.message == "Layers exported to: D:/Paint/demo_frames/demo");
    PP_EXPECT(harness, layers_collection.dialog.message == "Image layers exported to Files/PanoPainter");
    PP_EXPECT(harness, frames_collection.dialog.message == "Image layers exported to Files/PanoPainter");
}

void export_success_dialog_plans_depth_and_cube_destinations(pp::tests::Harness& harness)
{
    const auto depth_files = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::depth,
        pp::app::DocumentExportSuccessDestination::files_panopainter);
    const auto depth_pictures = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::depth,
        pp::app::DocumentExportSuccessDestination::pictures_panopainter);
    const auto depth_work = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::depth,
        pp::app::DocumentExportSuccessDestination::work_directory,
        "D:/Paint");
    const auto cube_work = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::cube_faces,
        pp::app::DocumentExportSuccessDestination::work_directory,
        "D:/Paint");

    PP_EXPECT(harness, depth_files.dialog.title == "Export 3D View + Depth");
    PP_EXPECT(harness, depth_files.dialog.message == "Image and depth exported to Files/PanoPainter");
    PP_EXPECT(harness, depth_pictures.dialog.message == "Image and depth exported to Pictures/PanoPainter folder");
    PP_EXPECT(harness, depth_work.dialog.message == "Image and depth exported to D:/Paint");
    PP_EXPECT(harness, cube_work.dialog.title == "Export Cube Faces");
    PP_EXPECT(harness, cube_work.dialog.message == "Image and depth exported to D:/Paint");
}

void export_success_dialog_plans_video_destinations(pp::tests::Harness& harness)
{
    const auto animation_path = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::animation_mp4,
        pp::app::DocumentExportSuccessDestination::path,
        "D:/Paint/animation.mp4");
    const auto animation_success = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::animation_mp4,
        pp::app::DocumentExportSuccessDestination::generic_success);
    const auto timelapse_path = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::timelapse,
        pp::app::DocumentExportSuccessDestination::path,
        "D:/Paint/timelapse.mp4");
    const auto timelapse_success = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::timelapse,
        pp::app::DocumentExportSuccessDestination::generic_success);

    PP_EXPECT(harness, animation_path.dialog.title == "Export Animation");
    PP_EXPECT(harness, animation_path.dialog.message == "Animation exported to: D:/Paint/animation.mp4");
    PP_EXPECT(harness, animation_success.dialog.message == "Animation exported successfully.");
    PP_EXPECT(harness, timelapse_path.dialog.title == "Export Timelapse");
    PP_EXPECT(harness, timelapse_path.dialog.message == "Timelapse exported to: D:/Paint/timelapse.mp4");
    PP_EXPECT(harness, timelapse_success.dialog.message == "Timelapse exported successfully.");
}

void export_success_dialog_suppresses_unsupported_destinations(pp::tests::Harness& harness)
{
    const auto suppressed = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::depth,
        pp::app::DocumentExportSuccessDestination::suppressed);
    const auto invalid_combo = pp::app::plan_document_export_success_dialog(
        pp::app::DocumentExportSuccessKind::equirectangular,
        pp::app::DocumentExportSuccessDestination::files_panopainter);

    PP_EXPECT(harness, !suppressed.show_dialog);
    PP_EXPECT(harness, suppressed.dialog.title.empty());
    PP_EXPECT(harness, !invalid_combo.show_dialog);
    PP_EXPECT(harness, invalid_combo.dialog.message.empty());
}

void export_failure_dialog_plans_titles_and_messages(pp::tests::Harness& harness)
{
    const auto equirect = pp::app::plan_document_export_failure_dialog(
        pp::app::DocumentExportSuccessKind::equirectangular,
        "document name must not be empty");
    const auto frames = pp::app::plan_document_export_failure_dialog(
        pp::app::DocumentExportSuccessKind::animation_frames,
        "document name must not be empty");
    const auto animation = pp::app::plan_document_export_failure_dialog(
        pp::app::DocumentExportSuccessKind::animation_mp4,
        "video export path must not be empty");

    PP_EXPECT(harness, equirect.title == "Export Equirectangular");
    PP_EXPECT(harness, equirect.message == "document name must not be empty");
    PP_EXPECT(harness, !equirect.show_cancel);
    PP_EXPECT(harness, frames.title == "Export Layers");
    PP_EXPECT(harness, animation.title == "Export Animation");
    PP_EXPECT(harness, animation.message == "video export path must not be empty");
}

void export_license_disabled_dialog_preserves_legacy_warning(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_export_license_disabled_dialog();

    PP_EXPECT(harness, plan.title == "License");
    PP_EXPECT(harness, plan.message == "This function is disabled in demo mode.");
    PP_EXPECT(harness, !plan.show_cancel);
}

void export_execution_log_messages_cover_legacy_paths(pp::tests::Harness& harness)
{
    PP_EXPECT(
        harness,
        std::string_view(pp::app::document_export_execution_log_message(
            pp::app::DocumentExportExecutionKind::equirectangular_file))
            == "Document export file action failed");
    PP_EXPECT(
        harness,
        std::string_view(pp::app::document_export_execution_log_message(
            pp::app::DocumentExportExecutionKind::layers_collection))
            == "Document layer collection export failed");
    PP_EXPECT(
        harness,
        std::string_view(pp::app::document_export_execution_log_message(
            pp::app::DocumentExportExecutionKind::animation_frames_stem))
            == "Document animation frame stem export failed");
    PP_EXPECT(
        harness,
        std::string_view(pp::app::document_export_execution_log_message(
            pp::app::DocumentExportExecutionKind::cube_faces))
            == "Document cube-face export failed");
    PP_EXPECT(
        harness,
        std::string_view(pp::app::document_export_execution_log_message(
            pp::app::DocumentExportExecutionKind::timelapse))
            == "Document timelapse export failed");
}

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

    const auto plan = pp::app::plan_document_export_snapshot_route(
        pp::app::DocumentExportExecutionKind::equirectangular_file,
        report,
        true,
        true);

    PP_EXPECT(harness, plan.kind == pp::app::DocumentExportExecutionKind::equirectangular_file);
    PP_EXPECT(harness, plan.action == pp::app::DocumentExportSnapshotRouteAction::use_document_snapshot_writer);
    PP_EXPECT(harness, plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, plan.payload_complete);
    PP_EXPECT(harness, plan.target_supported);
    PP_EXPECT(harness, plan.platform_supported);
    PP_EXPECT(harness, plan.fallback_reason.empty());
}

void export_snapshot_target_support_covers_document_writer_formats(pp::tests::Harness& harness)
{
    PP_EXPECT(
        harness,
        pp::app::document_export_snapshot_target_supported(
            pp::app::DocumentExportExecutionKind::equirectangular_file,
            "D:/Paint/demo.PNG"));
    PP_EXPECT(
        harness,
        pp::app::document_export_snapshot_target_supported(
            pp::app::DocumentExportExecutionKind::equirectangular_file,
            "D:/Paint/demo.jpeg"));
    PP_EXPECT(
        harness,
        !pp::app::document_export_snapshot_target_supported(
            pp::app::DocumentExportExecutionKind::equirectangular_file,
            "D:/Paint/demo.tif"));
    PP_EXPECT(
        harness,
        pp::app::document_export_snapshot_target_supported(
            pp::app::DocumentExportExecutionKind::layers_collection));
    PP_EXPECT(
        harness,
        pp::app::document_export_snapshot_target_supported(
            pp::app::DocumentExportExecutionKind::cube_faces));
    PP_EXPECT(
        harness,
        pp::app::document_export_snapshot_target_supported(
            pp::app::DocumentExportExecutionKind::depth));
}

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

    const auto plan = pp::app::plan_document_export_snapshot_route_for_target(
        pp::app::DocumentExportExecutionKind::equirectangular_file,
        report,
        "D:/Paint/demo.tif",
        true);

    PP_EXPECT(harness, !plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, !plan.target_supported);
    PP_EXPECT(
        harness,
        plan.fallback_reason == "document snapshot export does not support this target");
}

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

    const auto plan = pp::app::plan_document_export_snapshot_route_for_current_platform(
        pp::app::DocumentExportExecutionKind::equirectangular_file,
        report,
        "D:/Paint/demo.png");

#if __WEB__
    PP_EXPECT(harness, !pp::app::document_export_snapshot_platform_supported());
    PP_EXPECT(harness, !plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, !plan.platform_supported);
    PP_EXPECT(
        harness,
        plan.fallback_reason == "document snapshot export is disabled on this platform");
#else
    PP_EXPECT(harness, pp::app::document_export_snapshot_platform_supported());
    PP_EXPECT(harness, plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, plan.platform_supported);
    PP_EXPECT(harness, plan.target_supported);
    PP_EXPECT(harness, plan.fallback_reason.empty());
#endif
}

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

    const auto plan = pp::app::plan_document_export_snapshot_route_for_current_platform(
        pp::app::DocumentExportExecutionKind::depth,
        report);

#if __WEB__
    PP_EXPECT(harness, !plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, !plan.platform_supported);
    PP_EXPECT(
        harness,
        plan.fallback_reason == "document snapshot export is disabled on this platform");
#else
    PP_EXPECT(harness, plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, plan.platform_supported);
    PP_EXPECT(harness, plan.target_supported);
    PP_EXPECT(harness, plan.fallback_reason.empty());
#endif
}

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

    const auto plan = pp::app::plan_document_export_snapshot_route(
        pp::app::DocumentExportExecutionKind::layers_collection,
        report,
        true,
        true);

    PP_EXPECT(harness, plan.action == pp::app::DocumentExportSnapshotRouteAction::use_legacy_export);
    PP_EXPECT(harness, !plan.uses_document_snapshot_writer);
    PP_EXPECT(harness, !plan.payload_complete);
    PP_EXPECT(
        harness,
        plan.fallback_reason == "document snapshot still requires renderer payload readback");
}

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

    const auto unsupported_target = pp::app::plan_document_export_snapshot_route(
        pp::app::DocumentExportExecutionKind::animation_mp4,
        report,
        false,
        true);
    const auto unsupported_platform = pp::app::plan_document_export_snapshot_route(
        pp::app::DocumentExportExecutionKind::animation_frames_collection,
        report,
        true,
        false);

    PP_EXPECT(harness, !unsupported_target.uses_document_snapshot_writer);
    PP_EXPECT(
        harness,
        unsupported_target.fallback_reason == "document snapshot export does not support this target");
    PP_EXPECT(harness, !unsupported_platform.uses_document_snapshot_writer);
    PP_EXPECT(
        harness,
        unsupported_platform.fallback_reason == "document snapshot export is disabled on this platform");
}

void export_start_allows_valid_canvas_state(pp::tests::Harness& harness)
{
    PP_EXPECT(
        harness,
        pp::app::plan_document_export_start(true, true, true)
            == pp::app::DocumentExportStartDecision::start_now);
    PP_EXPECT(
        harness,
        pp::app::plan_document_export_start(false, false, true)
            == pp::app::DocumentExportStartDecision::start_now);
}

void export_start_blocks_demo_only_when_license_required(pp::tests::Harness& harness)
{
    PP_EXPECT(
        harness,
        pp::app::plan_document_export_start(true, false, true)
            == pp::app::DocumentExportStartDecision::show_license_disabled);
}

void export_start_reports_missing_canvas_after_license_gate(pp::tests::Harness& harness)
{
    PP_EXPECT(
        harness,
        pp::app::plan_document_export_start(false, true, false)
            == pp::app::DocumentExportStartDecision::unavailable_no_canvas);
    PP_EXPECT(
        harness,
        pp::app::plan_document_export_start(true, false, false)
            == pp::app::DocumentExportStartDecision::show_license_disabled);
}

void export_menu_routes_image_dialogs_without_license(pp::tests::Harness& harness)
{
    const auto jpeg = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::jpeg,
        true,
        false);
    const auto png = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::png,
        true,
        false);

    PP_EXPECT(harness, jpeg.action == pp::app::DocumentExportMenuAction::show_jpeg_dialog);
    PP_EXPECT(harness, jpeg.opens_dialog);
    PP_EXPECT(harness, png.action == pp::app::DocumentExportMenuAction::show_png_dialog);
    PP_EXPECT(harness, png.opens_dialog);
}

void export_menu_blocks_video_when_license_is_missing(pp::tests::Harness& harness)
{
    const auto mp4 = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::animation_mp4,
        true,
        false);
    const auto timelapse = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::timelapse,
        true,
        false);

    PP_EXPECT(harness, mp4.action == pp::app::DocumentExportMenuAction::show_license_disabled);
    PP_EXPECT(harness, !mp4.opens_dialog);
    PP_EXPECT(harness, timelapse.action == pp::app::DocumentExportMenuAction::show_license_disabled);
    PP_EXPECT(harness, !timelapse.opens_dialog);
}

void export_menu_reports_missing_canvas_for_unlicensed_image_exports(pp::tests::Harness& harness)
{
    const auto plan = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::layers,
        false,
        true);

    PP_EXPECT(harness, plan.action == pp::app::DocumentExportMenuAction::unavailable_no_canvas);
    PP_EXPECT(harness, !plan.opens_dialog);
}

void export_menu_executor_dispatches_all_dialog_actions(pp::tests::Harness& harness)
{
    FakeDocumentExportMenuServices services;

    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::jpeg,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::png,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::layers,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::cube_faces,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::depth,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::animation_frames,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::animation_mp4,
                true,
                true),
            services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_menu_plan(
            pp::app::plan_document_export_menu_action(
                pp::app::DocumentExportMenuKind::timelapse,
                true,
                true),
            services).ok());

    PP_EXPECT(harness, services.jpeg_dialogs == 1);
    PP_EXPECT(harness, services.png_dialogs == 1);
    PP_EXPECT(harness, services.layer_dialogs == 1);
    PP_EXPECT(harness, services.cube_dialogs == 1);
    PP_EXPECT(harness, services.depth_dialogs == 1);
    PP_EXPECT(harness, services.animation_frame_dialogs == 1);
    PP_EXPECT(harness, services.animation_mp4_dialogs == 1);
    PP_EXPECT(harness, services.timelapse_dialogs == 1);
}

void export_menu_executor_preserves_blocked_and_unavailable_actions(pp::tests::Harness& harness)
{
    FakeDocumentExportMenuServices services;

    const auto blocked = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::animation_mp4,
        true,
        false);
    PP_EXPECT(harness, blocked.action == pp::app::DocumentExportMenuAction::show_license_disabled);
    PP_EXPECT(harness, pp::app::execute_document_export_menu_plan(blocked, services).ok());
    PP_EXPECT(harness, services.license_messages == 1);

    const auto unavailable = pp::app::plan_document_export_menu_action(
        pp::app::DocumentExportMenuKind::png,
        false,
        true);
    PP_EXPECT(harness, unavailable.action == pp::app::DocumentExportMenuAction::unavailable_no_canvas);
    PP_EXPECT(harness, pp::app::execute_document_export_menu_plan(unavailable, services).ok());
    PP_EXPECT(harness, services.total_calls() == 1);
}

void export_executor_dispatches_file_stem_collection_and_named_exports(pp::tests::Harness& harness)
{
    FakeDocumentExportServices services;
    const auto file = pp::app::make_document_export_file_target("D:/Paint", "demo", ".png");
    const auto collection = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers");
    const auto stem = pp::app::make_document_export_stem_target("D:/Exports", "demo");

    PP_EXPECT(harness, file);
    PP_EXPECT(harness, collection);
    PP_EXPECT(harness, stem);
    PP_EXPECT(harness, pp::app::execute_document_export_file(file.value(), services).ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_stem(
            pp::app::DocumentExportCollectionKind::layers,
            stem.value(),
            services)
            .ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_stem(
            pp::app::DocumentExportCollectionKind::animation_frames,
            stem.value(),
            services)
            .ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_collection(
            pp::app::DocumentExportCollectionKind::layers,
            collection.value(),
            services)
            .ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_collection(
            pp::app::DocumentExportCollectionKind::animation_frames,
            collection.value(),
            services)
            .ok());
    PP_EXPECT(harness, pp::app::execute_document_export_depth("demo", services).ok());
    PP_EXPECT(harness, pp::app::execute_document_export_cube_faces("demo", services).ok());

    PP_EXPECT(harness, services.create_dir_calls == 2);
    PP_EXPECT(harness, services.equirectangular_exports == 1);
    PP_EXPECT(harness, services.layer_stem_exports == 1);
    PP_EXPECT(harness, services.layer_collection_exports == 1);
    PP_EXPECT(harness, services.frame_stem_exports == 1);
    PP_EXPECT(harness, services.frame_collection_exports == 1);
    PP_EXPECT(harness, services.depth_exports == 1);
    PP_EXPECT(harness, services.cube_exports == 1);
    PP_EXPECT(
        harness,
        services.call_order
            == "equirect;layers-stem;frames-stem;layers-collection;frames-collection;depth;cube;");
}

void export_executor_preserves_failed_directory_creation(pp::tests::Harness& harness)
{
    FakeDocumentExportServices services;
    services.create_directory_result = false;
    const auto collection = pp::app::make_document_export_collection_target("D:/Paint", "demo", "_layers");

    PP_EXPECT(harness, collection);
    PP_EXPECT(
        harness,
        pp::app::execute_document_export_collection(
            pp::app::DocumentExportCollectionKind::layers,
            collection.value(),
            services)
            .ok());
    PP_EXPECT(harness, services.create_dir_calls == 1);
    PP_EXPECT(harness, services.layer_collection_exports == 0);
    PP_EXPECT(harness, services.call_order.empty());
}

void export_executor_rejects_malformed_targets(pp::tests::Harness& harness)
{
    FakeDocumentExportServices services;

    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_file(pp::app::DocumentExportFileTarget {}, services).ok());
    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_stem(
            pp::app::DocumentExportCollectionKind::layers,
            pp::app::DocumentExportStemTarget {},
            services)
             .ok());
    PP_EXPECT(
        harness,
        !pp::app::execute_document_export_collection(
            pp::app::DocumentExportCollectionKind::animation_frames,
            pp::app::DocumentExportCollectionTarget {},
            services)
             .ok());
    PP_EXPECT(harness, !pp::app::execute_document_export_depth("", services).ok());
    PP_EXPECT(harness, !pp::app::execute_document_export_cube_faces("", services).ok());
    PP_EXPECT(harness, services.call_order.empty());
}

void video_export_executor_dispatches_animation_and_timelapse(pp::tests::Harness& harness)
{
    FakeDocumentVideoExportServices services;

    PP_EXPECT(
        harness,
        pp::app::execute_document_video_export(
            pp::app::DocumentVideoExportKind::animation_mp4,
            "D:/Paint/animation.mp4",
            services)
            .ok());
    PP_EXPECT(
        harness,
        pp::app::execute_document_video_export(
            pp::app::DocumentVideoExportKind::timelapse,
            "D:/Paint/timelapse.mp4",
            services)
            .ok());

    PP_EXPECT(harness, services.animation_exports == 1);
    PP_EXPECT(harness, services.timelapse_exports == 1);
    PP_EXPECT(harness, services.animation_messages == 1);
    PP_EXPECT(harness, services.timelapse_messages == 1);
    PP_EXPECT(harness, services.last_path == "D:/Paint/timelapse.mp4");
    PP_EXPECT(harness, services.last_message_path == "D:/Paint/timelapse.mp4");
    PP_EXPECT(
        harness,
        services.call_order
            == "animation-export;animation-message;timelapse-export;timelapse-message;");
}

void video_export_executor_rejects_empty_paths(pp::tests::Harness& harness)
{
    FakeDocumentVideoExportServices services;

    PP_EXPECT(
        harness,
        !pp::app::execute_document_video_export(
            pp::app::DocumentVideoExportKind::animation_mp4,
            "",
            services)
             .ok());
    PP_EXPECT(harness, services.call_order.empty());
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("equirectangular export builds file target", equirectangular_export_builds_file_target);
    harness.run("export file target rejects invalid input", export_file_target_rejects_invalid_input);
    harness.run("collection export builds directory and stem", collection_export_builds_directory_and_stem);
    harness.run("picked directory export builds stem", picked_directory_export_builds_stem);
    harness.run("cube face export builds legacy work paths", cube_face_export_builds_legacy_work_paths);
    harness.run("cube face export rejects invalid target inputs", cube_face_export_rejects_invalid_target_inputs);
    harness.run("depth export builds legacy work paths", depth_export_builds_legacy_work_paths);
    harness.run("depth export rejects invalid target inputs", depth_export_rejects_invalid_target_inputs);
    harness.run(
        "cube face export writer writes and publishes faces in order",
        cube_face_export_writer_writes_and_publishes_faces_in_order);
    harness.run(
        "cube face export writer stops before publish on write failure",
        cube_face_export_writer_stops_before_publish_on_write_failure);
    harness.run("cube face export writer rejects malformed inputs", cube_face_export_writer_rejects_malformed_inputs);
    harness.run(
        "depth export writer writes and publishes image then depth",
        depth_export_writer_writes_and_publishes_image_then_depth);
    harness.run(
        "depth export writer stops before depth publish on write failure",
        depth_export_writer_stops_before_depth_publish_on_write_failure);
    harness.run("depth export writer rejects malformed inputs", depth_export_writer_rejects_malformed_inputs);
    harness.run("file export writer writes and publishes target", file_export_writer_writes_and_publishes_target);
    harness.run(
        "file export writer stops before publish on write failure",
        file_export_writer_stops_before_publish_on_write_failure);
    harness.run("file export writer rejects malformed inputs", file_export_writer_rejects_malformed_inputs);
    harness.run("collection export builds legacy path suffixes", collection_export_builds_legacy_path_suffixes);
    harness.run(
        "collection export writer writes and publishes payloads in order",
        collection_export_writer_writes_and_publishes_payloads_in_order);
    harness.run("collection export writer rejects malformed inputs", collection_export_writer_rejects_malformed_inputs);
    harness.run("video export builds suggested name", video_export_builds_suggested_name);
    harness.run("collection export target plan selects platform destination", collection_export_target_plan_selects_platform_destination);
    harness.run("export success dialog plans image destinations", export_success_dialog_plans_image_destinations);
    harness.run("export success dialog plans collection destinations", export_success_dialog_plans_collection_destinations);
    harness.run("export success dialog plans depth and cube destinations", export_success_dialog_plans_depth_and_cube_destinations);
    harness.run("export success dialog plans video destinations", export_success_dialog_plans_video_destinations);
    harness.run("export success dialog suppresses unsupported destinations", export_success_dialog_suppresses_unsupported_destinations);
    harness.run("export failure dialog plans titles and messages", export_failure_dialog_plans_titles_and_messages);
    harness.run("export license disabled dialog preserves legacy warning", export_license_disabled_dialog_preserves_legacy_warning);
    harness.run("export execution log messages cover legacy paths", export_execution_log_messages_cover_legacy_paths);
    harness.run("export snapshot route uses document writer when ready", export_snapshot_route_uses_document_writer_when_ready);
    harness.run(
        "export snapshot target support covers document writer formats",
        export_snapshot_target_support_covers_document_writer_formats);
    harness.run(
        "export snapshot route for target rejects unsupported extension",
        export_snapshot_route_for_target_rejects_unsupported_extension);
    harness.run(
        "export snapshot route for current platform uses platform policy",
        export_snapshot_route_for_current_platform_uses_platform_policy);
    harness.run(
        "export snapshot route for current platform supports depth writer",
        export_snapshot_route_for_current_platform_supports_depth_writer);
    harness.run(
        "export snapshot route falls back for pending renderer payloads",
        export_snapshot_route_falls_back_for_pending_renderer_payloads);
    harness.run(
        "export snapshot route falls back for unsupported target or platform",
        export_snapshot_route_falls_back_for_unsupported_target_or_platform);
    harness.run("export start allows valid canvas state", export_start_allows_valid_canvas_state);
    harness.run("export start blocks demo only when license required", export_start_blocks_demo_only_when_license_required);
    harness.run("export start reports missing canvas after license gate", export_start_reports_missing_canvas_after_license_gate);
    harness.run("export menu routes image dialogs without license", export_menu_routes_image_dialogs_without_license);
    harness.run("export menu blocks video when license is missing", export_menu_blocks_video_when_license_is_missing);
    harness.run("export menu reports missing canvas for unlicensed image exports", export_menu_reports_missing_canvas_for_unlicensed_image_exports);
    harness.run("export menu executor dispatches all dialog actions", export_menu_executor_dispatches_all_dialog_actions);
    harness.run("export menu executor preserves blocked and unavailable actions", export_menu_executor_preserves_blocked_and_unavailable_actions);
    harness.run(
        "export executor dispatches file stem collection and named exports",
        export_executor_dispatches_file_stem_collection_and_named_exports);
    harness.run("export executor preserves failed directory creation", export_executor_preserves_failed_directory_creation);
    harness.run("export executor rejects malformed targets", export_executor_rejects_malformed_targets);
    harness.run("video export executor dispatches animation and timelapse", video_export_executor_dispatches_animation_and_timelapse);
    harness.run("video export executor rejects empty paths", video_export_executor_rejects_empty_paths);
    return harness.finish();
}
