#include "assets/image_metadata.h"
#include "test_harness.h"

#include <array>
#include <cstddef>
#include <string_view>

using pp::assets::ImageColorType;
using pp::assets::image_color_type_name;
using pp::assets::max_image_dimension;
using pp::assets::parse_png_metadata;
using pp::foundation::StatusCode;

namespace {

using PngHeader = std::array<std::byte, 33>;

PngHeader make_png_header(std::uint32_t width, std::uint32_t height, std::byte bit_depth, std::byte color_type)
{
    PngHeader bytes {
        std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
        std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
        std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
        std::byte { 'I' }, std::byte { 'H' }, std::byte { 'D' }, std::byte { 'R' },
        std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
        std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
        bit_depth, color_type, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
        std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
    };

    bytes[16] = static_cast<std::byte>((width >> 24U) & 0xffU);
    bytes[17] = static_cast<std::byte>((width >> 16U) & 0xffU);
    bytes[18] = static_cast<std::byte>((width >> 8U) & 0xffU);
    bytes[19] = static_cast<std::byte>(width & 0xffU);
    bytes[20] = static_cast<std::byte>((height >> 24U) & 0xffU);
    bytes[21] = static_cast<std::byte>((height >> 16U) & 0xffU);
    bytes[22] = static_cast<std::byte>((height >> 8U) & 0xffU);
    bytes[23] = static_cast<std::byte>(height & 0xffU);
    return bytes;
}

void parses_png_ihdr_metadata(pp::tests::Harness& h)
{
    const auto rgba = make_png_header(320, 240, std::byte { 8 }, std::byte { 6 });
    const auto rgb = make_png_header(17, 9, std::byte { 8 }, std::byte { 2 });

    const auto rgba_result = parse_png_metadata(rgba);
    const auto rgb_result = parse_png_metadata(rgb);

    PP_EXPECT(h, rgba_result.ok());
    PP_EXPECT(h, rgba_result.value().width == 320U);
    PP_EXPECT(h, rgba_result.value().height == 240U);
    PP_EXPECT(h, rgba_result.value().bit_depth == 8U);
    PP_EXPECT(h, rgba_result.value().components == 4U);
    PP_EXPECT(h, rgba_result.value().color_type == ImageColorType::rgba);
    PP_EXPECT(h, image_color_type_name(rgba_result.value().color_type) == std::string_view("rgba"));

    PP_EXPECT(h, rgb_result.ok());
    PP_EXPECT(h, rgb_result.value().components == 3U);
    PP_EXPECT(h, rgb_result.value().color_type == ImageColorType::rgb);
}

void maps_png_color_type_components(pp::tests::Harness& h)
{
    const auto grayscale = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 0 }));
    const auto indexed = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 3 }));
    const auto gray_alpha = parse_png_metadata(make_png_header(1, 1, std::byte { 8 }, std::byte { 4 }));

    PP_EXPECT(h, grayscale.ok());
    PP_EXPECT(h, grayscale.value().components == 1U);
    PP_EXPECT(h, grayscale.value().color_type == ImageColorType::grayscale);
    PP_EXPECT(h, indexed.ok());
    PP_EXPECT(h, indexed.value().components == 1U);
    PP_EXPECT(h, indexed.value().color_type == ImageColorType::indexed);
    PP_EXPECT(h, gray_alpha.ok());
    PP_EXPECT(h, gray_alpha.value().components == 2U);
    PP_EXPECT(h, gray_alpha.value().color_type == ImageColorType::grayscale_alpha);
}

void rejects_corrupt_or_extreme_png_metadata(pp::tests::Harness& h)
{
    const std::array<std::byte, 8> truncated {
        std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
        std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
    };
    auto bad_signature = make_png_header(1, 1, std::byte { 8 }, std::byte { 6 });
    bad_signature[0] = std::byte { 0x00 };
    auto bad_ihdr = make_png_header(1, 1, std::byte { 8 }, std::byte { 6 });
    bad_ihdr[15] = std::byte { 'X' };
    const auto zero_width = make_png_header(0, 1, std::byte { 8 }, std::byte { 6 });
    const auto too_large = make_png_header(max_image_dimension + 1U, 1, std::byte { 8 }, std::byte { 6 });
    const auto bad_depth = make_png_header(1, 1, std::byte { 0 }, std::byte { 6 });
    const auto bad_color = make_png_header(1, 1, std::byte { 8 }, std::byte { 5 });

    PP_EXPECT(h, parse_png_metadata(truncated).status().code == StatusCode::out_of_range);
    PP_EXPECT(h, parse_png_metadata(bad_signature).status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, parse_png_metadata(bad_ihdr).status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, parse_png_metadata(zero_width).status().code == StatusCode::out_of_range);
    PP_EXPECT(h, parse_png_metadata(too_large).status().code == StatusCode::out_of_range);
    PP_EXPECT(h, parse_png_metadata(bad_depth).status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, parse_png_metadata(bad_color).status().code == StatusCode::invalid_argument);
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("parses_png_ihdr_metadata", parses_png_ihdr_metadata);
    harness.run("maps_png_color_type_components", maps_png_color_type_components);
    harness.run("rejects_corrupt_or_extreme_png_metadata", rejects_corrupt_or_extreme_png_metadata);
    return harness.finish();
}
