/*
 * 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 <chrono>
#include <format>
#include <memory>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include "iceberg/arrow/arrow_io_util.h"
#include "iceberg/avro/avro_register.h"
#include "iceberg/expression/expressions.h"
#include "iceberg/manifest/manifest_entry.h"
#include "iceberg/manifest/manifest_list.h"
#include "iceberg/manifest/manifest_reader.h"
#include "iceberg/manifest/manifest_writer.h"
#include "iceberg/partition_spec.h"
#include "iceberg/schema.h"
#include "iceberg/test/matchers.h"
#include "iceberg/transform.h"
#include "iceberg/type.h"

namespace iceberg {

class TestManifestReaderStats : public testing::TestWithParam<int8_t> {
 protected:
  void SetUp() override {
    avro::RegisterAll();
    file_io_ = arrow::MakeMockFileIO();

    schema_ = std::make_shared<Schema>(std::vector<SchemaField>{
        SchemaField::MakeRequired(/*field_id=*/3, "id", int32()),
        SchemaField::MakeRequired(/*field_id=*/4, "data", string())});

    ICEBERG_UNWRAP_OR_FAIL(
        spec_,
        PartitionSpec::Make(
            /*spec_id=*/0, {PartitionField(/*source_id=*/4, /*field_id=*/1000,
                                           "data_bucket", Transform::Bucket(16))}));
  }

  std::string MakeManifestPath() {
    return std::format("manifest-{}.avro",
                       std::chrono::system_clock::now().time_since_epoch().count());
  }

  inline static const std::map<int32_t, int64_t> kValueCounts = {{3, 3L}};
  inline static const std::map<int32_t, int64_t> kNullValueCounts = {{3, 0L}};
  inline static const std::map<int32_t, int64_t> kNanValueCounts = {{3, 1L}};
  inline static const std::map<int32_t, std::vector<uint8_t>> kLowerBounds = {
      {3, Literal::Int(2).Serialize().value()}};
  inline static const std::map<int32_t, std::vector<uint8_t>> kUpperBounds = {
      {3, Literal::Int(4).Serialize().value()}};

  std::unique_ptr<DataFile> MakeDataFileWithStats() {
    return std::make_unique<DataFile>(DataFile{
        .file_path = "/path/to/data-with-stats.parquet",
        .file_format = FileFormatType::kParquet,
        .partition = PartitionValues({Literal::Int(0)}),
        .record_count = 3,
        .file_size_in_bytes = 10,
        .value_counts = kValueCounts,
        .null_value_counts = kNullValueCounts,
        .nan_value_counts = kNanValueCounts,
        .lower_bounds = kLowerBounds,
        .upper_bounds = kUpperBounds,
        .sort_order_id = 0,
    });
  }

  ManifestFile WriteManifest(int8_t format_version, std::unique_ptr<DataFile> data_file) {
    const std::string manifest_path = MakeManifestPath();

    auto writer_result = ManifestWriter::MakeWriter(
        format_version, /*snapshot_id=*/1000L, manifest_path, file_io_, spec_, schema_,
        ManifestContent::kData, /*first_row_id=*/0L);
    EXPECT_THAT(writer_result, IsOk());
    auto writer = std::move(writer_result.value());

    EXPECT_THAT(writer->WriteAddedEntry(std::move(data_file)), IsOk());
    EXPECT_THAT(writer->Close(), IsOk());

    auto manifest_result = writer->ToManifestFile();
    EXPECT_THAT(manifest_result, IsOk());
    return std::move(manifest_result.value());
  }

  void AssertFullStats(const DataFile& file) {
    EXPECT_EQ(file.record_count, 3);
    EXPECT_EQ(file.value_counts, kValueCounts);
    EXPECT_EQ(file.null_value_counts, kNullValueCounts);
    EXPECT_EQ(file.nan_value_counts, kNanValueCounts);
    EXPECT_EQ(file.lower_bounds, kLowerBounds);
    EXPECT_EQ(file.upper_bounds, kUpperBounds);
  }

  void AssertStatsDropped(const DataFile& file) {
    EXPECT_EQ(file.record_count, 3);  // record count is not dropped
    EXPECT_TRUE(file.value_counts.empty());
    EXPECT_TRUE(file.null_value_counts.empty());
    EXPECT_TRUE(file.nan_value_counts.empty());
    EXPECT_TRUE(file.lower_bounds.empty());
    EXPECT_TRUE(file.upper_bounds.empty());
  }

  std::shared_ptr<FileIO> file_io_;
  std::shared_ptr<Schema> schema_;
  std::shared_ptr<PartitionSpec> spec_;
};

TEST_P(TestManifestReaderStats, TestReadIncludesFullStats) {
  auto version = GetParam();
  auto manifest = WriteManifest(version, MakeDataFileWithStats());

  auto reader_result = ManifestReader::Make(manifest, file_io_, schema_, spec_);
  ASSERT_THAT(reader_result, IsOk());
  auto reader = std::move(reader_result.value());

  auto entries_result = reader->Entries();
  ASSERT_THAT(entries_result, IsOk());
  auto entries = entries_result.value();
  ASSERT_EQ(entries.size(), 1);
  AssertFullStats(*entries[0].data_file);
}

TEST_P(TestManifestReaderStats, TestReadEntriesWithFilterIncludesFullStats) {
  auto version = GetParam();
  auto manifest = WriteManifest(version, MakeDataFileWithStats());

  auto reader_result = ManifestReader::Make(manifest, file_io_, schema_, spec_);
  ASSERT_THAT(reader_result, IsOk());
  auto reader = std::move(reader_result.value());

  reader->FilterRows(Expressions::Equal("id", Literal::Int(3)));

  auto entries_result = reader->Entries();
  ASSERT_THAT(entries_result, IsOk());
  auto entries = entries_result.value();
  ASSERT_EQ(entries.size(), 1);
  AssertFullStats(*entries[0].data_file);
}

TEST_P(TestManifestReaderStats, TestReadEntriesWithFilterAndSelectIncludesFullStats) {
  auto version = GetParam();
  auto manifest = WriteManifest(version, MakeDataFileWithStats());

  auto reader_result = ManifestReader::Make(manifest, file_io_, schema_, spec_);
  ASSERT_THAT(reader_result, IsOk());
  auto reader = std::move(reader_result.value());

  reader->Select({"file_path"});
  reader->FilterRows(Expressions::Equal("id", Literal::Int(3)));

  auto entries_result = reader->Entries();
  ASSERT_THAT(entries_result, IsOk());
  auto entries = entries_result.value();
  ASSERT_EQ(entries.size(), 1);
  AssertFullStats(*entries[0].data_file);
}

TEST_P(TestManifestReaderStats, TestReadEntriesWithSelectNotProjectStats) {
  auto version = GetParam();
  auto manifest = WriteManifest(version, MakeDataFileWithStats());

  auto reader_result = ManifestReader::Make(manifest, file_io_, schema_, spec_);
  ASSERT_THAT(reader_result, IsOk());
  auto reader = std::move(reader_result.value());

  reader->Select({"file_path"});

  auto entries_result = reader->Entries();
  ASSERT_THAT(entries_result, IsOk());
  auto entries = entries_result.value();
  ASSERT_EQ(entries.size(), 1);
  AssertStatsDropped(*entries[0].data_file);
}

TEST_P(TestManifestReaderStats, TestReadEntriesWithSelectCertainStatNotProjectStats) {
  auto version = GetParam();
  auto manifest = WriteManifest(version, MakeDataFileWithStats());

  auto reader_result = ManifestReader::Make(manifest, file_io_, schema_, spec_);
  ASSERT_THAT(reader_result, IsOk());
  auto reader = std::move(reader_result.value());

  reader->Select({"file_path", "value_counts"});

  auto entries_result = reader->Entries();
  ASSERT_THAT(entries_result, IsOk());
  auto entries = entries_result.value();
  ASSERT_EQ(entries.size(), 1);

  const auto& file = *entries[0].data_file;
  EXPECT_FALSE(file.value_counts.empty());
  EXPECT_TRUE(file.null_value_counts.empty());
  EXPECT_TRUE(file.nan_value_counts.empty());
  EXPECT_TRUE(file.lower_bounds.empty());
  EXPECT_TRUE(file.upper_bounds.empty());
}

INSTANTIATE_TEST_SUITE_P(ManifestReaderStatsVersions, TestManifestReaderStats,
                         testing::Values(1, 2, 3));

}  // namespace iceberg
