# 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.

module ReaderTests
  def roundtrip(data)
    Dir.mktmpdir do |tmp_dir|
      case data
      when Arrow::Array
        table = Arrow::Table.new(value: data)
      when Arrow::RecordBatch
        table = data.to_table
      else
        table = data
      end
      open_input(table, tmp_dir) do |input|
        reader = reader_class.new(input)
        case data
        when Arrow::Array
          values = []
          reader.each do |record_batch|
            values.concat(record_batch.columns[0].to_a)
          end
          [reader.schema.fields[0].type, values]
        else
          record_batches = reader.collect do |record_batch|
            record_batch.to_h.tap do |hash|
              hash.each do |key, value|
                hash[key] = value.to_a
              end
            end
          end
          [reader.schema, record_batches]
        end
      end
    ensure
      GC.start
    end
  end

  def test_custom_metadata_field
    field =
      Arrow::Field.new("value", :boolean)
        .with_metadata("key1" => "value1",
                       "key2" => "value2")
    schema = Arrow::Schema.new([field])
    values = [true, nil, false]
    record_batch = Arrow::RecordBatch.new(schema, {"value" => values})
    schema, record_batches = roundtrip(record_batch)
    assert_equal([
                   {
                     "key1" => "value1",
                     "key2" => "value2",
                   },
                   [{"value" => values}],
                 ],
                 [schema.fields[0].metadata, record_batches])
  end

  def test_custom_metadata_schema
    field = Arrow::Field.new("value", :boolean)
    schema =
      Arrow::Schema.new([field])
        .with_metadata("key1" => "value1",
                       "key2" => "value2")
    values = [true, nil, false]
    record_batch = Arrow::RecordBatch.new(schema, {"value" => values})
    schema, record_batches = roundtrip(record_batch)
    assert_equal([
                   {
                     "key1" => "value1",
                     "key2" => "value2",
                   },
                   [{"value" => values}],
                 ],
                 [schema.metadata, record_batches])
  end

  def test_null
    type, values = roundtrip(Arrow::NullArray.new(3))
    assert_equal(["Null", [nil, nil, nil]],
                 [type.to_s, values])
  end

  def test_boolean
    type, values = roundtrip(Arrow::BooleanArray.new([true, nil, false]))
    assert_equal(["Boolean", [true, nil, false]],
                 [type.to_s, values])
  end

  def test_int8
    type, values = roundtrip(Arrow::Int8Array.new([-128, nil, 127]))
    assert_equal(["Int8", [-128, nil, 127]],
                 [type.to_s, values])
  end

  def test_uint8
    type, values = roundtrip(Arrow::UInt8Array.new([0, nil, 255]))
    assert_equal(["UInt8", [0, nil, 255]],
                 [type.to_s, values])
  end

  def test_int16
    type, values = roundtrip(Arrow::Int16Array.new([-32768, nil, 32767]))
    assert_equal(["Int16", [-32768, nil, 32767]],
                 [type.to_s, values])
  end

  def test_uint16
    type, values = roundtrip(Arrow::UInt16Array.new([0, nil, 65535]))
    assert_equal(["UInt16", [0, nil, 65535]],
                 [type.to_s, values])
  end

  def test_int32
    array = Arrow::Int32Array.new([-2147483648, nil, 2147483647])
    type, values = roundtrip(array)
    assert_equal(["Int32", [-2147483648, nil, 2147483647]],
                 [type.to_s, values])
  end

  def test_uint32
    array = Arrow::UInt32Array.new([0, nil, 4294967295])
    type, values = roundtrip(array)
    assert_equal(["UInt32", [0, nil, 4294967295]],
                 [type.to_s, values])
  end

  def test_int64
    array = Arrow::Int64Array.new([
                                    -9223372036854775808,
                                    nil,
                                    9223372036854775807
                                  ])
    type, values = roundtrip(array)
    assert_equal(["Int64", [-9223372036854775808, nil, 9223372036854775807]],
                 [type.to_s, values])
  end

  def test_uint64
    array = Arrow::UInt64Array.new([0, nil, 18446744073709551615])
    type, values = roundtrip(array)
    assert_equal(["UInt64", [0, nil, 18446744073709551615]],
                 [type.to_s, values])
  end

  def test_float32
    type, values = roundtrip(Arrow::FloatArray.new([-0.5, nil, 0.5]))
    assert_equal(["Float32", [-0.5, nil, 0.5]],
                 [type.to_s, values])
  end

  def test_float64
    type, values = roundtrip(Arrow::DoubleArray.new([-0.5, nil, 0.5]))
    assert_equal(["Float64", [-0.5, nil, 0.5]],
                 [type.to_s, values])
  end

  def test_date32
    date_2017_08_28 = 17406
    date_2025_12_09 = 20431
    array = Arrow::Date32Array.new([date_2017_08_28, nil, date_2025_12_09])
    type, values = roundtrip(array)
    assert_equal(["Date32", [date_2017_08_28, nil, date_2025_12_09]],
                 [type.to_s, values])
  end

  def test_date64
    date_2017_08_28_00_00_00 = 1503878400000
    date_2025_12_10_00_00_00 = 1765324800000
    array = Arrow::Date64Array.new([
                                     date_2017_08_28_00_00_00,
                                     nil,
                                     date_2025_12_10_00_00_00,
                                   ])
    type, values = roundtrip(array)
    assert_equal([
                   "Date64",
                   [date_2017_08_28_00_00_00, nil, date_2025_12_10_00_00_00],
                 ],
                 [type.to_s, values])
  end

  def test_time32_second
    time_00_00_10 = 10
    time_00_01_10 = 60 + 10
    array = Arrow::Time32Array.new(:second,
                                   [time_00_00_10, nil, time_00_01_10])
    type, values = roundtrip(array)
    assert_equal(["Time32(second)", [time_00_00_10, nil, time_00_01_10]],
                 [type.to_s, values])
  end

  def test_time32_millisecond
    time_00_00_10_000 = 10 * 1000
    time_00_01_10_000 = (60 + 10) * 1000
    array = Arrow::Time32Array.new(:milli,
                                   [
                                     time_00_00_10_000,
                                     nil,
                                     time_00_01_10_000,
                                   ])
    type, values = roundtrip(array)
    assert_equal([
                   "Time32(millisecond)",
                   [time_00_00_10_000, nil, time_00_01_10_000],
                 ],
                 [type.to_s, values])
  end

  def test_time64_microsecond
    time_00_00_10_000_000 = 10 * 1_000_000
    time_00_01_10_000_000 = (60 + 10) * 1_000_000
    array = Arrow::Time64Array.new(:micro,
                                   [
                                     time_00_00_10_000_000,
                                     nil,
                                     time_00_01_10_000_000,
                                   ])
    type, values = roundtrip(array)
    assert_equal([
                   "Time64(microsecond)",
                   [time_00_00_10_000_000, nil, time_00_01_10_000_000],
                 ],
                 [type.to_s, values])
  end

  def test_time64_nanosecond
    time_00_00_10_000_000_000 = 10 * 1_000_000_000
    time_00_01_10_000_000_000 = (60 + 10) * 1_000_000_000
    array = Arrow::Time64Array.new(:nano,
                                   [
                                     time_00_00_10_000_000_000,
                                     nil,
                                     time_00_01_10_000_000_000,
                                   ])
    type, values = roundtrip(array)
    assert_equal([
                   "Time64(nanosecond)",
                   [
                     time_00_00_10_000_000_000,
                     nil,
                     time_00_01_10_000_000_000,
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_timestamp_second
    timestamp_2019_11_17_15_09_11 = 1574003351
    timestamp_2025_12_16_05_33_58 = 1765863238
    array = Arrow::TimestampArray.new(:second,
                                      [
                                        timestamp_2019_11_17_15_09_11,
                                        nil,
                                        timestamp_2025_12_16_05_33_58,
                                      ])
    type, values = roundtrip(array)
    assert_equal([
                   "Timestamp(second)",
                   [
                     timestamp_2019_11_17_15_09_11,
                     nil,
                     timestamp_2025_12_16_05_33_58,
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_timestamp_millisecond
    timestamp_2019_11_17_15_09_11 = 1574003351 * 1_000
    timestamp_2025_12_16_05_33_58 = 1765863238 * 1_000
    array = Arrow::TimestampArray.new(:milli,
                                      [
                                        timestamp_2019_11_17_15_09_11,
                                        nil,
                                        timestamp_2025_12_16_05_33_58,
                                      ])
    type, values = roundtrip(array)
    assert_equal([
                   "Timestamp(millisecond)",
                   [
                     timestamp_2019_11_17_15_09_11,
                     nil,
                     timestamp_2025_12_16_05_33_58,
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_timestamp_microsecond
    timestamp_2019_11_17_15_09_11 = 1574003351 * 1_000_000
    timestamp_2025_12_16_05_33_58 = 1765863238 * 1_000_000
    array = Arrow::TimestampArray.new(:micro,
                                      [
                                        timestamp_2019_11_17_15_09_11,
                                        nil,
                                        timestamp_2025_12_16_05_33_58,
                                      ])
    type, values = roundtrip(array)
    assert_equal([
                   "Timestamp(microsecond)",
                   [
                     timestamp_2019_11_17_15_09_11,
                     nil,
                     timestamp_2025_12_16_05_33_58,
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_timestamp_nanosecond
    timestamp_2019_11_17_15_09_11 = 1574003351 * 1_000_000_000
    timestamp_2025_12_16_05_33_58 = 1765863238 * 1_000_000_000
    array = Arrow::TimestampArray.new(:nano,
                                      [
                                        timestamp_2019_11_17_15_09_11,
                                        nil,
                                        timestamp_2025_12_16_05_33_58,
                                      ])
    type, values = roundtrip(array)
    assert_equal([
                   "Timestamp(nanosecond)",
                   [
                     timestamp_2019_11_17_15_09_11,
                     nil,
                     timestamp_2025_12_16_05_33_58,
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_timestamp_time_zone
    time_zone = "UTC"
    timestamp_2019_11_17_15_09_11 = 1574003351
    timestamp_2025_12_16_05_33_58 = 1765863238
    data_type = Arrow::TimestampDataType.new(:second, time_zone)
    array = Arrow::TimestampArray.new(data_type,
                                      [
                                        timestamp_2019_11_17_15_09_11,
                                        nil,
                                        timestamp_2025_12_16_05_33_58,
                                      ])
    type, values = roundtrip(array)
    assert_equal([
                   "Timestamp(second, #{time_zone})",
                   [
                     timestamp_2019_11_17_15_09_11,
                     nil,
                     timestamp_2025_12_16_05_33_58,
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_year_month_interval
    type, values = roundtrip(Arrow::MonthIntervalArray.new([0, nil, 100]))
    assert_equal(["YearMonthInterval", [0, nil, 100]],
                 [type.to_s, values])
  end

  def test_day_time_interval
    array = Arrow::DayTimeIntervalArray.new([
                                              {day: 1, millisecond: 100},
                                              nil,
                                              {day: 3, millisecond: 300},
                                            ])
    type, values = roundtrip(array)
    assert_equal([
                   "DayTimeInterval",
                   [
                     [1, 100],
                     nil,
                     [3, 300],
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_month_day_nano_interval
    array = Arrow::MonthDayNanoIntervalArray.new([
                                                   {
                                                     month: 1,
                                                     day: 1,
                                                     nanosecond: 100,
                                                   },
                                                   nil,
                                                   {
                                                     month: 3,
                                                     day: 3,
                                                     nanosecond: 300,
                                                   },
                                                 ])
    type, values = roundtrip(array)
    assert_equal([
                   "MonthDayNanoInterval",
                    [
                      [1, 1, 100],
                      nil,
                      [3, 3, 300],
                    ],
                 ],
                 [type.to_s, values])
  end

  def test_duration_second
    type, values = roundtrip(Arrow::DurationArray.new(:second, [0, nil, 100]))
    assert_equal(["Duration(second)", [0, nil, 100]],
                 [type.to_s, values])
  end

  def test_duration_millisecond
    array = Arrow::DurationArray.new(:milli, [0, nil, 100_000])
    type, values = roundtrip(array)
    assert_equal(["Duration(millisecond)", [0, nil, 100_000]],
                 [type.to_s, values])
  end

  def test_duration_microsecond
    array = Arrow::DurationArray.new(:micro, [0, nil, 100_000_000])
    type, values = roundtrip(array)
    assert_equal(["Duration(microsecond)", [0, nil, 100_000_000]],
                 [type.to_s, values])
  end

  def test_duration_nanosecond
    array = Arrow::DurationArray.new(:nano, [0, nil, 100_000_000_000])
    type, values = roundtrip(array)
    assert_equal(["Duration(nanosecond)", [0, nil, 100_000_000_000]],
                 [type.to_s, values])
  end

  def test_binary
    array = Arrow::BinaryArray.new(["Hello".b, nil, "World".b])
    type, values = roundtrip(array)
    assert_equal(["Binary", ["Hello".b, nil, "World".b]],
                 [type.to_s, values])
  end

  def test_large_binary
    array = Arrow::LargeBinaryArray.new(["Hello".b, nil, "World".b])
    type, values = roundtrip(array)
    assert_equal(["LargeBinary", ["Hello".b, nil, "World".b]],
                 [type.to_s, values])
  end

  def test_utf8
    array = Arrow::StringArray.new(["Hello", nil, "World"])
    type, values = roundtrip(array)
    assert_equal(["UTF8", ["Hello", nil, "World"]],
                 [type.to_s, values])
  end

  def test_large_utf8
    array = Arrow::LargeStringArray.new(["Hello", nil, "World"])
    type, values = roundtrip(array)
    assert_equal(["LargeUTF8", ["Hello", nil, "World"]],
                 [type.to_s, values])
  end

  def test_fixed_size_binary
    data_type = Arrow::FixedSizeBinaryDataType.new(4)
    array = Arrow::FixedSizeBinaryArray.new(data_type,
                                            ["0124".b, nil, "abcd".b])
    type, values = roundtrip(array)
    assert_equal(["FixedSizeBinary(4)", ["0124".b, nil, "abcd".b]],
                 [type.to_s, values])
  end

  def test_decimal128
    positive_small = "1.200"
    positive_large = ("1234567890" * 3) + "12345.678"
    negative_small = "-1.200"
    negative_large = "-" + ("1234567890" * 3) + "12345.678"
    array = Arrow::Decimal128Array.new({precision: 38, scale: 3},
                                       [
                                         positive_large,
                                         positive_small,
                                         nil,
                                         negative_small,
                                         negative_large,
                                       ])
    type, values = roundtrip(array)
    assert_equal([
                   "Decimal128(38, 3)",
                   [
                     BigDecimal(positive_large),
                     BigDecimal(positive_small),
                     nil,
                     BigDecimal(negative_small),
                     BigDecimal(negative_large),
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_decimal256
    positive_small = "1.200"
    positive_large = ("1234567890" * 7) + "123.456"
    negative_small = "-1.200"
    negative_large = "-" + ("1234567890" * 7) + "123.456"
    array = Arrow::Decimal256Array.new({precision: 76, scale: 3},
                                       [
                                         positive_large,
                                         positive_small,
                                         nil,
                                         negative_small,
                                         negative_large,
                                       ])
    type, values = roundtrip(array)
    assert_equal([
                   "Decimal256(76, 3)",
                   [
                     BigDecimal(positive_large),
                     BigDecimal(positive_small),
                     nil,
                     BigDecimal(negative_small),
                     BigDecimal(negative_large),
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_list
    data_type = Arrow::ListDataType.new(name: "count", type: :int8)
    array = Arrow::ListArray.new(data_type, [[-128, 127], nil, [-1, 0, 1]])
    type, values = roundtrip(array)
    assert_equal(["List<count: Int8>", [[-128, 127], nil, [-1, 0, 1]]],
                 [type.to_s, values])
  end

  def test_large_list
    data_type = Arrow::LargeListDataType.new(name: "count",
                                             type: :int8)
    array = Arrow::LargeListArray.new(data_type,
                                      [[-128, 127], nil, [-1, 0, 1]])
    type, values = roundtrip(array)
    assert_equal([
                   "LargeList<count: Int8>",
                   [
                     [-128, 127],
                     nil,
                     [-1, 0, 1],
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_fixed_size_list
    data_type = Arrow::FixedSizeListDataType.new({
                                                   name: "count",
                                                   type: :int8,
                                                 },
                                                 2)
    array = Arrow::FixedSizeListArray.new(data_type,
                                          [[-128, 127], nil, [-1, 1]])
    type, values = roundtrip(array)
    assert_equal([
                   "FixedSizeList<count: Int8>(2)",
                   [
                     [-128, 127],
                     nil,
                     [-1, 1],
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_struct
    data_type = Arrow::StructDataType.new(count: :int8,
                                          visible: :boolean)
    array = Arrow::StructArray.new(data_type,
                                   [[-128, nil], nil, [nil, true]])
    type, values = roundtrip(array)
    assert_equal([
                   "Struct<count: Int8, visible: Boolean>",
                   [
                     [-128, nil],
                     nil,
                     [nil, true],
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_dense_union
    fields = [
      Arrow::Field.new("number", :int8),
      Arrow::Field.new("text", :string),
    ]
    type_ids = [11, 13]
    data_type = Arrow::DenseUnionDataType.new(fields, type_ids)
    types = Arrow::Int8Array.new([11, 13, 11, 13, 13])
    value_offsets = Arrow::Int32Array.new([0, 0, 1, 1, 2])
    children = [
      Arrow::Int8Array.new([1, nil]),
      Arrow::StringArray.new(["a", "b", "c"])
    ]
    array = Arrow::DenseUnionArray.new(data_type,
                                       types,
                                       value_offsets,
                                       children)
    type, values = roundtrip(array)
    assert_equal([
                   "DenseUnion<number: Int8=11, text: UTF8=13>",
                   [1, "a", nil, "b", "c"],
                 ],
                 [type.to_s, values])
  end

  def test_sparse_union
    fields = [
      Arrow::Field.new("number", :int8),
      Arrow::Field.new("text", :string),
    ]
    type_ids = [11, 13]
    data_type = Arrow::SparseUnionDataType.new(fields, type_ids)
    types = Arrow::Int8Array.new([11, 13, 11, 13, 11])
    children = [
      Arrow::Int8Array.new([1, nil, nil, nil, 5]),
      Arrow::StringArray.new([nil, "b", nil, "d", nil])
    ]
    array = Arrow::SparseUnionArray.new(data_type, types, children)
    type, values = roundtrip(array)
    assert_equal([
                   "SparseUnion<number: Int8=11, text: UTF8=13>",
                   [1, "b", nil, "d", 5],
                 ],
                 [type.to_s, values])
  end

  def test_map
    data_type = Arrow::MapDataType.new(:string, :int8)
    array = Arrow::MapArray.new(data_type,
                                [
                                  {"a" => -128, "b" => 127},
                                  nil,
                                  {"c" => nil},
                                ])
    type, values = roundtrip(array)
    assert_equal([
                   "Map<UTF8, Int8>",
                   [
                     {"a" => -128, "b" => 127},
                     nil,
                     {"c" => nil},
                   ],
                 ],
                 [type.to_s, values])
  end

  def test_dictionary
    values = ["a", "b", "c", nil, "a"]
    string_array = Arrow::StringArray.new(values)
    array = string_array.dictionary_encode
    type, values = roundtrip(array)
    assert_equal([
                   "Dictionary<id=0, index=Int32, value=UTF8, ordered=false>",
                   ["a", "b", "c", nil, "a"],
                 ],
                 [type.to_s, values])
  end
end

module FileReaderTests
  def test_custom_metadata_footer
    Dir.mktmpdir do |tmp_dir|
      table = Arrow::Table.new(value: Arrow::Int8Array.new([1, 2, 3]))
      metadata = {
        "key1" => "value1",
        "key2" => "value2",
      }
      open_input(table, tmp_dir, metadata: metadata) do |input|
        reader = reader_class.new(input)
        assert_equal(metadata, reader.metadata)
      end
    ensure
      GC.start
    end
  end
end

module FileInput
  def open_input(table, tmp_dir, **options, &block)
    path = File.join(tmp_dir, "data.#{file_extension}")
    table.save(path, **options)
    File.open(path, "rb", &block)
  end
end

module PipeInput
  def open_input(table, tmp_dir, **options)
    buffer = Arrow::ResizableBuffer.new(4096)
    table.save(buffer, format: format, **options)
    IO.pipe do |input, output|
      write_thread = Thread.new do
        output.write(buffer.data.to_s)
      end
      begin
        yield(input)
      ensure
        write_thread.join
      end
    end
  end
end

module StringInput
  def open_input(table, tmp_dir, **options)
    buffer = Arrow::ResizableBuffer.new(4096)
    table.save(buffer, format: format, **options)
    yield(buffer.data.to_s)
  end
end

class TestFileReaderFileInput < Test::Unit::TestCase
  include ReaderTests
  include FileReaderTests
  include FileInput

  def file_extension
    "arrow"
  end

  def reader_class
    ArrowFormat::FileReader
  end
end

class TestFileReaderStringInput < Test::Unit::TestCase
  include ReaderTests
  include FileReaderTests
  include StringInput

  def format
    :arrow_file
  end

  def reader_class
    ArrowFormat::FileReader
  end
end

class TestStreamingReaderFileInupt < Test::Unit::TestCase
  include ReaderTests
  include FileInput

  def file_extension
    "arrows"
  end

  def reader_class
    ArrowFormat::StreamingReader
  end
end

class TestStreamingReaderPipeInupt < Test::Unit::TestCase
  include ReaderTests
  include PipeInput

  def format
    :arrow_streaming
  end

  def reader_class
    ArrowFormat::StreamingReader
  end
end

class TestStreamingReaderStringInupt < Test::Unit::TestCase
  include ReaderTests
  include StringInput

  def format
    :arrow_streaming
  end

  def reader_class
    ArrowFormat::StreamingReader
  end
end
