/*
 * 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 "rust/icc/FFI.h"
#include "rust/icc/FFI.rs.h"
#include "modules/skcms/skcms.h"
#include "src/codec/SkCodecColorProfileRust.h"
#include "src/codec/SkCodecPriv.h"
#include "tests/Test.h"
#include "tools/Resources.h"

#include <cmath>
#include <cstring>

// Helper to compare skcms_Matrix3x3 against expected rust_icc::Matrix3x3
static void assert_matrix_eq(skiatest::Reporter* r,
                              const skcms_Matrix3x3& skcms_matrix,
                              const rust_icc::Matrix3x3& expected,
                              float tolerance = 0.0001f) {
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++) {
            REPORTER_ASSERT(r,
                            fabsf(skcms_matrix.vals[row][col] -
                                  expected.vals[row][col]) < tolerance);
        }
    }
}

// Helper to compare skcms_TransferFunction against expected rust_icc::TransferFunction
static void assert_transfer_function_eq(skiatest::Reporter* r,
                                        const skcms_TransferFunction& skcms_tf,
                                        const rust_icc::TransferFunction& expected,
                                        float tolerance = 0.0001f) {
    REPORTER_ASSERT(r, fabsf(skcms_tf.g - expected.g) < tolerance);
    REPORTER_ASSERT(r, fabsf(skcms_tf.a - expected.a) < tolerance);
    REPORTER_ASSERT(r, fabsf(skcms_tf.b - expected.b) < tolerance);
    REPORTER_ASSERT(r, fabsf(skcms_tf.c - expected.c) < tolerance);
    REPORTER_ASSERT(r, fabsf(skcms_tf.d - expected.d) < tolerance);
    REPORTER_ASSERT(r, fabsf(skcms_tf.e - expected.e) < tolerance);
    REPORTER_ASSERT(r, fabsf(skcms_tf.f - expected.f) < tolerance);
}

// Helper to compare two skcms_TransferFunction objects
static void compare_transfer_functions(skiatest::Reporter* r,
                                       const char* path,
                                       int channel,
                                       const skcms_TransferFunction& rust_tf,
                                       const skcms_TransferFunction& skcms_tf,
                                       float tolerance = 0.0001f) {
    if (fabsf(rust_tf.g - skcms_tf.g) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.g mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.g, skcms_tf.g);
    }
    if (fabsf(rust_tf.a - skcms_tf.a) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.a mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.a, skcms_tf.a);
    }
    if (fabsf(rust_tf.b - skcms_tf.b) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.b mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.b, skcms_tf.b);
    }
    if (fabsf(rust_tf.c - skcms_tf.c) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.c mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.c, skcms_tf.c);
    }
    if (fabsf(rust_tf.d - skcms_tf.d) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.d mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.d, skcms_tf.d);
    }
    if (fabsf(rust_tf.e - skcms_tf.e) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.e mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.e, skcms_tf.e);
    }
    if (fabsf(rust_tf.f - skcms_tf.f) > tolerance) {
        ERRORF(r, "[%s] trc[%d].parametric.f mismatch: rust=%f, skcms=%f",
               path, channel, rust_tf.f, skcms_tf.f);
    }
}


DEF_TEST(RustIcc_matrix_conversion, r) {
    // Create a rust_icc::Matrix3x3
    rust_icc::Matrix3x3 rust_matrix;
    rust_matrix.vals[0][0] = 0.4124f;
    rust_matrix.vals[0][1] = 0.3576f;
    rust_matrix.vals[0][2] = 0.1805f;
    rust_matrix.vals[1][0] = 0.2126f;
    rust_matrix.vals[1][1] = 0.7152f;
    rust_matrix.vals[1][2] = 0.0722f;
    rust_matrix.vals[2][0] = 0.0193f;
    rust_matrix.vals[2][1] = 0.1192f;
    rust_matrix.vals[2][2] = 0.9505f;

    // Convert to skcms
    skcms_Matrix3x3 skcms_matrix;
    rust_icc::ToSkcmsMatrix3x3(rust_matrix, &skcms_matrix);

    // Verify binary compatibility - values should match exactly
    assert_matrix_eq(r, skcms_matrix, rust_matrix);
}

DEF_TEST(RustIcc_transfer_function_conversion, r) {
    // Create a rust_icc::TransferFunction (sRGB parameters)
    rust_icc::TransferFunction rust_tf;
    rust_tf.g = 2.4f;
    rust_tf.a = 1.0f / 1.055f;
    rust_tf.b = 0.055f / 1.055f;
    rust_tf.c = 1.0f / 12.92f;
    rust_tf.d = 0.04045f;
    rust_tf.e = 0.0f;
    rust_tf.f = 1.0f;

    // Convert to skcms
    skcms_TransferFunction skcms_tf;
    rust_icc::ToSkcmsTransferFunction(rust_tf, &skcms_tf);

    // Verify binary compatibility
    assert_transfer_function_eq(r, skcms_tf, rust_tf);
}

DEF_TEST(RustIcc_profile_conversion, r) {
    // Create a minimal rust_icc::IccProfile
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up a simple identity matrix
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[0][1] = 0.0f;
    rust_profile.to_xyzd50.vals[0][2] = 0.0f;
    rust_profile.to_xyzd50.vals[1][0] = 0.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[1][2] = 0.0f;
    rust_profile.to_xyzd50.vals[2][0] = 0.0f;
    rust_profile.to_xyzd50.vals[2][1] = 0.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    // Set up simple gamma curves
    rust_profile.has_trc = true;
    for (auto* ch : {&rust_profile.trc_r, &rust_profile.trc_g, &rust_profile.trc_b}) {
        ch->table_entries = 0;  // parametric curve (no table)
        ch->parametric.g = 2.2f;
        ch->parametric.a = 1.0f;
        ch->parametric.b = 0.0f;
        ch->parametric.c = 0.0f;
        ch->parametric.d = 0.0f;
        ch->parametric.e = 0.0f;
        ch->parametric.f = 0.0f;
    }

    // Convert to skcms
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_toXYZD50);
    REPORTER_ASSERT(r, skcms_profile.has_trc);

    // Verify matrix was copied correctly
    REPORTER_ASSERT(r, fabsf(skcms_profile.toXYZD50.vals[0][0] - 1.0f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.toXYZD50.vals[1][1] - 1.0f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.toXYZD50.vals[2][2] - 1.0f) < 0.0001f);

    // Verify transfer functions were copied (all channels should be gamma 2.2)
    REPORTER_ASSERT(r, skcms_profile.trc[0].table_entries == 0);
    REPORTER_ASSERT(r, fabsf(skcms_profile.trc[0].parametric.g - 2.2f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.trc[1].parametric.g - 2.2f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.trc[2].parametric.g - 2.2f) < 0.0001f);
}

DEF_TEST(RustIcc_profile_conversion_fails_without_data, r) {
    // Profile without matrix or TRC should fail conversion
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;
    rust_profile.has_to_xyzd50 = false;
    rust_profile.has_trc = false;

    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, !success);
}

DEF_TEST(RustIcc_cicp_conversion, r) {
    // Create a profile with CICP data
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up minimal matrix to make conversion succeed
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    // Set CICP data (e.g., BT.709 primaries, BT.709 transfer, BT.709 matrix, full range)
    rust_profile.has_cicp = true;
    rust_profile.cicp.color_primaries = 1;  // BT.709
    rust_profile.cicp.transfer_characteristics = 1;  // BT.709
    rust_profile.cicp.matrix_coefficients = 1;  // BT.709
    rust_profile.cicp.video_full_range_flag = 1;  // Full range

    // Convert to skcms
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_CICP);
    REPORTER_ASSERT(r, skcms_profile.CICP.color_primaries == 1);
    REPORTER_ASSERT(r, skcms_profile.CICP.transfer_characteristics == 1);
    REPORTER_ASSERT(r, skcms_profile.CICP.matrix_coefficients == 1);
    REPORTER_ASSERT(r, skcms_profile.CICP.video_full_range_flag == 1);
}

DEF_TEST(RustIcc_a2b_b2a_flags, r) {
    // Create a minimal profile to test A2B/B2A flags
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up minimal matrix
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    // Simulate a profile with A2B but no B2A transforms
    rust_profile.has_a2b = true;
    rust_profile.has_b2a = false;

    // Convert to skcms
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_A2B);
    REPORTER_ASSERT(r, !skcms_profile.has_B2A);
}

DEF_TEST(RustIcc_profile_with_a2b_curves, r) {
    // Create a profile with A2B transform including curves
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up basic matrix/TRC for compatibility
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    rust_profile.has_trc = true;
    for (auto* ch : {&rust_profile.trc_r, &rust_profile.trc_g, &rust_profile.trc_b}) {
        ch->table_entries = 0;  // parametric curve (no table)
        ch->parametric.g = 2.2f;
        ch->parametric.a = 1.0f;
        ch->parametric.b = 0.0f;
        ch->parametric.c = 0.0f;
        ch->parametric.d = 0.0f;
        ch->parametric.e = 0.0f;
        ch->parametric.f = 0.0f;
    }

    // Add A2B transform with input curves
    rust_profile.has_a2b = true;
    rust_profile.a2b.input_channels = 3;
    rust_profile.a2b.output_channels = 3;

    // Set up 3 input curves (RGB) - parametric with table_entries = 0
    rust::Vec<rust_icc::Curve> input_curves;
    for (int i = 0; i < 3; i++) {
        rust_icc::Curve curve;
        curve.table_entries = 0;  // 0 = parametric
        curve.parametric.g = 2.4f;
        curve.parametric.a = 1.0f;
        curve.parametric.b = 0.0f;
        curve.parametric.c = 0.0f;
        curve.parametric.d = 0.0f;
        curve.parametric.e = 0.0f;
        curve.parametric.f = 0.0f;
        input_curves.push_back(std::move(curve));
    }
    rust_profile.a2b.input_curves = std::move(input_curves);

    // Simple 2x2x2 grid (3 input channels, 3 output channels)
    rust::Vec<uint8_t> grid_data;
    // 8 grid points × 3 output channels = 24 values
    for (int i = 0; i < 24; i++) {
        grid_data.push_back(static_cast<uint8_t>(i * 10));
    }
    rust_profile.a2b.grid_data = std::move(grid_data);
    rust_profile.a2b.grid_points[0] = 2;
    rust_profile.a2b.grid_points[1] = 2;
    rust_profile.a2b.grid_points[2] = 2;

    // Set up 3 output curves
    rust::Vec<rust_icc::Curve> output_curves;
    for (int i = 0; i < 3; i++) {
        rust_icc::Curve curve;
        curve.table_entries = 0;  // 0 = parametric
        curve.parametric.g = 1.0f;
        curve.parametric.a = 1.0f;
        curve.parametric.b = 0.0f;
        curve.parametric.c = 0.0f;
        curve.parametric.d = 0.0f;
        curve.parametric.e = 0.0f;
        curve.parametric.f = 0.0f;
        output_curves.push_back(std::move(curve));
    }
    rust_profile.a2b.output_curves = std::move(output_curves);

    // Convert to skcms
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_A2B);
    REPORTER_ASSERT(r, skcms_profile.A2B.input_channels == 3);
    REPORTER_ASSERT(r, skcms_profile.A2B.output_channels == 3);

    // Verify input curves were set up
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.input_curves[0].parametric.g - 2.4f) < 0.0001f);

    // Verify grid
    REPORTER_ASSERT(r, skcms_profile.A2B.grid_points[0] == 2);
    REPORTER_ASSERT(r, skcms_profile.A2B.grid_points[1] == 2);
    REPORTER_ASSERT(r, skcms_profile.A2B.grid_points[2] == 2);
    REPORTER_ASSERT(r, skcms_profile.A2B.grid_8 != nullptr);

    // Verify output curves
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.output_curves[0].parametric.g - 1.0f) < 0.0001f);
}

DEF_TEST(RustIcc_profile_with_a2b_matrix, r) {
    // Create a profile with A2B transform including matrix
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up basic matrix/TRC
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    rust_profile.has_trc = true;
    for (auto* ch : {&rust_profile.trc_r, &rust_profile.trc_g, &rust_profile.trc_b}) {
        ch->table_entries = 0;  // parametric curve (no table)
        ch->parametric.g = 2.2f;
        ch->parametric.a = 1.0f;
        ch->parametric.b = 0.0f;
        ch->parametric.c = 0.0f;
        ch->parametric.d = 0.0f;
        ch->parametric.e = 0.0f;
        ch->parametric.f = 0.0f;
    }

    // Add A2B transform with matrix
    rust_profile.has_a2b = true;
    rust_profile.a2b.input_channels = 3;
    rust_profile.a2b.output_channels = 3;

    // Set up matrix curves
    rust::Vec<rust_icc::Curve> matrix_curves;
    for (int i = 0; i < 3; i++) {
        rust_icc::Curve curve;
        curve.table_entries = 0;  // 0 = parametric
        curve.parametric.g = 1.8f;
        curve.parametric.a = 1.0f;
        curve.parametric.b = 0.0f;
        curve.parametric.c = 0.0f;
        curve.parametric.d = 0.0f;
        curve.parametric.e = 0.0f;
        curve.parametric.f = 0.0f;
        matrix_curves.push_back(std::move(curve));
    }
    rust_profile.a2b.matrix_curves = std::move(matrix_curves);

    // Set up a color conversion matrix
    rust_profile.a2b.matrix.vals[0][0] = 0.4124f;
    rust_profile.a2b.matrix.vals[0][1] = 0.3576f;
    rust_profile.a2b.matrix.vals[0][2] = 0.1805f;
    rust_profile.a2b.matrix.vals[1][0] = 0.2126f;
    rust_profile.a2b.matrix.vals[1][1] = 0.7152f;
    rust_profile.a2b.matrix.vals[1][2] = 0.0722f;
    rust_profile.a2b.matrix.vals[2][0] = 0.0193f;
    rust_profile.a2b.matrix.vals[2][1] = 0.1192f;
    rust_profile.a2b.matrix.vals[2][2] = 0.9505f;

    // Set up minimal grid
    rust::Vec<uint8_t> grid_data;
    grid_data.push_back(0x80);
    rust_profile.a2b.grid_data = std::move(grid_data);
    rust_profile.a2b.grid_points[0] = 1;
    rust_profile.a2b.grid_points[1] = 1;
    rust_profile.a2b.grid_points[2] = 1;

    // Convert to skcms
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_A2B);

    // Verify matrix curves
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.matrix_curves[0].parametric.g - 1.8f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.matrix_curves[1].parametric.g - 1.8f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.matrix_curves[2].parametric.g - 1.8f) < 0.0001f);

    // Verify matrix values
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.matrix.vals[0][0] - 0.4124f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.matrix.vals[1][1] - 0.7152f) < 0.0001f);
    REPORTER_ASSERT(r, fabsf(skcms_profile.A2B.matrix.vals[2][2] - 0.9505f) < 0.0001f);
}

// Test that verifies table-based curves work through the FFI layer.
// NOTE: The rust_profile must remain alive while skcms_profile is in use because
// skcms_profile contains pointers into rust_profile's table_data vectors.
DEF_TEST(RustIcc_profile_with_table_curves, r) {
    // Create a profile with A2B transform using table-based curves
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up basic matrix/TRC
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    rust_profile.has_trc = true;
    for (auto* ch : {&rust_profile.trc_r, &rust_profile.trc_g, &rust_profile.trc_b}) {
        ch->table_entries = 0;  // parametric curve (no table)
        ch->parametric.g = 2.2f;
        ch->parametric.a = 1.0f;
        ch->parametric.b = 0.0f;
        ch->parametric.c = 0.0f;
        ch->parametric.d = 0.0f;
        ch->parametric.e = 0.0f;
        ch->parametric.f = 0.0f;
    }

    // Add A2B transform with table-based curves
    rust_profile.has_a2b = true;
    rust_profile.a2b.input_channels = 3;
    rust_profile.a2b.output_channels = 3;

    // Set up input curves with table data
    // Table data stores u16 values as little-endian bytes
    rust::Vec<rust_icc::Curve> input_curves;
    for (int i = 0; i < 3; i++) {
        rust_icc::Curve curve;

        // Create a simple linear table with 3 u16 entries
        rust::Vec<uint8_t> table_data;
        // Entry 0: 0x0000 (0.0)
        table_data.push_back(0x00);
        table_data.push_back(0x00);
        // Entry 1: 0x8000 (0.5)
        table_data.push_back(0x00);
        table_data.push_back(0x80);
        // Entry 2: 0xFFFF (1.0)
        table_data.push_back(0xFF);
        table_data.push_back(0xFF);

        curve.table_entries = 3;
        curve.table_data = std::move(table_data);

        // Parametric fields unused for table curves
        curve.parametric.g = 0.0f;
        curve.parametric.a = 0.0f;
        curve.parametric.b = 0.0f;
        curve.parametric.c = 0.0f;
        curve.parametric.d = 0.0f;
        curve.parametric.e = 0.0f;
        curve.parametric.f = 0.0f;

        input_curves.push_back(std::move(curve));
    }
    rust_profile.a2b.input_curves = std::move(input_curves);

    // Minimal grid (1x1x1 = 1 point, 3 output channels = 3 bytes)
    rust::Vec<uint8_t> grid_data;
    grid_data.push_back(0x80);
    grid_data.push_back(0x80);
    grid_data.push_back(0x80);
    rust_profile.a2b.grid_data = std::move(grid_data);
    rust_profile.a2b.grid_points[0] = 1;
    rust_profile.a2b.grid_points[1] = 1;
    rust_profile.a2b.grid_points[2] = 1;
    rust_profile.a2b.is_16bit_grid = false;

    // Convert to skcms (rust_profile must remain alive after this!)
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_A2B);
    REPORTER_ASSERT(r, skcms_profile.A2B.input_channels == 3);

    // Verify table curves were set up correctly
    // FFI.cpp sets table_16 (which points to u16 data stored as bytes)
    REPORTER_ASSERT(r, skcms_profile.A2B.input_curves[0].table_entries == 3);
    REPORTER_ASSERT(r, skcms_profile.A2B.input_curves[0].table_8 == nullptr);
    REPORTER_ASSERT(r, skcms_profile.A2B.input_curves[0].table_16 != nullptr);

    // Verify the table data values (interpret as u16 little-endian)
    const uint8_t* data = skcms_profile.A2B.input_curves[0].table_16;
    if (data) {
        uint16_t val0 = data[0] | (data[1] << 8);
        uint16_t val1 = data[2] | (data[3] << 8);
        uint16_t val2 = data[4] | (data[5] << 8);

        REPORTER_ASSERT(r, val0 == 0x0000);
        REPORTER_ASSERT(r, val1 == 0x8000);
        REPORTER_ASSERT(r, val2 == 0xFFFF);
    }

    // rust_profile goes out of scope here, invalidating pointers in skcms_profile
}

DEF_TEST(RustIcc_profile_with_b2a, r) {
    // Create a profile with B2A transform (inverse)
    rust_icc::IccProfile rust_profile;
    rust_profile.data_color_space = skcms_Signature_RGB;
    rust_profile.connection_space = skcms_Signature_XYZ;

    // Set up basic matrix/TRC
    rust_profile.has_to_xyzd50 = true;
    rust_profile.to_xyzd50.vals[0][0] = 1.0f;
    rust_profile.to_xyzd50.vals[1][1] = 1.0f;
    rust_profile.to_xyzd50.vals[2][2] = 1.0f;

    rust_profile.has_trc = true;
    for (auto* ch : {&rust_profile.trc_r, &rust_profile.trc_g, &rust_profile.trc_b}) {
        ch->table_entries = 0;  // parametric curve (no table)
        ch->parametric.g = 2.2f;
        ch->parametric.a = 1.0f;
        ch->parametric.b = 0.0f;
        ch->parametric.c = 0.0f;
        ch->parametric.d = 0.0f;
        ch->parametric.e = 0.0f;
        ch->parametric.f = 0.0f;
    }

    // Add B2A transform
    rust_profile.has_b2a = true;
    rust_profile.b2a.input_channels = 3;
    rust_profile.b2a.output_channels = 3;

    // Set up input curves with inverse gamma
    rust::Vec<rust_icc::Curve> input_curves;
    for (int i = 0; i < 3; i++) {
        rust_icc::Curve curve;
        curve.table_entries = 0;  // 0 = parametric
        curve.parametric.g = 1.0f / 2.2f;  // Inverse gamma
        curve.parametric.a = 1.0f;
        curve.parametric.b = 0.0f;
        curve.parametric.c = 0.0f;
        curve.parametric.d = 0.0f;
        curve.parametric.e = 0.0f;
        curve.parametric.f = 0.0f;
        input_curves.push_back(std::move(curve));
    }
    rust_profile.b2a.input_curves = std::move(input_curves);

    // Set up a 2x2x2 grid
    rust::Vec<uint8_t> grid_data;
    for (int i = 0; i < 24; i++) {
        grid_data.push_back(static_cast<uint8_t>(255 - i * 10));
    }
    rust_profile.b2a.grid_data = std::move(grid_data);
    rust_profile.b2a.grid_points[0] = 2;
    rust_profile.b2a.grid_points[1] = 2;
    rust_profile.b2a.grid_points[2] = 2;

    // Set up output curves
    rust::Vec<rust_icc::Curve> output_curves;
    for (int i = 0; i < 3; i++) {
        rust_icc::Curve curve;
        curve.table_entries = 0;  // 0 = parametric
        curve.parametric.g = 1.0f;
        curve.parametric.a = 1.0f;
        curve.parametric.b = 0.0f;
        curve.parametric.c = 0.0f;
        curve.parametric.d = 0.0f;
        curve.parametric.e = 0.0f;
        curve.parametric.f = 0.0f;
        output_curves.push_back(std::move(curve));
    }
    rust_profile.b2a.output_curves = std::move(output_curves);

    // Convert to skcms
    skcms_ICCProfile skcms_profile;
    bool success = rust_icc::ToSkcmsIccProfile(rust_profile, &skcms_profile);

    REPORTER_ASSERT(r, success);
    REPORTER_ASSERT(r, skcms_profile.has_B2A);
    REPORTER_ASSERT(r, skcms_profile.B2A.input_channels == 3);
    REPORTER_ASSERT(r, skcms_profile.B2A.output_channels == 3);

    // Verify input curves (inverse gamma)
    REPORTER_ASSERT(r,
                    fabsf(skcms_profile.B2A.input_curves[0].parametric.g -
                          1.0f / 2.2f) < 0.0001f);

    // Verify grid
    REPORTER_ASSERT(r, skcms_profile.B2A.grid_points[0] == 2);
    REPORTER_ASSERT(r, skcms_profile.B2A.grid_8 != nullptr);
    REPORTER_ASSERT(r, skcms_profile.B2A.grid_8[0] == 255);

    // Verify output curves
    REPORTER_ASSERT(r, fabsf(skcms_profile.B2A.output_curves[0].parametric.g - 1.0f) < 0.0001f);
}

// Helper to compare two skcms_Curve objects by evaluating them at sample points.
// Works for both parametric and table-based curves. For table curves, reads the
// raw big-endian bytes the same way skcms_Transform would.
// Evaluate a skcms_Curve at a normalised x in [0, 1].
// Handles both parametric (table_entries == 0) and table-based curves
// (table_16 stores big-endian uint16_t pairs).
static float eval_skcms_curve(const skcms_Curve& c, float x) {
    if (c.table_entries == 0) {
        return skcms_TransferFunction_eval(&c.parametric, x);
    }
    x = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
    float idx = x * (float)(c.table_entries - 1);
    int lo = (int)idx;
    int hi = lo + 1 < (int)c.table_entries ? lo + 1 : lo;
    float frac = idx - (float)lo;
    auto read16 = [&](int i) -> float {
        uint16_t v = (uint16_t)((c.table_16[2*i] << 8) | c.table_16[2*i+1]);
        return (float)v / 65535.0f;
    };
    return read16(lo) * (1.0f - frac) + read16(hi) * frac;
}

static void compare_curves_by_evaluation(
        skiatest::Reporter* r,
        const char* path,
        const char* stage,
        int channel,
        const skcms_Curve& rust_curve,
        const skcms_Curve& skcms_curve) {
    // If both are parametric, compare the transfer function parameters directly.
    if (rust_curve.table_entries == 0 && skcms_curve.table_entries == 0) {
        compare_transfer_functions(r, path, channel,
                                  rust_curve.parametric, skcms_curve.parametric);
        return;
    }
    // If both are table-based with the same number of entries, compare bytes.
    if (rust_curve.table_entries != 0 &&
        rust_curve.table_entries == skcms_curve.table_entries) {
        const uint32_t n = rust_curve.table_entries;
        const uint8_t* rust_data  = rust_curve.table_16;
        const uint8_t* skcms_data = skcms_curve.table_16;
        if (!rust_data || !skcms_data) {
            ERRORF(r, "[%s] %s[%d] null table_16 pointer", path, stage, channel);
            return;
        }
        for (uint32_t i = 0; i < n; ++i) {
            uint16_t rv = (uint16_t)(rust_data[2*i]  << 8 | rust_data[2*i+1]);
            uint16_t sv = (uint16_t)(skcms_data[2*i] << 8 | skcms_data[2*i+1]);
            if (rv != sv) {
                ERRORF(r, "[%s] %s[%d] table entry %u mismatch: rust=%u, skcms=%u",
                       path, stage, channel, i, rv, sv);
                return;
            }
        }
        return;
    }
    // Mixed representation (e.g. Rust approximates table as parametric):
    // compare by sampling both curves at several points with 1% tolerance.
    static const float kSamples[] = {0.0f, 0.05f, 0.1f, 0.25f,
                                     0.5f, 0.75f, 0.9f, 1.0f};
    for (float x : kSamples) {
        float rv = eval_skcms_curve(rust_curve,  x);
        float sv = eval_skcms_curve(skcms_curve, x);
        if (fabsf(rv - sv) > 0.01f) {
            ERRORF(r, "[%s] %s[%d] curve eval mismatch at x=%.2f: rust=%f, skcms=%f",
                   path, stage, channel, x, rv, sv);
            return;
        }
    }
}

// Helper to compare A2B grid data (CLUT) between rust and skcms parsed profiles.
static void compare_a2b_grid_data(
        skiatest::Reporter* r,
        const char* path,
        const skcms_A2B& rust_a2b,
        const skcms_A2B& skcms_a2b) {
    // Compute grid size
    uint64_t grid_size = rust_a2b.output_channels;
    for (uint32_t i = 0; i < rust_a2b.input_channels; ++i) {
        grid_size *= rust_a2b.grid_points[i];
    }
    if (grid_size == 0) return;

    if (!rust_a2b.grid_8 && !rust_a2b.grid_16 && !skcms_a2b.grid_8 && !skcms_a2b.grid_16) {
        // Both parsers found no CLUT - consistent, no error.
        return;
    }
    if (rust_a2b.grid_16 && skcms_a2b.grid_16) {
        // 16-bit grid: compare big-endian bytes
        for (uint64_t i = 0; i < grid_size; ++i) {
            uint16_t rv = (uint16_t)(rust_a2b.grid_16[2*i] << 8 |
                                     rust_a2b.grid_16[2*i+1]);
            uint16_t sv = (uint16_t)(skcms_a2b.grid_16[2*i] << 8 |
                                     skcms_a2b.grid_16[2*i+1]);
            if (rv != sv) {
                ERRORF(r, "[%s] A2B grid_16 entry %llu mismatch: rust=%u, skcms=%u",
                       path, (unsigned long long)i, rv, sv);
                return;
            }
        }
    } else if (rust_a2b.grid_8 && skcms_a2b.grid_8) {
        // 8-bit grid
        if (memcmp(rust_a2b.grid_8, skcms_a2b.grid_8, grid_size) != 0) {
            ERRORF(r, "[%s] A2B grid_8 data mismatch", path);
        }
    } else {
        // One parser has grid data, the other doesn't, or they use mismatched formats.
        ERRORF(r, "[%s] A2B grid format mismatch (8 vs 16 bit)", path);
    }
}

DEF_TEST(RustIcc_equivalence_with_skcms_resource_files, r) {
    // List of ICC profile files in resources/icc_profiles.
    // - apng19.icc is an ICCv4 scanner profile with only A2B tags (no TRC/XYZ),
    //   including 256-entry M curve tables and 16-bit CLUT grid data.
    // - swapped.icc is an ICCv2 display profile with 1024-entry table TRC curves;
    //   exercises the TRC table pass-through path (raw big-endian bytes, no approximation).
    // - mu_gray.icc is a GRAY/prtr printer profile with kTRC (256-entry curv) and
    //   no colorant matrix; exercises the GRAY toXYZD50 synthesis path.
    // - tiles.icc is the Apple RGB monitor profile (RGB/mntr, 1024-entry curv TRCs)
    //   embedded in tiles.png, which is used by the svg/as-border-image Blink layout
    //   test.  Same TRC structure as swapped.icc but a distinct profile.
    const char* icc_files[] = {
        "icc_profiles/AdobeRGB1998.icc",
        "icc_profiles/HP_Z32x.icc",
        "icc_profiles/HP_ZR30w.icc",
        "icc_profiles/srgb_lab_pcs.icc",
        "icc_profiles/upperLeft.icc",
        "icc_profiles/upperRight.icc",
        // Files added after regressions found during development of the Rust parser, not covered
        // by existing ICC profiles in Skia's resources:
        "icc_profiles/apng19.icc",
        "icc_profiles/swapped.icc",
        "icc_profiles/mu_gray.icc",
        "icc_profiles/tiles.icc",
    };

    for (const char* path : icc_files) {
        auto data = GetResourceAsData(path);
        if (!data) {
            ERRORF(r, "Failed to load ICC profile: %s", path);
            continue;
        }

        auto rust_profile = SkCodecs::MakeICCProfileWithRust(data);
        auto skcms_profile = SkCodecs::ColorProfile::MakeICCProfileWithSkCMS(data);

        if (!rust_profile) {
            ERRORF(r, "Rust parser failed for: %s", path);
            continue;
        }
        if (!skcms_profile) {
            ERRORF(r, "SkCMS parser failed for: %s", path);
            continue;
        }

        const auto& rust = *rust_profile->profile();
        const auto& skcms = *skcms_profile->profile();

        // Compare has_toXYZD50 and matrix values
        if (rust.has_toXYZD50 != skcms.has_toXYZD50) {
            ERRORF(r, "[%s] has_toXYZD50 mismatch: rust=%d, skcms=%d",
                   path, rust.has_toXYZD50, skcms.has_toXYZD50);
        }
        if (rust.has_toXYZD50 && skcms.has_toXYZD50) {
            for (int i = 0; i < 3; ++i) {
                for (int j = 0; j < 3; ++j) {
                    if (fabsf(rust.toXYZD50.vals[i][j] - skcms.toXYZD50.vals[i][j]) >= 0.0001f) {
                        ERRORF(r, "[%s] toXYZD50[%d][%d] mismatch: rust=%f, skcms=%f",
                               path, i, j, rust.toXYZD50.vals[i][j], skcms.toXYZD50.vals[i][j]);
                    }
                }
            }
        }

        // Compare has_trc and transfer functions.
        // Note: the Rust path approximates multi-entry table TRC curves as
        // parametric functions via skcms_ApproximateCurve, so trc[c].table_entries
        // may be 0 in the Rust profile and non-zero in the skcms profile.  Use
        // compare_curves_by_evaluation which handles both representations.
        if (rust.has_trc != skcms.has_trc) {
            ERRORF(r, "[%s] has_trc mismatch: rust=%d, skcms=%d",
                   path, rust.has_trc, skcms.has_trc);
        }
        if (rust.has_trc && skcms.has_trc) {
            for (int c = 0; c < 3; ++c) {
                compare_curves_by_evaluation(r, path, "trc", c,
                                             rust.trc[c], skcms.trc[c]);
            }
        }

        // Compare CICP if present
        if (rust.has_CICP != skcms.has_CICP) {
            ERRORF(r, "[%s] has_CICP mismatch: rust=%d, skcms=%d",
                   path, rust.has_CICP, skcms.has_CICP);
        }
        if (rust.has_CICP && skcms.has_CICP) {
            if (rust.CICP.color_primaries != skcms.CICP.color_primaries ||
                rust.CICP.transfer_characteristics != skcms.CICP.transfer_characteristics ||
                rust.CICP.matrix_coefficients != skcms.CICP.matrix_coefficients ||
                rust.CICP.video_full_range_flag != skcms.CICP.video_full_range_flag) {
                ERRORF(r, "[%s] CICP mismatch", path);
            }
        }

        // Compare data_color_space
        if (rust.data_color_space != skcms.data_color_space) {
            ERRORF(r, "[%s] data_color_space mismatch: rust=%u, skcms=%u",
                   path, rust.data_color_space, skcms.data_color_space);
        }

        // Compare A2B transform if present
        if (rust.has_A2B != skcms.has_A2B) {
            ERRORF(r, "[%s] has_A2B mismatch: rust=%d, skcms=%d",
                   path, rust.has_A2B, skcms.has_A2B);
        }
        if (rust.has_A2B && skcms.has_A2B) {
            if (rust.A2B.input_channels != skcms.A2B.input_channels) {
                ERRORF(r, "[%s] A2B.input_channels mismatch: rust=%u, skcms=%u",
                       path, rust.A2B.input_channels, skcms.A2B.input_channels);
            }
            if (rust.A2B.output_channels != skcms.A2B.output_channels) {
                ERRORF(r, "[%s] A2B.output_channels mismatch: rust=%u, skcms=%u",
                       path, rust.A2B.output_channels, skcms.A2B.output_channels);
            }
            // Compare grid points
            for (int i = 0; i < 4; ++i) {
                if (rust.A2B.grid_points[i] != skcms.A2B.grid_points[i]) {
                    ERRORF(r, "[%s] A2B.grid_points[%d] mismatch: rust=%u, skcms=%u",
                           path, i, rust.A2B.grid_points[i], skcms.A2B.grid_points[i]);
                }
            }

            // Compare input curves (A curves)
            for (uint32_t i = 0; i < rust.A2B.input_channels && i < skcms.A2B.input_channels; ++i) {
                compare_curves_by_evaluation(r, path, "A2B.input_curves",
                                             i, rust.A2B.input_curves[i],
                                             skcms.A2B.input_curves[i]);
            }

            // Compare matrix stage
            if (rust.A2B.matrix_channels != skcms.A2B.matrix_channels) {
                ERRORF(r, "[%s] A2B.matrix_channels mismatch: rust=%u, skcms=%u",
                       path, rust.A2B.matrix_channels, skcms.A2B.matrix_channels);
            }
            if (rust.A2B.matrix_channels > 0 && skcms.A2B.matrix_channels > 0) {
                // Compare matrix values
                for (int i = 0; i < 3; ++i) {
                    for (int j = 0; j < 3; ++j) {
                        if (fabsf(rust.A2B.matrix.vals[i][j] - skcms.A2B.matrix.vals[i][j]) >= 0.0001f) {
                            ERRORF(r, "[%s] A2B.matrix[%d][%d] mismatch: rust=%f, skcms=%f",
                                   path, i, j, rust.A2B.matrix.vals[i][j], skcms.A2B.matrix.vals[i][j]);
                        }
                    }
                }
                // Compare matrix bias (4th column of 3x4 matrix)
                for (int i = 0; i < 3; ++i) {
                    if (fabsf(rust.A2B.matrix.vals[i][3] - skcms.A2B.matrix.vals[i][3]) >= 0.0001f) {
                        ERRORF(r, "[%s] A2B.matrix_bias[%d] mismatch: rust=%f, skcms=%f",
                               path, i, rust.A2B.matrix.vals[i][3], skcms.A2B.matrix.vals[i][3]);
                    }
                }
                // Compare matrix curves (M curves)
                for (int i = 0; i < 3; ++i) {
                    compare_curves_by_evaluation(r, path, "A2B.matrix_curves",
                                                 i, rust.A2B.matrix_curves[i],
                                                 skcms.A2B.matrix_curves[i]);
                }
            }

            // Compare output curves (B curves)
            for (uint32_t i = 0; i < rust.A2B.output_channels && i < skcms.A2B.output_channels; ++i) {
                compare_curves_by_evaluation(r, path, "A2B.output_curves",
                                             i, rust.A2B.output_curves[i],
                                             skcms.A2B.output_curves[i]);
            }

            // Compare grid data (CLUT) byte-by-byte
            compare_a2b_grid_data(r, path, rust.A2B, skcms.A2B);
        }

        // Compare B2A transform if present
        if (rust.has_B2A != skcms.has_B2A) {
            ERRORF(r, "[%s] has_B2A mismatch: rust=%d, skcms=%d",
                   path, rust.has_B2A, skcms.has_B2A);
        }
        if (rust.has_B2A && skcms.has_B2A) {
            if (rust.B2A.input_channels != skcms.B2A.input_channels) {
                ERRORF(r, "[%s] B2A.input_channels mismatch: rust=%u, skcms=%u",
                       path, rust.B2A.input_channels, skcms.B2A.input_channels);
            }
            if (rust.B2A.output_channels != skcms.B2A.output_channels) {
                ERRORF(r, "[%s] B2A.output_channels mismatch: rust=%u, skcms=%u",
                       path, rust.B2A.output_channels, skcms.B2A.output_channels);
            }
            // Compare grid points
            for (int i = 0; i < 4; ++i) {
                if (rust.B2A.grid_points[i] != skcms.B2A.grid_points[i]) {
                    ERRORF(r, "[%s] B2A.grid_points[%d] mismatch: rust=%u, skcms=%u",
                           path, i, rust.B2A.grid_points[i], skcms.B2A.grid_points[i]);
                }
            }

            // Compare input curves (B curves in B2A)
            for (uint32_t i = 0; i < rust.B2A.input_channels && i < skcms.B2A.input_channels; ++i) {
                compare_curves_by_evaluation(r, path, "B2A.input_curves",
                                             i, rust.B2A.input_curves[i],
                                             skcms.B2A.input_curves[i]);
            }

            // Compare matrix stage
            if (rust.B2A.matrix_channels != skcms.B2A.matrix_channels) {
                ERRORF(r, "[%s] B2A.matrix_channels mismatch: rust=%u, skcms=%u",
                       path, rust.B2A.matrix_channels, skcms.B2A.matrix_channels);
            }
            if (rust.B2A.matrix_channels > 0 && skcms.B2A.matrix_channels > 0) {
                // Compare matrix values
                for (int i = 0; i < 3; ++i) {
                    for (int j = 0; j < 3; ++j) {
                        if (fabsf(rust.B2A.matrix.vals[i][j] - skcms.B2A.matrix.vals[i][j]) >= 0.0001f) {
                            ERRORF(r, "[%s] B2A.matrix[%d][%d] mismatch: rust=%f, skcms=%f",
                                   path, i, j, rust.B2A.matrix.vals[i][j], skcms.B2A.matrix.vals[i][j]);
                        }
                    }
                }
                // Compare matrix bias (4th column of 3x4 matrix)
                for (int i = 0; i < 3; ++i) {
                    if (fabsf(rust.B2A.matrix.vals[i][3] - skcms.B2A.matrix.vals[i][3]) >= 0.0001f) {
                        ERRORF(r, "[%s] B2A.matrix_bias[%d] mismatch: rust=%f, skcms=%f",
                               path, i, rust.B2A.matrix.vals[i][3], skcms.B2A.matrix.vals[i][3]);
                    }
                }
                // Compare matrix curves (M curves)
                for (int i = 0; i < 3; ++i) {
                    compare_curves_by_evaluation(r, path, "B2A.matrix_curves",
                                                 i, rust.B2A.matrix_curves[i],
                                                 skcms.B2A.matrix_curves[i]);
                }
            }

            // Compare output curves (A curves in B2A)
            for (uint32_t i = 0; i < rust.B2A.output_channels && i < skcms.B2A.output_channels; ++i) {
                compare_curves_by_evaluation(r, path, "B2A.output_curves",
                                             i, rust.B2A.output_curves[i],
                                             skcms.B2A.output_curves[i]);
            }
        }
    }
}

// Regression test for multi-entry TRC table pass-through.
//
// swapped.icc is a real ICC v2 display profile with 1024-entry curv TRCs.
// The Rust bridge must pass the raw big-endian u16 table bytes through to
// skcms unchanged (table_entries > 0, table_16 set) rather than approximating
// the curve as a parametric function.  Passing the table exactly eliminates
// the ±1 ULP rounding difference that previously caused a max_difference=1
// pixel error across the entire image in svg/as-border-image.
DEF_TEST(RustIcc_trc_table_passthrough, r) {
    auto data = GetResourceAsData("icc_profiles/swapped.icc");
    if (!data) {
        ERRORF(r, "Failed to load icc_profiles/swapped.icc");
        return;
    }

    // skcms reference: must parse and expose table-based TRC.
    skcms_ICCProfile skcms_prof;
    if (!skcms_Parse(data->data(), data->size(), &skcms_prof)) {
        ERRORF(r, "skcms_Parse failed on swapped.icc");
        return;
    }
    REPORTER_ASSERT(r, skcms_prof.has_trc,
                    "skcms should report has_trc=true for swapped.icc");

    // Rust/moxcms path: must expose the 1024-entry table TRC bit-exactly.
    auto rust_profile = SkCodecs::MakeICCProfileWithRust(data);
    if (!rust_profile) {
        ERRORF(r, "Rust ICC parser failed to parse swapped.icc");
        return;
    }
    const skcms_ICCProfile& rp = *rust_profile->profile();

    REPORTER_ASSERT(r, rp.has_trc,
                    "Rust ICC path must set has_trc=true for swapped.icc; "
                    "failure indicates the TRC table pass-through is broken");

    if (!rp.has_trc || !skcms_prof.has_trc) {
        return;
    }

    // The Rust path now passes the table through bit-exactly.  Both profiles
    // should have table_entries > 0 and identical big-endian byte content.
    for (int c = 0; c < 3; ++c) {
        compare_curves_by_evaluation(r, "swapped.icc", "trc", c,
                                     rp.trc[c], skcms_prof.trc[c]);
    }
}
