/*
 * 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_access_utils.h"

#include <memory>
#include <string>
#include <vector>

#include "arrow/api.h"
#include "arrow/util/key_value_metadata.h"
#include "gtest/gtest.h"
#include "paimon/common/data/variant/variant_defs.h"
#include "paimon/common/types/data_field.h"
#include "paimon/testing/utils/testharness.h"

namespace paimon::test {

namespace {

// An Arrow field carrying a `paimon.description` metadata entry.
std::shared_ptr<arrow::Field> DescribedField(const std::string& name,
                                             const std::shared_ptr<arrow::DataType>& type,
                                             const std::string& description) {
    return arrow::field(name, type, /*nullable=*/true,
                        arrow::key_value_metadata({DataField::DESCRIPTION}, {description}));
}

std::shared_ptr<arrow::Field> AccessProjection(
    const std::vector<std::shared_ptr<arrow::Field>>& children) {
    return arrow::field("v", arrow::struct_(children));
}

// A shredded file field: struct{[metadata], value, typed_value: struct{<typed_children>}}.
std::shared_ptr<arrow::Field> ShreddedFile(
    const std::vector<std::shared_ptr<arrow::Field>>& typed_children, bool with_metadata = true) {
    arrow::FieldVector fields;
    if (with_metadata) {
        fields.push_back(
            arrow::field(std::string(VariantDefs::kMetadataFieldName), arrow::binary()));
    }
    fields.push_back(arrow::field(std::string(VariantDefs::kValueFieldName), arrow::binary()));
    fields.push_back(arrow::field(std::string(VariantDefs::kTypedValueFieldName),
                                  arrow::struct_(typed_children)));
    return arrow::field("v", arrow::struct_(fields));
}

std::vector<VariantAccessSpec> ObjectKeySpecs() {
    auto proj = AccessProjection({DescribedField(
        "a", arrow::int32(), VariantAccessUtils::BuildVariantMetadata("$.a", false, "UTC"))});
    auto result = VariantAccessUtils::ParseAccessSpecs(proj);
    EXPECT_TRUE(result.ok()) << result.status().ToString();
    return result.value();
}

}  // namespace

TEST(VariantAccessUtilsTest, ParseAccessSpecsRejectsNonProjection) {
    // A plain struct without access descriptions is not a variant-access projection.
    auto plain = arrow::field("v", arrow::struct_({arrow::field("x", arrow::int32())}));
    ASSERT_FALSE(VariantAccessUtils::IsVariantAccessType(plain->type()));
    ASSERT_NOK(VariantAccessUtils::ParseAccessSpecs(plain));
}

TEST(VariantAccessUtilsTest, ParseAccessSpecsRejectsMalformedDescription) {
    const std::string key = VariantAccessUtils::kMetadataKey;
    // A description with no delimiter splits into a single part.
    auto no_delim = AccessProjection({DescribedField("a", arrow::utf8(), key + "$.a")});
    ASSERT_TRUE(VariantAccessUtils::IsVariantAccessType(no_delim->type()));
    ASSERT_NOK(VariantAccessUtils::ParseAccessSpecs(no_delim));
    // A single delimiter splits into two parts (still not the three that a spec needs).
    auto one_delim = AccessProjection({DescribedField("a", arrow::utf8(), key + "$.a;true")});
    ASSERT_TRUE(VariantAccessUtils::IsVariantAccessType(one_delim->type()));
    ASSERT_NOK(VariantAccessUtils::ParseAccessSpecs(one_delim));
}

TEST(VariantAccessUtilsTest, ParseAccessSpecsRoundTripsBuildMetadata) {
    auto proj = AccessProjection({DescribedField(
        "a", arrow::int32(), VariantAccessUtils::BuildVariantMetadata("$.a", true, "+08:00"))});
    ASSERT_OK_AND_ASSIGN(std::vector<VariantAccessSpec> specs,
                         VariantAccessUtils::ParseAccessSpecs(proj));
    ASSERT_EQ(specs.size(), 1);
    EXPECT_EQ(specs[0].path, "$.a");
    EXPECT_TRUE(specs[0].cast_args.fail_on_error);
    EXPECT_EQ(specs[0].cast_args.zone_id, "+08:00");
}

TEST(VariantAccessUtilsTest, ClipRejectsNonStructFileField) {
    auto non_struct = arrow::field("v", arrow::int32());
    ASSERT_NOK(VariantAccessUtils::ClipShreddedFileField(ObjectKeySpecs(), non_struct));
}

TEST(VariantAccessUtilsTest, ClipUnprunablePathsReturnFileUnchanged) {
    auto file = ShreddedFile({arrow::field("a", arrow::int32())});

    // A root path needs the whole variant, so the file field is returned unchanged.
    auto root_proj = AccessProjection({DescribedField(
        "r", arrow::utf8(), VariantAccessUtils::BuildVariantMetadata("$", false, "UTC"))});
    ASSERT_OK_AND_ASSIGN(std::vector<VariantAccessSpec> root_specs,
                         VariantAccessUtils::ParseAccessSpecs(root_proj));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> root_clipped,
                         VariantAccessUtils::ClipShreddedFileField(root_specs, file));
    EXPECT_EQ(root_clipped, file);

    // An array-first path likewise cannot be pruned to a top-level key.
    auto array_proj = AccessProjection({DescribedField(
        "e", arrow::utf8(), VariantAccessUtils::BuildVariantMetadata("$[0]", false, "UTC"))});
    ASSERT_OK_AND_ASSIGN(std::vector<VariantAccessSpec> array_specs,
                         VariantAccessUtils::ParseAccessSpecs(array_proj));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> array_clipped,
                         VariantAccessUtils::ClipShreddedFileField(array_specs, file));
    EXPECT_EQ(array_clipped, file);
}

TEST(VariantAccessUtilsTest, ClipUnshreddedFileFieldReturnedUnchanged) {
    // No typed_value column: there is nothing to prune.
    auto unshredded = arrow::field(
        "v",
        arrow::struct_({arrow::field(std::string(VariantDefs::kMetadataFieldName), arrow::binary()),
                        arrow::field(std::string(VariantDefs::kValueFieldName), arrow::binary())}));
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> clipped,
                         VariantAccessUtils::ClipShreddedFileField(ObjectKeySpecs(), unshredded));
    EXPECT_EQ(clipped, unshredded);
}

TEST(VariantAccessUtilsTest, ClipMissingMetadataFails) {
    auto file_no_metadata =
        ShreddedFile({arrow::field("a", arrow::int32())}, /*with_metadata=*/false);
    ASSERT_NOK(VariantAccessUtils::ClipShreddedFileField(ObjectKeySpecs(), file_no_metadata));
}

TEST(VariantAccessUtilsTest, ClipNarrowsToRequestedKeys) {
    // "$.a" is requested; the shredded typed_value keeps only "a" and drops the unrelated "b".
    auto file = ShreddedFile({arrow::field("a", arrow::int32()), arrow::field("b", arrow::utf8())});
    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> clipped,
                         VariantAccessUtils::ClipShreddedFileField(ObjectKeySpecs(), file));
    ASSERT_EQ(clipped->type()->id(), arrow::Type::STRUCT);
    const auto& clipped_struct = static_cast<const arrow::StructType&>(*clipped->type());
    // metadata is always kept; value is dropped because "a" is shredded; typed_value keeps "a".
    ASSERT_NE(clipped_struct.GetFieldByName(VariantDefs::kMetadataFieldName), nullptr);
    EXPECT_EQ(clipped_struct.GetFieldByName(VariantDefs::kValueFieldName), nullptr);
    auto typed = clipped_struct.GetFieldByName(VariantDefs::kTypedValueFieldName);
    ASSERT_NE(typed, nullptr);
    ASSERT_EQ(typed->type()->num_fields(), 1);
    EXPECT_EQ(typed->type()->field(0)->name(), "a");
}

}  // namespace paimon::test
