// 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 <vector>

#include "gmock/gmock-matchers.h"
#include "gtest/gtest.h"

#include "parquet/arrow/reader.h"
#include "parquet/arrow/reader_internal.h"
#include "parquet/arrow/schema.h"
#include "parquet/arrow/variant_internal.h"
#include "parquet/file_reader.h"
#include "parquet/schema.h"
#include "parquet/schema_internal.h"
#include "parquet/test_util.h"
#include "parquet/thrift_internal.h"

#include "arrow/array.h"
#include "arrow/extension/json.h"
#include "arrow/extension/uuid.h"
#include "arrow/ipc/writer.h"
#include "arrow/testing/extension_type.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/type.h"
#include "arrow/util/base64.h"
#include "arrow/util/key_value_metadata.h"

using arrow::Field;
using arrow::TimeUnit;

using ParquetType = parquet::Type;
using parquet::ConvertedType;
using parquet::LogicalType;
using parquet::Repetition;
using parquet::format::SchemaElement;
using parquet::internal::LevelInfo;
using parquet::schema::GroupNode;
using parquet::schema::NodePtr;
using parquet::schema::PrimitiveNode;

using ::testing::ElementsAre;

namespace parquet::arrow {

const auto BOOL = ::arrow::boolean();
const auto UINT8 = ::arrow::uint8();
const auto INT32 = ::arrow::int32();
const auto INT64 = ::arrow::int64();
const auto FLOAT = ::arrow::float32();
const auto DOUBLE = ::arrow::float64();
const auto UTF8 = ::arrow::utf8();
const auto TIMESTAMP_MS = ::arrow::timestamp(TimeUnit::MILLI);
const auto TIMESTAMP_US = ::arrow::timestamp(TimeUnit::MICRO);
const auto TIMESTAMP_NS = ::arrow::timestamp(TimeUnit::NANO);
const auto BINARY = ::arrow::binary();
const auto DECIMAL_8_4 = std::make_shared<::arrow::Decimal128Type>(8, 4);

struct ListCase {
  ::arrow::Type::type type_id;
  std::function<std::shared_ptr<::arrow::DataType>(std::shared_ptr<::arrow::Field>)>
      type_factory;
};

static const std::vector<ListCase> kListCases = {
    {::arrow::Type::LIST,
     [](std::shared_ptr<::arrow::Field> field) { return ::arrow::list(field); }},
    {::arrow::Type::LARGE_LIST,
     [](std::shared_ptr<::arrow::Field> field) { return ::arrow::large_list(field); }},
};

class TestConvertParquetSchema : public ::testing::Test {
 public:
  virtual void SetUp() {}

  void CheckFlatSchema(const std::shared_ptr<::arrow::Schema>& expected_schema,
                       bool check_metadata = false) {
    ASSERT_EQ(expected_schema->num_fields(), result_schema_->num_fields());
    for (int i = 0; i < expected_schema->num_fields(); ++i) {
      auto result_field = result_schema_->field(i);
      auto expected_field = expected_schema->field(i);
      EXPECT_TRUE(result_field->Equals(expected_field, check_metadata))
          << "Field " << i << "\n  result: " << result_field->ToString(check_metadata)
          << "\n  expected: " << expected_field->ToString(check_metadata);
    }
  }

  ::arrow::Status ConvertSchema(
      const std::vector<NodePtr>& nodes,
      const std::shared_ptr<const KeyValueMetadata>& key_value_metadata = nullptr,
      ArrowReaderProperties props = ArrowReaderProperties()) {
    NodePtr schema = GroupNode::Make("schema", Repetition::REPEATED, nodes);
    descr_.Init(schema);
    return FromParquetSchema(&descr_, props, key_value_metadata, &result_schema_);
  }

 protected:
  SchemaDescriptor descr_;
  std::shared_ptr<::arrow::Schema> result_schema_;
};

TEST_F(TestConvertParquetSchema, ParquetFlatPrimitives) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  parquet_fields.push_back(
      PrimitiveNode::Make("boolean", Repetition::REQUIRED, ParquetType::BOOLEAN));
  arrow_fields.push_back(::arrow::field("boolean", BOOL, false));

  parquet_fields.push_back(
      PrimitiveNode::Make("int32", Repetition::REQUIRED, ParquetType::INT32));
  arrow_fields.push_back(::arrow::field("int32", INT32, false));

  parquet_fields.push_back(
      PrimitiveNode::Make("int64", Repetition::REQUIRED, ParquetType::INT64));
  arrow_fields.push_back(::arrow::field("int64", INT64, false));

  parquet_fields.push_back(PrimitiveNode::Make("timestamp", Repetition::REQUIRED,
                                               ParquetType::INT64,
                                               ConvertedType::TIMESTAMP_MILLIS));
  arrow_fields.push_back(
      ::arrow::field("timestamp", ::arrow::timestamp(TimeUnit::MILLI, "UTC"), false));

  parquet_fields.push_back(PrimitiveNode::Make("timestamp[us]", Repetition::REQUIRED,
                                               ParquetType::INT64,
                                               ConvertedType::TIMESTAMP_MICROS));
  arrow_fields.push_back(
      ::arrow::field("timestamp[us]", ::arrow::timestamp(TimeUnit::MICRO, "UTC"), false));

  parquet_fields.push_back(PrimitiveNode::Make("date", Repetition::REQUIRED,
                                               ParquetType::INT32, ConvertedType::DATE));
  arrow_fields.push_back(::arrow::field("date", ::arrow::date32(), false));

  parquet_fields.push_back(PrimitiveNode::Make(
      "time32", Repetition::REQUIRED, ParquetType::INT32, ConvertedType::TIME_MILLIS));
  arrow_fields.push_back(
      ::arrow::field("time32", ::arrow::time32(TimeUnit::MILLI), false));

  parquet_fields.push_back(PrimitiveNode::Make(
      "time64", Repetition::REQUIRED, ParquetType::INT64, ConvertedType::TIME_MICROS));
  arrow_fields.push_back(
      ::arrow::field("time64", ::arrow::time64(TimeUnit::MICRO), false));

  parquet_fields.push_back(
      PrimitiveNode::Make("timestamp96", Repetition::REQUIRED, ParquetType::INT96));
  arrow_fields.push_back(::arrow::field("timestamp96", TIMESTAMP_NS, false));

  parquet_fields.push_back(
      PrimitiveNode::Make("float", Repetition::OPTIONAL, ParquetType::FLOAT));
  arrow_fields.push_back(::arrow::field("float", FLOAT));

  parquet_fields.push_back(
      PrimitiveNode::Make("double", Repetition::OPTIONAL, ParquetType::DOUBLE));
  arrow_fields.push_back(::arrow::field("double", DOUBLE));

  parquet_fields.push_back(
      PrimitiveNode::Make("binary", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY));
  arrow_fields.push_back(::arrow::field("binary", BINARY));

  parquet_fields.push_back(PrimitiveNode::Make(
      "string", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::UTF8));
  arrow_fields.push_back(::arrow::field("string", UTF8));

  parquet_fields.push_back(PrimitiveNode::Make("flba-binary", Repetition::OPTIONAL,
                                               ParquetType::FIXED_LEN_BYTE_ARRAY,
                                               ConvertedType::NONE, 12));
  arrow_fields.push_back(::arrow::field("flba-binary", ::arrow::fixed_size_binary(12)));

  auto arrow_schema = ::arrow::schema(arrow_fields);
  ASSERT_OK(ConvertSchema(parquet_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
}

TEST_F(TestConvertParquetSchema, ParquetAnnotatedFields) {
  struct FieldConstructionArguments {
    std::string name;
    std::shared_ptr<const LogicalType> logical_type;
    parquet::Type::type physical_type;
    int physical_length;
    std::shared_ptr<::arrow::DataType> datatype;
  };

  std::vector<FieldConstructionArguments> cases = {
      {"string", LogicalType::String(), ParquetType::BYTE_ARRAY, -1, ::arrow::utf8()},
      {"enum", LogicalType::Enum(), ParquetType::BYTE_ARRAY, -1, ::arrow::binary()},
      {"decimal(8, 2)", LogicalType::Decimal(8, 2), ParquetType::INT32, -1,
       ::arrow::decimal128(8, 2)},
      {"decimal(16, 4)", LogicalType::Decimal(16, 4), ParquetType::INT64, -1,
       ::arrow::decimal128(16, 4)},
      {"decimal(32, 8)", LogicalType::Decimal(32, 8), ParquetType::FIXED_LEN_BYTE_ARRAY,
       16, ::arrow::decimal128(32, 8)},
      {"date", LogicalType::Date(), ParquetType::INT32, -1, ::arrow::date32()},
      {"time(ms)", LogicalType::Time(true, LogicalType::TimeUnit::MILLIS),
       ParquetType::INT32, -1, ::arrow::time32(::arrow::TimeUnit::MILLI)},
      {"time(us)", LogicalType::Time(true, LogicalType::TimeUnit::MICROS),
       ParquetType::INT64, -1, ::arrow::time64(::arrow::TimeUnit::MICRO)},
      {"time(ns)", LogicalType::Time(true, LogicalType::TimeUnit::NANOS),
       ParquetType::INT64, -1, ::arrow::time64(::arrow::TimeUnit::NANO)},
      {"time(ms)", LogicalType::Time(false, LogicalType::TimeUnit::MILLIS),
       ParquetType::INT32, -1, ::arrow::time32(::arrow::TimeUnit::MILLI)},
      {"time(us)", LogicalType::Time(false, LogicalType::TimeUnit::MICROS),
       ParquetType::INT64, -1, ::arrow::time64(::arrow::TimeUnit::MICRO)},
      {"time(ns)", LogicalType::Time(false, LogicalType::TimeUnit::NANOS),
       ParquetType::INT64, -1, ::arrow::time64(::arrow::TimeUnit::NANO)},
      {"timestamp(true, ms)", LogicalType::Timestamp(true, LogicalType::TimeUnit::MILLIS),
       ParquetType::INT64, -1, ::arrow::timestamp(::arrow::TimeUnit::MILLI, "UTC")},
      {"timestamp(true, us)", LogicalType::Timestamp(true, LogicalType::TimeUnit::MICROS),
       ParquetType::INT64, -1, ::arrow::timestamp(::arrow::TimeUnit::MICRO, "UTC")},
      {"timestamp(true, ns)", LogicalType::Timestamp(true, LogicalType::TimeUnit::NANOS),
       ParquetType::INT64, -1, ::arrow::timestamp(::arrow::TimeUnit::NANO, "UTC")},
      {"timestamp(false, ms)",
       LogicalType::Timestamp(false, LogicalType::TimeUnit::MILLIS), ParquetType::INT64,
       -1, ::arrow::timestamp(::arrow::TimeUnit::MILLI)},
      {"timestamp(false, us)",
       LogicalType::Timestamp(false, LogicalType::TimeUnit::MICROS), ParquetType::INT64,
       -1, ::arrow::timestamp(::arrow::TimeUnit::MICRO)},
      {"timestamp(false, ns)",
       LogicalType::Timestamp(false, LogicalType::TimeUnit::NANOS), ParquetType::INT64,
       -1, ::arrow::timestamp(::arrow::TimeUnit::NANO)},
      {"int(8, false)", LogicalType::Int(8, false), ParquetType::INT32, -1,
       ::arrow::uint8()},
      {"int(8, true)", LogicalType::Int(8, true), ParquetType::INT32, -1,
       ::arrow::int8()},
      {"int(16, false)", LogicalType::Int(16, false), ParquetType::INT32, -1,
       ::arrow::uint16()},
      {"int(16, true)", LogicalType::Int(16, true), ParquetType::INT32, -1,
       ::arrow::int16()},
      {"int(32, false)", LogicalType::Int(32, false), ParquetType::INT32, -1,
       ::arrow::uint32()},
      {"int(32, true)", LogicalType::Int(32, true), ParquetType::INT32, -1,
       ::arrow::int32()},
      {"int(64, false)", LogicalType::Int(64, false), ParquetType::INT64, -1,
       ::arrow::uint64()},
      {"int(64, true)", LogicalType::Int(64, true), ParquetType::INT64, -1,
       ::arrow::int64()},
      {"json", LogicalType::JSON(), ParquetType::BYTE_ARRAY, -1, ::arrow::utf8()},
      {"bson", LogicalType::BSON(), ParquetType::BYTE_ARRAY, -1, ::arrow::binary()},
      {"geometry", LogicalType::Geometry(), ParquetType::BYTE_ARRAY, -1,
       ::arrow::binary()},
      {"geography", LogicalType::Geography(), ParquetType::BYTE_ARRAY, -1,
       ::arrow::binary()},
      {"interval", LogicalType::Interval(), ParquetType::FIXED_LEN_BYTE_ARRAY, 12,
       ::arrow::fixed_size_binary(12)},
      {"uuid", LogicalType::UUID(), ParquetType::FIXED_LEN_BYTE_ARRAY, 16,
       ::arrow::fixed_size_binary(16)},
      {"float16", LogicalType::Float16(), ParquetType::FIXED_LEN_BYTE_ARRAY, 2,
       ::arrow::float16()},
      {"none", LogicalType::None(), ParquetType::BOOLEAN, -1, ::arrow::boolean()},
      {"none", LogicalType::None(), ParquetType::INT32, -1, ::arrow::int32()},
      {"none", LogicalType::None(), ParquetType::INT64, -1, ::arrow::int64()},
      {"none", LogicalType::None(), ParquetType::FLOAT, -1, ::arrow::float32()},
      {"none", LogicalType::None(), ParquetType::DOUBLE, -1, ::arrow::float64()},
      {"none", LogicalType::None(), ParquetType::BYTE_ARRAY, -1, ::arrow::binary()},
      {"none", LogicalType::None(), ParquetType::FIXED_LEN_BYTE_ARRAY, 64,
       ::arrow::fixed_size_binary(64)},
      {"null", LogicalType::Null(), ParquetType::BYTE_ARRAY, -1, ::arrow::null()},
  };

  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  for (const FieldConstructionArguments& c : cases) {
    parquet_fields.push_back(PrimitiveNode::Make(c.name, Repetition::OPTIONAL,
                                                 c.logical_type, c.physical_type,
                                                 c.physical_length));
    arrow_fields.push_back(::arrow::field(c.name, c.datatype));
  }

  ASSERT_OK(ConvertSchema(parquet_fields));
  auto arrow_schema = ::arrow::schema(arrow_fields);
  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
}

TEST_F(TestConvertParquetSchema, DuplicateFieldNames) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  parquet_fields.push_back(
      PrimitiveNode::Make("xxx", Repetition::REQUIRED, ParquetType::BOOLEAN));
  auto arrow_field1 = ::arrow::field("xxx", BOOL, false);

  parquet_fields.push_back(
      PrimitiveNode::Make("xxx", Repetition::REQUIRED, ParquetType::INT32));
  auto arrow_field2 = ::arrow::field("xxx", INT32, false);

  ASSERT_OK(ConvertSchema(parquet_fields));
  arrow_fields = {arrow_field1, arrow_field2};
  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(::arrow::schema(arrow_fields)));
}

TEST_F(TestConvertParquetSchema, ParquetKeyValueMetadata) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  parquet_fields.push_back(
      PrimitiveNode::Make("boolean", Repetition::REQUIRED, ParquetType::BOOLEAN));
  arrow_fields.push_back(::arrow::field("boolean", BOOL, false));

  parquet_fields.push_back(
      PrimitiveNode::Make("int32", Repetition::REQUIRED, ParquetType::INT32));
  arrow_fields.push_back(::arrow::field("int32", INT32, false));

  auto key_value_metadata = std::make_shared<KeyValueMetadata>();
  key_value_metadata->Append("foo", "bar");
  key_value_metadata->Append("biz", "baz");
  ASSERT_OK(ConvertSchema(parquet_fields, key_value_metadata));

  auto arrow_metadata = result_schema_->metadata();
  ASSERT_EQ("foo", arrow_metadata->key(0));
  ASSERT_EQ("bar", arrow_metadata->value(0));
  ASSERT_EQ("biz", arrow_metadata->key(1));
  ASSERT_EQ("baz", arrow_metadata->value(1));
}

TEST_F(TestConvertParquetSchema, ParquetEmptyKeyValueMetadata) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  parquet_fields.push_back(
      PrimitiveNode::Make("int32", Repetition::REQUIRED, ParquetType::INT32));
  arrow_fields.push_back(::arrow::field("int32", INT32, false));

  std::shared_ptr<KeyValueMetadata> key_value_metadata = nullptr;
  ASSERT_OK(ConvertSchema(parquet_fields, key_value_metadata));

  auto arrow_metadata = result_schema_->metadata();
  ASSERT_EQ(arrow_metadata, nullptr);
}

TEST_F(TestConvertParquetSchema, ParquetFlatDecimals) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  parquet_fields.push_back(PrimitiveNode::Make("flba-decimal", Repetition::OPTIONAL,
                                               ParquetType::FIXED_LEN_BYTE_ARRAY,
                                               ConvertedType::DECIMAL, 4, 8, 4));
  arrow_fields.push_back(::arrow::field("flba-decimal", DECIMAL_8_4));

  parquet_fields.push_back(PrimitiveNode::Make("binary-decimal", Repetition::OPTIONAL,
                                               ParquetType::BYTE_ARRAY,
                                               ConvertedType::DECIMAL, -1, 8, 4));
  arrow_fields.push_back(::arrow::field("binary-decimal", DECIMAL_8_4));

  parquet_fields.push_back(PrimitiveNode::Make("int32-decimal", Repetition::OPTIONAL,
                                               ParquetType::INT32, ConvertedType::DECIMAL,
                                               -1, 8, 4));
  arrow_fields.push_back(::arrow::field("int32-decimal", DECIMAL_8_4));

  parquet_fields.push_back(PrimitiveNode::Make("int64-decimal", Repetition::OPTIONAL,
                                               ParquetType::INT64, ConvertedType::DECIMAL,
                                               -1, 8, 4));
  arrow_fields.push_back(::arrow::field("int64-decimal", DECIMAL_8_4));

  auto arrow_schema = ::arrow::schema(arrow_fields);
  ASSERT_OK(ConvertSchema(parquet_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
}

TEST_F(TestConvertParquetSchema, ParquetMaps) {
  // MAP encoding example taken from parquet-format/LogicalTypes.md

  for (const auto& list_case : kListCases) {
    std::vector<NodePtr> parquet_fields;
    std::vector<std::shared_ptr<Field>> arrow_fields;

    // Two column map.
    {
      auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                     ConvertedType::UTF8);
      auto value = PrimitiveNode::Make("value", Repetition::OPTIONAL,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);

      auto list = GroupNode::Make("key_value", Repetition::REPEATED, {key, value});
      parquet_fields.push_back(
          GroupNode::Make("my_map", Repetition::REQUIRED, {list}, LogicalType::Map()));
      auto arrow_key = ::arrow::field("key", UTF8, /*nullable=*/false);
      auto arrow_value = ::arrow::field("value", UTF8, /*nullable=*/true);
      auto arrow_map = std::make_shared<::arrow::MapType>(
          ::arrow::field("my_map", ::arrow::struct_({arrow_key, arrow_value}),
                         /*nullable=*/false),
          /*nullable=*/false);

      arrow_fields.push_back(::arrow::field("my_map", arrow_map, /*nullable=*/false));
    }
    // Single column map (i.e. set) gets converted to list of struct.
    {
      auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                     ConvertedType::UTF8);

      auto list = GroupNode::Make("key_value", Repetition::REPEATED, {key});
      parquet_fields.push_back(
          GroupNode::Make("my_set", Repetition::REQUIRED, {list}, LogicalType::Map()));
      auto arrow_list =
          list_case.type_factory(::arrow::field("key", UTF8, /*nullable=*/false));
      arrow_fields.push_back(::arrow::field("my_set", arrow_list, false));
    }
    // Two column map with non-standard field names.
    {
      auto key = PrimitiveNode::Make("int_key", Repetition::REQUIRED, ParquetType::INT32,
                                     ConvertedType::INT_32);
      auto value = PrimitiveNode::Make("str_value", Repetition::OPTIONAL,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);

      auto list = GroupNode::Make("items", Repetition::REPEATED, {key, value});
      parquet_fields.push_back(
          GroupNode::Make("items", Repetition::REQUIRED, {list}, LogicalType::Map()));
      auto arrow_value = ::arrow::field("str_value", UTF8, /*nullable=*/true);
      auto arrow_key = ::arrow::field("int_key", INT32, /*nullable=*/false);
      auto arrow_map = std::make_shared<::arrow::MapType>(
          ::arrow::field("items", ::arrow::struct_({arrow_key, arrow_value}), false),
          false);

      arrow_fields.push_back(::arrow::field("items", arrow_map, false));
    }

    auto arrow_schema = ::arrow::schema(arrow_fields);

    ArrowReaderProperties props;
    props.set_list_type(list_case.type_id);
    ASSERT_OK(ConvertSchema(parquet_fields, /*key_value_metadata=*/{}, props));

    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
    for (int i = 0; i < arrow_schema->num_fields(); ++i) {
      auto result_field = result_schema_->field(i);
      auto expected_field = arrow_schema->field(i);
      if (expected_field->type()->id() == ::arrow::Type::MAP) {
        EXPECT_TRUE(
            expected_field->type()->field(0)->Equals(result_field->type()->field(0)))
            << "Map's struct in field " << i
            << "\n result: " << result_field->type()->field(0)->ToString() << " "
            << "\n expected: " << expected_field->type()->field(0)->ToString() << "\n";
      }
    }
  }
}

TEST_F(TestConvertParquetSchema, ParquetLists) {
  // LIST encoding example taken from parquet-format/LogicalTypes.md

  for (const auto& list_case : kListCases) {
    std::vector<NodePtr> parquet_fields;
    std::vector<std::shared_ptr<Field>> arrow_fields;

    // // List<String> (list non-null, elements nullable)
    // required group my_list (LIST) {
    //   repeated group list {
    //     optional binary element (UTF8);
    //   }
    // }
    {
      auto element = PrimitiveNode::Make("string", Repetition::OPTIONAL,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::REQUIRED, {list}, ConvertedType::LIST));
      auto arrow_element = ::arrow::field("string", UTF8, true);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, false));
    }

    // // List<String> (list nullable, elements non-null)
    // optional group my_list (LIST) {
    //   repeated group list {
    //     required binary element (UTF8);
    //   }
    // }
    {
      auto element = PrimitiveNode::Make("string", Repetition::REQUIRED,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::OPTIONAL, {list}, ConvertedType::LIST));
      auto arrow_element = ::arrow::field("string", UTF8, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // Element types can be nested structures. For example, a list of lists:
    //
    // // List<List<Integer>>
    // optional group array_of_arrays (LIST) {
    //   repeated group list {
    //     required group element (LIST) {
    //       repeated group list {
    //         required int32 element;
    //       }
    //     }
    //   }
    // }
    {
      auto inner_element =
          PrimitiveNode::Make("int32", Repetition::REQUIRED, ParquetType::INT32);
      auto inner_list = GroupNode::Make("list", Repetition::REPEATED, {inner_element});
      auto element = GroupNode::Make("element", Repetition::REQUIRED, {inner_list},
                                     ConvertedType::LIST);
      auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
      parquet_fields.push_back(GroupNode::Make("array_of_arrays", Repetition::OPTIONAL,
                                               {list}, ConvertedType::LIST));
      auto arrow_inner_element = ::arrow::field("int32", INT32, false);
      auto arrow_inner_list = list_case.type_factory(arrow_inner_element);
      auto arrow_element = ::arrow::field("element", arrow_inner_list, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("array_of_arrays", arrow_list, true));
    }

    // // List<String> (list nullable, elements non-null)
    // optional group my_list (LIST) {
    //   repeated group element {
    //     required binary str (UTF8);
    //   };
    // }
    {
      auto element = PrimitiveNode::Make("str", Repetition::REQUIRED,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto list = GroupNode::Make("element", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::OPTIONAL, {list}, ConvertedType::LIST));
      auto arrow_element = ::arrow::field("str", UTF8, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // // List<Integer> (nullable list, non-null elements)
    // optional group my_list (LIST) {
    //   repeated int32 element;
    // }
    {
      auto element =
          PrimitiveNode::Make("element", Repetition::REPEATED, ParquetType::INT32);
      parquet_fields.push_back(GroupNode::Make("my_list", Repetition::OPTIONAL, {element},
                                               ConvertedType::LIST));
      auto arrow_element = ::arrow::field("element", INT32, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // // List<Tuple<String, Integer>> (nullable list, non-null elements)
    // optional group my_list (LIST) {
    //   repeated group element {
    //     required binary str (UTF8);
    //     required int32 num;
    //   };
    // }
    {
      auto str_element = PrimitiveNode::Make(
          "str", Repetition::REQUIRED, ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto num_element =
          PrimitiveNode::Make("num", Repetition::REQUIRED, ParquetType::INT32);
      auto element =
          GroupNode::Make("element", Repetition::REPEATED, {str_element, num_element});
      parquet_fields.push_back(GroupNode::Make("my_list", Repetition::OPTIONAL, {element},
                                               ConvertedType::LIST));
      auto arrow_str = ::arrow::field("str", UTF8, false);
      auto arrow_num = ::arrow::field("num", INT32, false);
      std::vector<std::shared_ptr<Field>> fields({arrow_str, arrow_num});
      auto arrow_struct = ::arrow::struct_(fields);
      auto arrow_element = ::arrow::field("element", arrow_struct, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // // List<OneTuple<String>> (nullable list, non-null elements)
    // optional group my_list (LIST) {
    //   repeated group array {
    //     required binary str (UTF8);
    //   };
    // }
    // Special case: group is named array
    {
      auto element = PrimitiveNode::Make("str", Repetition::REQUIRED,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto array = GroupNode::Make("array", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::OPTIONAL, {array}, ConvertedType::LIST));
      auto arrow_str = ::arrow::field("str", UTF8, false);
      std::vector<std::shared_ptr<Field>> fields({arrow_str});
      auto arrow_struct = ::arrow::struct_(fields);
      auto arrow_element = ::arrow::field("array", arrow_struct, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // // List<OneTuple<String>> (nullable list, non-null elements)
    // optional group my_list (LIST) {
    //   repeated group my_list_tuple {
    //     required binary str (UTF8);
    //   };
    // }
    // Special case: group named ends in _tuple
    {
      auto element = PrimitiveNode::Make("str", Repetition::REQUIRED,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto array = GroupNode::Make("my_list_tuple", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::OPTIONAL, {array}, ConvertedType::LIST));
      auto arrow_str = ::arrow::field("str", UTF8, false);
      std::vector<std::shared_ptr<Field>> fields({arrow_str});
      auto arrow_struct = ::arrow::struct_(fields);
      auto arrow_element = ::arrow::field("my_list_tuple", arrow_struct, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // One-level encoding: Only allows required lists with required cells
    //   repeated value_type name
    {
      parquet_fields.push_back(
          PrimitiveNode::Make("name", Repetition::REPEATED, ParquetType::INT32));
      auto arrow_element = ::arrow::field("name", INT32, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("name", arrow_list, false));
    }

    // Two-level encoding List<List<Integer>>:
    // optional group my_list (LIST) {
    //   repeated group array (LIST) {
    //     repeated int32 array;
    //   }
    // }
    {
      auto inner_array =
          PrimitiveNode::Make("array", Repetition::REPEATED, ParquetType::INT32);
      auto outer_array = GroupNode::Make("array", Repetition::REPEATED, {inner_array},
                                         ConvertedType::LIST);
      parquet_fields.push_back(GroupNode::Make("my_list", Repetition::OPTIONAL,
                                               {outer_array}, ConvertedType::LIST));
      auto arrow_inner_array = ::arrow::field("array", INT32, /*nullable=*/false);
      auto arrow_outer_array = ::arrow::field(
          "array", list_case.type_factory(arrow_inner_array), /*nullable=*/false);
      auto arrow_list = list_case.type_factory(arrow_outer_array);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    // List<Map<String, String>> in three-level list encoding:
    // optional group my_list (LIST) {
    //   repeated group list {
    //     required group element (MAP) {
    //       repeated group key_value {
    //         required binary key (STRING);
    //         optional binary value (STRING);
    //       }
    //     }
    //   }
    // }
    {
      auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                     ConvertedType::UTF8);
      auto value = PrimitiveNode::Make("value", Repetition::OPTIONAL,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto key_value = GroupNode::Make("key_value", Repetition::REPEATED, {key, value});
      auto element = GroupNode::Make("element", Repetition::REQUIRED, {key_value},
                                     ConvertedType::MAP);
      auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::OPTIONAL, {list}, ConvertedType::LIST));

      auto arrow_key = ::arrow::field("key", UTF8, /*nullable=*/false);
      auto arrow_value = ::arrow::field("value", UTF8, /*nullable=*/true);
      auto arrow_element = ::arrow::field(
          "element", std::make_shared<::arrow::MapType>(arrow_key, arrow_value),
          /*nullable=*/false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, /*nullable=*/true));
    }

    auto arrow_schema = ::arrow::schema(arrow_fields);

    ArrowReaderProperties props;
    props.set_list_type(list_case.type_id);
    ASSERT_OK(ConvertSchema(parquet_fields, /*key_value_metadata=*/{}, props));

    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
  }
}

TEST_F(TestConvertParquetSchema, UnsupportedThings) {
  std::vector<NodePtr> unsupported_nodes;

  for (const NodePtr& node : unsupported_nodes) {
    ASSERT_RAISES(NotImplemented, ConvertSchema({node}));
  }
}

TEST_F(TestConvertParquetSchema, ParquetNestedSchema) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // required group group1 {
  //   required bool leaf1;
  //   required int32 leaf2;
  // }
  // required int64 leaf3;
  {
    parquet_fields.push_back(GroupNode::Make(
        "group1", Repetition::REQUIRED,
        {PrimitiveNode::Make("leaf1", Repetition::REQUIRED, ParquetType::BOOLEAN),
         PrimitiveNode::Make("leaf2", Repetition::REQUIRED, ParquetType::INT32)}));
    parquet_fields.push_back(
        PrimitiveNode::Make("leaf3", Repetition::REQUIRED, ParquetType::INT64));

    auto group1_fields = {::arrow::field("leaf1", BOOL, false),
                          ::arrow::field("leaf2", INT32, false)};
    auto arrow_group1_type = ::arrow::struct_(group1_fields);
    arrow_fields.push_back(::arrow::field("group1", arrow_group1_type, false));
    arrow_fields.push_back(::arrow::field("leaf3", INT64, false));
  }

  auto arrow_schema = ::arrow::schema(arrow_fields);
  ASSERT_OK(ConvertSchema(parquet_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
}

TEST_F(TestConvertParquetSchema, ParquetNestedSchema2) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // Full Parquet Schema:
  // required group group1 {
  //   required int64 leaf1;
  //   required int64 leaf2;
  // }
  // required group group2 {
  //   required int64 leaf3;
  //   required int64 leaf4;
  // }
  // required int64 leaf5;
  {
    parquet_fields.push_back(GroupNode::Make(
        "group1", Repetition::REQUIRED,
        {PrimitiveNode::Make("leaf1", Repetition::REQUIRED, ParquetType::INT64),
         PrimitiveNode::Make("leaf2", Repetition::REQUIRED, ParquetType::INT64)}));
    parquet_fields.push_back(GroupNode::Make(
        "group2", Repetition::REQUIRED,
        {PrimitiveNode::Make("leaf3", Repetition::REQUIRED, ParquetType::INT64),
         PrimitiveNode::Make("leaf4", Repetition::REQUIRED, ParquetType::INT64)}));
    parquet_fields.push_back(
        PrimitiveNode::Make("leaf5", Repetition::REQUIRED, ParquetType::INT64));

    auto group1_fields = {::arrow::field("leaf1", INT64, false),
                          ::arrow::field("leaf2", INT64, false)};
    auto arrow_group1_type = ::arrow::struct_(group1_fields);
    auto group2_fields = {::arrow::field("leaf3", INT64, false),
                          ::arrow::field("leaf4", INT64, false)};
    auto arrow_group2_type = ::arrow::struct_(group2_fields);
    arrow_fields.push_back(::arrow::field("group1", arrow_group1_type, false));
    arrow_fields.push_back(::arrow::field("group2", arrow_group2_type, false));
    arrow_fields.push_back(::arrow::field("leaf5", INT64, false));
  }

  auto arrow_schema = ::arrow::schema(arrow_fields);
  ASSERT_OK(ConvertSchema(parquet_fields));
  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
}

TEST_F(TestConvertParquetSchema, ParquetUndefinedType) {
  std::vector<NodePtr> parquet_fields;

  // Make a node and intentionally modify it such that it comes back
  // as UndefinedLogicalType::Make()
  NodePtr node = PrimitiveNode::Make("undefined", Repetition::OPTIONAL,
                                     StringLogicalType::Make(), Type::BYTE_ARRAY);

  format::SchemaElement string_intermediary;
  node->ToParquet(&string_intermediary);

  string_intermediary.logicalType.__isset.STRING = false;
  node = PrimitiveNode::FromParquet(&string_intermediary);
  parquet_fields.push_back(std::move(node));

  // With default options, the field should be interpreted according to its physical type
  ASSERT_OK(ConvertSchema(parquet_fields, nullptr));

  auto arrow_schema = ::arrow::schema({{"undefined", BINARY}});

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
}

TEST_F(TestConvertParquetSchema, ParquetRepeatedNestedSchema) {
  for (const auto& list_case : kListCases) {
    std::vector<NodePtr> parquet_fields;
    std::vector<std::shared_ptr<Field>> arrow_fields;

    {
      //   optional int32 leaf1;
      //   repeated group outerGroup {
      //     optional int32 leaf2;
      //     repeated group innerGroup {
      //       optional int32 leaf3;
      //     }
      //   }
      parquet_fields.push_back(
          PrimitiveNode::Make("leaf1", Repetition::OPTIONAL, ParquetType::INT32));
      parquet_fields.push_back(GroupNode::Make(
          "outerGroup", Repetition::REPEATED,
          {PrimitiveNode::Make("leaf2", Repetition::OPTIONAL, ParquetType::INT32),
           GroupNode::Make("innerGroup", Repetition::REPEATED,
                           {PrimitiveNode::Make("leaf3", Repetition::OPTIONAL,
                                                ParquetType::INT32)})}));

      auto inner_group_fields = {::arrow::field("leaf3", INT32, true)};
      auto inner_group_type = ::arrow::struct_(inner_group_fields);
      auto outer_group_fields = {
          ::arrow::field("leaf2", INT32, true),
          ::arrow::field("innerGroup",
                         list_case.type_factory(::arrow::field(
                             "innerGroup", inner_group_type, /*nullable=*/false)),
                         /*nullable=*/false)};
      auto outer_group_type = ::arrow::struct_(outer_group_fields);

      arrow_fields.push_back(::arrow::field("leaf1", INT32, true));
      arrow_fields.push_back(
          ::arrow::field("outerGroup",
                         list_case.type_factory(::arrow::field(
                             "outerGroup", outer_group_type, /*nullable=*/false)),
                         /*nullable=*/false));
    }
    auto arrow_schema = ::arrow::schema(arrow_fields);

    ArrowReaderProperties props;
    props.set_list_type(list_case.type_id);
    ASSERT_OK(ConvertSchema(parquet_fields, /*key_value_metadata=*/{}, props));

    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema));
  }
}

TEST_F(TestConvertParquetSchema, IllegalParquetNestedSchema) {
  // List<Map<String, String>> in two-level list encoding:
  //
  // optional group my_list (LIST) {
  //   repeated group array (MAP) {
  //     repeated group key_value {
  //       required binary key (STRING);
  //       optional binary value (STRING);
  //     }
  //   }
  // }
  {
    auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                   ConvertedType::UTF8);
    auto value = PrimitiveNode::Make("value", Repetition::OPTIONAL,
                                     ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
    auto key_value = GroupNode::Make("key_value", Repetition::REPEATED, {key, value});
    auto array =
        GroupNode::Make("array", Repetition::REPEATED, {key_value}, ConvertedType::MAP);
    std::vector<NodePtr> parquet_fields;
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::OPTIONAL, {array}, ConvertedType::LIST));

    EXPECT_RAISES_WITH_MESSAGE_THAT(
        Invalid,
        testing::HasSubstr("Group with one repeated child must be LIST-annotated."),
        ConvertSchema(parquet_fields));
  }

  // List<List<String>>: outer list is two-level encoding, inner list is three-level
  //
  // optional group my_list (LIST) {
  //   repeated group array (LIST) {
  //     repeated group list {
  //       required binary element (STRING);
  //     }
  //   }
  // }
  {
    auto element = PrimitiveNode::Make("element", Repetition::REQUIRED,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
    auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
    auto array =
        GroupNode::Make("array", Repetition::REPEATED, {list}, ConvertedType::LIST);
    std::vector<NodePtr> parquet_fields;
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::OPTIONAL, {array}, ConvertedType::LIST));

    EXPECT_RAISES_WITH_MESSAGE_THAT(
        Invalid, testing::HasSubstr("LIST-annotated groups must not be repeated."),
        ConvertSchema(parquet_fields));
  }
  // List<List<>>: outer list is two-level encoding, inner list is empty.
  //
  // optional group my_list (LIST) {
  //   repeated group array (LIST) {
  //     repeated group list {
  //     }
  //   }
  // }
  {
    auto list = GroupNode::Make("list", Repetition::REPEATED, {});
    auto array =
        GroupNode::Make("array", Repetition::REPEATED, {list}, ConvertedType::LIST);
    std::vector<NodePtr> parquet_fields;
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::OPTIONAL, {array}, ConvertedType::LIST));

    EXPECT_RAISES_WITH_MESSAGE_THAT(
        Invalid,
        testing::HasSubstr("LIST-annotated groups must have at least one child."),
        ConvertSchema(parquet_fields));
  }
}

Status ArrowSchemaToParquetMetadata(std::shared_ptr<::arrow::Schema>& arrow_schema,
                                    std::shared_ptr<KeyValueMetadata>& metadata) {
  ARROW_ASSIGN_OR_RAISE(
      std::shared_ptr<Buffer> serialized,
      ::arrow::ipc::SerializeSchema(*arrow_schema, ::arrow::default_memory_pool()));
  std::string schema_as_string = serialized->ToString();
  std::string schema_base64 = ::arrow::util::base64_encode(schema_as_string);
  metadata = ::arrow::key_value_metadata({"ARROW:schema"}, {schema_base64});
  return Status::OK();
}

TEST_F(TestConvertParquetSchema, ParquetVariant) {
  // Unshredded variant
  // optional group variant_col {
  //  required binary metadata;
  //  required binary value;
  // }
  //
  // GH-45948: add shredded variants
  std::vector<NodePtr> parquet_fields;
  auto metadata =
      PrimitiveNode::Make("metadata", Repetition::REQUIRED, ParquetType::BYTE_ARRAY);
  auto value =
      PrimitiveNode::Make("value", Repetition::REQUIRED, ParquetType::BYTE_ARRAY);
  auto variant = GroupNode::Make("variant_unshredded", Repetition::OPTIONAL,
                                 {metadata, value}, LogicalType::Variant());
  parquet_fields.push_back(variant);

  // Arrow schema for unshredded variant struct.
  auto arrow_metadata = ::arrow::field("metadata", ::arrow::binary(), /*nullable=*/false);
  auto arrow_value = ::arrow::field("value", ::arrow::binary(), /*nullable=*/false);
  auto arrow_variant = ::arrow::struct_({arrow_metadata, arrow_value});
  auto variant_extension = std::make_shared<VariantExtensionType>(arrow_variant);

  {
    // Parquet file does not contain Arrow schema.
    // By default, field should be treated as a normal struct in Arrow.
    auto arrow_schema =
        ::arrow::schema({::arrow::field("variant_unshredded", arrow_variant)});
    ASSERT_OK(ConvertSchema(parquet_fields));
    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema, /*check_metadata=*/true));
  }

  for (bool register_extension : {true, false}) {
    ::arrow::ExtensionTypeGuard guard(register_extension
                                          ? ::arrow::DataTypeVector{variant_extension}
                                          : ::arrow::DataTypeVector{});

    // Parquet file does not contain Arrow schema.
    // If Arrow extensions are enabled, field should be interpreted as Parquet Variant
    // extension type if registered.
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(true);

    auto arrow_schema = ::arrow::schema({::arrow::field(
        "variant_unshredded", register_extension ? variant_extension : arrow_variant)});

    ASSERT_OK(ConvertSchema(parquet_fields, /*metadata=*/nullptr, props));
    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema, /*check_metadata=*/true));
  }

  for (bool register_extension : {true, false}) {
    ::arrow::ExtensionTypeGuard guard(register_extension
                                          ? ::arrow::DataTypeVector{variant_extension}
                                          : ::arrow::DataTypeVector{});

    // Parquet file does contain Arrow schema.
    // Field should be interpreted as Parquet Variant extension, if registered,
    // even though extensions are not enabled.
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(false);

    auto arrow_schema = ::arrow::schema({::arrow::field(
        "variant_unshredded", register_extension ? variant_extension : arrow_variant)});

    std::shared_ptr<KeyValueMetadata> metadata;
    ASSERT_OK(ArrowSchemaToParquetMetadata(arrow_schema, metadata));
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(arrow_schema, /*check_metadata=*/true));
  }
}

TEST_F(TestConvertParquetSchema, ParquetSchemaArrowJsonExtension) {
  std::vector<NodePtr> parquet_fields;
  parquet_fields.push_back(PrimitiveNode::Make(
      "json_1", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::JSON));
  parquet_fields.push_back(PrimitiveNode::Make(
      "json_2", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::JSON));

  {
    // Parquet file does not contain Arrow schema.
    // By default, both fields should be treated as utf8() fields in Arrow.
    auto arrow_schema = ::arrow::schema(
        {::arrow::field("json_1", UTF8, true), ::arrow::field("json_2", UTF8, true)});
    std::shared_ptr<KeyValueMetadata> metadata{};
    ASSERT_OK(ConvertSchema(parquet_fields, metadata));
    CheckFlatSchema(arrow_schema);
  }

  {
    // Parquet file does not contain Arrow schema.
    // If Arrow extensions are enabled, fields will be interpreted as json(utf8())
    // extension fields.
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(true);
    auto arrow_schema = ::arrow::schema(
        {::arrow::field("json_1", ::arrow::extension::json(), true),
         ::arrow::field("json_2", ::arrow::extension::json(::arrow::utf8()), true)});
    std::shared_ptr<KeyValueMetadata> metadata{};
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    CheckFlatSchema(arrow_schema);

    // If original data was e.g. json(large_utf8()) it will be interpreted as json(utf8())
    // in absence of Arrow schema.
    arrow_schema = ::arrow::schema(
        {::arrow::field("json_1", ::arrow::extension::json(), true),
         ::arrow::field("json_2", ::arrow::extension::json(::arrow::large_utf8()),
                        true)});
    metadata = std::shared_ptr<KeyValueMetadata>{};
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    EXPECT_TRUE(result_schema_->field(1)->type()->Equals(
        ::arrow::extension::json(::arrow::utf8())));
    EXPECT_FALSE(
        result_schema_->field(1)->type()->Equals(arrow_schema->field(1)->type()));
  }

  {
    // Parquet file contains Arrow schema.
    // json_1 and json_2 will be interpreted as json(utf8()) and json(large_utf8())
    // fields even though extensions are not enabled.
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(false);
    std::shared_ptr<KeyValueMetadata> field_metadata =
        ::arrow::key_value_metadata({"foo", "bar"}, {"biz", "baz"});
    auto arrow_schema = ::arrow::schema(
        {::arrow::field("json_1", ::arrow::extension::json(), true, field_metadata),
         ::arrow::field("json_2", ::arrow::extension::json(::arrow::large_utf8()),
                        true)});

    std::shared_ptr<KeyValueMetadata> metadata;
    ASSERT_OK(ArrowSchemaToParquetMetadata(arrow_schema, metadata));
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    CheckFlatSchema(arrow_schema, true /* check_metadata */);
  }

  {
    // Parquet file contains Arrow schema. Extensions are enabled.
    // json_1 and json_2 will be interpreted as json(utf8()) and json(large_utf8()).
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(true);
    std::shared_ptr<KeyValueMetadata> field_metadata =
        ::arrow::key_value_metadata({"foo", "bar"}, {"biz", "baz"});
    auto arrow_schema = ::arrow::schema(
        {::arrow::field("json_1", ::arrow::extension::json(), true, field_metadata),
         ::arrow::field("json_2", ::arrow::extension::json(::arrow::large_utf8()),
                        true)});

    std::shared_ptr<KeyValueMetadata> metadata;
    ASSERT_OK(ArrowSchemaToParquetMetadata(arrow_schema, metadata));
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    CheckFlatSchema(arrow_schema, true /* check_metadata */);
  }
}

TEST_F(TestConvertParquetSchema, ParquetSchemaArrowUuidExtension) {
  std::vector<NodePtr> parquet_fields;
  parquet_fields.push_back(PrimitiveNode::Make("uuid", Repetition::OPTIONAL,
                                               LogicalType::UUID(),
                                               ParquetType::FIXED_LEN_BYTE_ARRAY, 16));

  {
    // Parquet file does not contain Arrow schema.
    // By default, field should be treated as fixed_size_binary(16) in Arrow.
    auto arrow_schema =
        ::arrow::schema({::arrow::field("uuid", ::arrow::fixed_size_binary(16))});
    std::shared_ptr<KeyValueMetadata> metadata{};
    ASSERT_OK(ConvertSchema(parquet_fields, metadata));
    CheckFlatSchema(arrow_schema);
  }

  {
    // Parquet file does not contain Arrow schema.
    // If Arrow extensions are enabled, field will be interpreted as uuid()
    // extension field.
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(true);
    auto arrow_schema =
        ::arrow::schema({::arrow::field("uuid", ::arrow::extension::uuid())});
    std::shared_ptr<KeyValueMetadata> metadata{};
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    CheckFlatSchema(arrow_schema);
  }

  {
    // Parquet file contains Arrow schema.
    // uuid will be interpreted as uuid() field even though extensions are not enabled.
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(false);
    std::shared_ptr<KeyValueMetadata> field_metadata =
        ::arrow::key_value_metadata({"foo", "bar"}, {"biz", "baz"});
    auto arrow_schema = ::arrow::schema({::arrow::field(
        "uuid", ::arrow::extension::uuid(), /*nullable=*/true, field_metadata)});

    std::shared_ptr<KeyValueMetadata> metadata;
    ASSERT_OK(ArrowSchemaToParquetMetadata(arrow_schema, metadata));
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    CheckFlatSchema(arrow_schema, true /* check_metadata */);
  }

  {
    // Parquet file contains Arrow schema.
    // uuid will be interpreted as uuid() field also when extensions *are* enabled
    ArrowReaderProperties props;
    props.set_arrow_extensions_enabled(true);
    std::shared_ptr<KeyValueMetadata> field_metadata =
        ::arrow::key_value_metadata({"foo", "bar"}, {"biz", "baz"});
    auto arrow_schema = ::arrow::schema({::arrow::field(
        "uuid", ::arrow::extension::uuid(), /*nullable=*/true, field_metadata)});

    std::shared_ptr<KeyValueMetadata> metadata;
    ASSERT_OK(ArrowSchemaToParquetMetadata(arrow_schema, metadata));
    ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
    CheckFlatSchema(arrow_schema, true /* check_metadata */);
  }
}

TEST_F(TestConvertParquetSchema, ParquetSchemaGeoArrowExtensions) {
  std::vector<NodePtr> parquet_fields;
  parquet_fields.push_back(PrimitiveNode::Make("geometry", Repetition::OPTIONAL,
                                               LogicalType::Geometry(),
                                               ParquetType::BYTE_ARRAY));
  parquet_fields.push_back(PrimitiveNode::Make("geography", Repetition::OPTIONAL,
                                               LogicalType::Geography(),
                                               ParquetType::BYTE_ARRAY));

  std::vector<std::shared_ptr<::arrow::DataType>> binary_types = {
      ::arrow::binary(),
      ::arrow::large_binary(),
      ::arrow::binary_view(),
  };

  {
    // Parquet file does not contain Arrow schema.
    // By default, both fields should be treated as binary_type fields in Arrow.
    for (const auto& binary_type : binary_types) {
      auto arrow_schema =
          ::arrow::schema({::arrow::field("geometry", binary_type, true),
                           ::arrow::field("geography", binary_type, true)});
      std::shared_ptr<KeyValueMetadata> metadata{};
      ArrowReaderProperties props;
      props.set_binary_type(binary_type->id());
      ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
      CheckFlatSchema(arrow_schema);
    }
  }

  {
    // Parquet file does not contain Arrow schema.
    // If Arrow extensions are enabled and extensions are registered,
    // fields will be interpreted as geoarrow_wkb(binary_type) extension fields.
    ::arrow::ExtensionTypeGuard guard(test::geoarrow_wkb());

    for (const auto& binary_type : binary_types) {
      ArrowReaderProperties props;
      props.set_arrow_extensions_enabled(true);
      props.set_binary_type(binary_type->id());
      auto arrow_schema = ::arrow::schema(
          {::arrow::field(
               "geometry",
               test::geoarrow_wkb(R"({"crs": "OGC:CRS84", "crs_type": "authority_code"})",
                                  binary_type),
               true),
           ::arrow::field(
               "geography",
               test::geoarrow_wkb(
                   R"({"crs": "OGC:CRS84", "crs_type": "authority_code", "edges": "spherical"})",
                   binary_type),
               true)});
      std::shared_ptr<KeyValueMetadata> metadata{};
      ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
      CheckFlatSchema(arrow_schema);
    }
  }

  {
    // Parquet file does not contain Arrow schema.
    // If Arrow extensions are enabled and extensions are NOT registered,
    // fields will be interpreted as binary_type.
    for (const auto& binary_type : binary_types) {
      ArrowReaderProperties props;
      props.set_arrow_extensions_enabled(true);
      props.set_binary_type(binary_type->id());
      auto arrow_schema =
          ::arrow::schema({::arrow::field("geometry", binary_type, true),
                           ::arrow::field("geography", binary_type, true)});
      std::shared_ptr<KeyValueMetadata> metadata{};
      ASSERT_OK(ConvertSchema(parquet_fields, metadata, props));
      CheckFlatSchema(arrow_schema);
    }
  }
}

class TestConvertArrowSchema : public ::testing::Test {
 public:
  virtual void SetUp() {}

  void CheckFlatSchema(const std::vector<NodePtr>& nodes) {
    NodePtr schema_node = GroupNode::Make("schema", Repetition::REPEATED, nodes);
    const GroupNode* expected_schema_node =
        static_cast<const GroupNode*>(schema_node.get());
    const GroupNode* result_schema_node = result_schema_->group_node();

    ASSERT_EQ(expected_schema_node->field_count(), result_schema_node->field_count());

    for (int i = 0; i < expected_schema_node->field_count(); i++) {
      auto lhs = result_schema_node->field(i);
      auto rhs = expected_schema_node->field(i);
      EXPECT_TRUE(lhs->Equals(rhs.get()));
    }
  }

  ::arrow::Status ConvertSchema(
      const std::vector<std::shared_ptr<Field>>& fields,
      std::shared_ptr<::parquet::ArrowWriterProperties> arrow_properties =
          ::parquet::default_arrow_writer_properties()) {
    arrow_schema_ = ::arrow::schema(fields);
    std::shared_ptr<::parquet::WriterProperties> properties =
        ::parquet::default_writer_properties();
    return ToParquetSchema(arrow_schema_.get(), *properties.get(), *arrow_properties,
                           &result_schema_);
  }

 protected:
  std::shared_ptr<::arrow::Schema> arrow_schema_;
  std::shared_ptr<SchemaDescriptor> result_schema_;
};

TEST_F(TestConvertArrowSchema, ParquetFlatPrimitives) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  parquet_fields.push_back(
      PrimitiveNode::Make("boolean", Repetition::REQUIRED, ParquetType::BOOLEAN));
  arrow_fields.push_back(::arrow::field("boolean", BOOL, false));

  parquet_fields.push_back(
      PrimitiveNode::Make("int32", Repetition::REQUIRED, ParquetType::INT32));
  arrow_fields.push_back(::arrow::field("int32", INT32, false));

  parquet_fields.push_back(
      PrimitiveNode::Make("int64", Repetition::REQUIRED, ParquetType::INT64));
  arrow_fields.push_back(::arrow::field("int64", INT64, false));

  parquet_fields.push_back(PrimitiveNode::Make("date", Repetition::REQUIRED,
                                               ParquetType::INT32, ConvertedType::DATE));
  arrow_fields.push_back(::arrow::field("date", ::arrow::date32(), false));

  parquet_fields.push_back(PrimitiveNode::Make("date64", Repetition::REQUIRED,
                                               ParquetType::INT32, ConvertedType::DATE));
  arrow_fields.push_back(::arrow::field("date64", ::arrow::date64(), false));

  parquet_fields.push_back(PrimitiveNode::Make("timestamp", Repetition::REQUIRED,
                                               ParquetType::INT64,
                                               ConvertedType::TIMESTAMP_MILLIS));
  arrow_fields.push_back(
      ::arrow::field("timestamp", ::arrow::timestamp(TimeUnit::MILLI, "UTC"), false));

  parquet_fields.push_back(PrimitiveNode::Make("timestamp[us]", Repetition::REQUIRED,
                                               ParquetType::INT64,
                                               ConvertedType::TIMESTAMP_MICROS));
  arrow_fields.push_back(
      ::arrow::field("timestamp[us]", ::arrow::timestamp(TimeUnit::MICRO, "UTC"), false));

  parquet_fields.push_back(
      PrimitiveNode::Make("float", Repetition::OPTIONAL, ParquetType::FLOAT));
  arrow_fields.push_back(::arrow::field("float", FLOAT));

  parquet_fields.push_back(
      PrimitiveNode::Make("double", Repetition::OPTIONAL, ParquetType::DOUBLE));
  arrow_fields.push_back(::arrow::field("double", DOUBLE));

  parquet_fields.push_back(PrimitiveNode::Make(
      "string", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::UTF8));
  arrow_fields.push_back(::arrow::field("string", UTF8));

  parquet_fields.push_back(PrimitiveNode::Make(
      "binary", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::NONE));
  arrow_fields.push_back(::arrow::field("binary", BINARY));

  ASSERT_OK(ConvertSchema(arrow_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ArrowFields) {
  struct FieldConstructionArguments {
    std::string name;
    std::shared_ptr<::arrow::DataType> datatype;
    std::shared_ptr<const LogicalType> logical_type;
    parquet::Type::type physical_type;
    int physical_length;
  };

  std::vector<FieldConstructionArguments> cases = {
      {"boolean", ::arrow::boolean(), LogicalType::None(), ParquetType::BOOLEAN, -1},
      {"binary", ::arrow::binary(), LogicalType::None(), ParquetType::BYTE_ARRAY, -1},
      {"large_binary", ::arrow::large_binary(), LogicalType::None(),
       ParquetType::BYTE_ARRAY, -1},
      {"fixed_size_binary", ::arrow::fixed_size_binary(64), LogicalType::None(),
       ParquetType::FIXED_LEN_BYTE_ARRAY, 64},
      {"uint8", ::arrow::uint8(), LogicalType::Int(8, false), ParquetType::INT32, -1},
      {"int8", ::arrow::int8(), LogicalType::Int(8, true), ParquetType::INT32, -1},
      {"uint16", ::arrow::uint16(), LogicalType::Int(16, false), ParquetType::INT32, -1},
      {"int16", ::arrow::int16(), LogicalType::Int(16, true), ParquetType::INT32, -1},
      {"uint32", ::arrow::uint32(), LogicalType::Int(32, false), ParquetType::INT32, -1},
      {"int32", ::arrow::int32(), LogicalType::None(), ParquetType::INT32, -1},
      {"uint64", ::arrow::uint64(), LogicalType::Int(64, false), ParquetType::INT64, -1},
      {"int64", ::arrow::int64(), LogicalType::None(), ParquetType::INT64, -1},
      {"float32", ::arrow::float32(), LogicalType::None(), ParquetType::FLOAT, -1},
      {"float64", ::arrow::float64(), LogicalType::None(), ParquetType::DOUBLE, -1},
      {"utf8", ::arrow::utf8(), LogicalType::String(), ParquetType::BYTE_ARRAY, -1},
      {"large_utf8", ::arrow::large_utf8(), LogicalType::String(),
       ParquetType::BYTE_ARRAY, -1},
      {"decimal(1, 0)", ::arrow::decimal128(1, 0), LogicalType::Decimal(1, 0),
       ParquetType::FIXED_LEN_BYTE_ARRAY, 1},
      {"decimal(8, 2)", ::arrow::decimal128(8, 2), LogicalType::Decimal(8, 2),
       ParquetType::FIXED_LEN_BYTE_ARRAY, 4},
      {"decimal(16, 4)", ::arrow::decimal128(16, 4), LogicalType::Decimal(16, 4),
       ParquetType::FIXED_LEN_BYTE_ARRAY, 7},
      {"decimal(32, 8)", ::arrow::decimal128(32, 8), LogicalType::Decimal(32, 8),
       ParquetType::FIXED_LEN_BYTE_ARRAY, 14},
      {"float16", ::arrow::float16(), LogicalType::Float16(),
       ParquetType::FIXED_LEN_BYTE_ARRAY, 2},
      {"time32", ::arrow::time32(::arrow::TimeUnit::MILLI),
       LogicalType::Time(false, LogicalType::TimeUnit::MILLIS), ParquetType::INT32, -1},
      {"time64(microsecond)", ::arrow::time64(::arrow::TimeUnit::MICRO),
       LogicalType::Time(false, LogicalType::TimeUnit::MICROS), ParquetType::INT64, -1},
      {"time64(nanosecond)", ::arrow::time64(::arrow::TimeUnit::NANO),
       LogicalType::Time(false, LogicalType::TimeUnit::NANOS), ParquetType::INT64, -1},
      {"timestamp(millisecond)", ::arrow::timestamp(::arrow::TimeUnit::MILLI),
       LogicalType::Timestamp(false, LogicalType::TimeUnit::MILLIS,
                              /*is_from_converted_type=*/false,
                              /*force_set_converted_type=*/true),
       ParquetType::INT64, -1},
      {"timestamp(microsecond)", ::arrow::timestamp(::arrow::TimeUnit::MICRO),
       LogicalType::Timestamp(false, LogicalType::TimeUnit::MICROS,
                              /*is_from_converted_type=*/false,
                              /*force_set_converted_type=*/true),
       ParquetType::INT64, -1},
      {"timestamp(nanosecond)", ::arrow::timestamp(::arrow::TimeUnit::NANO),
       LogicalType::Timestamp(false, LogicalType::TimeUnit::NANOS,
                              /*is_from_converted_type=*/false,
                              /*force_set_converted_type=*/true),
       ParquetType::INT64, -1},
      {"timestamp(millisecond, UTC)", ::arrow::timestamp(::arrow::TimeUnit::MILLI, "UTC"),
       LogicalType::Timestamp(true, LogicalType::TimeUnit::MILLIS), ParquetType::INT64,
       -1},
      {"timestamp(microsecond, UTC)", ::arrow::timestamp(::arrow::TimeUnit::MICRO, "UTC"),
       LogicalType::Timestamp(true, LogicalType::TimeUnit::MICROS), ParquetType::INT64,
       -1},
      {"timestamp(nanosecond, UTC)", ::arrow::timestamp(::arrow::TimeUnit::NANO, "UTC"),
       LogicalType::Timestamp(true, LogicalType::TimeUnit::NANOS), ParquetType::INT64,
       -1},
      {"timestamp(millisecond, CET)", ::arrow::timestamp(::arrow::TimeUnit::MILLI, "CET"),
       LogicalType::Timestamp(true, LogicalType::TimeUnit::MILLIS), ParquetType::INT64,
       -1},
      {"timestamp(microsecond, CET)", ::arrow::timestamp(::arrow::TimeUnit::MICRO, "CET"),
       LogicalType::Timestamp(true, LogicalType::TimeUnit::MICROS), ParquetType::INT64,
       -1},
      {"timestamp(nanosecond, CET)", ::arrow::timestamp(::arrow::TimeUnit::NANO, "CET"),
       LogicalType::Timestamp(true, LogicalType::TimeUnit::NANOS), ParquetType::INT64,
       -1}};

  std::vector<std::shared_ptr<Field>> arrow_fields;
  std::vector<NodePtr> parquet_fields;

  for (const FieldConstructionArguments& c : cases) {
    arrow_fields.push_back(::arrow::field(c.name, c.datatype, false));
    parquet_fields.push_back(PrimitiveNode::Make(c.name, Repetition::REQUIRED,
                                                 c.logical_type, c.physical_type,
                                                 c.physical_length));
  }

  ASSERT_OK(ConvertSchema(arrow_fields));
  CheckFlatSchema(parquet_fields);
  // ASSERT_NO_FATAL_FAILURE();
}

TEST_F(TestConvertArrowSchema, ArrowNonconvertibleFields) {
  struct FieldConstructionArguments {
    std::string name;
    std::shared_ptr<::arrow::DataType> datatype;
  };

  std::vector<FieldConstructionArguments> cases = {
      {"run_end_encoded",
       ::arrow::run_end_encoded(::arrow::int32(), ::arrow::list(::arrow::int8()))},
  };

  for (const FieldConstructionArguments& c : cases) {
    auto field = ::arrow::field(c.name, c.datatype);
    ASSERT_RAISES(NotImplemented, ConvertSchema({field}));
  }
}

TEST_F(TestConvertArrowSchema, ParquetFlatPrimitivesAsDictionaries) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;
  std::shared_ptr<::arrow::Array> dict;

  parquet_fields.push_back(
      PrimitiveNode::Make("int32", Repetition::REQUIRED, ParquetType::INT32));
  arrow_fields.push_back(::arrow::field(
      "int32", ::arrow::dictionary(::arrow::int8(), ::arrow::int32()), false));

  parquet_fields.push_back(
      PrimitiveNode::Make("int64", Repetition::REQUIRED, ParquetType::INT64));
  arrow_fields.push_back(::arrow::field(
      "int64", ::arrow::dictionary(::arrow::int8(), ::arrow::int64()), false));

  parquet_fields.push_back(PrimitiveNode::Make("date", Repetition::REQUIRED,
                                               ParquetType::INT32, ConvertedType::DATE));
  arrow_fields.push_back(::arrow::field(
      "date", ::arrow::dictionary(::arrow::int8(), ::arrow::date32()), false));

  parquet_fields.push_back(PrimitiveNode::Make("date64", Repetition::REQUIRED,
                                               ParquetType::INT32, ConvertedType::DATE));
  arrow_fields.push_back(::arrow::field(
      "date64", ::arrow::dictionary(::arrow::int8(), ::arrow::date64()), false));

  parquet_fields.push_back(
      PrimitiveNode::Make("float", Repetition::OPTIONAL, ParquetType::FLOAT));
  arrow_fields.push_back(
      ::arrow::field("float", ::arrow::dictionary(::arrow::int8(), ::arrow::float32())));

  parquet_fields.push_back(
      PrimitiveNode::Make("double", Repetition::OPTIONAL, ParquetType::DOUBLE));
  arrow_fields.push_back(
      ::arrow::field("double", ::arrow::dictionary(::arrow::int8(), ::arrow::float64())));

  parquet_fields.push_back(PrimitiveNode::Make(
      "string", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::UTF8));
  arrow_fields.push_back(
      ::arrow::field("string", ::arrow::dictionary(::arrow::int8(), ::arrow::utf8())));

  parquet_fields.push_back(PrimitiveNode::Make(
      "binary", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY, ConvertedType::NONE));
  arrow_fields.push_back(
      ::arrow::field("binary", ::arrow::dictionary(::arrow::int8(), ::arrow::binary())));

  ASSERT_OK(ConvertSchema(arrow_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetGeoArrowCrsLonLat) {
  // All the Arrow Schemas below should convert to the type defaults for GEOMETRY
  // and GEOGRAPHY when GeoArrow extension types are registered and the appropriate
  // writer option is set.
  ::arrow::ExtensionTypeGuard guard(test::geoarrow_wkb());

  std::vector<NodePtr> parquet_fields;
  parquet_fields.push_back(PrimitiveNode::Make("geometry", Repetition::OPTIONAL,
                                               LogicalType::Geometry(),
                                               ParquetType::BYTE_ARRAY));
  parquet_fields.push_back(PrimitiveNode::Make("geography", Repetition::OPTIONAL,
                                               LogicalType::Geography(),
                                               ParquetType::BYTE_ARRAY));

  // There are several ways that longitude/latitude could be specified when coming from
  // GeoArrow, which allows null, missing, arbitrary strings (e.g., Authority:Code), and
  // PROJJSON.
  std::vector<std::string> geoarrow_lonlat = {
      "null", R"("OGC:CRS84")", R"("EPSG:4326")",
      // Purely the parts of the PROJJSON that we inspect to check the lon/lat case
      R"({"id": {"authority": "OGC", "code": "CRS84"}})",
      R"({"id": {"authority": "EPSG", "code": "4326"}})",
      R"({"id": {"authority": "EPSG", "code": 4326}})"};

  for (const auto& geoarrow_lonlatish_crs : geoarrow_lonlat) {
    ARROW_SCOPED_TRACE("crs = ", geoarrow_lonlatish_crs);
    std::vector<std::shared_ptr<Field>> arrow_fields = {
        ::arrow::field("geometry",
                       test::geoarrow_wkb(R"({"crs": )" + geoarrow_lonlatish_crs + "}"),
                       true),
        ::arrow::field("geography",
                       test::geoarrow_wkb(R"({"crs": )" + geoarrow_lonlatish_crs +
                                          R"(, "edges": "spherical"})"),
                       true)};

    ASSERT_OK(ConvertSchema(arrow_fields));
    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
  }
}

TEST_F(TestConvertArrowSchema, ParquetGeoArrowCrsSrid) {
  // Checks that it is possible to write the srid:xxxx reccomendation from GeoArrow
  ::arrow::ExtensionTypeGuard guard(test::geoarrow_wkb());

  std::vector<NodePtr> parquet_fields;
  parquet_fields.push_back(PrimitiveNode::Make("geometry", Repetition::OPTIONAL,
                                               LogicalType::Geometry("srid:1234"),
                                               ParquetType::BYTE_ARRAY));
  parquet_fields.push_back(PrimitiveNode::Make("geography", Repetition::OPTIONAL,
                                               LogicalType::Geography("srid:5678"),
                                               ParquetType::BYTE_ARRAY));

  std::vector<std::shared_ptr<Field>> arrow_fields = {
      ::arrow::field("geometry", test::geoarrow_wkb(R"({"crs": "srid:1234"})"), true),
      ::arrow::field("geography",
                     test::geoarrow_wkb(R"({"crs": "srid:5678", "edges": "spherical"})"),
                     true)};

  ASSERT_OK(ConvertSchema(arrow_fields));
  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetGeoArrowCrsProjjson) {
  // Checks the conversion from GeoArrow that contains non-lon/lat PROJJSON
  // to Parquet. Almost all GeoArrow types that arrive at the Parquet reader
  // will have their CRS expressed in this way.
  ::arrow::ExtensionTypeGuard guard(test::geoarrow_wkb());

  std::vector<std::shared_ptr<Field>> arrow_fields = {
      ::arrow::field("geometry", test::geoarrow_wkb(R"({"crs": {"key0": "value0"}})"),
                     true),
      ::arrow::field(
          "geography",
          test::geoarrow_wkb(R"({"crs": {"key1": "value1"}, "edges": "spherical"})"),
          true)};

  auto arrow_properties = default_arrow_writer_properties();
  ASSERT_OK(ConvertSchema(arrow_fields, arrow_properties));

  std::vector<NodePtr> parquet_fields;
  parquet_fields.push_back(PrimitiveNode::Make(
      "geometry", Repetition::OPTIONAL, LogicalType::Geometry(R"({"key0":"value0"})"),
      ParquetType::BYTE_ARRAY));
  parquet_fields.push_back(PrimitiveNode::Make(
      "geography", Repetition::OPTIONAL, LogicalType::Geography(R"({"key1":"value1"})"),
      ParquetType::BYTE_ARRAY));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetLists) {
  for (const auto& list_case : kListCases) {
    std::vector<NodePtr> parquet_fields;
    std::vector<std::shared_ptr<Field>> arrow_fields;

    // parquet_arrow will always generate 3-level LIST encodings

    // // List<String> (list non-null, elements nullable)
    // required group my_list (LIST) {
    //   repeated group list {
    //     optional binary element (UTF8);
    //   }
    // }
    {
      auto element = PrimitiveNode::Make("element", Repetition::OPTIONAL,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::REQUIRED, {list}, ConvertedType::LIST));
      auto arrow_element = ::arrow::field("string", UTF8, true);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, false));
    }

    // // List<String> (list nullable, elements non-null)
    // optional group my_list (LIST) {
    //   repeated group list {
    //     required binary element (UTF8);
    //   }
    // }
    {
      auto element = PrimitiveNode::Make("element", Repetition::REQUIRED,
                                         ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
      auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
      parquet_fields.push_back(
          GroupNode::Make("my_list", Repetition::OPTIONAL, {list}, ConvertedType::LIST));
      auto arrow_element = ::arrow::field("string", UTF8, false);
      auto arrow_list = list_case.type_factory(arrow_element);
      arrow_fields.push_back(::arrow::field("my_list", arrow_list, true));
    }

    ASSERT_OK(ConvertSchema(arrow_fields));

    ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
  }
}

TEST_F(TestConvertArrowSchema, ParquetMaps) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // // Map<String, String> (map and map values nullable)
  // optional group my_map (MAP) {
  //   repeated group key_value {
  //     required binary key (UTF8);
  //     optional binary value (UTF8);
  //   }
  // }
  {
    auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                   ConvertedType::UTF8);
    auto value = PrimitiveNode::Make("value", Repetition::OPTIONAL,
                                     ParquetType::BYTE_ARRAY, ConvertedType::UTF8);

    auto list = GroupNode::Make("key_value", Repetition::REPEATED, {key, value});
    parquet_fields.push_back(
        GroupNode::Make("my_map", Repetition::OPTIONAL, {list}, ConvertedType::MAP));
    auto arrow_key = ::arrow::field("string", UTF8, /*nullable=*/false);
    auto arrow_value = ::arrow::field("other_string", UTF8, /*nullable=*/true);
    auto arrow_map = ::arrow::map(arrow_key->type(), arrow_value, /*nullable=*/false);
    arrow_fields.push_back(::arrow::field("my_map", arrow_map, /*nullable=*/true));
  }

  // // Map<String, String> (non-nullable)
  // required group my_map (MAP) {
  //   repeated group key_value {
  //     required binary key (UTF8);
  //     required binary value (UTF8);
  //   }
  // }
  {
    auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                   ConvertedType::UTF8);
    auto value = PrimitiveNode::Make("value", Repetition::REQUIRED,
                                     ParquetType::BYTE_ARRAY, ConvertedType::UTF8);

    auto list = GroupNode::Make("key_value", Repetition::REPEATED, {key, value});
    parquet_fields.push_back(
        GroupNode::Make("my_map", Repetition::REQUIRED, {list}, ConvertedType::MAP));
    auto arrow_key = ::arrow::field("string", UTF8, /*nullable=*/false);
    auto arrow_value = ::arrow::field("other_string", UTF8, /*nullable=*/false);
    auto arrow_map = ::arrow::map(arrow_key->type(), arrow_value);
    arrow_fields.push_back(::arrow::field("my_map", arrow_map, /*nullable=*/false));
  }

  ASSERT_OK(ConvertSchema(arrow_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetOtherLists) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // parquet_arrow will always generate 3-level LIST encodings

  // // LargeList<String> (list-like non-null, elements nullable)
  // required group my_list (LIST) {
  //   repeated group list {
  //     optional binary element (UTF8);
  //   }
  // }
  {
    auto element = PrimitiveNode::Make("element", Repetition::OPTIONAL,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
    auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::REQUIRED, {list}, ConvertedType::LIST));
    auto arrow_element = ::arrow::field("string", UTF8, true);
    auto arrow_list = ::arrow::large_list(arrow_element);
    arrow_fields.push_back(::arrow::field("my_list", arrow_list, false));
  }
  // // FixedSizeList[10]<String> (list-like non-null, elements nullable)
  // required group my_list (LIST) {
  //   repeated group list {
  //     optional binary element (UTF8);
  //   }
  // }
  {
    auto element = PrimitiveNode::Make("element", Repetition::OPTIONAL,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
    auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::REQUIRED, {list}, ConvertedType::LIST));
    auto arrow_element = ::arrow::field("string", UTF8, true);
    auto arrow_list = ::arrow::fixed_size_list(arrow_element, 10);
    arrow_fields.push_back(::arrow::field("my_list", arrow_list, false));
  }

  ASSERT_OK(ConvertSchema(arrow_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetNestedComplianceEnabledNullable) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // parquet_arrow will always generate 3-level LIST encodings

  // // List<String> (list non-null, elements nullable)
  // required group my_list (LIST) {
  //   repeated group list {
  //     optional binary element (UTF8);
  //   }
  // }
  {
    auto element = PrimitiveNode::Make("element", Repetition::OPTIONAL,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
    auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::REQUIRED, {list}, ConvertedType::LIST));
    auto arrow_element = ::arrow::field("string", UTF8, true);
    auto arrow_list = ::arrow::large_list(arrow_element);
    arrow_fields.push_back(::arrow::field("my_list", arrow_list, false));
  }

  ArrowWriterProperties::Builder builder;
  builder.enable_compliant_nested_types();
  auto arrow_properties = builder.build();

  ASSERT_OK(ConvertSchema(arrow_fields, arrow_properties));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetNestedComplianceEnabledNotNullable) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // parquet_arrow will always generate 3-level LIST encodings

  // // List<String> (list non-null, elements nullable)
  // optional group my_list (LIST) {
  //   repeated group list {
  //     optional binary element (UTF8);
  //   }
  // }
  {
    auto element = PrimitiveNode::Make("element", Repetition::REQUIRED,
                                       ParquetType::BYTE_ARRAY, ConvertedType::UTF8);
    auto list = GroupNode::Make("list", Repetition::REPEATED, {element});
    parquet_fields.push_back(
        GroupNode::Make("my_list", Repetition::REQUIRED, {list}, ConvertedType::LIST));
    auto arrow_element = ::arrow::field("string", UTF8, false);
    auto arrow_list = ::arrow::large_list(arrow_element);
    arrow_fields.push_back(::arrow::field("my_list", arrow_list, false));
  }

  ArrowWriterProperties::Builder builder;
  builder.enable_compliant_nested_types();
  auto arrow_properties = builder.build();

  ASSERT_OK(ConvertSchema(arrow_fields, arrow_properties));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetFlatDecimals) {
  std::vector<NodePtr> parquet_fields;
  std::vector<std::shared_ptr<Field>> arrow_fields;

  // TODO: Test Decimal Arrow -> Parquet conversion

  ASSERT_OK(ConvertSchema(arrow_fields));

  ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}

TEST_F(TestConvertArrowSchema, ParquetTimeAdjustedToUTC) {
  // Verify Parquet Time types have the appropriate isAdjustedToUTC value, depending
  // on the return value of ArrowWriterProperties::write_time_adjusted_to_utc()

  struct FieldConstructionArguments {
    std::string name;
    std::shared_ptr<::arrow::DataType> datatype;
    std::shared_ptr<const LogicalType> logical_type;
    parquet::Type::type physical_type;
    int physical_length;
  };

  auto run_test =
      [this](const std::shared_ptr<ArrowWriterProperties>& arrow_writer_properties,
             bool time_adjusted_to_utc) {
        std::vector<FieldConstructionArguments> cases = {
            {"time32", ::arrow::time32(::arrow::TimeUnit::MILLI),
             LogicalType::Time(time_adjusted_to_utc, LogicalType::TimeUnit::MILLIS),
             ParquetType::INT32, -1},
            {"time64(microsecond)", ::arrow::time64(::arrow::TimeUnit::MICRO),
             LogicalType::Time(time_adjusted_to_utc, LogicalType::TimeUnit::MICROS),
             ParquetType::INT64, -1},
            {"time64(nanosecond)", ::arrow::time64(::arrow::TimeUnit::NANO),
             LogicalType::Time(time_adjusted_to_utc, LogicalType::TimeUnit::NANOS),
             ParquetType::INT64, -1}};

        std::vector<std::shared_ptr<Field>> arrow_fields;
        std::vector<NodePtr> parquet_fields;
        for (const FieldConstructionArguments& c : cases) {
          arrow_fields.push_back(::arrow::field(c.name, c.datatype, false));
          parquet_fields.push_back(PrimitiveNode::Make(c.name, Repetition::REQUIRED,
                                                       c.logical_type, c.physical_type,
                                                       c.physical_length));
        }

        EXPECT_EQ(arrow_writer_properties->write_time_adjusted_to_utc(),
                  time_adjusted_to_utc);
        ASSERT_OK(ConvertSchema(arrow_fields, arrow_writer_properties));
        CheckFlatSchema(parquet_fields);
      };

  // Verify write_time_adjusted_to_utc is false by default.
  ArrowWriterProperties::Builder builder;
  auto arrow_writer_properties = builder.build();
  run_test(arrow_writer_properties, false);

  arrow_writer_properties = builder.set_time_adjusted_to_utc(true)->build();
  run_test(arrow_writer_properties, true);

  arrow_writer_properties = builder.set_time_adjusted_to_utc(false)->build();
  run_test(arrow_writer_properties, false);
}

class TestConvertRoundTrip : public ::testing::Test {
 public:
  ::arrow::Status RoundTripSchema(
      const std::vector<std::shared_ptr<Field>>& fields,
      std::shared_ptr<::parquet::ArrowWriterProperties> arrow_properties =
          ::parquet::default_arrow_writer_properties()) {
    arrow_schema_ = ::arrow::schema(fields);
    std::shared_ptr<::parquet::WriterProperties> properties =
        ::parquet::default_writer_properties();
    RETURN_NOT_OK(ToParquetSchema(arrow_schema_.get(), *properties.get(),
                                  *arrow_properties, &parquet_schema_));
    ::parquet::schema::ToParquet(parquet_schema_->group_node(), &parquet_format_schema_);
    auto parquet_schema = ::parquet::schema::FromParquet(parquet_format_schema_);
    return FromParquetSchema(parquet_schema.get(), &result_schema_);
  }

 protected:
  std::shared_ptr<::arrow::Schema> arrow_schema_;
  std::shared_ptr<SchemaDescriptor> parquet_schema_;
  std::vector<SchemaElement> parquet_format_schema_;
  std::shared_ptr<::arrow::Schema> result_schema_;
};

int GetFieldId(const ::arrow::Field& field) {
  if (field.metadata() == nullptr) {
    return -1;
  }
  auto maybe_field = field.metadata()->Get("PARQUET:field_id");
  if (!maybe_field.ok()) {
    return -1;
  }
  return std::stoi(maybe_field.ValueOrDie());
}

void GetFieldIdsDfs(const ::arrow::FieldVector& fields, std::vector<int>* field_ids) {
  for (const auto& field : fields) {
    field_ids->push_back(GetFieldId(*field));
    GetFieldIdsDfs(field->type()->fields(), field_ids);
  }
}

std::vector<int> GetFieldIdsDfs(const ::arrow::FieldVector& fields) {
  std::vector<int> field_ids;
  GetFieldIdsDfs(fields, &field_ids);
  return field_ids;
}

std::vector<int> GetParquetFieldIdsHelper(const parquet::schema::Node* node) {
  std::vector<int> field_ids;
  field_ids.push_back(node->field_id());
  if (node->is_group()) {
    const GroupNode* group_node = static_cast<const GroupNode*>(node);
    for (int i = 0; i < group_node->field_count(); i++) {
      for (auto id : GetParquetFieldIdsHelper(group_node->field(i).get())) {
        field_ids.push_back(id);
      }
    }
  }
  return field_ids;
}

std::vector<int> GetParquetFieldIds(std::shared_ptr<SchemaDescriptor> parquet_schema) {
  return GetParquetFieldIdsHelper(
      static_cast<const parquet::schema::Node*>(parquet_schema->group_node()));
}

std::vector<int> GetThriftFieldIds(
    const std::vector<SchemaElement>& parquet_format_schema) {
  std::vector<int> field_ids;
  for (const auto& element : parquet_format_schema) {
    field_ids.push_back(element.field_id);
  }
  return field_ids;
}

TEST_F(TestConvertRoundTrip, FieldIdMissingIfNotSpecified) {
  std::vector<std::shared_ptr<Field>> arrow_fields;
  arrow_fields.push_back(::arrow::field("simple", ::arrow::int32(), false));
  /// { "nested": { "outer": { "inner" }, "sibling" } }
  arrow_fields.push_back(::arrow::field(
      "nested",
      ::arrow::struct_({::arrow::field("outer", ::arrow::struct_({::arrow::field(
                                                    "inner", ::arrow::utf8())})),
                        ::arrow::field("sibling", ::arrow::date32())}),
      false));

  ASSERT_OK(RoundTripSchema(arrow_fields));
  auto field_ids = GetFieldIdsDfs(result_schema_->fields());
  for (int actual_id : field_ids) {
    ASSERT_EQ(actual_id, -1);
  }
  auto parquet_field_ids = GetParquetFieldIds(parquet_schema_);
  for (int actual_id : parquet_field_ids) {
    ASSERT_EQ(actual_id, -1);
  }
  // In our unit test a "not set" thrift field has a value of 0
  auto thrift_field_ids = GetThriftFieldIds(parquet_format_schema_);
  for (int actual_id : thrift_field_ids) {
    ASSERT_EQ(actual_id, 0);
  }
}

std::shared_ptr<::arrow::KeyValueMetadata> FieldIdMetadata(int field_id) {
  return ::arrow::key_value_metadata({"PARQUET:field_id"}, {std::to_string(field_id)});
}

TEST_F(TestConvertRoundTrip, FieldIdPreserveExisting) {
  std::vector<std::shared_ptr<Field>> arrow_fields;
  arrow_fields.push_back(
      ::arrow::field("simple", ::arrow::int32(), /*nullable=*/true, FieldIdMetadata(2)));
  /// { "nested": { "outer": { "inner" }, "sibling" }
  arrow_fields.push_back(::arrow::field(
      "nested",
      ::arrow::struct_({::arrow::field("outer", ::arrow::struct_({::arrow::field(
                                                    "inner", ::arrow::utf8())})),
                        ::arrow::field("sibling", ::arrow::date32(), /*nullable=*/true,
                                       FieldIdMetadata(17))}),
      false));

  ASSERT_OK(RoundTripSchema(arrow_fields));
  auto field_ids = GetFieldIdsDfs(result_schema_->fields());
  auto expected_field_ids = std::vector<int>{2, -1, -1, -1, 17};
  ASSERT_EQ(field_ids, expected_field_ids);

  // Parquet has a field id for the schema itself
  expected_field_ids = std::vector<int>{-1, 2, -1, -1, -1, 17};
  auto parquet_ids = GetParquetFieldIds(parquet_schema_);
  ASSERT_EQ(parquet_ids, expected_field_ids);

  // In our unit test a "not set" thrift field has a value of 0
  expected_field_ids = std::vector<int>{0, 2, 0, 0, 0, 17};
  auto thrift_field_ids = GetThriftFieldIds(parquet_format_schema_);
  ASSERT_EQ(thrift_field_ids, expected_field_ids);
}

TEST_F(TestConvertRoundTrip, FieldIdPreserveAllColumnTypes) {
  std::vector<std::shared_ptr<Field>> arrow_fields;
  arrow_fields.push_back(::arrow::field("c1", INT32, true, FieldIdMetadata(2)));
  arrow_fields.push_back(::arrow::field("c2", DOUBLE, true, FieldIdMetadata(4)));
  arrow_fields.push_back(::arrow::field("c3", DECIMAL_8_4, true, FieldIdMetadata(6)));
  arrow_fields.push_back(::arrow::field("c4", UTF8, true, FieldIdMetadata(8)));

  auto inner_struct =
      ::arrow::struct_({::arrow::field("c5_1_1", UTF8, true, FieldIdMetadata(14))});
  arrow_fields.push_back(::arrow::field(
      "c5",
      ::arrow::struct_({::arrow::field("c5_1", inner_struct, true, FieldIdMetadata(12)),
                        ::arrow::field("c5_2", INT64, true, FieldIdMetadata(16))}),
      true, FieldIdMetadata(10)));

  auto list_element = ::arrow::field("c6_1", UTF8, true, FieldIdMetadata(20));
  arrow_fields.push_back(::arrow::field("c6", ::arrow::list(std::move(list_element)),
                                        true, FieldIdMetadata(18)));

  auto map_key = ::arrow::field("c7_1", UTF8, false, FieldIdMetadata(24));
  auto map_value = ::arrow::field("c7_2", UTF8, true, FieldIdMetadata(26));
  arrow_fields.push_back(::arrow::field(
      "c7", std::make_shared<::arrow::MapType>(std::move(map_key), std::move(map_value)),
      true, FieldIdMetadata(22)));

  ASSERT_OK(RoundTripSchema(arrow_fields));
  auto field_ids = GetFieldIdsDfs(result_schema_->fields());
  // c7 field is an arrow map type which is in fact list<struct<key_field, value_field>>
  // and the middle struct type does not have field-id.
  auto expected_field_ids =
      std::vector<int>{2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, -1, 24, 26};
  ASSERT_EQ(field_ids, expected_field_ids);

  // Parquet has a field id for the schema itself.
  // c6 field is a three-level list type where the middle level does not have field-id
  // c7 field is a map type which in turn is a three-level list type, too.
  expected_field_ids =
      std::vector<int>{-1, 2, 4, 6, 8, 10, 12, 14, 16, 18, -1, 20, 22, -1, 24, 26};
  auto parquet_ids = GetParquetFieldIds(parquet_schema_);
  ASSERT_EQ(parquet_ids, expected_field_ids);

  // In our unit test a "not set" thrift field has a value of 0
  expected_field_ids =
      std::vector<int>{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 0, 20, 22, 0, 24, 26};
  auto thrift_field_ids = GetThriftFieldIds(parquet_format_schema_);
  ASSERT_EQ(thrift_field_ids, expected_field_ids);
}

TEST(InvalidSchema, ParquetNegativeDecimalScale) {
  const auto& type = ::arrow::decimal128(23, -2);
  const auto& field = ::arrow::field("f0", type);
  const auto& arrow_schema = ::arrow::schema({field});
  std::shared_ptr<::parquet::WriterProperties> properties =
      ::parquet::default_writer_properties();
  std::shared_ptr<SchemaDescriptor> result_schema;

  ASSERT_RAISES(IOError,
                ToParquetSchema(arrow_schema.get(), *properties.get(), &result_schema));
}

TEST(InvalidSchema, NonNullableNullType) {
  const auto& field = ::arrow::field("f0", ::arrow::null(), /*nullable=*/false);
  const auto& arrow_schema = ::arrow::schema({field});
  std::shared_ptr<::parquet::WriterProperties> properties =
      ::parquet::default_writer_properties();
  std::shared_ptr<SchemaDescriptor> result_schema;
  ASSERT_RAISES(Invalid,
                ToParquetSchema(arrow_schema.get(), *properties.get(), &result_schema));
}

TEST(TestFromParquetSchema, CorruptMetadata) {
  // PARQUET-1565: ensure that an IOError is returned when the parquet file contains
  // corrupted metadata.
  auto path = test::get_data_file("PARQUET-1481.parquet", /*is_good=*/false);

  std::unique_ptr<parquet::ParquetFileReader> reader =
      parquet::ParquetFileReader::OpenFile(path);
  const auto parquet_schema = reader->metadata()->schema();
  std::shared_ptr<::arrow::Schema> arrow_schema;
  ArrowReaderProperties props;
  ASSERT_RAISES(IOError, FromParquetSchema(parquet_schema, props, &arrow_schema));
}

TEST(TestFromParquetSchema, UndefinedLogicalType) {
  // Arrow GH-45522
  auto path = test::get_data_file("unknown-logical-type.parquet");

  std::unique_ptr<parquet::ParquetFileReader> reader =
      parquet::ParquetFileReader::OpenFile(path);
  const auto parquet_schema = reader->metadata()->schema();
  std::shared_ptr<::arrow::Schema> arrow_schema;

  // The underlying physical type is used for conversion to the Arrow type
  ASSERT_OK(FromParquetSchema(parquet_schema, &arrow_schema));
  ASSERT_EQ(*arrow_schema->field(1),
            *::arrow::field("column with unknown type", ::arrow::binary()));
}

//
// Test LevelInfo computation from a Parquet schema
// (for Parquet -> Arrow reading).
//

::arrow::Result<std::deque<LevelInfo>> RootToTreeLeafLevels(
    const SchemaManifest& manifest, int column_number) {
  std::deque<LevelInfo> out;
  const SchemaField* field = nullptr;
  RETURN_NOT_OK(manifest.GetColumnField(column_number, &field));
  while (field != nullptr) {
    out.push_front(field->level_info);
    field = manifest.GetParent(field);
  }
  return out;
}

class TestLevels : public ::testing::Test {
 public:
  virtual void SetUp() {}

  ::arrow::Status MaybeSetParquetSchema(const NodePtr& column) {
    descriptor_.reset(new SchemaDescriptor());
    manifest_.reset(new SchemaManifest());
    descriptor_->Init(GroupNode::Make("root", Repetition::REQUIRED, {column}));
    return SchemaManifest::Make(descriptor_.get(),
                                std::shared_ptr<const ::arrow::KeyValueMetadata>(),
                                ArrowReaderProperties(), manifest_.get());
  }
  void SetParquetSchema(const NodePtr& column) {
    ASSERT_OK(MaybeSetParquetSchema(column));
  }

 protected:
  std::unique_ptr<SchemaDescriptor> descriptor_;
  std::unique_ptr<SchemaManifest> manifest_;
};

TEST_F(TestLevels, TestPrimitive) {
  SetParquetSchema(
      PrimitiveNode::Make("node_name", Repetition::REQUIRED, ParquetType::BOOLEAN));
  ASSERT_OK_AND_ASSIGN(std::deque<LevelInfo> levels,
                       RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(levels, ElementsAre(LevelInfo{/*null_slot_usage=*/1,
                                            /*def_level=*/0, /*rep_level=*/0,
                                            /*ancestor_list_def_level*/ 0}));
  SetParquetSchema(
      PrimitiveNode::Make("node_name", Repetition::OPTIONAL, ParquetType::BOOLEAN));
  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(levels, ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1,
                                            /*rep_level=*/0,
                                            /*ancestor_list_def_level*/ 0}));

  // Arrow schema: list(bool not null) not null
  SetParquetSchema(
      PrimitiveNode::Make("node_name", Repetition::REPEATED, ParquetType::BOOLEAN));
  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},  // List Field
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1}));  //  primitive field
}

TEST_F(TestLevels, TestMaps) {
  // Two column map.
  auto key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                                 ConvertedType::UTF8);
  auto value = PrimitiveNode::Make("value", Repetition::OPTIONAL, ParquetType::BYTE_ARRAY,
                                   ConvertedType::UTF8);

  auto list = GroupNode::Make("key_value", Repetition::REPEATED, {key, value});
  SetParquetSchema(
      GroupNode::Make("my_map", Repetition::OPTIONAL, {list}, LogicalType::Map()));
  ASSERT_OK_AND_ASSIGN(std::deque<LevelInfo> levels,
                       RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 2},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 2}));

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/1));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 2},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 2}));

  // single column map.
  key = PrimitiveNode::Make("key", Repetition::REQUIRED, ParquetType::BYTE_ARRAY,
                            ConvertedType::UTF8);

  list = GroupNode::Make("key_value", Repetition::REPEATED, {key});
  SetParquetSchema(
      GroupNode::Make("my_set", Repetition::REQUIRED, {list}, LogicalType::Map()));

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1}));
}

TEST_F(TestLevels, TestSimpleGroups) {
  // Arrow schema: struct(child: struct(inner: boolean not null))
  SetParquetSchema(GroupNode::Make(
      "parent", Repetition::OPTIONAL,
      {GroupNode::Make(
          "child", Repetition::OPTIONAL,
          {PrimitiveNode::Make("inner", Repetition::REQUIRED, ParquetType::BOOLEAN)})}));
  ASSERT_OK_AND_ASSIGN(std::deque<LevelInfo> levels,
                       RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0}));

  // Arrow schema: struct(child: struct(inner: boolean ))
  SetParquetSchema(GroupNode::Make(
      "parent", Repetition::OPTIONAL,
      {GroupNode::Make(
          "child", Repetition::OPTIONAL,
          {PrimitiveNode::Make("inner", Repetition::OPTIONAL, ParquetType::BOOLEAN)})}));
  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0}));

  // Arrow schema: struct(child: struct(inner: boolean)) not null
  SetParquetSchema(GroupNode::Make(
      "parent", Repetition::REQUIRED,
      {GroupNode::Make(
          "child", Repetition::OPTIONAL,
          {PrimitiveNode::Make("inner", Repetition::OPTIONAL, ParquetType::BOOLEAN)})}));
  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/0, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/0,
                            /*ancestor_list_def_level*/ 0}));
}

TEST_F(TestLevels, TestRepeatedGroups) {
  // Arrow schema: list(bool)
  SetParquetSchema(GroupNode::Make(
      "child_list", Repetition::OPTIONAL,
      {GroupNode::Make(
          "list", Repetition::REPEATED,
          {PrimitiveNode::Make("element", Repetition::OPTIONAL, ParquetType::BOOLEAN)})},
      LogicalType::List()));

  ASSERT_OK_AND_ASSIGN(std::deque<LevelInfo> levels,
                       RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 2}));

  // Arrow schema: list(bool) not null
  SetParquetSchema(GroupNode::Make(
      "child_list", Repetition::REQUIRED,
      {GroupNode::Make(
          "list", Repetition::REPEATED,
          {PrimitiveNode::Make("element", Repetition::OPTIONAL, ParquetType::BOOLEAN)})},
      LogicalType::List()));

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1}));

  // Arrow schema: list(bool not null)
  SetParquetSchema(GroupNode::Make(
      "child_list", Repetition::OPTIONAL,
      {GroupNode::Make(
          "list", Repetition::REPEATED,
          {PrimitiveNode::Make("element", Repetition::REQUIRED, ParquetType::BOOLEAN)})},
      LogicalType::List()));

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 2}));

  // Arrow schema: list(bool not null) not null
  SetParquetSchema(GroupNode::Make(
      "child_list", Repetition::REQUIRED,
      {GroupNode::Make(
          "list", Repetition::REPEATED,
          {PrimitiveNode::Make("element", Repetition::REQUIRED, ParquetType::BOOLEAN)})},
      LogicalType::List()));

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1}));

  // Arrow schema: list(struct(child: struct(list(bool not null) not null)) non null) not
  // null
  SetParquetSchema(GroupNode::Make(
      "parent", Repetition::REPEATED,
      {GroupNode::Make(
          "child", Repetition::OPTIONAL,
          {PrimitiveNode::Make("inner", Repetition::REPEATED, ParquetType::BOOLEAN)})}));
  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1},

                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/2, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1},  // optional child struct
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 1},  // repeated field
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 3}));  // inner field

  // Arrow schema: list(struct(child_list: list(struct(f0: bool f1: bool))) not null) not
  // null
  SetParquetSchema(GroupNode::Make(
      "parent", Repetition::REPEATED,
      {GroupNode::Make(
          "child_list", Repetition::OPTIONAL,
          {GroupNode::Make(
              "list", Repetition::REPEATED,
              {GroupNode::Make(
                  "element", Repetition::OPTIONAL,
                  {PrimitiveNode::Make("f0", Repetition::OPTIONAL, ParquetType::BOOLEAN),
                   PrimitiveNode::Make("f1", Repetition::REQUIRED,
                                       ParquetType::BOOLEAN)})})},
          LogicalType::List())}));
  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},  // parent list
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1},  // parent struct

                  // Def_level=2 is handled together with def_level=3
                  // When decoding.  Def_level=2 indicates present but empty
                  // list.  def_level=3 indicates a present element in the
                  // list.
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 1},  // list field
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/4, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 3},  // inner struct field

                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/5, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 3}));  // f0 bool field

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/1));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},  // parent list
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1},  // parent struct
                  // Def_level=2 is handled together with def_level=3
                  // When decoding.  Def_level=2 indicate present but empty
                  // list.  def_level=3 indicates a present element in the
                  // list.
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 1},  // list field
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/4, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 3},  // inner struct field

                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/4, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 3}));  // f1 bool field

  // Arrow schema: list(struct(child_list: list(bool not null)) not null) not null
  // Legacy 2-level encoding (required for backwards compatibility.  See
  // https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#nested-types
  // for definitions).
  SetParquetSchema(GroupNode::Make(
      "parent", Repetition::REPEATED,
      {GroupNode::Make(
          "child_list", Repetition::OPTIONAL,
          {PrimitiveNode::Make("bool", Repetition::REPEATED, ParquetType::BOOLEAN)},
          LogicalType::List())}));

  ASSERT_OK_AND_ASSIGN(levels, RootToTreeLeafLevels(*manifest_, /*column_number=*/0));
  EXPECT_THAT(
      levels,
      ElementsAre(LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 0},  // parent list
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/1, /*rep_level=*/1,
                            /*ancestor_list_def_level*/ 1},  // parent struct

                  // Def_level=2 is handled together with def_level=3
                  // When decoding.  Def_level=2 indicate present but empty
                  // list.  def_level=3 indicates a present element in the
                  // list.
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 1},  // list field
                  LevelInfo{/*null_slot_usage=*/1, /*def_level=*/3, /*rep_level=*/2,
                            /*ancestor_list_def_level*/ 3}));  // inner bool
}

TEST_F(TestLevels, ListErrors) {
  {
    ::arrow::Status error = MaybeSetParquetSchema(GroupNode::Make(
        "child_list", Repetition::REPEATED,
        {GroupNode::Make("list", Repetition::REPEATED,
                         {PrimitiveNode::Make("element", Repetition::REQUIRED,
                                              ParquetType::BOOLEAN)})},
        LogicalType::List()));
    ASSERT_RAISES(Invalid, error);
    std::string expected("LIST-annotated groups must not be repeated.");
    EXPECT_EQ(error.message().substr(0, expected.size()), expected);
  }
  {
    ::arrow::Status error = MaybeSetParquetSchema(GroupNode::Make(
        "child_list", Repetition::OPTIONAL,
        {PrimitiveNode::Make("f1", Repetition::REPEATED, ParquetType::BOOLEAN),
         PrimitiveNode::Make("f2", Repetition::REPEATED, ParquetType::BOOLEAN)},
        LogicalType::List()));
    ASSERT_RAISES(Invalid, error);
    std::string expected("LIST-annotated groups must have a single child.");
    EXPECT_EQ(error.message().substr(0, expected.size()), expected);
  }

  {
    ::arrow::Status error = MaybeSetParquetSchema(GroupNode::Make(
        "child_list", Repetition::OPTIONAL,
        {PrimitiveNode::Make("f1", Repetition::OPTIONAL, ParquetType::BOOLEAN)},
        LogicalType::List()));
    ASSERT_RAISES(Invalid, error);
    std::string expected(
        "Non-repeated nodes in a LIST-annotated group are not supported.");
    EXPECT_EQ(error.message().substr(0, expected.size()), expected);
  }
}

}  // namespace parquet::arrow
