/*
 * 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 <functional>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>

#include <arrow/api.h>
#include <arrow/c/bridge.h>
#include <arrow/extension/uuid.h>
#include <arrow/result.h>
#include <arrow/type_fwd.h>
#include <arrow/util/config.h>
#include <arrow/util/key_value_metadata.h>
#include <arrow/util/thread_pool.h>
#include <gtest/gtest.h>

#include "iceberg/arrow/arrow_status_internal.h"
#include "iceberg/constants.h"
#include "iceberg/result.h"
#include "iceberg/schema.h"
#include "iceberg/schema_internal.h"
#include "iceberg/test/matchers.h"
#include "iceberg/util/executor.h"
#include "iceberg/util/task_group.h"

namespace iceberg {

struct ToArrowSchemaParam {
  std::shared_ptr<Type> iceberg_type;
  bool optional = true;
  std::shared_ptr<::arrow::DataType> arrow_type;
};

class ToArrowSchemaTest : public ::testing::TestWithParam<ToArrowSchemaParam> {};

TEST_P(ToArrowSchemaTest, PrimitiveType) {
  constexpr std::string_view kFieldName = "foo";
  constexpr int32_t kFieldId = 1024;
  const auto& param = GetParam();
  Schema schema(
      {param.optional ? SchemaField::MakeOptional(kFieldId, std::string(kFieldName),
                                                  param.iceberg_type)
                      : SchemaField::MakeRequired(kFieldId, std::string(kFieldName),
                                                  param.iceberg_type)},
      /*schema_id=*/0);
  ArrowSchema arrow_schema;
  ASSERT_THAT(ToArrowSchema(schema, &arrow_schema), IsOk());

  auto imported_schema = ::arrow::ImportSchema(&arrow_schema).ValueOrDie();
  ASSERT_EQ(imported_schema->num_fields(), 1);

  auto field = imported_schema->field(0);
  ASSERT_EQ(field->name(), kFieldName);
  ASSERT_EQ(field->nullable(), param.optional);
  ASSERT_TRUE(field->type()->Equals(param.arrow_type));

  auto metadata = field->metadata();
  ASSERT_TRUE(metadata->Contains(kParquetFieldIdKey));
  ASSERT_EQ(metadata->Get(kParquetFieldIdKey), std::to_string(kFieldId));
}

INSTANTIATE_TEST_SUITE_P(
    SchemaConversion, ToArrowSchemaTest,
    ::testing::Values(
        ToArrowSchemaParam{.iceberg_type = iceberg::boolean(),
                           .optional = false,
                           .arrow_type = ::arrow::boolean()},
        ToArrowSchemaParam{.iceberg_type = iceberg::int32(),
                           .optional = true,
                           .arrow_type = ::arrow::int32()},
        ToArrowSchemaParam{.iceberg_type = iceberg::int64(),
                           .arrow_type = ::arrow::int64()},
        ToArrowSchemaParam{.iceberg_type = iceberg::float32(),
                           .arrow_type = ::arrow::float32()},
        ToArrowSchemaParam{.iceberg_type = iceberg::float64(),
                           .arrow_type = ::arrow::float64()},
        ToArrowSchemaParam{.iceberg_type = iceberg::decimal(10, 2),
                           .arrow_type = ::arrow::decimal128(10, 2)},
        ToArrowSchemaParam{.iceberg_type = iceberg::date(),
                           .arrow_type = ::arrow::date32()},
        ToArrowSchemaParam{.iceberg_type = iceberg::time(),
                           .arrow_type = ::arrow::time64(::arrow::TimeUnit::MICRO)},
        ToArrowSchemaParam{.iceberg_type = iceberg::timestamp(),
                           .arrow_type = ::arrow::timestamp(::arrow::TimeUnit::MICRO)},
        ToArrowSchemaParam{
            .iceberg_type = iceberg::timestamp_tz(),
            .arrow_type = ::arrow::timestamp(::arrow::TimeUnit::MICRO, "UTC")},
        ToArrowSchemaParam{.iceberg_type = iceberg::timestamp_ns(),
                           .arrow_type = ::arrow::timestamp(::arrow::TimeUnit::NANO)},
        ToArrowSchemaParam{
            .iceberg_type = iceberg::timestamptz_ns(),
            .arrow_type = ::arrow::timestamp(::arrow::TimeUnit::NANO, "UTC")},
        ToArrowSchemaParam{.iceberg_type = iceberg::string(),
                           .arrow_type = ::arrow::utf8()},
        ToArrowSchemaParam{.iceberg_type = iceberg::binary(),
                           .arrow_type = ::arrow::binary()},
        ToArrowSchemaParam{.iceberg_type = iceberg::uuid(),
                           .arrow_type = ::arrow::extension::uuid()},
        ToArrowSchemaParam{.iceberg_type = iceberg::fixed(20),
                           .arrow_type = ::arrow::fixed_size_binary(20)},
        ToArrowSchemaParam{.iceberg_type = iceberg::unknown(),
                           .arrow_type = ::arrow::null()}));

namespace {

void CheckArrowField(const ::arrow::Field& field, ::arrow::Type::type type_id,
                     std::string_view name, bool nullable, int32_t field_id) {
  ASSERT_EQ(field.name(), name);
  ASSERT_EQ(field.nullable(), nullable);
  ASSERT_EQ(field.type()->id(), type_id);

  auto metadata = field.metadata();
  ASSERT_TRUE(metadata != nullptr);
  ASSERT_TRUE(metadata->Contains(kParquetFieldIdKey));
  ASSERT_EQ(metadata->Get(kParquetFieldIdKey), std::to_string(field_id));
}

}  // namespace

TEST(ToArrowSchemaTest, StructType) {
  constexpr int32_t kStructFieldId = 1;
  constexpr int32_t kIntFieldId = 2;
  constexpr int32_t kStrFieldId = 3;

  constexpr std::string_view kStructFieldName = "struct_field";
  constexpr std::string_view kIntFieldName = "int_field";
  constexpr std::string_view kStrFieldName = "str_field";

  auto struct_type = std::make_shared<StructType>(std::vector<SchemaField>{
      SchemaField::MakeRequired(kIntFieldId, std::string(kIntFieldName),
                                iceberg::int32()),
      SchemaField::MakeOptional(kStrFieldId, std::string(kStrFieldName),
                                iceberg::string())});
  Schema schema({SchemaField::MakeRequired(kStructFieldId, std::string(kStructFieldName),
                                           struct_type)},
                /*schema_id=*/0);

  ArrowSchema arrow_schema;
  ASSERT_THAT(ToArrowSchema(schema, &arrow_schema), IsOk());

  auto imported_schema = ::arrow::ImportSchema(&arrow_schema).ValueOrDie();
  ASSERT_EQ(imported_schema->num_fields(), 1);

  auto field = imported_schema->field(0);
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*field, ::arrow::Type::STRUCT, kStructFieldName,
                                          /*nullable=*/false, kStructFieldId));

  auto struct_field = std::static_pointer_cast<::arrow::StructType>(field->type());
  ASSERT_EQ(struct_field->num_fields(), 2);

  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*struct_field->field(0), ::arrow::Type::INT32,
                                          kIntFieldName, /*nullable=*/false,
                                          kIntFieldId));
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*struct_field->field(1), ::arrow::Type::STRING,
                                          kStrFieldName, /*nullable=*/true, kStrFieldId));
}

TEST(ToArrowSchemaTest, ListType) {
  constexpr std::string_view kListFieldName = "list_field";
  constexpr std::string_view kElemFieldName = "element";
  constexpr int32_t kListFieldId = 1;
  constexpr int32_t kElemFieldId = 2;

  auto list_type = std::make_shared<ListType>(SchemaField::MakeOptional(
      kElemFieldId, std::string(kElemFieldName), iceberg::int64()));
  Schema schema(
      {SchemaField::MakeRequired(kListFieldId, std::string(kListFieldName), list_type)},
      /*schema_id=*/0);

  ArrowSchema arrow_schema;
  ASSERT_THAT(ToArrowSchema(schema, &arrow_schema), IsOk());

  auto imported_schema = ::arrow::ImportSchema(&arrow_schema).ValueOrDie();
  ASSERT_EQ(imported_schema->num_fields(), 1);

  auto field = imported_schema->field(0);
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*field, ::arrow::Type::LIST, kListFieldName,
                                          /*nullable=*/false, kListFieldId));

  auto list_field = std::static_pointer_cast<::arrow::ListType>(field->type());
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*list_field->value_field(),
                                          ::arrow::Type::INT64, kElemFieldName,
                                          /*nullable=*/true, kElemFieldId));
}

TEST(ToArrowSchemaTest, MapType) {
  constexpr std::string_view kMapFieldName = "map_field";
  constexpr std::string_view kKeyFieldName = "key";
  constexpr std::string_view kValueFieldName = "value";

  constexpr int32_t kFieldId = 1;
  constexpr int32_t kKeyFieldId = 2;
  constexpr int32_t kValueFieldId = 3;

  auto map_type = std::make_shared<MapType>(
      SchemaField::MakeRequired(kKeyFieldId, std::string(kKeyFieldName),
                                iceberg::string()),
      SchemaField::MakeOptional(kValueFieldId, std::string(kValueFieldName),
                                iceberg::int32()));

  Schema schema(
      {SchemaField::MakeRequired(kFieldId, std::string(kMapFieldName), map_type)},
      /*schema_id=*/0);

  ArrowSchema arrow_schema;
  ASSERT_THAT(ToArrowSchema(schema, &arrow_schema), IsOk());

  auto imported_schema = ::arrow::ImportSchema(&arrow_schema).ValueOrDie();
  ASSERT_EQ(imported_schema->num_fields(), 1);

  auto field = imported_schema->field(0);
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*field, ::arrow::Type::MAP, kMapFieldName,
                                          /*nullable=*/false, kFieldId));

  auto map_field = std::static_pointer_cast<::arrow::MapType>(field->type());

  auto key_field = map_field->key_field();
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*key_field, ::arrow::Type::STRING,
                                          kKeyFieldName,
                                          /*nullable=*/false, kKeyFieldId));

  auto value_field = map_field->item_field();
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*value_field, ::arrow::Type::INT32,
                                          kValueFieldName,
                                          /*nullable=*/true, kValueFieldId));
}

TEST(ToArrowSchemaTest, NestedUnknownFieldsRoundTrip) {
  Schema schema(
      {
          SchemaField::MakeOptional(
              /*field_id=*/1, "profile",
              std::make_shared<StructType>(std::vector<SchemaField>{
                  SchemaField::MakeOptional(/*field_id=*/2, "mystery",
                                            iceberg::unknown()),
              })),
          SchemaField::MakeOptional(
              /*field_id=*/3, "mysteries",
              std::make_shared<ListType>(SchemaField::MakeOptional(
                  /*field_id=*/4, "element", iceberg::unknown()))),
          SchemaField::MakeOptional(
              /*field_id=*/5, "properties",
              std::make_shared<MapType>(
                  SchemaField::MakeRequired(/*field_id=*/6, "key", iceberg::string()),
                  SchemaField::MakeOptional(/*field_id=*/7, "value",
                                            iceberg::unknown()))),
      },
      /*schema_id=*/0);

  ArrowSchema arrow_c_schema;
  ASSERT_THAT(ToArrowSchema(schema, &arrow_c_schema), IsOk());

  auto imported_schema = ::arrow::ImportSchema(&arrow_c_schema).ValueOrDie();
  ASSERT_EQ(imported_schema->num_fields(), 3);

  auto profile_type =
      std::static_pointer_cast<::arrow::StructType>(imported_schema->field(0)->type());
  ASSERT_EQ(profile_type->num_fields(), 1);
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*profile_type->field(0), ::arrow::Type::NA,
                                          "mystery", /*nullable=*/true,
                                          /*field_id=*/2));

  auto mysteries_type =
      std::static_pointer_cast<::arrow::ListType>(imported_schema->field(1)->type());
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*mysteries_type->value_field(),
                                          ::arrow::Type::NA, "element",
                                          /*nullable=*/true, /*field_id=*/4));

  auto properties_type =
      std::static_pointer_cast<::arrow::MapType>(imported_schema->field(2)->type());
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*properties_type->key_field(),
                                          ::arrow::Type::STRING, "key",
                                          /*nullable=*/false, /*field_id=*/6));
  ASSERT_NO_FATAL_FAILURE(CheckArrowField(*properties_type->item_field(),
                                          ::arrow::Type::NA, "value",
                                          /*nullable=*/true, /*field_id=*/7));

  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*imported_schema, &exported_schema).ok());
  auto schema_result = FromArrowSchema(exported_schema, /*schema_id=*/0);
  ASSERT_THAT(schema_result, IsOk());
  ArrowSchemaRelease(&exported_schema);

  const auto& round_tripped_schema = *schema_result.value();
  ASSERT_EQ(round_tripped_schema.fields().size(), 3);

  const auto* profile =
      dynamic_cast<const StructType*>(round_tripped_schema.fields()[0].type().get());
  ASSERT_NE(profile, nullptr);
  ASSERT_EQ(profile->fields()[0].type()->type_id(), TypeId::kUnknown);

  const auto* mysteries =
      dynamic_cast<const ListType*>(round_tripped_schema.fields()[1].type().get());
  ASSERT_NE(mysteries, nullptr);
  ASSERT_EQ(mysteries->fields()[0].type()->type_id(), TypeId::kUnknown);

  const auto* properties =
      dynamic_cast<const MapType*>(round_tripped_schema.fields()[2].type().get());
  ASSERT_NE(properties, nullptr);
  ASSERT_EQ(properties->value().type()->type_id(), TypeId::kUnknown);
}

struct FromArrowSchemaParam {
  std::shared_ptr<::arrow::DataType> arrow_type;
  bool optional = true;
  std::shared_ptr<Type> iceberg_type;
};

class FromArrowSchemaTest : public ::testing::TestWithParam<FromArrowSchemaParam> {};

TEST_P(FromArrowSchemaTest, PrimitiveType) {
  constexpr std::string_view kFieldName = "foo";
  constexpr int32_t kFieldId = 1024;
  const auto& param = GetParam();

  auto metadata =
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kFieldId)}});
  auto arrow_schema = ::arrow::schema({::arrow::field(
      std::string(kFieldName), param.arrow_type, param.optional, std::move(metadata))});
  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*arrow_schema, &exported_schema).ok());

  auto type_result = FromArrowSchema(exported_schema, /*schema_id=*/1);
  ASSERT_THAT(type_result, IsOk());
  ArrowSchemaRelease(&exported_schema);

  const auto& schema = type_result.value();
  ASSERT_EQ(schema->schema_id(), 1);
  ASSERT_EQ(schema->fields().size(), 1);

  const auto& field = schema->fields()[0];
  ASSERT_EQ(field.name(), kFieldName);
  ASSERT_EQ(field.field_id(), kFieldId);
  ASSERT_EQ(field.optional(), param.optional);
  ASSERT_EQ(*field.type(), *param.iceberg_type);
}

INSTANTIATE_TEST_SUITE_P(
    SchemaConversion, FromArrowSchemaTest,
    ::testing::Values(
        FromArrowSchemaParam{.arrow_type = ::arrow::boolean(),
                             .optional = false,
                             .iceberg_type = iceberg::boolean()},
        FromArrowSchemaParam{.arrow_type = ::arrow::int32(),
                             .optional = true,
                             .iceberg_type = iceberg::int32()},
        FromArrowSchemaParam{.arrow_type = ::arrow::int64(),
                             .iceberg_type = iceberg::int64()},
        FromArrowSchemaParam{.arrow_type = ::arrow::float32(),
                             .iceberg_type = iceberg::float32()},
        FromArrowSchemaParam{.arrow_type = ::arrow::float64(),
                             .iceberg_type = iceberg::float64()},
        FromArrowSchemaParam{.arrow_type = ::arrow::decimal128(10, 2),
                             .iceberg_type = iceberg::decimal(10, 2)},
        FromArrowSchemaParam{.arrow_type = ::arrow::date32(),
                             .iceberg_type = iceberg::date()},
        FromArrowSchemaParam{.arrow_type = ::arrow::time64(::arrow::TimeUnit::MICRO),
                             .iceberg_type = iceberg::time()},
        FromArrowSchemaParam{.arrow_type = ::arrow::timestamp(::arrow::TimeUnit::MICRO),
                             .iceberg_type = iceberg::timestamp()},
        FromArrowSchemaParam{
            .arrow_type = ::arrow::timestamp(::arrow::TimeUnit::MICRO, "UTC"),
            .iceberg_type = std::make_shared<TimestampTzType>()},
        FromArrowSchemaParam{.arrow_type = ::arrow::timestamp(::arrow::TimeUnit::NANO),
                             .iceberg_type = iceberg::timestamp_ns()},
        FromArrowSchemaParam{
            .arrow_type = ::arrow::timestamp(::arrow::TimeUnit::NANO, "UTC"),
            .iceberg_type = iceberg::timestamptz_ns()},
        FromArrowSchemaParam{.arrow_type = ::arrow::utf8(),
                             .iceberg_type = iceberg::string()},
        FromArrowSchemaParam{.arrow_type = ::arrow::binary(),
                             .iceberg_type = iceberg::binary()},
        FromArrowSchemaParam{.arrow_type = ::arrow::extension::uuid(),
                             .iceberg_type = iceberg::uuid()},
        FromArrowSchemaParam{.arrow_type = ::arrow::fixed_size_binary(20),
                             .iceberg_type = iceberg::fixed(20)},
        FromArrowSchemaParam{.arrow_type = ::arrow::null(),
                             .iceberg_type = iceberg::unknown()}));

TEST(FromArrowSchemaTest, RejectRequiredNullFieldAsUnknown) {
  auto metadata =
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), "1"}});
  auto arrow_schema = ::arrow::schema({::arrow::field(
      "mystery", ::arrow::null(), /*nullable=*/false, std::move(metadata))});

  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*arrow_schema, &exported_schema).ok());

  auto schema_result = FromArrowSchema(exported_schema, /*schema_id=*/0);
  ArrowSchemaRelease(&exported_schema);

  ASSERT_THAT(schema_result, IsError(ErrorKind::kInvalidSchema));
  ASSERT_THAT(schema_result,
              HasErrorMessage("Arrow null field 'mystery' must be nullable"));
}

TEST(FromArrowSchemaTest, RejectRequiredNullListElementAsUnknown) {
  auto list_metadata =
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), "1"}});
  auto element_metadata =
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), "2"}});
  auto element_field = ::arrow::field("element", ::arrow::null(), /*nullable=*/false,
                                      std::move(element_metadata));
  auto arrow_schema =
      ::arrow::schema({::arrow::field("mysteries", ::arrow::list(element_field),
                                      /*nullable=*/true, std::move(list_metadata))});

  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*arrow_schema, &exported_schema).ok());

  auto schema_result = FromArrowSchema(exported_schema, /*schema_id=*/0);
  ArrowSchemaRelease(&exported_schema);

  ASSERT_THAT(schema_result, IsError(ErrorKind::kInvalidSchema));
  ASSERT_THAT(schema_result,
              HasErrorMessage("Arrow null field 'element' must be nullable"));
}

TEST(FromArrowSchemaTest, StructType) {
  constexpr int32_t kStructFieldId = 1;
  constexpr int32_t kIntFieldId = 2;
  constexpr int32_t kStrFieldId = 3;

  constexpr std::string_view kStructFieldName = "struct_field";
  constexpr std::string_view kIntFieldName = "int_field";
  constexpr std::string_view kStrFieldName = "str_field";

  auto int_field = ::arrow::field(
      std::string(kIntFieldName), ::arrow::int32(), /*nullable=*/false,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kIntFieldId)}}));
  auto str_field = ::arrow::field(
      std::string(kStrFieldName), ::arrow::utf8(), /*nullable=*/true,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kStrFieldId)}}));
  auto struct_type = ::arrow::struct_({int_field, str_field});
  auto struct_field = ::arrow::field(
      std::string(kStructFieldName), struct_type, /*nullable=*/false,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kStructFieldId)}}));
  auto arrow_schema = ::arrow::schema({struct_field});
  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*arrow_schema, &exported_schema).ok());

  auto schema_result = FromArrowSchema(exported_schema, /*schema_id=*/0);
  ASSERT_THAT(schema_result, IsOk());
  ArrowSchemaRelease(&exported_schema);

  const auto& iceberg_schema = schema_result.value();
  ASSERT_EQ(iceberg_schema->schema_id(), 0);
  ASSERT_EQ(iceberg_schema->fields().size(), 1);

  const auto& field = iceberg_schema->fields()[0];
  ASSERT_EQ(field.name(), kStructFieldName);
  ASSERT_EQ(field.field_id(), kStructFieldId);
  ASSERT_FALSE(field.optional());
  ASSERT_EQ(field.type()->type_id(), TypeId::kStruct);

  auto* struct_field_type = dynamic_cast<const StructType*>(field.type().get());
  ASSERT_NE(struct_field_type, nullptr);
  ASSERT_EQ(struct_field_type->fields().size(), 2);

  const auto& int_iceberg_field = struct_field_type->fields()[0];
  ASSERT_EQ(int_iceberg_field.name(), kIntFieldName);
  ASSERT_EQ(int_iceberg_field.field_id(), kIntFieldId);
  ASSERT_FALSE(int_iceberg_field.optional());
  ASSERT_EQ(int_iceberg_field.type()->type_id(), TypeId::kInt);

  const auto& str_iceberg_field = struct_field_type->fields()[1];
  ASSERT_EQ(str_iceberg_field.name(), kStrFieldName);
  ASSERT_EQ(str_iceberg_field.field_id(), kStrFieldId);
  ASSERT_TRUE(str_iceberg_field.optional());
  ASSERT_EQ(str_iceberg_field.type()->type_id(), TypeId::kString);
}

TEST(FromArrowSchemaTest, ListType) {
  constexpr std::string_view kListFieldName = "list_field";
  constexpr std::string_view kElemFieldName = "element";
  constexpr int32_t kListFieldId = 1;
  constexpr int32_t kElemFieldId = 2;

  auto element_field = ::arrow::field(
      std::string(kElemFieldName), ::arrow::int64(), /*nullable=*/true,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kElemFieldId)}}));
  auto list_type = ::arrow::list(element_field);
  auto list_field = ::arrow::field(
      std::string(kListFieldName), list_type, /*nullable=*/false,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kListFieldId)}}));
  auto arrow_schema = ::arrow::schema({list_field});

  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*arrow_schema, &exported_schema).ok());

  auto schema_result = FromArrowSchema(exported_schema, /*schema_id=*/0);
  ASSERT_THAT(schema_result, IsOk());
  ArrowSchemaRelease(&exported_schema);

  const auto& iceberg_schema = schema_result.value();
  ASSERT_EQ(iceberg_schema->schema_id(), 0);
  ASSERT_EQ(iceberg_schema->fields().size(), 1);

  const auto& field = iceberg_schema->fields()[0];
  ASSERT_EQ(field.name(), kListFieldName);
  ASSERT_EQ(field.field_id(), kListFieldId);
  ASSERT_FALSE(field.optional());
  ASSERT_EQ(field.type()->type_id(), TypeId::kList);

  auto* list_field_type = dynamic_cast<const ListType*>(field.type().get());
  ASSERT_NE(list_field_type, nullptr);

  const auto& element = list_field_type->fields()[0];
  ASSERT_EQ(element.name(), kElemFieldName);
  ASSERT_EQ(element.field_id(), kElemFieldId);
  ASSERT_TRUE(element.optional());
  ASSERT_EQ(element.type()->type_id(), TypeId::kLong);
}

TEST(FromArrowSchemaTest, MapType) {
  constexpr std::string_view kMapFieldName = "map_field";
  constexpr std::string_view kKeyFieldName = "key";
  constexpr std::string_view kValueFieldName = "value";

  constexpr int32_t kFieldId = 1;
  constexpr int32_t kKeyFieldId = 2;
  constexpr int32_t kValueFieldId = 3;

  auto key_field = ::arrow::field(
      std::string(kKeyFieldName), ::arrow::utf8(), /*nullable=*/false,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kKeyFieldId)}}));
  auto value_field = ::arrow::field(
      std::string(kValueFieldName), ::arrow::int32(), /*nullable=*/true,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kValueFieldId)}}));
  auto map_type = std::make_shared<::arrow::MapType>(key_field, value_field);
  auto map_field = ::arrow::field(
      std::string(kMapFieldName), map_type, /*nullable=*/true,
      ::arrow::key_value_metadata(std::unordered_map<std::string, std::string>{
          {std::string(kParquetFieldIdKey), std::to_string(kFieldId)}}));
  auto arrow_schema = ::arrow::schema({map_field});

  ArrowSchema exported_schema;
  ASSERT_TRUE(::arrow::ExportSchema(*arrow_schema, &exported_schema).ok());

  auto schema_result = FromArrowSchema(exported_schema, /*schema_id=*/0);
  ASSERT_THAT(schema_result, IsOk());
  ArrowSchemaRelease(&exported_schema);

  const auto& iceberg_schema = schema_result.value();
  ASSERT_EQ(iceberg_schema->schema_id(), 0);
  ASSERT_EQ(iceberg_schema->fields().size(), 1);

  const auto& field = iceberg_schema->fields()[0];
  ASSERT_EQ(field.name(), kMapFieldName);
  ASSERT_EQ(field.field_id(), kFieldId);
  ASSERT_TRUE(field.optional());
  ASSERT_EQ(field.type()->type_id(), TypeId::kMap);

  auto* map_field_type = dynamic_cast<const MapType*>(field.type().get());
  ASSERT_NE(map_field_type, nullptr);

  const auto& key = map_field_type->key();
  ASSERT_EQ(key.name(), kKeyFieldName);
  ASSERT_EQ(key.field_id(), kKeyFieldId);
  ASSERT_FALSE(key.optional());
  ASSERT_EQ(key.type()->type_id(), TypeId::kString);

  const auto& value = map_field_type->value();
  ASSERT_EQ(value.name(), kValueFieldName);
  ASSERT_EQ(value.field_id(), kValueFieldId);
  ASSERT_TRUE(value.optional());
  ASSERT_EQ(value.type()->type_id(), TypeId::kInt);
}

TEST(ArrowExecutorAdapterTest, RunsTaskGroupOnThreadPool) {
#ifndef ARROW_ENABLE_THREADING
  GTEST_SKIP() << "Test requires ARROW_ENABLE_THREADING=ON";
#endif

  class ArrowExecutorAdapter final : public Executor {
   public:
    explicit ArrowExecutorAdapter(::arrow::internal::Executor& executor)
        : executor_(executor) {}

    Status Submit(ExecutorTask task) override {
      ICEBERG_ARROW_RETURN_NOT_OK(executor_.Spawn(std::move(task)));
      return {};
    }

   private:
    ::arrow::internal::Executor& executor_;
  };

  auto thread_pool = ::arrow::internal::ThreadPool::Make(2).ValueOrDie();
  ArrowExecutorAdapter executor(*thread_pool);

  std::mutex mutex;
  std::vector<std::thread::id> thread_ids;

  auto status = TaskGroup<>()
                    .SetExecutor(std::ref(executor))
                    .Submit([&]() -> Status {
                      std::lock_guard lock(mutex);
                      thread_ids.push_back(std::this_thread::get_id());
                      return {};
                    })
                    .Submit([&]() -> Status {
                      std::lock_guard lock(mutex);
                      thread_ids.push_back(std::this_thread::get_id());
                      return {};
                    })
                    .Run();

  EXPECT_THAT(status, IsOk());
  EXPECT_EQ(thread_ids.size(), 2);
  EXPECT_NE(thread_ids[0], std::this_thread::get_id());
  EXPECT_NE(thread_ids[1], std::this_thread::get_id());
  EXPECT_TRUE(thread_pool->Shutdown().ok());
}

}  // namespace iceberg
