/*
 * 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 "iceberg/deletes/position_delete_range_consumer.h"

#include <cstdint>
#include <limits>
#include <set>
#include <span>
#include <vector>

#include <gtest/gtest.h>

#include "iceberg/deletes/position_delete_index.h"
#include "iceberg/deletes/roaring_position_bitmap.h"

namespace iceberg {

namespace {

// Reference set: positions a per-pos `Delete(pos)` loop would accept.
std::set<int64_t> ExpectedValidSet(const std::vector<int64_t>& positions) {
  std::set<int64_t> expected;
  for (int64_t pos : positions) {
    if (pos >= 0 && pos <= RoaringPositionBitmap::kMaxPosition) {
      expected.insert(pos);
    }
  }
  return expected;
}

// Strict contents check: cardinality plus per-position membership.
// Weaker checks would miss divergences at the 32-bit key boundary.
void AssertMatchesBaseline(const std::vector<int64_t>& positions) {
  PositionDeleteIndex index;
  std::vector<uint32_t> scratch;
  ForEachPositionDelete(std::span<const int64_t>(positions), index, scratch);
  const auto expected = ExpectedValidSet(positions);
  ASSERT_EQ(index.Cardinality(), static_cast<int64_t>(expected.size()))
      << "input size=" << positions.size();
  for (int64_t pos : expected) {
    ASSERT_TRUE(index.IsDeleted(pos)) << "missing pos=" << pos;
  }
}

}  // namespace

TEST(PositionDeleteRangeConsumerTest, EmptySpan) { AssertMatchesBaseline({}); }

TEST(PositionDeleteRangeConsumerTest, SinglePosition) { AssertMatchesBaseline({42}); }

TEST(PositionDeleteRangeConsumerTest, FullyContiguousRunBecomesSingleRange) {
  std::vector<int64_t> positions;
  for (int64_t i = 100; i < 200; ++i) {
    positions.push_back(i);
  }
  AssertMatchesBaseline(positions);
}

TEST(PositionDeleteRangeConsumerTest, AlternatingPositionsProduceNoCoalescing) {
  std::vector<int64_t> positions;
  for (int64_t i = 0; i < 50; ++i) {
    positions.push_back(i * 2);
  }
  AssertMatchesBaseline(positions);
}

TEST(PositionDeleteRangeConsumerTest, MixedShortAndLongRuns) {
  AssertMatchesBaseline({1, 2, 3, 7, 10, 11, 20, 30, 31, 32, 33, 34});
}

TEST(PositionDeleteRangeConsumerTest, UnsortedInputStillCorrect) {
  AssertMatchesBaseline({10, 5, 11, 12, 4, 13, 100});
}

TEST(PositionDeleteRangeConsumerTest, DuplicatesAreIdempotent) {
  AssertMatchesBaseline({5, 5, 5, 6, 6, 7});
}

TEST(PositionDeleteRangeConsumerTest, InvalidPositionsSilentlySkipped) {
  // Invalids at the edges, mid-run, and mixed with valid contiguous runs
  // must all be dropped without breaking coalescing around them. We stay
  // well below `kMaxPosition` to avoid forcing the bitmap to resize its
  // backing vector to ~2^31 empty containers.
  AssertMatchesBaseline({std::numeric_limits<int64_t>::min(), -5, -4, 10, 11, -999, 12,
                         13, RoaringPositionBitmap::kMaxPosition + 1,
                         std::numeric_limits<int64_t>::max()});
}

TEST(PositionDeleteRangeConsumerTest, ContiguousRunAcrossKeyBoundary) {
  // Pins `last_position + 1` and the adjacency check at a non-zero
  // high-32 key. The coalesced run must survive the key transition.
  constexpr int64_t kBoundary = int64_t{1} << 32;
  std::vector<int64_t> positions;
  for (int64_t i = kBoundary - 3; i < kBoundary + 3; ++i) {
    positions.push_back(i);
  }
  AssertMatchesBaseline(positions);
}

TEST(PositionDeleteRangeConsumerTest, DispatcherAgreesAtBothDensities) {
  // Above the sniff threshold at densities below and above the 10%
  // cutoff. We can't observe the choice directly; agreement with the
  // baseline is the contract.
  std::vector<int64_t> low_density;
  std::vector<int64_t> high_density;
  int64_t lo = 0;
  int64_t hi = 0;
  for (int64_t i = 0; i < 2'048; ++i) {
    low_density.push_back(lo);
    ++lo;
    if ((i + 1) % 20 == 0) {
      lo += 5;
    }
    high_density.push_back(hi);
    hi += ((i % 5 == 0) ? 5 : 1);
  }
  AssertMatchesBaseline(low_density);
  AssertMatchesBaseline(high_density);
}

TEST(PositionDeleteRangeConsumerTest, DispatcherSkipsSniffOnSmallInputs) {
  // Below the 64-element threshold the dispatcher bypasses the sniff.
  // Exercise both a scattered tiny input (where bulk would win at large
  // n) and a contiguous tiny input (the range path always wins).
  std::vector<int64_t> scattered;
  std::vector<int64_t> contiguous;
  for (int64_t i = 0; i < 32; ++i) {
    scattered.push_back(i * 100);
    contiguous.push_back(i);
  }
  AssertMatchesBaseline(scattered);
  AssertMatchesBaseline(contiguous);
}

TEST(PositionDeleteRangeConsumerTest, DispatcherAgreesAtThresholdBoundary) {
  // The dispatcher selects the bulk path when
  //   boundaries * 100 > (sniff - 1) * kBulkThresholdPercent
  // With `sniff = 1024` and `kBulkThresholdPercent = 10`, the cutoff is
  // 102.3 boundaries: 102 stays on coalesce, 103 flips to bulk. Both
  // inputs must still produce the same cardinality and membership as
  // the per-position baseline; this test guards against arithmetic
  // regressions around the threshold constant.
  auto build = [](int64_t target_boundaries) {
    std::vector<int64_t> positions;
    positions.reserve(1024);
    int64_t pos = 0;
    positions.push_back(pos);
    for (int64_t i = 1; i < 1024; ++i) {
      pos += (i <= target_boundaries) ? 2 : 1;
      positions.push_back(pos);
    }
    return positions;
  };
  AssertMatchesBaseline(build(/*target_boundaries=*/102));
  AssertMatchesBaseline(build(/*target_boundaries=*/103));
}

}  // namespace iceberg
