#include "renderer_gl/command_plan.h"
#include "test_harness.h"

#include <string_view>
#include <vector>

namespace {

pp::renderer::RecordedRenderCommand begin_render_pass_command() noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::begin_render_pass;
    command.target_desc.extent = pp::renderer::Extent2D { .width = 64U, .height = 64U };
    command.target_desc.format = pp::renderer::TextureFormat::rgba8;
    command.clear_color_enabled = true;
    command.clear_depth_enabled = true;
    return command;
}

pp::renderer::RecordedRenderCommand command_with_kind(
    pp::renderer::RecordedRenderCommandKind kind) noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = kind;
    return command;
}

pp::renderer::RecordedRenderCommand viewport_command() noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::set_viewport;
    command.viewport = pp::renderer::Viewport { .width = 64U, .height = 64U };
    return command;
}

pp::renderer::RecordedRenderCommand draw_command() noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::draw;
    command.mesh_desc.topology = pp::renderer::PrimitiveTopology::triangles;
    command.draw_desc.vertex_count = 3U;
    return command;
}

pp::renderer::RecordedRenderCommand bind_shader_command(const char* name) noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::bind_shader;
    command.name = name;
    return command;
}

pp::renderer::RecordedRenderCommand shader_uniform_command(const char* name, std::uint64_t bytes) noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::set_shader_uniform;
    command.name = name;
    command.uniform_bytes = bytes;
    return command;
}

pp::renderer::RecordedRenderCommand bind_mesh_command() noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::bind_mesh;
    command.mesh_desc.topology = pp::renderer::PrimitiveTopology::triangles;
    command.mesh_desc.vertex_count = 3U;
    command.mesh_desc.index_count = 3U;
    return command;
}

pp::renderer::RecordedRenderCommand blit_command(pp::renderer::BlitFilter filter) noexcept
{
    pp::renderer::RecordedRenderCommand command;
    command.kind = pp::renderer::RecordedRenderCommandKind::blit_render_target;
    command.source_desc.format = pp::renderer::TextureFormat::rgba8;
    command.destination_desc.format = pp::renderer::TextureFormat::rgba8;
    command.blit_filter = filter;
    return command;
}

pp::renderer::ReadbackRegion region(
    std::uint32_t x,
    std::uint32_t y,
    std::uint32_t width,
    std::uint32_t height) noexcept
{
    pp::renderer::ReadbackRegion result;
    result.x = x;
    result.y = y;
    result.width = width;
    result.height = height;
    return result;
}

void maps_render_pass_and_state_commands(pp::tests::Harness& h)
{
    const auto begin = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::begin_render_pass,
        .target_desc = pp::renderer::TextureDesc {
            .extent = pp::renderer::Extent2D { .width = 64U, .height = 32U },
            .format = pp::renderer::TextureFormat::rgba8,
        },
        .clear_color_enabled = true,
        .clear_color = pp::renderer::ClearColor { .r = 0.25F, .g = 0.5F, .b = 0.75F, .a = 1.0F },
        .clear_depth_enabled = true,
        .clear_depth = 0.875F,
        .clear_stencil_enabled = true,
        .clear_stencil = 3U,
    });
    const auto viewport = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::set_viewport,
        .viewport = pp::renderer::Viewport { .x = 2, .y = 4, .width = 32U, .height = 16U },
    });
    const auto scissor = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::set_scissor,
        .scissor = pp::renderer::ScissorRect { .enabled = true, .x = 3, .y = 5, .width = 8U, .height = 6U },
    });
    const auto blend = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::set_blend_state,
        .blend_state = pp::renderer::BlendState {
            .enabled = true,
            .source_color = pp::renderer::BlendFactor::source_alpha,
            .destination_color = pp::renderer::BlendFactor::one_minus_source_alpha,
        },
    });
    const auto depth = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::set_depth_state,
        .depth_state = pp::renderer::DepthState {
            .test_enabled = true,
            .write_enabled = true,
            .compare = pp::renderer::CompareOp::greater,
        },
    });

    PP_EXPECT(h, begin.supported);
    PP_EXPECT(h, begin.kind == pp::renderer::gl::OpenGlPlannedCommandKind::begin_render_pass);
    PP_EXPECT(h, begin.clear_mask == 0x00004500U);
    PP_EXPECT(h, begin.clear_values.color[2] == 0.75F);
    PP_EXPECT(h, begin.clear_values.depth == 0.875F);
    PP_EXPECT(h, begin.clear_values.stencil == 3U);
    PP_EXPECT(h, begin.texture_format.internal_format == 0x8058U);
    PP_EXPECT(h, !begin.requires_render_pass);
    PP_EXPECT(h, viewport.supported);
    PP_EXPECT(h, viewport.requires_render_pass);
    PP_EXPECT(h, viewport.viewport.x == 2);
    PP_EXPECT(h, viewport.viewport.width == 32);
    PP_EXPECT(h, scissor.supported);
    PP_EXPECT(h, scissor.scissor.enabled == 1U);
    PP_EXPECT(h, scissor.scissor.height == 6);
    PP_EXPECT(h, blend.supported);
    PP_EXPECT(h, blend.blend.enabled == 1U);
    PP_EXPECT(h, blend.blend.source_color_factor == 0x0302U);
    PP_EXPECT(h, depth.supported);
    PP_EXPECT(h, depth.depth.test_enabled == 1U);
    PP_EXPECT(h, depth.depth.compare_function == 0x0204U);
}

void maps_binding_draw_and_blit_commands(pp::tests::Harness& h)
{
    const auto texture = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::bind_texture,
        .texture_desc = pp::renderer::TextureDesc {
            .extent = pp::renderer::Extent2D { .width = 16U, .height = 16U },
            .format = pp::renderer::TextureFormat::depth24_stencil8,
        },
        .texture_slot = 3U,
    });
    const auto sampler = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::bind_sampler,
        .sampler_desc = pp::renderer::SamplerDesc {
            .min_filter = pp::renderer::SamplerFilter::nearest,
            .mag_filter = pp::renderer::SamplerFilter::linear,
            .mip_filter = pp::renderer::SamplerFilter::linear,
            .address_u = pp::renderer::SamplerAddressMode::repeat,
            .address_v = pp::renderer::SamplerAddressMode::mirrored_repeat,
            .address_w = pp::renderer::SamplerAddressMode::clamp_to_border,
        },
        .sampler_slot = 3U,
    });
    const auto mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::bind_mesh,
        .mesh_desc = pp::renderer::MeshDesc { .topology = pp::renderer::PrimitiveTopology::triangle_strip },
    });
    const auto draw = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::draw,
        .mesh_desc = pp::renderer::MeshDesc { .topology = pp::renderer::PrimitiveTopology::lines },
        .draw_desc = pp::renderer::DrawDesc { .vertex_count = 4U, .index_count = 2U },
    });
    const auto blit = pp::renderer::gl::plan_recorded_render_command(
        blit_command(pp::renderer::BlitFilter::linear));

    PP_EXPECT(h, texture.supported);
    PP_EXPECT(h, texture.requires_render_pass);
    PP_EXPECT(h, texture.texture_format.internal_format == 0x88F0U);
    PP_EXPECT(h, texture.texture_slot == 3U);
    PP_EXPECT(h, sampler.supported);
    PP_EXPECT(h, sampler.sampler.min_filter == 0x2702U);
    PP_EXPECT(h, sampler.sampler.wrap_t == 0x8370U);
    PP_EXPECT(h, sampler.sampler_slot == 3U);
    PP_EXPECT(h, mesh.supported);
    PP_EXPECT(h, mesh.primitive_mode == 0x0005U);
    PP_EXPECT(h, draw.supported);
    PP_EXPECT(h, draw.primitive_mode == 0x0001U);
    PP_EXPECT(h, draw.draw_vertex_count == 4U);
    PP_EXPECT(h, draw.draw_index_count == 2U);
    PP_EXPECT(h, blit.supported);
    PP_EXPECT(h, !blit.requires_render_pass);
    PP_EXPECT(h, blit.blit_filter.value == 0x2601U);
    PP_EXPECT(h, blit.source_texture_format.internal_format == 0x8058U);
    PP_EXPECT(h, blit.destination_texture_format.internal_format == 0x8058U);
}

void maps_texture_io_and_capture_commands(pp::tests::Harness& h)
{
    pp::renderer::RecordedRenderCommand upload_command;
    upload_command.kind = pp::renderer::RecordedRenderCommandKind::upload_texture;
    upload_command.texture_desc.format = pp::renderer::TextureFormat::r8;
    upload_command.readback_region = region(1U, 2U, 3U, 4U);
    upload_command.upload_bytes = 12U;

    pp::renderer::RecordedRenderCommand mipmap_command;
    mipmap_command.kind = pp::renderer::RecordedRenderCommandKind::generate_mipmaps;
    mipmap_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    mipmap_command.generated_mip_levels = 4U;
    mipmap_command.generated_mip_bytes = 340U;

    pp::renderer::RecordedRenderCommand transition_command;
    transition_command.kind = pp::renderer::RecordedRenderCommandKind::transition_texture;
    transition_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    transition_command.before_state = pp::renderer::TextureState::upload_destination;
    transition_command.after_state = pp::renderer::TextureState::shader_read;

    pp::renderer::RecordedRenderCommand copy_command;
    copy_command.kind = pp::renderer::RecordedRenderCommandKind::copy_texture;
    copy_command.source_desc.format = pp::renderer::TextureFormat::rgba8;
    copy_command.destination_desc.format = pp::renderer::TextureFormat::r8;
    copy_command.source_region = region(0U, 1U, 8U, 4U);
    copy_command.destination_region = region(2U, 3U, 8U, 4U);
    copy_command.copy_source_bytes = 128U;
    copy_command.copy_destination_bytes = 32U;

    pp::renderer::RecordedRenderCommand read_command;
    read_command.kind = pp::renderer::RecordedRenderCommandKind::read_texture;
    read_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    read_command.readback_region = region(4U, 5U, 6U, 7U);
    read_command.readback_bytes = 168U;

    pp::renderer::RecordedRenderCommand capture_command;
    capture_command.kind = pp::renderer::RecordedRenderCommandKind::capture_frame;
    capture_command.target_desc.format = pp::renderer::TextureFormat::rgba8;
    capture_command.capture_bytes = 512U;

    const auto upload = pp::renderer::gl::plan_recorded_render_command(upload_command);
    const auto mipmap = pp::renderer::gl::plan_recorded_render_command(mipmap_command);
    const auto transition = pp::renderer::gl::plan_recorded_render_command(transition_command);
    const auto copy = pp::renderer::gl::plan_recorded_render_command(copy_command);
    const auto read = pp::renderer::gl::plan_recorded_render_command(read_command);
    const auto capture = pp::renderer::gl::plan_recorded_render_command(capture_command);

    PP_EXPECT(h, upload.supported);
    PP_EXPECT(h, upload.kind == pp::renderer::gl::OpenGlPlannedCommandKind::upload_texture);
    PP_EXPECT(h, upload.texture_format.internal_format == 0x8229U);
    PP_EXPECT(h, upload.readback_region.y == 2U);
    PP_EXPECT(h, upload.upload_bytes == 12U);
    PP_EXPECT(h, mipmap.supported);
    PP_EXPECT(h, mipmap.kind == pp::renderer::gl::OpenGlPlannedCommandKind::generate_mipmaps);
    PP_EXPECT(h, mipmap.generated_mip_levels == 4U);
    PP_EXPECT(h, mipmap.generated_mip_bytes == 340U);
    PP_EXPECT(h, transition.supported);
    PP_EXPECT(h, transition.kind == pp::renderer::gl::OpenGlPlannedCommandKind::transition_texture);
    PP_EXPECT(h, transition.before_state == pp::renderer::TextureState::upload_destination);
    PP_EXPECT(h, transition.after_state == pp::renderer::TextureState::shader_read);
    PP_EXPECT(h, copy.supported);
    PP_EXPECT(h, copy.kind == pp::renderer::gl::OpenGlPlannedCommandKind::copy_texture);
    PP_EXPECT(h, copy.source_texture_format.internal_format == 0x8058U);
    PP_EXPECT(h, copy.destination_texture_format.internal_format == 0x8229U);
    PP_EXPECT(h, copy.destination_region.x == 2U);
    PP_EXPECT(h, copy.copy_source_bytes == 128U);
    PP_EXPECT(h, copy.copy_destination_bytes == 32U);
    PP_EXPECT(h, read.supported);
    PP_EXPECT(h, read.kind == pp::renderer::gl::OpenGlPlannedCommandKind::read_texture);
    PP_EXPECT(h, read.readback_region.width == 6U);
    PP_EXPECT(h, read.readback_bytes == 168U);
    PP_EXPECT(h, capture.supported);
    PP_EXPECT(h, capture.kind == pp::renderer::gl::OpenGlPlannedCommandKind::capture_frame);
    PP_EXPECT(h, capture.capture_bytes == 512U);
}

void maps_shader_commands(pp::tests::Harness& h)
{
    const auto shader = pp::renderer::gl::plan_recorded_render_command(
        bind_shader_command("stroke-composite"));
    const auto uniform = pp::renderer::gl::plan_recorded_render_command(
        shader_uniform_command("mvp", 64U));

    PP_EXPECT(h, shader.supported);
    PP_EXPECT(h, shader.kind == pp::renderer::gl::OpenGlPlannedCommandKind::bind_shader);
    PP_EXPECT(h, shader.name == std::string_view("stroke-composite"));
    PP_EXPECT(h, shader.requires_render_pass);
    PP_EXPECT(h, uniform.supported);
    PP_EXPECT(h, uniform.kind == pp::renderer::gl::OpenGlPlannedCommandKind::set_shader_uniform);
    PP_EXPECT(h, uniform.name == std::string_view("mvp"));
    PP_EXPECT(h, uniform.uniform_bytes == 64U);
    PP_EXPECT(h, uniform.requires_render_pass);
}

void rejects_unsupported_command_tokens(pp::tests::Harness& h)
{
    const auto bad_blend = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::set_blend_state,
        .blend_state = pp::renderer::BlendState {
            .source_color = static_cast<pp::renderer::BlendFactor>(255U),
        },
    });
    const auto bad_depth = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::set_depth_state,
        .depth_state = pp::renderer::DepthState {
            .compare = static_cast<pp::renderer::CompareOp>(255U),
        },
    });
    const auto bad_sampler = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::bind_sampler,
        .sampler_desc = pp::renderer::SamplerDesc {
            .address_u = static_cast<pp::renderer::SamplerAddressMode>(255U),
        },
    });
    const auto bad_texture_slot = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::bind_texture,
        .texture_desc = pp::renderer::TextureDesc {
            .extent = pp::renderer::Extent2D { .width = 1U, .height = 1U },
            .format = pp::renderer::TextureFormat::rgba8,
        },
        .texture_slot = pp::renderer::max_texture_slots,
    });
    const auto bad_sampler_slot = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::bind_sampler,
        .sampler_slot = pp::renderer::max_texture_slots,
    });
    const auto bad_mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = pp::renderer::RecordedRenderCommandKind::draw,
        .mesh_desc = pp::renderer::MeshDesc {
            .topology = static_cast<pp::renderer::PrimitiveTopology>(255U),
        },
    });
    const auto bad_blit = pp::renderer::gl::plan_recorded_render_command(
        blit_command(static_cast<pp::renderer::BlitFilter>(255U)));
    pp::renderer::RecordedRenderCommand bad_transition_command;
    bad_transition_command.kind = pp::renderer::RecordedRenderCommandKind::transition_texture;
    bad_transition_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    bad_transition_command.before_state = static_cast<pp::renderer::TextureState>(255U);
    bad_transition_command.after_state = pp::renderer::TextureState::shader_read;
    const auto bad_transition = pp::renderer::gl::plan_recorded_render_command(bad_transition_command);
    const auto bad_shader = pp::renderer::gl::plan_recorded_render_command(
        bind_shader_command(""));
    const auto bad_uniform = pp::renderer::gl::plan_recorded_render_command(
        shader_uniform_command("mvp", 0U));
    const auto unknown = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
        .kind = static_cast<pp::renderer::RecordedRenderCommandKind>(255U),
    });

    PP_EXPECT(h, !bad_blend.supported);
    PP_EXPECT(h, bad_blend.blend.source_color_factor == 0U);
    PP_EXPECT(h, !bad_depth.supported);
    PP_EXPECT(h, bad_depth.depth.compare_function == 0U);
    PP_EXPECT(h, !bad_sampler.supported);
    PP_EXPECT(h, bad_sampler.sampler.wrap_s == 0U);
    PP_EXPECT(h, !bad_texture_slot.supported);
    PP_EXPECT(h, bad_texture_slot.texture_slot == pp::renderer::max_texture_slots);
    PP_EXPECT(h, !bad_sampler_slot.supported);
    PP_EXPECT(h, bad_sampler_slot.sampler_slot == pp::renderer::max_texture_slots);
    PP_EXPECT(h, !bad_mesh.supported);
    PP_EXPECT(h, bad_mesh.primitive_mode == 0U);
    PP_EXPECT(h, !bad_blit.supported);
    PP_EXPECT(h, bad_blit.blit_filter.value == 0U);
    PP_EXPECT(h, !bad_transition.supported);
    PP_EXPECT(h, bad_transition.kind == pp::renderer::gl::OpenGlPlannedCommandKind::transition_texture);
    PP_EXPECT(h, !bad_shader.supported);
    PP_EXPECT(h, bad_shader.kind == pp::renderer::gl::OpenGlPlannedCommandKind::bind_shader);
    PP_EXPECT(h, !bad_uniform.supported);
    PP_EXPECT(h, bad_uniform.kind == pp::renderer::gl::OpenGlPlannedCommandKind::set_shader_uniform);
    PP_EXPECT(h, !unknown.supported);
    PP_EXPECT(h, unknown.kind == pp::renderer::gl::OpenGlPlannedCommandKind::unknown);
}

void names_planned_command_kinds(pp::tests::Harness& h)
{
    PP_EXPECT(h, pp::renderer::gl::planned_command_kind_name(
                     pp::renderer::gl::OpenGlPlannedCommandKind::begin_render_pass)
                     == std::string_view("begin_render_pass"));
    PP_EXPECT(h, pp::renderer::gl::planned_command_kind_name(
                     pp::renderer::gl::OpenGlPlannedCommandKind::passthrough)
                     == std::string_view("passthrough"));
    PP_EXPECT(h, pp::renderer::gl::planned_command_kind_name(
                     pp::renderer::gl::OpenGlPlannedCommandKind::copy_texture)
                     == std::string_view("copy_texture"));
    PP_EXPECT(h, pp::renderer::gl::planned_command_kind_name(
                     pp::renderer::gl::OpenGlPlannedCommandKind::set_shader_uniform)
                     == std::string_view("set_shader_uniform"));
    PP_EXPECT(h, pp::renderer::gl::planned_command_kind_name(
                     static_cast<pp::renderer::gl::OpenGlPlannedCommandKind>(255U))
                     == std::string_view("unknown"));
}

void plans_valid_recorded_command_streams(pp::tests::Harness& h)
{
    const std::vector<pp::renderer::RecordedRenderCommand> commands {
        command_with_kind(pp::renderer::RecordedRenderCommandKind::trace_begin_scope),
        begin_render_pass_command(),
        viewport_command(),
        bind_shader_command("stream-shader"),
        shader_uniform_command("mvp", 64U),
        bind_mesh_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
        blit_command(pp::renderer::BlitFilter::nearest),
    };

    const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands);

    PP_EXPECT(h, plan.supported);
    PP_EXPECT(h, plan.commands.size() == commands.size());
    PP_EXPECT(h, plan.render_pass_count == 1U);
    PP_EXPECT(h, plan.draw_command_count == 1U);
    PP_EXPECT(h, plan.shader_bind_command_count == 1U);
    PP_EXPECT(h, plan.uniform_command_count == 1U);
    PP_EXPECT(h, plan.texture_bind_command_count == 0U);
    PP_EXPECT(h, plan.sampler_bind_command_count == 0U);
    PP_EXPECT(h, plan.passthrough_command_count == 0U);
    PP_EXPECT(h, plan.trace_command_count == 1U);
    PP_EXPECT(h, plan.unsupported_command_count == 0U);
    PP_EXPECT(h, plan.render_pass_order_error_count == 0U);
    PP_EXPECT(h, plan.dependency_error_count == 0U);
    PP_EXPECT(h, plan.first_unsupported_command == pp::renderer::gl::OpenGlCommandPlan::npos);
    PP_EXPECT(h, plan.first_render_pass_order_error == pp::renderer::gl::OpenGlCommandPlan::npos);
    PP_EXPECT(h, plan.first_dependency_error == pp::renderer::gl::OpenGlCommandPlan::npos);
    PP_EXPECT(h, !plan.ended_in_render_pass);
    PP_EXPECT(h, plan.commands[1].kind == pp::renderer::gl::OpenGlPlannedCommandKind::begin_render_pass);
    PP_EXPECT(h, plan.commands[3].kind == pp::renderer::gl::OpenGlPlannedCommandKind::bind_shader);
    PP_EXPECT(h, plan.commands[4].kind == pp::renderer::gl::OpenGlPlannedCommandKind::set_shader_uniform);
    PP_EXPECT(h, plan.commands[5].kind == pp::renderer::gl::OpenGlPlannedCommandKind::bind_mesh);
    PP_EXPECT(h, plan.commands[6].kind == pp::renderer::gl::OpenGlPlannedCommandKind::draw);
    PP_EXPECT(h, plan.commands[8].kind == pp::renderer::gl::OpenGlPlannedCommandKind::blit_render_target);
}

void counts_texture_and_sampler_bindings_in_streams(pp::tests::Harness& h)
{
    pp::renderer::RecordedRenderCommand texture_command;
    texture_command.kind = pp::renderer::RecordedRenderCommandKind::bind_texture;
    texture_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    texture_command.texture_slot = 2U;

    pp::renderer::RecordedRenderCommand sampler_command;
    sampler_command.kind = pp::renderer::RecordedRenderCommandKind::bind_sampler;
    sampler_command.sampler_slot = 2U;

    const std::vector<pp::renderer::RecordedRenderCommand> commands {
        begin_render_pass_command(),
        bind_shader_command("shader"),
        texture_command,
        sampler_command,
        bind_mesh_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };

    const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands);

    PP_EXPECT(h, plan.supported);
    PP_EXPECT(h, plan.texture_bind_command_count == 1U);
    PP_EXPECT(h, plan.sampler_bind_command_count == 1U);
    PP_EXPECT(h, plan.unsupported_command_count == 0U);
    PP_EXPECT(h, plan.render_pass_order_error_count == 0U);
    PP_EXPECT(h, plan.dependency_error_count == 0U);
    PP_EXPECT(h, plan.commands[2].texture_slot == 2U);
    PP_EXPECT(h, plan.commands[3].sampler_slot == 2U);
}

void expect_inside_pass_order_error(
    pp::tests::Harness& h,
    pp::renderer::RecordedRenderCommand command,
    pp::renderer::gl::OpenGlPlannedCommandKind expected_kind)
{
    const std::vector<pp::renderer::RecordedRenderCommand> commands {
        begin_render_pass_command(),
        command,
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };

    const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands);

    PP_EXPECT(h, !plan.supported);
    PP_EXPECT(h, plan.unsupported_command_count == 0U);
    PP_EXPECT(h, plan.render_pass_order_error_count == 1U);
    PP_EXPECT(h, plan.first_render_pass_order_error == 1U);
    PP_EXPECT(h, plan.dependency_error_count == 0U);
    PP_EXPECT(h, plan.commands[1].kind == expected_kind);
}

void flags_texture_io_inside_render_pass(pp::tests::Harness& h)
{
    pp::renderer::RecordedRenderCommand upload_command;
    upload_command.kind = pp::renderer::RecordedRenderCommandKind::upload_texture;
    upload_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;

    pp::renderer::RecordedRenderCommand mipmap_command;
    mipmap_command.kind = pp::renderer::RecordedRenderCommandKind::generate_mipmaps;
    mipmap_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;

    pp::renderer::RecordedRenderCommand transition_command;
    transition_command.kind = pp::renderer::RecordedRenderCommandKind::transition_texture;
    transition_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    transition_command.before_state = pp::renderer::TextureState::upload_destination;
    transition_command.after_state = pp::renderer::TextureState::shader_read;

    pp::renderer::RecordedRenderCommand copy_command;
    copy_command.kind = pp::renderer::RecordedRenderCommandKind::copy_texture;
    copy_command.source_desc.format = pp::renderer::TextureFormat::rgba8;
    copy_command.destination_desc.format = pp::renderer::TextureFormat::rgba8;

    pp::renderer::RecordedRenderCommand read_command;
    read_command.kind = pp::renderer::RecordedRenderCommandKind::read_texture;
    read_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;

    pp::renderer::RecordedRenderCommand capture_command;
    capture_command.kind = pp::renderer::RecordedRenderCommandKind::capture_frame;
    capture_command.target_desc.format = pp::renderer::TextureFormat::rgba8;

    expect_inside_pass_order_error(
        h,
        upload_command,
        pp::renderer::gl::OpenGlPlannedCommandKind::upload_texture);
    expect_inside_pass_order_error(
        h,
        mipmap_command,
        pp::renderer::gl::OpenGlPlannedCommandKind::generate_mipmaps);
    expect_inside_pass_order_error(
        h,
        transition_command,
        pp::renderer::gl::OpenGlPlannedCommandKind::transition_texture);
    expect_inside_pass_order_error(
        h,
        copy_command,
        pp::renderer::gl::OpenGlPlannedCommandKind::copy_texture);
    expect_inside_pass_order_error(
        h,
        read_command,
        pp::renderer::gl::OpenGlPlannedCommandKind::read_texture);
    expect_inside_pass_order_error(
        h,
        capture_command,
        pp::renderer::gl::OpenGlPlannedCommandKind::capture_frame);
    expect_inside_pass_order_error(
        h,
        blit_command(pp::renderer::BlitFilter::nearest),
        pp::renderer::gl::OpenGlPlannedCommandKind::blit_render_target);
}

void flags_broken_render_pass_command_order(pp::tests::Harness& h)
{
    const std::vector<pp::renderer::RecordedRenderCommand> outside_pass {
        viewport_command(),
    };
    const std::vector<pp::renderer::RecordedRenderCommand> nested_pass {
        begin_render_pass_command(),
        begin_render_pass_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };
    const std::vector<pp::renderer::RecordedRenderCommand> unclosed_pass {
        begin_render_pass_command(),
        draw_command(),
    };

    const auto outside = pp::renderer::gl::plan_recorded_render_commands(outside_pass);
    const auto nested = pp::renderer::gl::plan_recorded_render_commands(nested_pass);
    const auto unclosed = pp::renderer::gl::plan_recorded_render_commands(unclosed_pass);

    PP_EXPECT(h, !outside.supported);
    PP_EXPECT(h, outside.render_pass_order_error_count == 1U);
    PP_EXPECT(h, outside.first_render_pass_order_error == 0U);
    PP_EXPECT(h, !nested.supported);
    PP_EXPECT(h, nested.render_pass_order_error_count == 1U);
    PP_EXPECT(h, nested.first_render_pass_order_error == 1U);
    PP_EXPECT(h, nested.render_pass_count == 2U);
    PP_EXPECT(h, !unclosed.supported);
    PP_EXPECT(h, unclosed.render_pass_order_error_count == 1U);
    PP_EXPECT(h, unclosed.first_render_pass_order_error == unclosed_pass.size());
    PP_EXPECT(h, unclosed.ended_in_render_pass);
}

void flags_missing_render_dependencies(pp::tests::Harness& h)
{
    const std::vector<pp::renderer::RecordedRenderCommand> uniform_before_shader {
        begin_render_pass_command(),
        shader_uniform_command("mvp", 64U),
        bind_shader_command("shader"),
        bind_mesh_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };
    const std::vector<pp::renderer::RecordedRenderCommand> draw_without_mesh {
        begin_render_pass_command(),
        bind_shader_command("shader"),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };
    const std::vector<pp::renderer::RecordedRenderCommand> draw_without_shader {
        begin_render_pass_command(),
        bind_mesh_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };
    const std::vector<pp::renderer::RecordedRenderCommand> second_pass_missing_bindings {
        begin_render_pass_command(),
        bind_shader_command("shader"),
        bind_mesh_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
        begin_render_pass_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
    };

    const auto bad_uniform = pp::renderer::gl::plan_recorded_render_commands(uniform_before_shader);
    const auto bad_mesh = pp::renderer::gl::plan_recorded_render_commands(draw_without_mesh);
    const auto bad_shader = pp::renderer::gl::plan_recorded_render_commands(draw_without_shader);
    const auto bad_second_pass = pp::renderer::gl::plan_recorded_render_commands(second_pass_missing_bindings);

    PP_EXPECT(h, !bad_uniform.supported);
    PP_EXPECT(h, bad_uniform.dependency_error_count == 1U);
    PP_EXPECT(h, bad_uniform.first_dependency_error == 1U);
    PP_EXPECT(h, !bad_mesh.supported);
    PP_EXPECT(h, bad_mesh.dependency_error_count == 1U);
    PP_EXPECT(h, bad_mesh.first_dependency_error == 2U);
    PP_EXPECT(h, !bad_shader.supported);
    PP_EXPECT(h, bad_shader.dependency_error_count == 1U);
    PP_EXPECT(h, bad_shader.first_dependency_error == 2U);
    PP_EXPECT(h, !bad_second_pass.supported);
    PP_EXPECT(h, bad_second_pass.dependency_error_count == 1U);
    PP_EXPECT(h, bad_second_pass.first_dependency_error == 6U);
    PP_EXPECT(h, bad_second_pass.render_pass_order_error_count == 0U);
}

void tracks_unsupported_commands_in_streams(pp::tests::Harness& h)
{
    std::vector<pp::renderer::RecordedRenderCommand> commands {
        begin_render_pass_command(),
        bind_shader_command("shader"),
        bind_mesh_command(),
        draw_command(),
        command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
        command_with_kind(static_cast<pp::renderer::RecordedRenderCommandKind>(255U)),
    };

    const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands);

    PP_EXPECT(h, !plan.supported);
    PP_EXPECT(h, plan.unsupported_command_count == 1U);
    PP_EXPECT(h, plan.first_unsupported_command == 5U);
    PP_EXPECT(h, plan.render_pass_order_error_count == 0U);
    PP_EXPECT(h, plan.dependency_error_count == 0U);
    PP_EXPECT(h, plan.commands[5].kind == pp::renderer::gl::OpenGlPlannedCommandKind::unknown);
}

void counts_typed_texture_commands_in_streams(pp::tests::Harness& h)
{
    pp::renderer::RecordedRenderCommand upload_command;
    upload_command.kind = pp::renderer::RecordedRenderCommandKind::upload_texture;
    upload_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    upload_command.upload_bytes = 4U;

    pp::renderer::RecordedRenderCommand mipmap_command;
    mipmap_command.kind = pp::renderer::RecordedRenderCommandKind::generate_mipmaps;
    mipmap_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    mipmap_command.generated_mip_levels = 2U;

    pp::renderer::RecordedRenderCommand transition_command;
    transition_command.kind = pp::renderer::RecordedRenderCommandKind::transition_texture;
    transition_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
    transition_command.before_state = pp::renderer::TextureState::copy_destination;
    transition_command.after_state = pp::renderer::TextureState::shader_read;

    pp::renderer::RecordedRenderCommand copy_command;
    copy_command.kind = pp::renderer::RecordedRenderCommandKind::copy_texture;
    copy_command.source_desc.format = pp::renderer::TextureFormat::rgba8;
    copy_command.destination_desc.format = pp::renderer::TextureFormat::rgba8;

    pp::renderer::RecordedRenderCommand read_command;
    read_command.kind = pp::renderer::RecordedRenderCommandKind::read_texture;
    read_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;

    pp::renderer::RecordedRenderCommand capture_command;
    capture_command.kind = pp::renderer::RecordedRenderCommandKind::capture_frame;
    capture_command.target_desc.format = pp::renderer::TextureFormat::rgba8;

    const std::vector<pp::renderer::RecordedRenderCommand> commands {
        upload_command,
        mipmap_command,
        transition_command,
        copy_command,
        read_command,
        capture_command,
    };

    const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands);

    PP_EXPECT(h, plan.supported);
    PP_EXPECT(h, plan.upload_command_count == 1U);
    PP_EXPECT(h, plan.mipmap_command_count == 1U);
    PP_EXPECT(h, plan.transition_command_count == 1U);
    PP_EXPECT(h, plan.copy_command_count == 1U);
    PP_EXPECT(h, plan.readback_command_count == 1U);
    PP_EXPECT(h, plan.capture_command_count == 1U);
    PP_EXPECT(h, plan.passthrough_command_count == 0U);
    PP_EXPECT(h, plan.render_pass_order_error_count == 0U);
    PP_EXPECT(h, plan.commands[0].kind == pp::renderer::gl::OpenGlPlannedCommandKind::upload_texture);
    PP_EXPECT(h, plan.commands[5].kind == pp::renderer::gl::OpenGlPlannedCommandKind::capture_frame);
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("maps_render_pass_and_state_commands", maps_render_pass_and_state_commands);
    harness.run("maps_binding_draw_and_blit_commands", maps_binding_draw_and_blit_commands);
    harness.run("maps_texture_io_and_capture_commands", maps_texture_io_and_capture_commands);
    harness.run("maps_shader_commands", maps_shader_commands);
    harness.run("rejects_unsupported_command_tokens", rejects_unsupported_command_tokens);
    harness.run("names_planned_command_kinds", names_planned_command_kinds);
    harness.run("plans_valid_recorded_command_streams", plans_valid_recorded_command_streams);
    harness.run("counts_texture_and_sampler_bindings_in_streams", counts_texture_and_sampler_bindings_in_streams);
    harness.run("flags_texture_io_inside_render_pass", flags_texture_io_inside_render_pass);
    harness.run("flags_broken_render_pass_command_order", flags_broken_render_pass_command_order);
    harness.run("flags_missing_render_dependencies", flags_missing_render_dependencies);
    harness.run("tracks_unsupported_commands_in_streams", tracks_unsupported_commands_in_streams);
    harness.run("counts_typed_texture_commands_in_streams", counts_typed_texture_commands_in_streams);
    return harness.finish();
}
