/*
 * Copyright 2025 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_ico/decoder/SkIcoRustDecoder.h"

#include <memory>
#include <utility>

#include "include/codec/SkCodec.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorType.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/ComparePixels.h"
#include "tests/FakeStreams.h"
#include "tests/Test.h"
#include "tools/Resources.h"

#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 `SkIcoRustDecoder::Decode`.
static std::unique_ptr<SkCodec> decode_ico(skiatest::Reporter* r, const char* path) {
    skiatest::ReporterContext ctx(r, 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 =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result);
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);

    return codec;
}

// Test decoding a valid ICO file with multiple BMP images.
DEF_TEST(RustIcoCodec_decode_multi_bmp, r) {
    std::unique_ptr<SkCodec> codec = decode_ico(r, "images/color_wheel.ico");
    if (!codec) {
        return;
    }

    // color_wheel.ico has 5 images, largest should be reported
    SkISize dimensions = codec->dimensions();
    REPORTER_ASSERT(r, dimensions.width() > 0, "width=%d", dimensions.width());
    REPORTER_ASSERT(r, dimensions.height() > 0, "height=%d", dimensions.height());

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

// Test decoding a valid ICO file with PNG images (google_chrome.ico has PNG).
DEF_TEST(RustIcoCodec_decode_with_png, r) {
    std::unique_ptr<SkCodec> codec = decode_ico(r, "images/google_chrome.ico");
    if (!codec) {
        return;
    }

    // google_chrome.ico has 9 images including 256x256 PNG
    SkISize dimensions = codec->dimensions();
    REPORTER_ASSERT(r, dimensions.width() == 256,
                    "Expected 256, got width=%d", dimensions.width());
    REPORTER_ASSERT(r, dimensions.height() == 256,
                    "Expected 256, got height=%d", dimensions.height());

    auto [image, result] = codec->getImage();
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
    REPORTER_ASSERT(r, image);
    REPORTER_ASSERT(r, image->width() == 256);
    REPORTER_ASSERT(r, image->height() == 256);
}

// Test that Decode handles nullptr for the Result parameter.
DEF_TEST(RustIcoCodec_nullptr_result, r) {
    sk_sp<SkData> data = GetResourceAsData("images/color_wheel.ico");
    REPORTER_ASSERT(r, data);

    // This should not crash even when result is nullptr
    std::unique_ptr<SkCodec> codec =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), nullptr);
    REPORTER_ASSERT(r, codec);
}

// Test that SkIcoRustDecoder correctly rejects non-ICO data.
DEF_TEST(RustIcoCodec_reject_non_ico, r) {
    sk_sp<SkData> data = GetResourceAsData("images/color_wheel.png");
    if (!data) {
        ERRORF(r, "Missing resource: images/color_wheel.png");
        return;
    }

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result);

    // Should fail to decode PNG data as ICO
    REPORTER_ASSERT(r, !codec, "SkIcoRustDecoder should reject PNG data");
}

// Test codec reuse functionality.
DEF_TEST(RustIcoCodec_rewind, r) {
    std::unique_ptr<SkCodec> codec = decode_ico(r, "images/color_wheel.ico");
    if (!codec) {
        return;
    }

    // Decode first time
    auto [image1, result1] = codec->getImage();
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result1);
    REPORTER_ASSERT(r, image1);

    // Decode again using the same codec (tests reuse)
    auto [image2, result2] = codec->getImage();
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result2);
    REPORTER_ASSERT(r, image2);

    // Both images should have the same dimensions
    REPORTER_ASSERT(r,
                    image1->dimensions() == image2->dimensions(),
                    "Images after rewind should have same dimensions");

    // Verify pixel data is identical
    SkBitmap bm1, bm2;
    REPORTER_ASSERT(r, bm1.tryAllocPixels(image1->imageInfo()));
    REPORTER_ASSERT(r, bm2.tryAllocPixels(image2->imageInfo()));
    REPORTER_ASSERT(r, image1->readPixels(nullptr, bm1.pixmap(), 0, 0));
    REPORTER_ASSERT(r, image2->readPixels(nullptr, bm2.pixmap(), 0, 0));

    // Use zero tolerance for exact pixel match
    const float tols[4] = {0, 0, 0, 0};
    auto error = std::function<ComparePixmapsErrorReporter>(
            [&](int x, int y, const float diffs[4]) {
        ERRORF(r, "Pixels differ at (%d, %d) after rewind. Diffs: (%f, %f, %f, %f)",
               x, y, diffs[0], diffs[1], diffs[2], diffs[3]);
    });
    ComparePixels(bm1.pixmap(), bm2.pixmap(), tols, error);
}

// Test that IsIco correctly identifies ICO data.
DEF_TEST(RustIcoCodec_IsIco_positive, r) {
    sk_sp<SkData> data = GetResourceAsData("images/color_wheel.ico");
    if (!data) {
        ERRORF(r, "Missing resource: images/color_wheel.ico");
        return;
    }

    bool isIco = SkIcoRustDecoder::IsIco(data->data(), data->size());
    REPORTER_ASSERT(r, isIco, "IsIco should return true for ICO data");
}

// Test that IsIco correctly rejects non-ICO data.
DEF_TEST(RustIcoCodec_IsIco_negative, r) {
    sk_sp<SkData> data = GetResourceAsData("images/color_wheel.png");
    if (!data) {
        ERRORF(r, "Missing resource: images/color_wheel.png");
        return;
    }

    bool isIco = SkIcoRustDecoder::IsIco(data->data(), data->size());
    REPORTER_ASSERT(r, !isIco, "IsIco should return false for PNG data");
}

// Test IsIco with insufficient data.
DEF_TEST(RustIcoCodec_IsIco_insufficient_data, r) {
    // ICO signature needs at least 4 bytes (00 00 01 00 or 00 00 02 00)
    const uint8_t shortData[] = {0x00, 0x00, 0x01};
    bool isIco = SkIcoRustDecoder::IsIco(shortData, sizeof(shortData));
    REPORTER_ASSERT(r, !isIco, "IsIco should return false for insufficient data");
}

// Test IsIco recognizes CUR files (cursor format, similar to ICO).
DEF_TEST(RustIcoCodec_IsIco_cursor, r) {
    // CUR signature: 00 00 02 00
    const uint8_t curData[] = {0x00, 0x00, 0x02, 0x00, 0x01, 0x00};
    bool isIco = SkIcoRustDecoder::IsIco(curData, sizeof(curData));
    REPORTER_ASSERT(r, isIco, "IsIco should return true for CUR data");
}

// Table-based test for handling invalid/corrupted ICO files.
DEF_TEST(RustIcoCodec_invalid_ico_handling, r) {
    auto test = [&r](const char* description, const char* file) {
        skiatest::ReporterContext ctx(r, description);
        sk_sp<SkData> data = GetResourceAsData(file);
        if (!data) {
            ERRORF(r, "Missing resource: %s", file);
            return;
        }

        SkCodec::Result result;
        std::unique_ptr<SkCodec> codec =
                SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result);

        // If we got a codec, try to decode to ensure we don't crash
        if (codec) {
            auto [image, decodeResult] = codec->getImage();
            // Any result is acceptable as long as we don't crash
            (void)image;
            (void)decodeResult;
        }
    };

    test("zero embedded", "empty_images/zero-embedded.ico");
    test("fuzz0", "invalid_images/ico_fuzz0.ico");
    test("fuzz1", "invalid_images/ico_fuzz1.ico");
    test("leak01", "invalid_images/ico_leak01.ico");
    test("int_overflow", "invalid_images/int_overflow.ico");
    test("mask-bmp-ico", "invalid_images/mask-bmp-ico.ico");
    test("sigabort_favicon", "invalid_images/sigabort_favicon.ico");
    test("sigsegv_favicon", "invalid_images/sigsegv_favicon.ico");
    test("sigsegv_favicon_2", "invalid_images/sigsegv_favicon_2.ico");
    test("b37623797", "invalid_images/b37623797.ico");
    test("b38116746", "invalid_images/b38116746.ico");
}

// Test getPixels with a pre-allocated bitmap.
DEF_TEST(RustIcoCodec_getPixels, r) {
    std::unique_ptr<SkCodec> codec = decode_ico(r, "images/color_wheel.ico");
    if (!codec) {
        return;
    }

    SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType);
    SkBitmap bitmap;
    bitmap.allocPixels(info);

    SkCodec::Result result = codec->getPixels(bitmap.pixmap());
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);

    // Verify bitmap dimensions match codec dimensions
    REPORTER_ASSERT(r, bitmap.width() == codec->dimensions().width());
    REPORTER_ASSERT(r, bitmap.height() == codec->dimensions().height());
}

// Test explicit rewind through multiple getPixels calls.
DEF_TEST(RustIcoCodec_explicit_rewind, r) {
    std::unique_ptr<SkCodec> codec = decode_ico(r, "images/color_wheel.ico");
    if (!codec) {
        return;
    }

    // First decode
    SkBitmap bitmap1;
    bitmap1.allocPixels(codec->getInfo());
    SkCodec::Result result1 = codec->getPixels(bitmap1.pixmap());
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result1);

    // Second decode (implicitly rewinds internally)
    SkBitmap bitmap2;
    bitmap2.allocPixels(codec->getInfo());
    SkCodec::Result result2 = codec->getPixels(bitmap2.pixmap());
    REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result2);

    // Verify dimensions match
    REPORTER_ASSERT(r, bitmap1.dimensions() == bitmap2.dimensions(),
                    "Dimensions should match after rewind");

    // Verify pixel data matches
    REPORTER_ASSERT(r, bitmap1.computeByteSize() == bitmap2.computeByteSize(),
                    "Byte sizes should match");
    REPORTER_ASSERT(r,
                    memcmp(bitmap1.getPixels(), bitmap2.getPixels(),
                           bitmap1.computeByteSize()) == 0,
                    "Pixel data should match after rewind");
}

// Test decoding ICO files with non-standard (non-square) dimensions.
// These files have non-square aspect ratios, which is unusual for ICO files.
DEF_TEST(RustIcoCodec_nonstandard_dimensions, r) {
    struct TestCase {
        const char* file;
        int expectedWidth;
        int expectedHeight;
    };
    const TestCase testCases[] = {
        {"images/ico_nonsquare_48x32.ico", 48, 32},
        {"images/ico_nonsquare_64x48.ico", 64, 48},
        {"images/ico_nonsquare_128x96.ico", 128, 96},
        {"images/ico_nonsquare_200x150.ico", 200, 150},
    };

    for (const auto& testCase : testCases) {
        skiatest::ReporterContext ctx(r, testCase.file);
        std::unique_ptr<SkCodec> codec = decode_ico(r, testCase.file);
        if (!codec) {
            continue;
        }

        SkISize dimensions = codec->dimensions();
        REPORTER_ASSERT(r, dimensions.width() == testCase.expectedWidth,
                        "Expected width=%d, got width=%d",
                        testCase.expectedWidth, dimensions.width());
        REPORTER_ASSERT(r, dimensions.height() == testCase.expectedHeight,
                        "Expected height=%d, got height=%d",
                        testCase.expectedHeight, dimensions.height());

        auto [image, result] = codec->getImage();
        REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
        REPORTER_ASSERT(r, image);
        REPORTER_ASSERT(r, image->width() == testCase.expectedWidth);
        REPORTER_ASSERT(r, image->height() == testCase.expectedHeight);
    }
}

// Test that ICO files with oversized dimensions (>256, requiring PNG payload) are rejected.
// The Rust ICO decoder currently only supports BMP payloads with dimensions <= 256.
DEF_TEST(RustIcoCodec_reject_oversized, r) {
    sk_sp<SkData> data = GetResourceAsData("images/ico_oversized_512x384.ico");
    if (!data) {
        ERRORF(r, "Missing resource: images/ico_oversized_512x384.ico");
        return;
    }

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result);

    // Should fail to decode ICO with PNG payload (oversized dimensions)
    REPORTER_ASSERT(r, !codec,
                    "SkIcoRustDecoder should reject ICO files with oversized dimensions");
}

// Test incremental decoding with partial data using HaltingStream.
// This simulates network streaming where data arrives incrementally.
// Note: ICO codec reads the entire file upfront to parse the directory,
// so it cannot truly resume mid-stream. However, this test verifies that
// the codec correctly handles partial data and succeeds when full data is available.
DEF_TEST(RustIcoCodec_IncrementalDecode_PartialStreaming, r) {
    const char* path = "images/color_wheel.ico";
    sk_sp<SkData> data = GetResourceAsData(path);
    if (!data) {
        ERRORF(r, "Missing resource: %s", path);
        return;
    }
    const size_t fullLength = data->size();

    // Start with only 1/4 of the data available
    const size_t initialBytes = fullLength / 4;
    auto streamForCodec = std::make_unique<HaltingStream>(data, initialBytes);
    HaltingStream* retainedStream = streamForCodec.get();

    // ICO codec needs the full file to parse the directory and embedded images,
    // so with partial data we expect the decode to fail initially.
    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec =
            SkIcoRustDecoder::Decode(std::move(streamForCodec), &result);

    // With only partial data, codec creation should fail
    if (codec) {
        // If codec was created with partial data, it means the initial bytes
        // were enough to parse the directory. Try to decode - it should fail
        // or return incomplete.
        SkBitmap bitmap;
        bitmap.allocPixels(codec->getInfo().makeColorType(kN32_SkColorType));
        SkCodec::Result decodeResult = codec->getPixels(bitmap.pixmap());
        // The decode might succeed if the initial bytes contained enough data,
        // or fail with incomplete input
        (void)decodeResult;
    } else {
        // Codec creation failed with partial data - this is expected.
        REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput ||
                           result == SkCodec::kInvalidInput,
                        "Expected kIncompleteInput or kInvalidInput with partial data, got %s",
                        SkCodec::ResultToString(result));

        // Now add the remaining data to the stream
        retainedStream->addNewData(fullLength - initialBytes);
        REPORTER_ASSERT(r, retainedStream->isAllDataReceived(),
                        "Stream should have all data after addNewData");

        // Rewind the stream to start fresh
        REPORTER_ASSERT(r, retainedStream->rewind(), "Stream should be rewindable");

        // Create a new codec with the now-complete stream
        // Note: We need to wrap the retained stream since Decode takes ownership
        // We'll create a new stream with the full data instead
        auto fullStream = SkMemoryStream::Make(data);
        std::unique_ptr<SkCodec> fullCodec =
                SkIcoRustDecoder::Decode(std::move(fullStream), &result);
        REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
        REPORTER_ASSERT(r, fullCodec, "Codec creation should succeed with full data");

        if (fullCodec) {
            auto [image, decodeResult] = fullCodec->getImage();
            REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, decodeResult);
            REPORTER_ASSERT(r, image, "Decoding should succeed with full data");
        }
    }
}

// Test that incremental decode API works for ICO codec.
// ICO codec delegates to embedded codecs which may or may not support incremental decode.
DEF_TEST(RustIcoCodec_IncrementalDecode_API, r) {
    std::unique_ptr<SkCodec> codec = decode_ico(r, "images/color_wheel.ico");
    if (!codec) {
        return;
    }

    SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType);
    SkBitmap bitmap;
    bitmap.allocPixels(info);

    SkCodec::Options options;
    options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
    options.fSubset = nullptr;
    options.fFrameIndex = 0;
    options.fPriorFrame = SkCodec::kNoFrame;

    // Try to start incremental decode
    SkCodec::Result result = codec->startIncrementalDecode(
            info, bitmap.getPixels(), bitmap.rowBytes(), &options);

    // ICO codec may return kUnimplemented if the embedded codec doesn't support
    // incremental decode, or kSuccess if it does.
    if (result == SkCodec::kSuccess) {
        // If incremental decode started, complete it
        int rowsDecoded = -1;
        result = codec->incrementalDecode(&rowsDecoded);
        REPORTER_ASSERT(r, result == SkCodec::kSuccess ||
                           result == SkCodec::kIncompleteInput,
                        "incrementalDecode should succeed or return incomplete, got %s",
                        SkCodec::ResultToString(result));
    } else if (result == SkCodec::kUnimplemented) {
        // Incremental decode not supported - this is acceptable for BMP-based ICO
        // Fall back to regular decode
        result = codec->getPixels(bitmap.pixmap());
        REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
    } else {
        ERRORF(r, "Unexpected result from startIncrementalDecode: %s",
               SkCodec::ResultToString(result));
    }

    // Verify we got valid pixel data
    REPORTER_ASSERT(r, bitmap.width() == codec->dimensions().width());
    REPORTER_ASSERT(r, bitmap.height() == codec->dimensions().height());
}

// Test that an ICO file truncated after the directory (so all entry data is missing)
// fails to create a codec. The directory is intact but none of the embedded images
// are present.
DEF_TEST(RustIcoCodec_truncated_all_entries_missing, r) {
    sk_sp<SkData> data = GetResourceAsData("images/color_wheel.ico");
    if (!data) {
        ERRORF(r, "Missing resource: images/color_wheel.ico");
        return;
    }

    // color_wheel.ico has 5 BMP images. The directory is 6 + 5*16 = 86 bytes.
    // Truncate to just the directory so no embedded image data is present.
    const size_t directorySize = 6 + 5 * 16;
    REPORTER_ASSERT(r, data->size() > directorySize);
    sk_sp<SkData> truncated = SkData::MakeSubset(data.get(), 0, directorySize);

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(truncated)), &result);

    // No embedded images can be decoded, so codec creation should fail.
    REPORTER_ASSERT(r, !codec,
                    "Codec should not be created when all entries are truncated");
    REPORTER_ASSERT(r, result == SkCodec::kInvalidInput,
                    "Expected kInvalidInput, got %s", SkCodec::ResultToString(result));
}

// Test that an ICO file truncated so that only some embedded images are present
// still reports kSuccess (matching SkIcoCodec) and produces a usable codec whose
// reported size is stable, derived from the ICO directory rather than from the
// subset of frames that happen to be decodable so far.
//
// This stability is required by progressive/deferred clients such as Blink: they
// lock the image size from the first successful parse, so a size that grew as
// more data arrived would leave earlier buffers undersized and overflow on a
// later decode. Reporting kIncompleteInput here would instead leave such clients
// with no size at all. See SkIcoRustCodec::MakeFromStream for details.
DEF_TEST(RustIcoCodec_truncated_some_entries_missing, r) {
    sk_sp<SkData> data = GetResourceAsData("images/color_wheel.ico");
    if (!data) {
        ERRORF(r, "Missing resource: images/color_wheel.ico");
        return;
    }
    // Reported size with the full, well-formed file, for the stability check.
    SkCodec::Result fullResult;
    std::unique_ptr<SkCodec> fullCodec =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(data), &fullResult);
    REPORTER_ASSERT(r, fullCodec, "Codec creation should succeed with full data");
    const SkISize fullDims = fullCodec ? fullCodec->dimensions() : SkISize::MakeEmpty();

    // color_wheel.ico has 5 BMP images. Truncate to roughly half the file so
    // some entries are fully present but later ones are cut off.
    const size_t truncatedSize = data->size() / 2;
    sk_sp<SkData> truncated = SkData::MakeSubset(data.get(), 0, truncatedSize);

    SkCodec::Result result;
    std::unique_ptr<SkCodec> codec =
            SkIcoRustDecoder::Decode(SkMemoryStream::Make(std::move(truncated)), &result);

    if (codec) {
        // As long as we have a usable codec we report kSuccess, mirroring
        // SkIcoCodec.
        REPORTER_ASSERT(r, result == SkCodec::kSuccess,
                        "Expected kSuccess for partially truncated ICO with usable "
                        "frames, got %s",
                        SkCodec::ResultToString(result));

        // The codec should still be usable for the entries that are present.
        int frameCount = codec->getFrameCount();
        REPORTER_ASSERT(r, frameCount > 0, "Should have at least one decodable frame");
        REPORTER_ASSERT(r, frameCount < 5,
                        "Should have fewer than 5 frames (original has 5), got %d", frameCount);

        // The reported size must match the full-file size even though fewer
        // frames are decodable -- it comes from the directory, not the decodable
        // subset. This is the property that prevents progressive-decode overflows.
        REPORTER_ASSERT(r, codec->dimensions() == fullDims,
                        "Partial-data size (%dx%d) should equal full-data size (%dx%d)",
                        codec->dimensions().width(), codec->dimensions().height(),
                        fullDims.width(), fullDims.height());

        // getInfo() must describe a non-empty image so callers can size buffers.
        REPORTER_ASSERT(r, !codec->getInfo().isEmpty(),
                        "Codec info should describe a non-empty image");
    } else {
        // If no entries decoded at all, kInvalidInput is acceptable.
        REPORTER_ASSERT(r, result == SkCodec::kInvalidInput ||
                           result == SkCodec::kIncompleteInput,
                        "Expected kInvalidInput or kIncompleteInput, got %s",
                        SkCodec::ResultToString(result));
    }
}
