// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef V8_TEST_CCTEST_TEST_SWISS_NAME_DICTIONARY_INFRA_H_
#define V8_TEST_CCTEST_TEST_SWISS_NAME_DICTIONARY_INFRA_H_

#include <memory>
#include <utility>

#include "src/codegen/code-stub-assembler.h"
#include "src/init/v8.h"
#include "src/objects/objects-inl.h"
#include "src/objects/swiss-name-dictionary-inl.h"
#include "test/cctest/cctest.h"
#include "test/cctest/compiler/code-assembler-tester.h"
#include "test/cctest/compiler/function-tester.h"

namespace v8 {
namespace internal {
namespace test_swiss_hash_table {

using Value = std::string;
using ValueOpt = base::Optional<Value>;
using PropertyDetailsOpt = base::Optional<PropertyDetails>;
using IndexOpt = base::Optional<InternalIndex>;

static const ValueOpt kNoValue;
static const PropertyDetailsOpt kNoDetails;
static const base::Optional<int> kNoInt;
static const IndexOpt kIndexUnknown;

static const std::vector<int> interesting_initial_capacities = {
    4,
    8,
    16,
    128,
    1 << (sizeof(uint16_t) * 8),
    1 << (sizeof(uint16_t) * 8 + 1)};

// Capacities for tests that may timeout on larger capacities when
// sanitizers/CFI are enabled.
// TODO(v8:11330) Revisit this once the actual CSA/Torque versions are run by
// the test suite, which will speed things up.
#if defined(THREAD_SANITIZER) || defined(V8_ENABLE_CONTROL_FLOW_INTEGRITY)
static const std::vector<int> capacities_for_slow_sanitizer_tests = {4, 8, 16,
                                                                     128, 1024};
#else
static const std::vector<int> capacities_for_slow_sanitizer_tests =
    interesting_initial_capacities;
#endif

// Capacities for tests that are generally slow, so that they don't use the
// maximum capacities in debug mode.
// TODO(v8:11330) Revisit this once the actual CSA/Torque versions are run by
// the test suite, which will speed things up.
#if DEBUG
static const std::vector<int> capacities_for_slow_debug_tests = {4, 8, 16, 128,
                                                                 1024};
#else
static const std::vector<int> capacities_for_slow_debug_tests =
    interesting_initial_capacities;
#endif

extern const std::vector<PropertyDetails> distinct_property_details;

// Wrapping this in a struct makes the tests a bit more readable.
struct FakeH1 {
  uint32_t value;

  explicit FakeH1(int value) : value{static_cast<uint32_t>(value)} {}

  bool operator==(const FakeH1& other) const { return value == other.value; }
};

// Wrapping this in a struct makes the tests a bit more readable.
struct FakeH2 {
  uint8_t value;

  bool operator==(const FakeH2& other) const { return value == other.value; }
};

using FakeH1Opt = base::Optional<FakeH1>;
using FakeH2Opt = base::Optional<FakeH2>;

// Representation of keys used when writing test cases.
struct Key {
  std::string str;

  // If present, contains the value we faked the key's H1 hash with.
  FakeH1Opt h1_override = FakeH1Opt();

  // If present, contains the value we faked the key's H2 hash with.
  FakeH2Opt h2_override = FakeH2Opt();
};

// Internal representation of keys. See |create_key_with_hash| for details.
struct CachedKey {
  Handle<Symbol> key_symbol;

  // If present, contains the value we faked the key's H1 hash with.
  FakeH1Opt h1_override;

  // If present, contains the value we faked the key's H2 hash with.
  FakeH2Opt h2_override;
};

using KeyCache = std::unordered_map<std::string, CachedKey>;

Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
                               const Key& key);

class RuntimeTestRunner;
class CSATestRunner;

// Abstraction over executing a sequence of operations on a single hash table.
// Actually performing those operations is done by the TestRunner.
template <typename TestRunner>
class TestSequence {
 public:
  explicit TestSequence(Isolate* isolate, int initial_capacity)
      : isolate{isolate},
        initial_capacity{initial_capacity},
        keys_{},
        runner_{isolate, initial_capacity, keys_} {}

  // Determines whether or not to run VerifyHeap after each operation. Can make
  // debugging easier.
  static constexpr bool kVerifyAfterEachStep = false;

  void Add(Handle<Name> key, Handle<Object> value, PropertyDetails details) {
    runner_.Add(key, value, details);

    if (kVerifyAfterEachStep) {
      runner_.VerifyHeap();
    }
  }

  void Add(const Key& key, ValueOpt value = kNoValue,
           PropertyDetailsOpt details = kNoDetails) {
    if (!value) {
      value = "dummy_value";
    }

    if (!details) {
      details = PropertyDetails::Empty();
    }

    Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, key);
    Handle<Object> value_handle = isolate->factory()->NewStringFromAsciiChecked(
        value.value().c_str(), AllocationType::kYoung);

    Add(key_handle, value_handle, details.value());
  }

  void UpdateByKey(Handle<Name> key, Handle<Object> new_value,
                   PropertyDetails new_details) {
    InternalIndex entry = runner_.FindEntry(key);
    CHECK(entry.is_found());
    runner_.Put(entry, new_value, new_details);

    if (kVerifyAfterEachStep) {
      runner_.VerifyHeap();
    }
  }

  void UpdateByKey(const Key& existing_key, Value new_value,
                   PropertyDetails new_details) {
    Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, existing_key);
    Handle<Object> value_handle = isolate->factory()->NewStringFromAsciiChecked(
        new_value.c_str(), AllocationType::kYoung);

    UpdateByKey(key_handle, value_handle, new_details);
  }

  void DeleteByKey(Handle<Name> key) {
    InternalIndex entry = runner_.FindEntry(key);
    CHECK(entry.is_found());
    runner_.Delete(entry);

    if (kVerifyAfterEachStep) {
      runner_.VerifyHeap();
    }
  }

  void DeleteByKey(const Key& existing_key) {
    Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, existing_key);

    DeleteByKey(key_handle);
  }

  void CheckDataAtKey(Handle<Name> key, IndexOpt expected_index_opt,
                      base::Optional<Handle<Object>> expected_value_opt,
                      PropertyDetailsOpt expected_details_opt) {
    InternalIndex actual_index = runner_.FindEntry(key);

    if (expected_index_opt) {
      CHECK_EQ(expected_index_opt.value(), actual_index);
    }

    if (actual_index.is_found()) {
      Handle<FixedArray> data = runner_.GetData(actual_index);
      CHECK_EQ(*key, data->get(0));

      if (expected_value_opt) {
        CHECK(expected_value_opt.value()->StrictEquals(data->get(1)));
      }

      if (expected_details_opt) {
        CHECK_EQ(expected_details_opt.value().AsSmi(), data->get(2));
      }
    }
  }

  void CheckDataAtKey(const Key& expected_key, IndexOpt expected_index,
                      ValueOpt expected_value = kNoValue,
                      PropertyDetailsOpt expected_details = kNoDetails) {
    Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, expected_key);
    base::Optional<Handle<Object>> value_handle_opt;
    if (expected_value) {
      value_handle_opt = isolate->factory()->NewStringFromAsciiChecked(
          expected_value.value().c_str(), AllocationType::kYoung);
    }

    CheckDataAtKey(key_handle, expected_index, value_handle_opt,
                   expected_details);
  }

  void CheckKeyAbsent(Handle<Name> key) {
    CHECK(runner_.FindEntry(key).is_not_found());
  }

  void CheckKeyAbsent(const Key& expected_key) {
    Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, expected_key);
    CheckKeyAbsent(key_handle);
  }

  void CheckHasKey(const Key& expected_key) {
    Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, expected_key);

    CHECK(runner_.FindEntry(key_handle).is_found());
  }

  void CheckCounts(base::Optional<int> capacity,
                   base::Optional<int> elements = base::Optional<int>(),
                   base::Optional<int> deleted = base::Optional<int>()) {
    runner_.CheckCounts(capacity, elements, deleted);
  }

  void CheckEnumerationOrder(const std::vector<std::string>& keys) {
    runner_.CheckEnumerationOrder(keys);
  }

  void RehashInplace() { runner_.RehashInplace(); }

  void Shrink() { runner_.Shrink(); }

  void CheckCopy() { runner_.CheckCopy(); }

  static constexpr bool IsRuntimeTest() {
    return std::is_same<TestRunner, RuntimeTestRunner>::value;
  }

  void VerifyHeap() { runner_.VerifyHeap(); }

  // Just for debugging
  void Print() { runner_.PrintTable(); }

  static std::vector<int> boundary_indices(int capacity) {
    if (capacity == 4 && SwissNameDictionary::MaxUsableCapacity(4) < 4) {
      // If we cannot put 4 entries in a capacity 4 table without resizing, just
      // work with 3 boundary indices.
      return {0, capacity - 2, capacity - 1};
    }
    return {0, 1, capacity - 2, capacity - 1};
  }

  // Contains all possible PropertyDetails suitable for storing in a
  // SwissNameDictionary (i.e., PropertyDetails for dictionary mode objects
  // without storing an enumeration index). Used to ensure that we can correctly
  // store an retrieve all possible such PropertyDetails.
  static const std::vector<PropertyDetails> distinct_property_details;

  static void WithAllInterestingInitialCapacities(
      std::function<void(TestSequence&)> manipulate_sequence) {
    WithInitialCapacities(interesting_initial_capacities, manipulate_sequence);
  }

  static void WithInitialCapacity(
      int capacity, std::function<void(TestSequence&)> manipulate_sequence) {
    WithInitialCapacities({capacity}, manipulate_sequence);
  }

  // For each capacity in |capacities|, create a TestSequence and run the given
  // function on it.
  static void WithInitialCapacities(
      const std::vector<int>& capacities,
      std::function<void(TestSequence&)> manipulate_sequence) {
    for (int capacity : capacities) {
      Isolate* isolate = CcTest::InitIsolateOnce();
      HandleScope scope{isolate};
      TestSequence<TestRunner> s(isolate, capacity);
      manipulate_sequence(s);
    }
  }

  Isolate* const isolate;
  const int initial_capacity;

 private:
  // Caches keys used in this TestSequence. See |create_key_with_hash| for
  // details.
  KeyCache keys_;
  TestRunner runner_;
};

}  // namespace test_swiss_hash_table
}  // namespace internal
}  // namespace v8

#endif  // V8_TEST_CCTEST_TEST_SWISS_NAME_DICTIONARY_INFRA_H_
