/*
 * 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 <gtest/gtest.h>
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/string.h>

#include "./testing_object.h"

namespace {

using namespace tvm::ffi;
using namespace tvm::ffi::testing;

TEST(Array, Basic) {
  Array<TInt> arr = {TInt(11), TInt(12)};
  TInt v1 = arr[0];
  EXPECT_EQ(v1->value, 11);
  EXPECT_EQ(v1.use_count(), 2);
  EXPECT_EQ(arr[1]->value, 12);
}

TEST(Array, COWSet) {
  Array<TInt> arr = {TInt(11), TInt(12)};
  Array<TInt> arr2 = arr;
  EXPECT_EQ(arr.use_count(), 2);
  arr.Set(1, TInt(13));
  EXPECT_EQ(arr.use_count(), 1);
  EXPECT_EQ(arr[1]->value, 13);
  EXPECT_EQ(arr2[1]->value, 12);
}

TEST(Array, MutateInPlaceForUniqueReference) {
  TInt x(1);
  Array<TInt> arr{x, x};
  EXPECT_TRUE(arr.unique());
  auto* before = arr.get();

  // NOLINTNEXTLINE(performance-unnecessary-value-param)
  arr.MutateByApply([](TInt) { return TInt(2); });
  auto* after = arr.get();
  EXPECT_EQ(before, after);
}

TEST(Array, CopyWhenMutatingNonUniqueReference) {
  TInt x(1);
  Array<TInt> arr{x, x};
  Array<TInt> arr2 = arr;

  EXPECT_TRUE(!arr.unique());
  auto* before = arr.get();

  // NOLINTNEXTLINE(performance-unnecessary-value-param)
  arr.MutateByApply([](TInt) { return TInt(2); });
  auto* after = arr.get();
  EXPECT_NE(before, after);
}

TEST(Array, Map) {
  // Basic functionality
  TInt x(1), y(1);
  Array<TInt> var_arr{x, y};

  Array<TNumber> expr_arr =
      var_arr.Map([](TInt var) -> TNumber {  // NOLINT(performance-unnecessary-value-param)
        return TFloat(static_cast<double>(var->value + 1));
      });

  EXPECT_NE(var_arr.get(), expr_arr.get());
  EXPECT_TRUE(expr_arr[0]->IsInstance<TFloatObj>());
  EXPECT_TRUE(expr_arr[1]->IsInstance<TFloatObj>());
}

TEST(Array, Iterator) {
  Array<int> array{1, 2, 3};
  std::vector<int> vector(array.begin(), array.end());
  EXPECT_EQ(vector[1], 2);
}

TEST(Array, PushPop) {
  Array<int> a;
  std::vector<int> b;
  for (int i = 0; i < 10; ++i) {
    a.push_back(i);
    b.push_back(i);
    ASSERT_EQ(a.front(), b.front());
    ASSERT_EQ(a.back(), b.back());
    ASSERT_EQ(a.size(), b.size());
    int n = static_cast<int>(a.size());
    for (int j = 0; j < n; ++j) {
      ASSERT_EQ(a[j], b[j]);
    }
  }
  for (int i = 9; i >= 0; --i) {
    ASSERT_EQ(a.front(), b.front());
    ASSERT_EQ(a.back(), b.back());
    ASSERT_EQ(a.size(), b.size());
    a.pop_back();
    b.pop_back();
    int n = static_cast<int>(a.size());
    for (int j = 0; j < n; ++j) {
      ASSERT_EQ(a[j], b[j]);
    }
  }
  ASSERT_EQ(a.empty(), true);
}

TEST(Array, ResizeReserveClear) {
  for (size_t n = 0; n < 10; ++n) {
    Array<int> a;
    Array<int> b;
    a.resize(static_cast<int64_t>(n));
    b.reserve(static_cast<int64_t>(n));
    ASSERT_EQ(a.size(), n);
    ASSERT_GE(a.capacity(), n);
    a.clear();
    b.clear();
    ASSERT_EQ(a.size(), 0);
    ASSERT_EQ(b.size(), 0);
  }
}

TEST(Array, InsertErase) {
  Array<int> a;
  std::vector<int> b;
  for (int n = 1; n <= 10; ++n) {
    a.insert(a.end(), n);
    b.insert(b.end(), n);
    for (int pos = 0; pos <= n; ++pos) {
      a.insert(a.begin() + pos, pos);
      b.insert(b.begin() + pos, pos);
      ASSERT_EQ(a.front(), b.front());
      ASSERT_EQ(a.back(), b.back());
      ASSERT_EQ(a.size(), n + 1);
      ASSERT_EQ(b.size(), n + 1);
      for (int k = 0; k <= n; ++k) {
        ASSERT_EQ(a[k], b[k]);
      }
      a.erase(a.begin() + pos);
      b.erase(b.begin() + pos);
    }
    ASSERT_EQ(a.front(), b.front());
    ASSERT_EQ(a.back(), b.back());
    ASSERT_EQ(a.size(), n);
  }
}

TEST(Array, InsertEraseRange) {
  Array<int> range_a{-1, -2, -3, -4};
  std::vector<int> range_b{-1, -2, -3, -4};
  Array<int> a;
  std::vector<int> b;

  static_assert(std::is_same_v<decltype(*range_a.begin()), int>);
  for (size_t n = 1; n <= 10; ++n) {
    a.insert(a.end(), static_cast<int>(n));
    b.insert(b.end(), static_cast<int>(n));
    for (size_t pos = 0; pos <= n; ++pos) {
      a.insert(a.begin() + static_cast<std::ptrdiff_t>(pos), range_a.begin(), range_a.end());
      b.insert(b.begin() + static_cast<std::ptrdiff_t>(pos), range_b.begin(), range_b.end());
      ASSERT_EQ(a.front(), b.front());
      ASSERT_EQ(a.back(), b.back());
      ASSERT_EQ(a.size(), n + range_a.size());
      ASSERT_EQ(b.size(), n + range_b.size());
      size_t m = n + range_a.size();
      for (size_t k = 0; k < m; ++k) {
        ASSERT_EQ(a[k], b[k]);
      }
      a.erase(a.begin() + static_cast<std::ptrdiff_t>(pos),
              a.begin() + static_cast<std::ptrdiff_t>(pos + range_a.size()));
      b.erase(b.begin() + static_cast<std::ptrdiff_t>(pos),
              b.begin() + static_cast<std::ptrdiff_t>(pos + range_b.size()));
    }
    ASSERT_EQ(a.front(), b.front());
    ASSERT_EQ(a.back(), b.back());
    ASSERT_EQ(a.size(), n);
  }
}

TEST(Array, FuncArrayAnyArg) {
  // NOLINTNEXTLINE(performance-unnecessary-value-param)
  Function fadd_one = Function::FromTyped([](Array<Any> a) -> Any { return a[0].cast<int>() + 1; });
  EXPECT_EQ(fadd_one(Array<Any>{1}).cast<int>(), 2);
}

TEST(Array, MapUniquePropogation) {
  // Basic functionality
  Array<TInt> var_arr{TInt(1), TInt(2)};
  // NOLINTNEXTLINE(performance-unnecessary-value-param)
  var_arr.MutateByApply([](TInt x) -> TInt {
    EXPECT_TRUE(x.unique());
    return x;
  });
}

TEST(Array, AnyImplicitConversion) {
  Array<Any> arr0_mixed = {11.1, 1};
  EXPECT_EQ(arr0_mixed[1].cast<int>(), 1);

  AnyView view0 = arr0_mixed;
  auto arr0_float = view0.cast<Array<double>>();
  // they are not the same because arr_mixed
  // stores arr_mixed[1] as int but we need to convert to float
  EXPECT_TRUE(!arr0_float.same_as(arr0_mixed));
  EXPECT_EQ(arr0_float[1], 1.0);

  Any any1 = arr0_float;
  // if storage check passes, the same array get returned
  auto arr1_float = any1.cast<Array<double>>();
  EXPECT_TRUE(arr1_float.same_as(arr0_float));
  // total count equals 3 include any1
  EXPECT_EQ(arr1_float.use_count(), 3);

  // convert to Array<Any> do not need any conversion
  auto arr1_mixed = any1.cast<Array<Any>>();
  EXPECT_TRUE(arr1_mixed.same_as(arr1_float));
  EXPECT_EQ(arr1_float.use_count(), 4);
}

TEST(Array, AnyConvertCheck) {
  Array<Any> arr = {11.1, 1};
  EXPECT_EQ(arr[1].cast<int>(), 1);

  AnyView view0 = arr;
  auto arr1 = view0.cast<Array<double>>();
  EXPECT_EQ(arr1[0], 11.1);
  EXPECT_EQ(arr1[1], 1.0);

  Any any1 = arr;

  EXPECT_THROW(
      {
        try {
          [[maybe_unused]] auto arr2 = any1.cast<Array<int>>();
        } catch (const Error& error) {
          EXPECT_EQ(error.kind(), "TypeError");
          std::string what = error.what();
          EXPECT_NE(what.find("Cannot convert from type `Array[index 0: float]` to `Array<int>`"),
                    std::string::npos);
          throw;
        }
      },
      ::tvm::ffi::Error);

  Array<Array<TNumber>> arr_nested = {{}, {TInt(1), TFloat(2)}};
  any1 = arr_nested;
  auto arr1_nested = any1.cast<Array<Array<TNumber>>>();
  EXPECT_EQ(arr1_nested.use_count(), 3);

  EXPECT_THROW(
      {
        try {
          [[maybe_unused]] auto arr2 = any1.cast<Array<Array<int>>>();
        } catch (const Error& error) {
          EXPECT_EQ(error.kind(), "TypeError");
          std::string what = error.what();
          EXPECT_NE(what.find("`Array[index 1: Array[index 0: test.Int]]` to `Array<Array<int>>`"),
                    std::string::npos);
          throw;
        }
      },
      ::tvm::ffi::Error);
}

TEST(Array, Upcast) {
  Array<int> a0 = {1, 2, 3};
  Array<Any> a1 = a0;
  EXPECT_EQ(a1[0].cast<int>(), 1);
  EXPECT_EQ(a1[1].cast<int>(), 2);
  EXPECT_EQ(a1[2].cast<int>(), 3);

  Array<Array<int>> a2 = {a0};
  Array<Array<Any>> a3 = a2;
  Array<Array<Any>> a4 = a2;

  static_assert(details::type_contains_v<Array<Any>, Array<int>>);
  static_assert(details::type_contains_v<Any, Array<float>>);
}

TEST(Array, Contains) {
  Function f = Function::GetGlobalRequired("ffi.ArrayContains");

  Array<int> arr = {1, 2, 3, 4, 5};
  EXPECT_TRUE(f(arr, 3).cast<bool>());
  EXPECT_TRUE(f(arr, 1).cast<bool>());
  EXPECT_TRUE(f(arr, 5).cast<bool>());
  EXPECT_FALSE(f(arr, 10).cast<bool>());
  EXPECT_FALSE(f(arr, 0).cast<bool>());

  Array<int> empty_arr;
  EXPECT_FALSE(f(empty_arr, 1).cast<bool>());

  Array<String> str_arr = {String("hello"), String("world")};
  EXPECT_TRUE(f(str_arr, String("hello")).cast<bool>());
  EXPECT_TRUE(f(str_arr, String("world")).cast<bool>());
  EXPECT_FALSE(f(str_arr, String("foo")).cast<bool>());
}

TEST(Array, NegativeIndexThrows) {
  Array<int> arr = {1, 2, 3};
  // Directly test ArrayObj methods, which are the ones modified in this PR.
  // The Array<T> wrapper methods already had negative index checks.
  ArrayObj* arr_obj = arr.GetArrayObj();

  // Test ArrayObj::at (which calls operator[])
  EXPECT_THROW(
      {
        try {
          [[maybe_unused]] const auto& val = arr_obj->at(-1);
        } catch (const Error& error) {
          EXPECT_EQ(error.kind(), "IndexError");
          throw;
        }
      },
      ::tvm::ffi::Error);

  // Test ArrayObj::SetItem
  EXPECT_THROW(
      {
        try {
          arr_obj->SetItem(-1, Any(42));
        } catch (const Error& error) {
          EXPECT_EQ(error.kind(), "IndexError");
          throw;
        }
      },
      ::tvm::ffi::Error);
}

}  // namespace
