/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "paimon/common/data/variant/variant_get.h"

#include <functional>
#include <string>

#include "arrow/api.h"
#include "gtest/gtest.h"
#include "paimon/common/data/variant/variant_builder.h"
#include "paimon/common/data/variant/variant_path_segment.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/data/decimal.h"
#include "paimon/data/timestamp.h"
#include "paimon/testing/utils/testharness.h"

namespace paimon::test {

class VariantGetTest : public ::testing::Test {
 public:
    void SetUp() override {
        // The same document as the Java GenericVariantTest#testVariantGet.
        std::string json =
            "{\n"
            "  \"object\": {\n"
            "    \"name\": \"Apache Paimon\",\n"
            "    \"age\": 2,\n"
            "    \"address\": {\n"
            "      \"street\": \"Main St\",\n"
            "      \"city\": \"Hangzhou\"\n"
            "    }\n"
            "  },\n"
            "  \"array\": [1, 2, 3, 4, 5],\n"
            "  \"string\": \"Hello, World!\",\n"
            "  \"long\": 12345678901234,\n"
            "  \"double\": 1.0123456789012345678901234567890123456789,\n"
            "  \"decimal\": 100.99,\n"
            "  \"boolean1\": true,\n"
            "  \"boolean2\": false,\n"
            "  \"nullField\": null\n"
            "}\n";
        ASSERT_OK_AND_ASSIGN(variant_, GenericVariant::FromJson(json, pool_));
        cast_args_.fail_on_error = false;
    }

    std::optional<Literal> Get(const std::string& path,
                               const std::shared_ptr<arrow::DataType>& target) {
        auto result = VariantGetExecutor::Get(variant_, path, target, cast_args_);
        EXPECT_TRUE(result.ok()) << result.status().ToString();
        return result.value();
    }

    std::string GetString(const std::string& path, const std::shared_ptr<arrow::DataType>& target) {
        std::optional<Literal> literal = Get(path, target);
        EXPECT_TRUE(literal.has_value());
        return literal->ToString();
    }

    Result<std::shared_ptr<arrow::Array>> GetAsArrow(
        const std::string& path, const std::shared_ptr<arrow::Field>& target_field) {
        return VariantGetExecutor::GetAsArrow(variant_, path, target_field, cast_args_, pool_,
                                              arrow_pool_);
    }

    // Builds a single-scalar variant using the direct append API, which can encode types that
    // JSON parsing never produces (uuid/date/float/binary/timestamp).
    std::shared_ptr<GenericVariant> BuildScalar(
        const std::function<Status(VariantBuilder&)>& append) {
        VariantBuilder builder(/*allow_duplicate_keys=*/false);
        Status st = append(builder);
        EXPECT_TRUE(st.ok()) << st.ToString();
        auto result = builder.Build(pool_);
        EXPECT_TRUE(result.ok()) << result.status().ToString();
        return result.value();
    }

 protected:
    std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
    std::shared_ptr<arrow::MemoryPool> arrow_pool_ = GetArrowPool(pool_);
    std::shared_ptr<GenericVariant> variant_;
    VariantCastArgs cast_args_;
};

TEST_F(VariantGetTest, PathSegmentParse) {
    ASSERT_OK_AND_ASSIGN(auto segments,
                         VariantPathSegment::Parse("$[\"object\"]['address'].city[3]"));
    ASSERT_EQ(segments.size(), 4);
    ASSERT_EQ(segments[0].kind, VariantPathSegment::Kind::kObjectExtraction);
    ASSERT_EQ(segments[0].key, "object");
    ASSERT_EQ(segments[1].kind, VariantPathSegment::Kind::kObjectExtraction);
    ASSERT_EQ(segments[1].key, "address");
    ASSERT_EQ(segments[2].kind, VariantPathSegment::Kind::kObjectExtraction);
    ASSERT_EQ(segments[2].key, "city");
    ASSERT_EQ(segments[3].kind, VariantPathSegment::Kind::kArrayExtraction);
    ASSERT_EQ(segments[3].index, 3);

    ASSERT_OK_AND_ASSIGN(auto root_only, VariantPathSegment::Parse("$"));
    ASSERT_TRUE(root_only.empty());

    // Java parity: the root `$` is located anywhere in the path (Java uses Matcher#find), so a
    // prefix before the first `$` is tolerated and ignored.
    ASSERT_OK_AND_ASSIGN(auto prefixed, VariantPathSegment::Parse("abc$.x"));
    ASSERT_EQ(prefixed.size(), 1);
    ASSERT_EQ(prefixed[0].kind, VariantPathSegment::Kind::kObjectExtraction);
    ASSERT_EQ(prefixed[0].key, "x");

    ASSERT_NOK(VariantPathSegment::Parse(""));
    ASSERT_NOK(VariantPathSegment::Parse("no_root"));
    ASSERT_NOK(VariantPathSegment::Parse("$."));
    ASSERT_NOK(VariantPathSegment::Parse("$[abc]"));
    ASSERT_NOK(VariantPathSegment::Parse("$['unterminated]"));
}

TEST_F(VariantGetTest, ScalarTargets) {
    ASSERT_EQ(GetString("$.string", arrow::utf8()), "Hello, World!");
    auto long_value = Get("$.long", arrow::int64());
    ASSERT_TRUE(long_value.has_value());
    ASSERT_EQ(long_value->GetValue<int64_t>(), 12345678901234LL);
    ASSERT_EQ(GetString("$.long", arrow::utf8()), "12345678901234");
    auto double_value = Get("$.double", arrow::float64());
    ASSERT_TRUE(double_value.has_value());
    ASSERT_DOUBLE_EQ(double_value->GetValue<double>(), 1.0123456789012346);
    auto decimal_value = Get("$.decimal", arrow::decimal128(5, 2));
    ASSERT_TRUE(decimal_value.has_value());
    ASSERT_EQ(decimal_value->GetValue<Decimal>().ToUnscaledLong(), 10099);
    ASSERT_EQ(GetString("$.decimal", arrow::utf8()), "100.99");
    auto bool1 = Get("$.boolean1", arrow::boolean());
    ASSERT_TRUE(bool1.has_value());
    ASSERT_TRUE(bool1->GetValue<bool>());
    auto bool2 = Get("$.boolean2", arrow::boolean());
    ASSERT_TRUE(bool2.has_value());
    ASSERT_FALSE(bool2->GetValue<bool>());
    // Variant null maps to SQL NULL.
    ASSERT_FALSE(Get("$.nullField", arrow::boolean()).has_value());
    auto elem = Get("$.array[3]", arrow::int64());
    ASSERT_TRUE(elem.has_value());
    ASSERT_EQ(elem->GetValue<int64_t>(), 4);
}

TEST_F(VariantGetTest, ContainerToJsonString) {
    ASSERT_EQ(GetString("$.object", arrow::utf8()),
              "{\"address\":{\"city\":\"Hangzhou\",\"street\":\"Main St\"},\"age\":2,\"name\":"
              "\"Apache Paimon\"}");
    ASSERT_EQ(GetString("$.object.name", arrow::utf8()), "Apache Paimon");
    ASSERT_EQ(GetString("$.object.address.street", arrow::utf8()), "Main St");
    ASSERT_EQ(GetString("$[\"object\"]['address'].city", arrow::utf8()), "Hangzhou");
    ASSERT_EQ(GetString("$.array", arrow::utf8()), "[1,2,3,4,5]");
}

TEST_F(VariantGetTest, UnmatchedPathAndInvalidCast) {
    // A path that does not exist yields SQL NULL.
    ASSERT_FALSE(Get("$.missing", arrow::utf8()).has_value());
    ASSERT_FALSE(Get("$.string[0]", arrow::utf8()).has_value());
    ASSERT_FALSE(Get("$.array.key", arrow::utf8()).has_value());
    // An invalid cast yields SQL NULL when fail_on_error is false.
    ASSERT_FALSE(Get("$.object", arrow::int64()).has_value());
    // ... and an error when fail_on_error is true.
    cast_args_.fail_on_error = true;
    auto result = VariantGetExecutor::Get(variant_, "$.object", arrow::int64(), cast_args_);
    ASSERT_NOK(result);
}

TEST_F(VariantGetTest, NestedTargetsNotImplemented) {
    auto result =
        VariantGetExecutor::Get(variant_, "$.array", arrow::list(arrow::int32()), cast_args_);
    ASSERT_TRUE(result.status().IsNotImplemented());
}

TEST_F(VariantGetTest, BinaryAndTimestampSources) {
    // Java-encoded data can carry binary/timestamp scalars that JSON parsing never produces.
    {
        VariantBuilder builder(/*allow_duplicate_keys=*/false);
        ASSERT_OK(builder.AppendBinary(std::string_view("\x01\x02\x03", 3)));
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_));
        ASSERT_OK_AND_ASSIGN(std::optional<Literal> literal,
                             VariantGetExecutor::Get(variant, "$", arrow::binary(), cast_args_));
        ASSERT_TRUE(literal.has_value());
        ASSERT_EQ(literal->GetValue<std::string>(), std::string("\x01\x02\x03", 3));
    }
    {
        VariantBuilder builder(/*allow_duplicate_keys=*/false);
        ASSERT_OK(builder.AppendTimestamp(1700000000123456));
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_));
        ASSERT_OK_AND_ASSIGN(
            std::optional<Literal> literal,
            VariantGetExecutor::Get(variant, "$", arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"),
                                    cast_args_));
        ASSERT_TRUE(literal.has_value());
        ASSERT_EQ(literal->GetValue<Timestamp>().ToMicrosecond(), 1700000000123456);
    }
    {
        VariantBuilder builder(/*allow_duplicate_keys=*/false);
        ASSERT_OK(builder.AppendTimestampNtz(-1001));
        ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> variant, builder.Build(pool_));
        ASSERT_OK_AND_ASSIGN(
            std::optional<Literal> literal,
            VariantGetExecutor::Get(variant, "$", arrow::timestamp(arrow::TimeUnit::MICRO),
                                    cast_args_));
        ASSERT_TRUE(literal.has_value());
        // Negative epochs must floor, not truncate toward zero.
        ASSERT_EQ(literal->GetValue<Timestamp>().ToMicrosecond(), -1001);
    }
}

TEST_F(VariantGetTest, ExtractByPath) {
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> address,
                         VariantGetExecutor::ExtractByPath(variant_, "$.object.address"));
    ASSERT_NE(address, nullptr);
    ASSERT_OK_AND_ASSIGN(std::string json, address->ToJson());
    ASSERT_EQ(json, "{\"city\":\"Hangzhou\",\"street\":\"Main St\"}");
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> missing,
                         VariantGetExecutor::ExtractByPath(variant_, "$.object.missing"));
    ASSERT_EQ(missing, nullptr);
}

TEST_F(VariantGetTest, CastToStructTarget) {
    auto target = arrow::field(
        "r", arrow::struct_(
                 {arrow::field("name", arrow::utf8()), arrow::field("age", arrow::int64()),
                  arrow::field("address", arrow::struct_({arrow::field("city", arrow::utf8())})),
                  arrow::field("missing", arrow::utf8())}));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.object", target));
    ASSERT_EQ(array->length(), 1);
    const auto& row = static_cast<const arrow::StructArray&>(*array);
    ASSERT_FALSE(row.IsNull(0));
    ASSERT_EQ(static_cast<const arrow::StringArray&>(*row.field(0)).GetString(0), "Apache Paimon");
    ASSERT_EQ(static_cast<const arrow::Int64Array&>(*row.field(1)).Value(0), 2);
    const auto& address = static_cast<const arrow::StructArray&>(*row.field(2));
    ASSERT_EQ(static_cast<const arrow::StringArray&>(*address.field(0)).GetString(0), "Hangzhou");
    // A target field absent from the variant object is null.
    ASSERT_TRUE(row.field(3)->IsNull(0));

    // With fail_on_error == false a child that cannot cast becomes null while the parent row
    // stays non-null.
    auto mixed_target = arrow::field("r", arrow::struct_({arrow::field("string", arrow::int64()),
                                                          arrow::field("long", arrow::int64())}));
    ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$", mixed_target));
    const auto& mixed_row = static_cast<const arrow::StructArray&>(*array);
    ASSERT_FALSE(mixed_row.IsNull(0));
    // "string" holds "Hello, World!", which cannot cast to int64.
    ASSERT_TRUE(mixed_row.field(0)->IsNull(0));
    ASSERT_FALSE(mixed_row.field(1)->IsNull(0));
    ASSERT_EQ(static_cast<const arrow::Int64Array&>(*mixed_row.field(1)).Value(0), 12345678901234);
}

TEST_F(VariantGetTest, CastToStructFromNonObject) {
    auto target = arrow::field("r", arrow::struct_({arrow::field("a", arrow::int64())}));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.array", target));
    ASSERT_TRUE(array->IsNull(0));
    cast_args_.fail_on_error = true;
    auto result =
        VariantGetExecutor::GetAsArrow(variant_, "$.array", target, cast_args_, pool_, arrow_pool_);
    ASSERT_NOK(result);
}

TEST_F(VariantGetTest, CastToMapTarget) {
    auto target = arrow::field("m", arrow::map(arrow::utf8(), arrow::utf8()));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array,
                         GetAsArrow("$.object.address", target));
    const auto& map = static_cast<const arrow::MapArray&>(*array);
    ASSERT_EQ(map.value_length(0), 2);
    const auto& keys = static_cast<const arrow::StringArray&>(*map.keys());
    const auto& items = static_cast<const arrow::StringArray&>(*map.items());
    ASSERT_EQ(keys.GetString(0), "city");
    ASSERT_EQ(items.GetString(0), "Hangzhou");
    ASSERT_EQ(keys.GetString(1), "street");
    ASSERT_EQ(items.GetString(1), "Main St");
    // A map with a non-string key type is an invalid cast.
    auto bad_target = arrow::field("m", arrow::map(arrow::int64(), arrow::utf8()));
    ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.object.address", bad_target));
    ASSERT_TRUE(array->IsNull(0));
}

TEST_F(VariantGetTest, CastToListTarget) {
    auto target = arrow::field("l", arrow::list(arrow::int64()));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.array", target));
    const auto& list = static_cast<const arrow::ListArray&>(*array);
    ASSERT_EQ(list.value_length(0), 5);
    const auto& values = static_cast<const arrow::Int64Array&>(*list.values());
    for (int64_t i = 0; i < 5; ++i) {
        ASSERT_EQ(values.Value(i), i + 1);
    }
    auto list_of_structs =
        arrow::field("l", arrow::list(arrow::struct_({arrow::field("a", arrow::int64())})));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> rows,
                         GenericVariant::FromJson("[{\"a\": 1}, {\"a\": 2}]", pool_));
    ASSERT_OK_AND_ASSIGN(array, VariantGetExecutor::GetAsArrow(rows, "$", list_of_structs,
                                                               cast_args_, pool_, arrow_pool_));
    const auto& struct_list = static_cast<const arrow::ListArray&>(*array);
    ASSERT_EQ(struct_list.value_length(0), 2);
    const auto& elements = static_cast<const arrow::StructArray&>(*struct_list.values());
    ASSERT_EQ(static_cast<const arrow::Int64Array&>(*elements.field(0)).Value(0), 1);
    ASSERT_EQ(static_cast<const arrow::Int64Array&>(*elements.field(0)).Value(1), 2);
}

TEST_F(VariantGetTest, CastToVariantTarget) {
    // Re-encodes the extracted sub-variant against a fresh metadata dictionary.
    auto target = VariantTypeUtils::ToArrowField("v", /*nullable=*/true, {});
    auto read_variant_json = [&](const arrow::Array& array) {
        const auto& row = static_cast<const arrow::StructArray&>(array);
        EXPECT_FALSE(row.IsNull(0));
        std::string_view value = static_cast<const arrow::BinaryArray&>(*row.field(0)).GetView(0);
        std::string_view metadata =
            static_cast<const arrow::BinaryArray&>(*row.field(1)).GetView(0);
        EXPECT_OK_AND_ASSIGN(std::shared_ptr<GenericVariant> copied,
                             GenericVariant::Create(value, metadata, pool_));
        EXPECT_OK_AND_ASSIGN(std::string json, copied->ToJson());
        return json;
    };
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array,
                         GetAsArrow("$.object.address", target));
    ASSERT_EQ(read_variant_json(*array), "{\"city\":\"Hangzhou\",\"street\":\"Main St\"}");

    // A variant null cast to a VARIANT target stays an encoded variant null (a non-null row
    // whose value renders as JSON null); only scalar targets turn a variant null into SQL NULL.
    ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.nullField", target));
    ASSERT_EQ(read_variant_json(*array), "null");
    // An unmatched path is SQL NULL by contrast.
    ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.missing", target));
    ASSERT_TRUE(array->IsNull(0));
}

TEST_F(VariantGetTest, NestedTargetNullSemantics) {
    auto target = arrow::field("r", arrow::struct_({arrow::field("a", arrow::int64())}));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.missing", target));
    ASSERT_TRUE(array->IsNull(0));
    // A variant null yields SQL NULL even with fail_on_error == true.
    cast_args_.fail_on_error = true;
    ASSERT_OK_AND_ASSIGN(array, GetAsArrow("$.nullField", target));
    ASSERT_TRUE(array->IsNull(0));
}

TEST_F(VariantGetTest, UuidSourceCastsToStringOnly) {
    std::string uuid_bytes(16, '\0');
    for (int i = 0; i < 16; ++i) {
        uuid_bytes[i] = static_cast<char>(i);
    }
    std::shared_ptr<GenericVariant> variant =
        BuildScalar([&](VariantBuilder& b) { return b.AppendUuid(uuid_bytes); });
    // A UUID has no Paimon type, so it can only be rendered as its canonical string.
    ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_string,
                         VariantGetExecutor::Get(variant, "$", arrow::utf8(), cast_args_));
    ASSERT_TRUE(as_string.has_value());
    ASSERT_EQ(as_string->GetValue<std::string>(), "00010203-0405-0607-0809-0a0b0c0d0e0f");
    // Any non-string target is an invalid cast, which is SQL NULL when fail_on_error is false.
    ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_long,
                         VariantGetExecutor::Get(variant, "$", arrow::int64(), cast_args_));
    ASSERT_FALSE(as_long.has_value());
    cast_args_.fail_on_error = true;
    ASSERT_NOK(VariantGetExecutor::Get(variant, "$", arrow::int64(), cast_args_));
}

TEST_F(VariantGetTest, FloatingPointToStringMatchesJava) {
    // Doubles and floats are stringified via the Java formatting, not the arrow cast.
    ASSERT_EQ(GetString("$.double", arrow::utf8()), "1.0123456789012346");
    std::shared_ptr<GenericVariant> f =
        BuildScalar([](VariantBuilder& b) { return b.AppendFloat(1.5f); });
    ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_string,
                         VariantGetExecutor::Get(f, "$", arrow::utf8(), cast_args_));
    ASSERT_TRUE(as_string.has_value());
    ASSERT_EQ(as_string->GetValue<std::string>(), "1.5");
    ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_float,
                         VariantGetExecutor::Get(f, "$", arrow::float32(), cast_args_));
    ASSERT_TRUE(as_float.has_value());
    ASSERT_EQ(as_float->GetValue<float>(), 1.5f);
}

TEST_F(VariantGetTest, DateSource) {
    std::shared_ptr<GenericVariant> d =
        BuildScalar([](VariantBuilder& b) { return b.AppendDate(19000); });
    ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_date,
                         VariantGetExecutor::Get(d, "$", arrow::date32(), cast_args_));
    ASSERT_TRUE(as_date.has_value());
    ASSERT_EQ(as_date->GetValue<int32_t>(), 19000);
}

TEST_F(VariantGetTest, MissingCastExecutorYieldsNull) {
    // No binary->int64 cast executor exists, so the cast is treated as invalid (SQL NULL).
    std::shared_ptr<GenericVariant> bin = BuildScalar(
        [](VariantBuilder& b) { return b.AppendBinary(std::string_view("\x01\x02", 2)); });
    ASSERT_OK_AND_ASSIGN(std::optional<Literal> as_long,
                         VariantGetExecutor::Get(bin, "$", arrow::int64(), cast_args_));
    ASSERT_FALSE(as_long.has_value());
}

TEST_F(VariantGetTest, ScalarArrowBuilders) {
    auto build_array = [&](const std::shared_ptr<GenericVariant>& variant, const std::string& path,
                           const std::shared_ptr<arrow::DataType>& type) {
        auto result = VariantGetExecutor::GetAsArrow(variant, path, arrow::field("x", type),
                                                     cast_args_, pool_, arrow_pool_);
        EXPECT_TRUE(result.ok()) << result.status().ToString();
        return result.value();
    };

    std::shared_ptr<arrow::Array> boolean = build_array(variant_, "$.boolean1", arrow::boolean());
    ASSERT_TRUE(static_cast<const arrow::BooleanArray&>(*boolean).Value(0));

    // INT8/INT16/INT32 each require a narrowing cast from the int64 source.
    std::shared_ptr<arrow::Array> int8 = build_array(variant_, "$.object.age", arrow::int8());
    ASSERT_EQ(static_cast<const arrow::Int8Array&>(*int8).Value(0), 2);
    std::shared_ptr<arrow::Array> int16 = build_array(variant_, "$.object.age", arrow::int16());
    ASSERT_EQ(static_cast<const arrow::Int16Array&>(*int16).Value(0), 2);
    std::shared_ptr<arrow::Array> int32 = build_array(variant_, "$.object.age", arrow::int32());
    ASSERT_EQ(static_cast<const arrow::Int32Array&>(*int32).Value(0), 2);

    std::shared_ptr<arrow::Array> float64 = build_array(variant_, "$.double", arrow::float64());
    ASSERT_DOUBLE_EQ(static_cast<const arrow::DoubleArray&>(*float64).Value(0), 1.0123456789012346);

    std::shared_ptr<GenericVariant> f =
        BuildScalar([](VariantBuilder& b) { return b.AppendFloat(1.5f); });
    std::shared_ptr<arrow::Array> float32 = build_array(f, "$", arrow::float32());
    ASSERT_EQ(static_cast<const arrow::FloatArray&>(*float32).Value(0), 1.5f);

    std::shared_ptr<arrow::Array> decimal =
        build_array(variant_, "$.decimal", arrow::decimal128(5, 2));
    ASSERT_EQ(static_cast<const arrow::Decimal128Array&>(*decimal).FormatValue(0), "100.99");

    std::shared_ptr<GenericVariant> bin = BuildScalar(
        [](VariantBuilder& b) { return b.AppendBinary(std::string_view("\x01\x02\x03", 3)); });
    std::shared_ptr<arrow::Array> binary = build_array(bin, "$", arrow::binary());
    ASSERT_EQ(static_cast<const arrow::BinaryArray&>(*binary).GetView(0),
              std::string_view("\x01\x02\x03", 3));

    std::shared_ptr<GenericVariant> d =
        BuildScalar([](VariantBuilder& b) { return b.AppendDate(19000); });
    std::shared_ptr<arrow::Array> date = build_array(d, "$", arrow::date32());
    ASSERT_EQ(static_cast<const arrow::Date32Array&>(*date).Value(0), 19000);

    // Only the microsecond unit matches the variant's internal timestamp representation; other
    // units require a timestamp->timestamp literal cast that is intentionally unsupported.
    std::shared_ptr<GenericVariant> ts =
        BuildScalar([](VariantBuilder& b) { return b.AppendTimestamp(1700000000123456); });
    std::shared_ptr<arrow::Array> timestamp =
        build_array(ts, "$", arrow::timestamp(arrow::TimeUnit::MICRO, "UTC"));
    ASSERT_EQ(static_cast<const arrow::TimestampArray&>(*timestamp).Value(0), 1700000000123456);
}

TEST_F(VariantGetTest, ListFromNonArrayIsNull) {
    auto target = arrow::field("l", arrow::list(arrow::int64()));
    // An object cannot cast to a list: SQL NULL when fail_on_error is false ...
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array, GetAsArrow("$.object", target));
    ASSERT_TRUE(array->IsNull(0));
    // ... and an error when fail_on_error is true.
    cast_args_.fail_on_error = true;
    auto result = VariantGetExecutor::GetAsArrow(variant_, "$.object", target, cast_args_, pool_,
                                                 arrow_pool_);
    ASSERT_NOK(result);
}

}  // namespace paimon::test
