/*
 * Copyright 2024 Google LLC.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "experimental/rust_png/SkPngRustDecoder.h"
#include "include/codec/SkCodec.h"
#include "include/codec/SkCodecAnimation.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkColor.h"
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPixmap.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkStream.h"
#include "tests/FakeStreams.h"
#include "tests/Test.h"
#include "tools/Resources.h"

#include <memory>
#include <utility>

#define REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, actualResult) \
    REPORTER_ASSERT(r,                                           \
                    actualResult == SkCodec::kSuccess,           \
                    "actualResult=\"%s\" != kSuccess",           \
                    SkCodec::ResultToString(actualResult))

// Helper wrapping a call to `SkPngRustDecoder::Decode`.
std::unique_ptr<SkCodec> SkPngRustDecoderDecode(skiatest::Reporter* r, const char* path) {
    sk_sp<SkData> data = GetResourceAsData(path);
    if (!data) {
        ERRORF(r, "Missing resource: %s", path);
        return nullptr;
    }

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec =
            SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(std::move(data)), &result);
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);

    return codec;
}

void AssertPixelColor(skiatest::Reporter* r,
                      const SkPixmap& pixmap,
                      int x,
                      int y,
                      SkColor expectedColor,
                      const char* description) {
    SkASSERT(r);
    SkASSERT(x >= 0);
    SkASSERT(y >= 0);
    SkASSERT(description);

    REPORTER_ASSERT(r, x < pixmap.width(), "x=%d >= width=%d", x, pixmap.width());
    REPORTER_ASSERT(r, y < pixmap.height(), "y=%d >= height=%d", y, pixmap.height());
    REPORTER_ASSERT(r,
                    kN32_SkColorType == pixmap.colorType(),
                    "kN32_SkColorType != pixmap.ColorType()=%d",
                    pixmap.colorType());

    SkColor actualColor = pixmap.getColor(x, y);
    REPORTER_ASSERT(r,
                    actualColor == expectedColor,
                    "actualColor=0x%08X != expectedColor==0x%08X at (%d,%d) (%s)",
                    actualColor,
                    expectedColor,
                    x,
                    y,
                    description);
}

void AssertGreenPixel(skiatest::Reporter* r,
                      const SkPixmap& pixmap,
                      int x,
                      int y,
                      const char* description = "Expecting a green pixel") {
    AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0x00, 0xFF, 0x00), description);
}

void AssertRedPixel(skiatest::Reporter* r,
                    const SkPixmap& pixmap,
                    int x,
                    int y,
                    const char* description = "Expecting a red pixel") {
    AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0xFF, 0x00, 0x00), description);
}

void AssertBluePixel(skiatest::Reporter* r,
                     const SkPixmap& pixmap,
                     int x,
                     int y,
                     const char* description = "Expecting a blue pixel") {
    AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0x00, 0x00, 0xFF), description);
}

void AssertSingleGreenFrame(skiatest::Reporter* r,
                            int expectedWidth,
                            int expectedHeight,
                            const char* resourcePath) {
    std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath);
    if (!codec) {
        return;
    }

    REPORTER_ASSERT(r, codec->getFrameCount() == 1);
    REPORTER_ASSERT(r, codec->getRepetitionCount() == 0);

    SkCodec::FrameInfo info;
    REPORTER_ASSERT(r, codec->getFrameInfo(0, &info));
    REPORTER_ASSERT(r, info.fBlend == SkCodecAnimation::Blend::kSrc);
    REPORTER_ASSERT(r, info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep);
    REPORTER_ASSERT(r, info.fFrameRect == SkIRect::MakeWH(expectedWidth, expectedHeight));
    REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame);

    auto [image, result] = codec->getImage();
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);

    REPORTER_ASSERT(r, image);
    REPORTER_ASSERT(r,
                    image->width() == expectedWidth,
                    "actualWidth=%d != expectedWidth=%d",
                    image->width(),
                    expectedWidth);
    REPORTER_ASSERT(r,
                    image->height() == expectedHeight,
                    "actualHeight=%d != expectedHeight=%d",
                    image->height(),
                    expectedHeight);

    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));

    AssertGreenPixel(r, pixmap, 0, 0);
    AssertGreenPixel(r, pixmap, expectedWidth / 2, expectedHeight / 2);
}

sk_sp<SkImage> DecodeLastFrame(skiatest::Reporter* r, SkCodec* codec) {
    int frameCount = codec->getFrameCount();
    sk_sp<SkImage> image;
    SkCodec::Result result = SkCodec::kSuccess;
    for (int i = 0; i < frameCount; i++) {
        SkCodec::FrameInfo info;
        REPORTER_ASSERT(r, codec->getFrameInfo(i, &info));

        // This test method only supports `kKeep` disposal method.
        SkASSERT(info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep);

        if (!image) {
            std::tie(image, result) = codec->getImage();
            REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
            if (result != SkCodec::kSuccess) {
                return nullptr;
            }
        } else {
            SkPixmap pixmap;
            REPORTER_ASSERT(r, image->peekPixels(&pixmap));

            SkCodec::Options options;
            options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
            options.fSubset = nullptr;
            options.fFrameIndex = i;
            options.fPriorFrame = i - 1;

            result = codec->getPixels(pixmap, &options);
            REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
            if (result != SkCodec::kSuccess) {
                return nullptr;
            }
        }
    }

    return image;
}

sk_sp<SkImage> DecodeLastFrame(skiatest::Reporter* r, const char* resourcePath) {
    std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath);
    if (!codec) {
        return nullptr;
    }

    return DecodeLastFrame(r, codec.get());
}

void AssertAnimationRepetitionCount(skiatest::Reporter* r,
                                    int expectedRepetitionCount,
                                    const char* resourcePath) {
    std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath);
    if (!codec) {
        return;
    }

    int actualRepetitionCount = codec->getRepetitionCount();
    REPORTER_ASSERT(r,
                    actualRepetitionCount == expectedRepetitionCount,
                    "actualRepetitionCount=%d != expectedRepetitionCount=%d",
                    actualRepetitionCount,
                    expectedRepetitionCount);
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#trivial-static-image
DEF_TEST(Codec_apng_basic_trivial_static_image, r) {
    AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--trivial-static-image.png");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#trivial-animated-image-one-frame-using-default-image
DEF_TEST(Codec_apng_basic_using_default_image, r) {
    AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--using-default-image.png");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#trivial-animated-image-one-frame-ignoring-default-image
//
// The input file contains the following PNG chunks: IHDR, acTL, IDAT, fcTL,
// fdAT, IEND.  Presence of acTL chunk + no fcTL chunk before IDAT means that
// the IDAT chunk is *not* part of the animation (i.e. APNG-aware decoders
// should ignore IDAT frame and start with fdAT frame).  This test will fail
// with non-APNG-aware decoders (e.g. with `SkPngCodec`), because the `IDAT`
// chunk represents a red image (the `fdAT` chunk represents a green image).
DEF_TEST(Codec_apng_basic_ignoring_default_image, r) {
    AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--ignoring-default-image.png");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#apng-dispose-op-none-basic
//
// This test covers two aspects of `SkPngRustCodec` implementation:
//
// * Blink expects that `onGetFrameCount` returns the total frame count when
//   the complete image resource is available (i.e. a lower frame count should
//   only happen upon `SkCodec::kIncompleteInput`).  Before http://review.skia.org/911038
//   `SkPngRustCodec::onGetFrameCount` would not discover additional frames if
//   previous frames haven't been decoded yet.
// * TODO(https://crbug.com/356922876): Skia client (e.g. Blink; or here the
//   testcase) is expected to handle `SkCodecAnimation::DisposalMethod` and
//   populate the target buffer (and `SkCodec::Options::fPriorFrame`) with the
//   expected pixels.  OTOH, `SkPngRustCodec` needs to handle
//   `SkCodecAnimation::Blend` - without this the final frame in this test will
//   contain red pixels.
DEF_TEST(Codec_apng_dispose_op_none_basic, r) {
    std::unique_ptr<SkCodec> codec =
            SkPngRustDecoderDecode(r, "images/apng-test-suite--dispose-ops--none-basic.png");
    if (!codec) {
        return;
    }

    REPORTER_ASSERT(r, codec->getFrameCount() == 3);
    REPORTER_ASSERT(r, codec->getRepetitionCount() == 0);

    // We should have `FrameInfo` for all 3 frames.
    SkCodec::FrameInfo info[3];
    REPORTER_ASSERT(r, codec->getFrameInfo(0, &info[0]));
    REPORTER_ASSERT(r, codec->getFrameInfo(1, &info[1]));
    REPORTER_ASSERT(r, codec->getFrameInfo(2, &info[2]));

    // The codec should realize that the `SkStream` contains all the data of the
    // first 2 frames.  Currently `SkPngRustCodec::onGetFrameCount` stops after
    // parsing the final, 3rd `fcTL` chunk and therefore it can't tell if the
    // subsequent `fdAT` chunk has been fully received or not.
    REPORTER_ASSERT(r, info[0].fFullyReceived);
    REPORTER_ASSERT(r, info[1].fFullyReceived);
    REPORTER_ASSERT(r, !info[2].fFullyReceived);

    // Spot-check frame metadata.
    REPORTER_ASSERT(r, info[1].fAlphaType == kUnpremul_SkAlphaType);
    REPORTER_ASSERT(r, info[1].fBlend == SkCodecAnimation::Blend::kSrcOver);
    REPORTER_ASSERT(r, info[1].fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep);
    REPORTER_ASSERT(r, info[1].fDuration == 100, "dur = %d", info[1].fDuration);
    REPORTER_ASSERT(r, info[1].fFrameRect == SkIRect::MakeWH(128, 64));
    REPORTER_ASSERT(r, info[1].fHasAlphaWithinBounds);
    REPORTER_ASSERT(r, info[1].fRequiredFrame == 0);

    // Validate contents of the first frame.
    auto [image, result] = codec->getImage();
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
    REPORTER_ASSERT(r, image);
    REPORTER_ASSERT(r, image->width() == 128, "width %d != 128", image->width());
    REPORTER_ASSERT(r, image->height() == 64, "height %d != 64", image->height());
    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    AssertRedPixel(r, pixmap, 0, 0, "Frame #0 should be red");

    // Validate contents of the second frame.
    SkCodec::Options options;
    options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
    options.fSubset = nullptr;
    options.fFrameIndex = 1;  // We want to decode the second frame.
    options.fPriorFrame = 0;  // `pixmap` contains the first frame before `getPixels` call.
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(pixmap, &options));
    AssertGreenPixel(r, pixmap, 0, 0, "Frame #1 should be green");

    // Validate contents of the third frame.
    options.fFrameIndex = 2;  // We want to decode the second frame.
    options.fPriorFrame = 1;  // `pixmap` contains the second frame before `getPixels` call.
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(pixmap, &options));
    AssertGreenPixel(r, pixmap, 0, 0, "Frame #2 should be green");
}

// This test covers an incomplete input scenario:
//
// * Only half of 1st frame is available during `onGetFrameCount`.
//   In this situation `onGetFrameCount` may consume the whole input in a
//   (futile in this case) attempt to discover `fcTL` chunks for 2nd and 3rd
//   frame.  This will mean that the input stream is in the middle of the 1st
//   frame - no longer positioned correctly for decoding the 1st frame.
// * Full input is available when subsequently decoding 1st frame.
DEF_TEST(Codec_apng_dispose_op_none_basic_incomplete_input1, r) {
    const char* path = "images/apng-test-suite--dispose-ops--none-basic.png";
    sk_sp<SkData> data = GetResourceAsData(path);
    if (!data) {
        ERRORF(r, "Missing resource: %s", path);
        return;
    }
    size_t fullLength = data->size();

    // Initially expose roughly middle of `IDAT` chunk (in this image `fcTL` is
    // present before the `IDAT` chunk and therefore the `IDAT` chunk is part of
    // the animated image).
    constexpr size_t kInitialBytes = 0xAD;
    auto streamForCodec = std::make_unique<HaltingStream>(std::move(data), kInitialBytes);
    HaltingStream* retainedStream = streamForCodec.get();

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(streamForCodec), &result);
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
    if (!codec) {
        return;
    }

    SkBitmap bitmap;
    if (!bitmap.tryAllocN32Pixels(codec->dimensions().width(), codec->dimensions().height())) {
        ERRORF(r, "Failed to allocate SkBitmap");
        return;
    }

    // Try to provoke the codec to consume the currently-available part of the
    // input stream.
    //
    // At this point only the metadata for the first frame is available.
    int frameCount = codec->getFrameCount();
    REPORTER_ASSERT(r, frameCount == 1);

    // Make the rest of the input available to the codec.
    retainedStream->addNewData(fullLength);

    // Try to decode the first frame and check its contents.
    sk_sp<SkImage> image;
    std::tie(image, result) = codec->getImage();
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
    REPORTER_ASSERT(r, image);
    REPORTER_ASSERT(r, image->width() == 128, "width %d != 128", image->width());
    REPORTER_ASSERT(r, image->height() == 64, "height %d != 64", image->height());
    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    AssertRedPixel(r, pixmap, 0, 0, "Frame #0 should be red");
}

// This test covers an incomplete input scenario:
//
// * Only half of 1st frame is available during the initial `incrementalDecode`.
// * Before retrying, `getFrameCount` is called.  This should *not* reposition
//   the stream while in the middle of an active incremental decode.
// * Then we retry `incrementalDecode`.
DEF_TEST(Codec_apng_dispose_op_none_basic_incomplete_input2, r) {
    const char* path = "images/apng-test-suite--dispose-ops--none-basic.png";
    sk_sp<SkData> data = GetResourceAsData(path);
    if (!data) {
        ERRORF(r, "Missing resource: %s", path);
        return;
    }
    size_t fullLength = data->size();

    constexpr size_t kInitialBytes = 0x8D;  // Roughly middle of IDAT chunk.
    auto streamForCodec = std::make_unique<HaltingStream>(std::move(data), kInitialBytes);
    HaltingStream* retainedStream = streamForCodec.get();

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(streamForCodec), &result);
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
    if (!codec) {
        return;
    }

    SkBitmap bitmap;
    if (!bitmap.tryAllocN32Pixels(codec->dimensions().width(), codec->dimensions().height())) {
        ERRORF(r, "Failed to allocate SkBitmap");
        return;
    }
    const SkPixmap& pixmap = bitmap.pixmap();

    // Fill the `bitmap` with blue pixels to detect which pixels have been filled
    // by the codec during a partially-successful `incrementalDecode`.
    //
    // (The first frame has all red pixels.)
    bitmap.erase(SkColorSetRGB(0, 0, 0xFF), bitmap.bounds());
    AssertBluePixel(r, pixmap, 0, 0);
    AssertBluePixel(r, pixmap, 40, 29);
    AssertBluePixel(r, pixmap, 80, 35);
    AssertBluePixel(r, pixmap, 127, 63);

    // Decode partially-available, incomplete image.
    SkCodec::Options options;
    options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
    options.fSubset = nullptr;
    options.fFrameIndex = 0;
    options.fPriorFrame = SkCodec::kNoFrame;
    result = codec->startIncrementalDecode(bitmap.pixmap().info(),
                                           bitmap.pixmap().writable_addr(),
                                           bitmap.pixmap().rowBytes(),
                                           &options);
    REPORTER_ASSERT(r, result == SkCodec::kSuccess);
    int rowsDecoded = -1;
    result = codec->incrementalDecode(&rowsDecoded);
    REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput);
    REPORTER_ASSERT(r, rowsDecoded == 10, "actual rowsDecoded = %d", rowsDecoded);
    AssertRedPixel(r, pixmap, 0, 0);
    AssertBluePixel(r, pixmap, 40, 29);
    AssertBluePixel(r, pixmap, 80, 35);
    AssertBluePixel(r, pixmap, 127, 63);

    // Make the rest of the input available to the codec.
    retainedStream->addNewData(fullLength);

    // Try to provoke the codec to consume further into the input stream (doing
    // this would loose the position inside the currently active incremental
    // decode).
    //
    // At this point metadata of all the frames is available, but the codec
    // shouldn't read the other two `fcTL` chunks during an active incremental
    // decode.
    int frameCount = codec->getFrameCount();
    REPORTER_ASSERT(r, frameCount == 1);

    // Check that all the pixels of the first frame got decoded.
    rowsDecoded = -1;
    result = codec->incrementalDecode(&rowsDecoded);
    REPORTER_ASSERT(r, result == SkCodec::kSuccess);
    REPORTER_ASSERT(r, rowsDecoded == -1);  // Not set when `kSuccess`.
    AssertRedPixel(r, pixmap, 0, 0);
    AssertRedPixel(r, pixmap, 40, 29);
    AssertRedPixel(r, pixmap, 80, 35);
    AssertRedPixel(r, pixmap, 127, 63);
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#apng-blend-op-source-on-solid-colour
DEF_TEST(Codec_apng_blend_ops_source_on_solid, r) {
    std::unique_ptr<SkCodec> codec =
            SkPngRustDecoderDecode(r, "images/apng-test-suite--blend-ops--source-on-solid.png");
    if (!codec) {
        return;
    }
    sk_sp<SkImage> image = DecodeLastFrame(r, codec.get());
    if (!image) {
        return;
    }

    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    AssertGreenPixel(r, pixmap, 0, 0);

    SkCodec::FrameInfo info;
    REPORTER_ASSERT(r, codec->getFrameInfo(1, &info));
    REPORTER_ASSERT(r, info.fBlend == SkCodecAnimation::Blend::kSrc);
    REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame);
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#apng-blend-op-source-on-nearly-transparent-colour
DEF_TEST(Codec_apng_blend_ops_source_on_nearly_transparent, r) {
    sk_sp<SkImage> image = DecodeLastFrame(
            r, "images/apng-test-suite--blend-ops--source-on-nearly-transparent.png");
    if (!image) {
        return;
    }

    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    AssertPixelColor(r,
                     pixmap,
                     0,
                     0,
                     SkColorSetARGB(0x02, 0x00, 0xFF, 0x00),
                     "Expecting a nearly transparent pixel");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#apng-blend-op-over-on-solid-and-transparent-colours
DEF_TEST(Codec_apng_blend_ops_over_on_solid_and_transparent, r) {
    sk_sp<SkImage> image = DecodeLastFrame(
            r, "images/apng-test-suite--blend-ops--over-on-solid-and-transparent.png");
    if (!image) {
        return;
    }

    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    AssertGreenPixel(r, pixmap, 0, 0);
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#apng-blend-op-over-repeatedly-with-nearly-transparent-colours
DEF_TEST(Codec_apng_blend_ops_over_repeatedly, r) {
    sk_sp<SkImage> image =
            DecodeLastFrame(r, "images/apng-test-suite--blend-ops--over-repeatedly.png");
    if (!image) {
        return;
    }

    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    AssertGreenPixel(r, pixmap, 0, 0);
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#apng-dispose-op-none-in-region
DEF_TEST(Codec_apng_regions_dispose_op_none, r) {
    sk_sp<SkImage> image =
            DecodeLastFrame(r, "images/apng-test-suite--regions--dispose-op-none.png");
    if (!image) {
        return;
    }

    // Check all pixels.
    //
    // * The image (and the first frame) is 128x64
    // * The 2nd frame is 64x32 at offset (32,16)
    // * The 3rd frame is 1x1 at offset (0,0)
    SkPixmap pixmap;
    REPORTER_ASSERT(r, image->peekPixels(&pixmap));
    for (int y = 0; y < pixmap.height(); y++) {
        for (int x = 0; x < pixmap.width(); x++) {
            AssertGreenPixel(r, pixmap, x, y);
        }
    }
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#num-plays-0
DEF_TEST(Codec_apng_num_plays_0, r) {
    AssertAnimationRepetitionCount(
            r, SkCodec::kRepetitionCountInfinite, "images/apng-test-suite--num-plays--0.png");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#num-plays-1
DEF_TEST(Codec_apng_num_plays_1, r) {
    AssertAnimationRepetitionCount(r, 0, "images/apng-test-suite--num-plays--1.png");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#num-plays-2
DEF_TEST(Codec_apng_num_plays_2, r) {
    AssertAnimationRepetitionCount(r, 1, "images/apng-test-suite--num-plays--2.png");
}

// Test based on
// https://philip.html5.org/tests/apng/tests.html#num-frames-outside-valid-range
//
// In this test the `acTL` chunk sets `num_frames` to `2147483649u` (or `0x80000001u`):
//
// * AFAICT version 1.0 of the APNG spec only says that "0 is not a valid value"
// * The test suite webpage says that at one point the APNG spec said that
//   `num_frames` shall be "limited to the range 0 to (2^31)-1"
DEF_TEST(Codec_apng_invalid_num_frames_outside_valid_range, r) {
    std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(
            r, "images/apng-test-suite--invalid--num-frames-outside-valid-range.png");
    if (!codec) {
        return;
    }

    // Calling `codec->getFrameCount` exercises the code used to discover and
    // parse `fcTL` chunks.  With the initial implementation of
    // `SkPngRustCodec::getRawFrameCount` the call below would have failed
    // `SkASSERT(fFrameAtCurrentStreamPosition < this->getRawFrameCount())`
    // in `SkPngRustCodec::readToStartOfNextFrame`.
    //
    // Note that `SkPngRustCodec::onGetFrameCount` expectedly returns the number
    // of successfully parsed `fcTL` chunks (1 chunk in the test input) rather
    // than returning the raw `acTL.num_frames`.
    REPORTER_ASSERT(r, codec->getFrameCount() == 1);
}

DEF_TEST(Codec_png_swizzling_target_unimplemented, r) {
    std::unique_ptr<SkCodec> codec =
            SkPngRustDecoderDecode(r, "images/apng-test-suite--basic--ignoring-default-image.png");
    if (!codec) {
        return;
    }
    REPORTER_ASSERT(r, codec->getFrameCount() == 1);

    // Ask to decode into an esoteric `SkColorType`:
    //
    // * Unsupported by `SkSwizzler`.
    // * Supported by `SkCodec::conversionSupported`.
    SkImageInfo dstInfo = SkImageInfo::Make(
            codec->dimensions(), kBGRA_10101010_XR_SkColorType, kPremul_SkAlphaType);

    auto [image, result] = codec->getImage(dstInfo);
    REPORTER_ASSERT(r, result == SkCodec::kUnimplemented);
    REPORTER_ASSERT(r, !image);
}

DEF_TEST(Codec_png_was_encoded_with_16_bits_or_more_per_component, r) {
    struct Test {
        const char* fFilename;
        bool fEncodedWith16bits;
    };
    const std::array<Test, 4> kTests = {
            Test{"images/pngsuite/basn0g04.png", false},  // 4 bit (16 level) grayscale
            Test{"images/pngsuite/basn2c08.png", false},  // 3x8 bits rgb color
            Test{"images/pngsuite/basn2c16.png", true},   // 3x16 bits rgb color
            Test{"images/pngsuite/basn3p01.png", false}   // 1 bit (2 color) paletted
    };
    for (const auto& test : kTests) {
        std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, test.fFilename);
        if (codec) {
            REPORTER_ASSERT(r, codec->hasHighBitDepthEncodedData() == test.fEncodedWith16bits);
        }
    }
}
