// Copyright 2014 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_UNITTESTS_TEST_UTILS_H_
#define V8_UNITTESTS_TEST_UTILS_H_

#include <vector>

#include "include/v8.h"
#include "src/api/api-inl.h"
#include "src/base/macros.h"
#include "src/base/utils/random-number-generator.h"
#include "src/handles.h"
#include "src/objects-inl.h"
#include "src/objects.h"
#include "src/zone/accounting-allocator.h"
#include "src/zone/zone.h"
#include "testing/gtest-support.h"

namespace v8 {

class ArrayBufferAllocator;

typedef std::map<std::string, int> CounterMap;

// RAII-like Isolate instance wrapper.
class IsolateWrapper final {
 public:
  // When enforce_pointer_compression is true the Isolate is created with
  // enabled pointer compression. When it's false then the Isolate is created
  // with the default pointer compression state for current build.
  explicit IsolateWrapper(CounterLookupCallback counter_lookup_callback,
                          bool enforce_pointer_compression = false);
  ~IsolateWrapper();

  v8::Isolate* isolate() const { return isolate_; }

 private:
  v8::ArrayBuffer::Allocator* array_buffer_allocator_;
  v8::Isolate* isolate_;

  DISALLOW_COPY_AND_ASSIGN(IsolateWrapper);
};

class SharedIsolateHolder final {
 public:
  static v8::Isolate* isolate() { return isolate_wrapper_->isolate(); }

  static void CreateIsolate() {
    CHECK_NULL(isolate_wrapper_);
    isolate_wrapper_ =
        new IsolateWrapper([](const char* name) -> int* { return nullptr; });
  }

  static void DeleteIsolate() {
    CHECK_NOT_NULL(isolate_wrapper_);
    delete isolate_wrapper_;
    isolate_wrapper_ = nullptr;
  }

 private:
  static v8::IsolateWrapper* isolate_wrapper_;

  DISALLOW_IMPLICIT_CONSTRUCTORS(SharedIsolateHolder);
};

class SharedIsolateAndCountersHolder final {
 public:
  static v8::Isolate* isolate() { return isolate_wrapper_->isolate(); }

  static void CreateIsolate() {
    CHECK_NULL(counter_map_);
    CHECK_NULL(isolate_wrapper_);
    counter_map_ = new CounterMap();
    isolate_wrapper_ = new IsolateWrapper(LookupCounter);
  }

  static void DeleteIsolate() {
    CHECK_NOT_NULL(counter_map_);
    CHECK_NOT_NULL(isolate_wrapper_);
    delete isolate_wrapper_;
    isolate_wrapper_ = nullptr;
    delete counter_map_;
    counter_map_ = nullptr;
  }

 private:
  static int* LookupCounter(const char* name);
  static CounterMap* counter_map_;
  static v8::IsolateWrapper* isolate_wrapper_;

  DISALLOW_IMPLICIT_CONSTRUCTORS(SharedIsolateAndCountersHolder);
};

//
// A set of mixins from which the test fixtures will be constructed.
//
template <typename TMixin>
class WithPrivateIsolateMixin : public TMixin {
 public:
  explicit WithPrivateIsolateMixin(bool enforce_pointer_compression = false)
      : isolate_wrapper_([](const char* name) -> int* { return nullptr; },
                         enforce_pointer_compression) {}

  v8::Isolate* v8_isolate() const { return isolate_wrapper_.isolate(); }

  static void SetUpTestCase() { TMixin::SetUpTestCase(); }
  static void TearDownTestCase() { TMixin::TearDownTestCase(); }

 private:
  v8::IsolateWrapper isolate_wrapper_;

  DISALLOW_COPY_AND_ASSIGN(WithPrivateIsolateMixin);
};

template <typename TMixin, typename TSharedIsolateHolder = SharedIsolateHolder>
class WithSharedIsolateMixin : public TMixin {
 public:
  WithSharedIsolateMixin() = default;

  v8::Isolate* v8_isolate() const { return TSharedIsolateHolder::isolate(); }

  static void SetUpTestCase() {
    TMixin::SetUpTestCase();
    TSharedIsolateHolder::CreateIsolate();
  }

  static void TearDownTestCase() {
    TSharedIsolateHolder::DeleteIsolate();
    TMixin::TearDownTestCase();
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(WithSharedIsolateMixin);
};

template <typename TMixin>
class WithPointerCompressionIsolateMixin
    : public WithPrivateIsolateMixin<TMixin> {
 public:
  WithPointerCompressionIsolateMixin()
      : WithPrivateIsolateMixin<TMixin>(true) {}

 private:
  DISALLOW_COPY_AND_ASSIGN(WithPointerCompressionIsolateMixin);
};

template <typename TMixin>
class WithIsolateScopeMixin : public TMixin {
 public:
  WithIsolateScopeMixin()
      : isolate_scope_(v8_isolate()), handle_scope_(v8_isolate()) {}

  v8::Isolate* isolate() const { return v8_isolate(); }
  v8::Isolate* v8_isolate() const { return TMixin::v8_isolate(); }

  v8::internal::Isolate* i_isolate() const {
    return reinterpret_cast<v8::internal::Isolate*>(v8_isolate());
  }

  static void SetUpTestCase() { TMixin::SetUpTestCase(); }
  static void TearDownTestCase() { TMixin::TearDownTestCase(); }

 private:
  v8::Isolate::Scope isolate_scope_;
  v8::HandleScope handle_scope_;

  DISALLOW_COPY_AND_ASSIGN(WithIsolateScopeMixin);
};

template <typename TMixin>
class WithContextMixin : public TMixin {
 public:
  WithContextMixin()
      : context_(Context::New(v8_isolate())), context_scope_(context_) {}

  v8::Isolate* v8_isolate() const { return TMixin::v8_isolate(); }

  const Local<Context>& context() const { return v8_context(); }
  const Local<Context>& v8_context() const { return context_; }

  Local<Value> RunJS(const char* source) {
    return RunJS(v8::String::NewFromUtf8(v8_isolate(), source,
                                         v8::NewStringType::kNormal)
                     .ToLocalChecked());
  }

  Local<Value> RunJS(v8::String::ExternalOneByteStringResource* source) {
    return RunJS(
        v8::String::NewExternalOneByte(v8_isolate(), source).ToLocalChecked());
  }

  v8::Local<v8::String> NewString(const char* string) {
    return v8::String::NewFromUtf8(v8_isolate(), string,
                                   v8::NewStringType::kNormal)
        .ToLocalChecked();
  }

  void SetGlobalProperty(const char* name, v8::Local<v8::Value> value) {
    CHECK(v8_context()
              ->Global()
              ->Set(v8_context(), NewString(name), value)
              .FromJust());
  }

  static void SetUpTestCase() { TMixin::SetUpTestCase(); }
  static void TearDownTestCase() { TMixin::TearDownTestCase(); }

 private:
  Local<Value> RunJS(Local<String> source) {
    auto context = v8_isolate()->GetCurrentContext();
    Local<Script> script =
        v8::Script::Compile(context, source).ToLocalChecked();
    return script->Run(context).ToLocalChecked();
  }

  v8::Local<v8::Context> context_;
  v8::Context::Scope context_scope_;

  DISALLOW_COPY_AND_ASSIGN(WithContextMixin);
};

// Use v8::internal::TestWithIsolate if you are testing internals,
// aka. directly work with Handles.
using TestWithIsolate =          //
    WithIsolateScopeMixin<       //
        WithSharedIsolateMixin<  //
            ::testing::Test>>;

// Use v8::internal::TestWithNativeContext if you are testing internals,
// aka. directly work with Handles.
using TestWithContext =              //
    WithContextMixin<                //
        WithIsolateScopeMixin<       //
            WithSharedIsolateMixin<  //
                ::testing::Test>>>;

using TestWithIsolateAndPointerCompression =     //
    WithContextMixin<                            //
        WithIsolateScopeMixin<                   //
            WithPointerCompressionIsolateMixin<  //
                ::testing::Test>>>;

namespace internal {

// Forward declarations.
class Factory;

template <typename TMixin>
class WithInternalIsolateMixin : public TMixin {
 public:
  WithInternalIsolateMixin() = default;

  Factory* factory() const { return isolate()->factory(); }
  Isolate* isolate() const { return TMixin::i_isolate(); }

  Handle<NativeContext> native_context() const {
    return isolate()->native_context();
  }

  template <typename T = Object>
  Handle<T> RunJS(const char* source) {
    return Handle<T>::cast(RunJSInternal(source));
  }

  Handle<Object> RunJSInternal(const char* source) {
    return Utils::OpenHandle(*TMixin::RunJS(source));
  }

  template <typename T = Object>
  Handle<T> RunJS(::v8::String::ExternalOneByteStringResource* source) {
    return Handle<T>::cast(RunJSInternal(source));
  }

  Handle<Object> RunJSInternal(
      ::v8::String::ExternalOneByteStringResource* source) {
    return Utils::OpenHandle(*TMixin::RunJS(source));
  }

  base::RandomNumberGenerator* random_number_generator() const {
    return isolate()->random_number_generator();
  }

  static void SetUpTestCase() { TMixin::SetUpTestCase(); }
  static void TearDownTestCase() { TMixin::TearDownTestCase(); }

 private:
  DISALLOW_COPY_AND_ASSIGN(WithInternalIsolateMixin);
};

template <typename TMixin>
class WithZoneMixin : public TMixin {
 public:
  WithZoneMixin() : zone_(&allocator_, ZONE_NAME) {}

  Zone* zone() { return &zone_; }

  static void SetUpTestCase() { TMixin::SetUpTestCase(); }
  static void TearDownTestCase() { TMixin::TearDownTestCase(); }

 private:
  v8::internal::AccountingAllocator allocator_;
  Zone zone_;

  DISALLOW_COPY_AND_ASSIGN(WithZoneMixin);
};

using TestWithIsolate =              //
    WithInternalIsolateMixin<        //
        WithIsolateScopeMixin<       //
            WithSharedIsolateMixin<  //
                ::testing::Test>>>;

using TestWithZone = WithZoneMixin<::testing::Test>;

using TestWithIsolateAndZone =       //
    WithInternalIsolateMixin<        //
        WithIsolateScopeMixin<       //
            WithSharedIsolateMixin<  //
                WithZoneMixin<       //
                    ::testing::Test>>>>;

using TestWithNativeContext =            //
    WithInternalIsolateMixin<            //
        WithContextMixin<                //
            WithIsolateScopeMixin<       //
                WithSharedIsolateMixin<  //
                    ::testing::Test>>>>;

using TestWithNativeContextAndCounters =  //
    WithInternalIsolateMixin<             //
        WithContextMixin<                 //
            WithIsolateScopeMixin<        //
                WithSharedIsolateMixin<   //
                    ::testing::Test,      //
                    SharedIsolateAndCountersHolder>>>>;

using TestWithNativeContextAndZone =         //
    WithZoneMixin<                           //
        WithInternalIsolateMixin<            //
            WithContextMixin<                //
                WithIsolateScopeMixin<       //
                    WithSharedIsolateMixin<  //
                        ::testing::Test>>>>>;

class SaveFlags {
 public:
  SaveFlags();
  ~SaveFlags();

 private:
#define FLAG_MODE_APPLY(ftype, ctype, nam, def, cmt) ctype SAVED_##nam;
#include "src/flag-definitions.h"  // NOLINT
#undef FLAG_MODE_APPLY

  DISALLOW_COPY_AND_ASSIGN(SaveFlags);
};

// For GTest.
inline void PrintTo(Object o, ::std::ostream* os) {
  *os << reinterpret_cast<void*>(o.ptr());
}
inline void PrintTo(Smi o, ::std::ostream* os) {
  *os << reinterpret_cast<void*>(o.ptr());
}

}  // namespace internal
}  // namespace v8

#endif  // V8_UNITTESTS_TEST_UTILS_H_