// Copyright 2020 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.

#include "include/cppgc/member.h"

#include <algorithm>
#include <vector>

#include "include/cppgc/allocation.h"
#include "include/cppgc/garbage-collected.h"
#include "include/cppgc/persistent.h"
#include "include/cppgc/type-traits.h"
#include "test/unittests/heap/cppgc/tests.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cppgc {
namespace internal {

namespace {

struct GCed : GarbageCollected<GCed> {
  virtual void Trace(cppgc::Visitor*) const {}
};
struct DerivedGCed : GCed {
  void Trace(cppgc::Visitor* v) const override { GCed::Trace(v); }
};

// Compile tests.
static_assert(!IsWeakV<Member<GCed>>, "Member is always strong.");
static_assert(IsWeakV<WeakMember<GCed>>, "WeakMember is always weak.");

static_assert(IsMemberTypeV<Member<GCed>>, "Member must be Member.");
static_assert(!IsMemberTypeV<WeakMember<GCed>>,
              "WeakMember must not be Member.");
static_assert(!IsMemberTypeV<UntracedMember<GCed>>,
              "UntracedMember must not be Member.");
static_assert(!IsMemberTypeV<int>, "int must not be Member.");
static_assert(!IsWeakMemberTypeV<Member<GCed>>,
              "Member must not be WeakMember.");
static_assert(IsWeakMemberTypeV<WeakMember<GCed>>,
              "WeakMember must be WeakMember.");
static_assert(!IsWeakMemberTypeV<UntracedMember<GCed>>,
              "UntracedMember must not be WeakMember.");
static_assert(!IsWeakMemberTypeV<int>, "int must not be WeakMember.");
static_assert(!IsUntracedMemberTypeV<Member<GCed>>,
              "Member must not be UntracedMember.");
static_assert(!IsUntracedMemberTypeV<WeakMember<GCed>>,
              "WeakMember must not be UntracedMember.");
static_assert(IsUntracedMemberTypeV<UntracedMember<GCed>>,
              "UntracedMember must be UntracedMember.");
static_assert(!IsUntracedMemberTypeV<int>, "int must not be UntracedMember.");

struct CustomWriteBarrierPolicy {
  static size_t InitializingWriteBarriersTriggered;
  static size_t AssigningWriteBarriersTriggered;
  static void InitializingBarrier(const void* slot, const void* value) {
    ++InitializingWriteBarriersTriggered;
  }
  static void AssigningBarrier(const void* slot, const void* value) {
    ++AssigningWriteBarriersTriggered;
  }
};
size_t CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered = 0;
size_t CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered = 0;

using MemberWithCustomBarrier =
    BasicMember<GCed, StrongMemberTag, CustomWriteBarrierPolicy>;

struct CustomCheckingPolicy {
  static std::vector<GCed*> Cached;
  static size_t ChecksTriggered;
  void CheckPointer(const void* ptr) {
    EXPECT_NE(Cached.cend(), std::find(Cached.cbegin(), Cached.cend(), ptr));
    ++ChecksTriggered;
  }
};
std::vector<GCed*> CustomCheckingPolicy::Cached;
size_t CustomCheckingPolicy::ChecksTriggered = 0;

using MemberWithCustomChecking =
    BasicMember<GCed, StrongMemberTag, DijkstraWriteBarrierPolicy,
                CustomCheckingPolicy>;

class MemberTest : public testing::TestSupportingAllocationOnly {};

}  // namespace

template <template <typename> class MemberType>
void EmptyTest() {
  {
    MemberType<GCed> empty;
    EXPECT_EQ(nullptr, empty.Get());
    EXPECT_EQ(nullptr, empty.Release());
  }
  {
    MemberType<GCed> empty = nullptr;
    EXPECT_EQ(nullptr, empty.Get());
    EXPECT_EQ(nullptr, empty.Release());
  }
  {
    // Move-constructs empty from another Member that is created from nullptr.
    MemberType<const GCed> empty = nullptr;
    EXPECT_EQ(nullptr, empty.Get());
    EXPECT_EQ(nullptr, empty.Release());
  }
}

TEST_F(MemberTest, Empty) {
  EmptyTest<Member>();
  EmptyTest<WeakMember>();
  EmptyTest<UntracedMember>();
}

template <template <typename> class MemberType>
void ClearTest(cppgc::Heap* heap) {
  MemberType<GCed> member =
      MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
  EXPECT_NE(nullptr, member.Get());
  member.Clear();
  EXPECT_EQ(nullptr, member.Get());
}

TEST_F(MemberTest, Clear) {
  cppgc::Heap* heap = GetHeap();
  ClearTest<Member>(heap);
  ClearTest<WeakMember>(heap);
  ClearTest<UntracedMember>(heap);
}

template <template <typename> class MemberType>
void ReleaseTest(cppgc::Heap* heap) {
  GCed* gced = MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
  MemberType<GCed> member = gced;
  EXPECT_NE(nullptr, member.Get());
  GCed* raw = member.Release();
  EXPECT_EQ(gced, raw);
  EXPECT_EQ(nullptr, member.Get());
}

TEST_F(MemberTest, Release) {
  cppgc::Heap* heap = GetHeap();
  ReleaseTest<Member>(heap);
  ReleaseTest<WeakMember>(heap);
  ReleaseTest<UntracedMember>(heap);
}

template <template <typename> class MemberType1,
          template <typename> class MemberType2>
void SwapTest(cppgc::Heap* heap) {
  GCed* gced1 = MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
  GCed* gced2 = MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
  MemberType1<GCed> member1 = gced1;
  MemberType2<GCed> member2 = gced2;
  EXPECT_EQ(gced1, member1.Get());
  EXPECT_EQ(gced2, member2.Get());
  member1.Swap(member2);
  EXPECT_EQ(gced2, member1.Get());
  EXPECT_EQ(gced1, member2.Get());
}

TEST_F(MemberTest, Swap) {
  cppgc::Heap* heap = GetHeap();
  SwapTest<Member, Member>(heap);
  SwapTest<Member, WeakMember>(heap);
  SwapTest<Member, UntracedMember>(heap);
  SwapTest<WeakMember, Member>(heap);
  SwapTest<WeakMember, WeakMember>(heap);
  SwapTest<WeakMember, UntracedMember>(heap);
  SwapTest<UntracedMember, Member>(heap);
  SwapTest<UntracedMember, WeakMember>(heap);
  SwapTest<UntracedMember, UntracedMember>(heap);
}

template <template <typename> class MemberType1,
          template <typename> class MemberType2>
void MoveTest(cppgc::Heap* heap) {
  {
    GCed* gced1 = MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType1<GCed> member1 = gced1;
    MemberType2<GCed> member2(std::move(member1));
    // Move-from member must be in empty state.
    EXPECT_FALSE(member1);
    EXPECT_EQ(gced1, member2.Get());
  }
  {
    GCed* gced1 = MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType1<GCed> member1 = gced1;
    MemberType2<GCed> member2;
    member2 = std::move(member1);
    // Move-from member must be in empty state.
    EXPECT_FALSE(member1);
    EXPECT_EQ(gced1, member2.Get());
  }
}

TEST_F(MemberTest, Move) {
  cppgc::Heap* heap = GetHeap();
  MoveTest<Member, Member>(heap);
  MoveTest<Member, WeakMember>(heap);
  MoveTest<Member, UntracedMember>(heap);
  MoveTest<WeakMember, Member>(heap);
  MoveTest<WeakMember, WeakMember>(heap);
  MoveTest<WeakMember, UntracedMember>(heap);
  MoveTest<UntracedMember, Member>(heap);
  MoveTest<UntracedMember, WeakMember>(heap);
  MoveTest<UntracedMember, UntracedMember>(heap);
}

template <template <typename> class MemberType1,
          template <typename> class MemberType2>
void HeterogeneousConversionTest(cppgc::Heap* heap) {
  {
    MemberType1<GCed> member1 =
        MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType2<GCed> member2 = member1;
    EXPECT_EQ(member1.Get(), member2.Get());
  }
  {
    MemberType1<DerivedGCed> member1 =
        MakeGarbageCollected<DerivedGCed>(heap->GetAllocationHandle());
    MemberType2<GCed> member2 = member1;
    EXPECT_EQ(member1.Get(), member2.Get());
  }
  {
    MemberType1<GCed> member1 =
        MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType2<GCed> member2;
    member2 = member1;
    EXPECT_EQ(member1.Get(), member2.Get());
  }
  {
    MemberType1<DerivedGCed> member1 =
        MakeGarbageCollected<DerivedGCed>(heap->GetAllocationHandle());
    MemberType2<GCed> member2;
    member2 = member1;
    EXPECT_EQ(member1.Get(), member2.Get());
  }
}

TEST_F(MemberTest, HeterogeneousInterface) {
  cppgc::Heap* heap = GetHeap();
  HeterogeneousConversionTest<Member, Member>(heap);
  HeterogeneousConversionTest<Member, WeakMember>(heap);
  HeterogeneousConversionTest<Member, UntracedMember>(heap);
  HeterogeneousConversionTest<WeakMember, Member>(heap);
  HeterogeneousConversionTest<WeakMember, WeakMember>(heap);
  HeterogeneousConversionTest<WeakMember, UntracedMember>(heap);
  HeterogeneousConversionTest<UntracedMember, Member>(heap);
  HeterogeneousConversionTest<UntracedMember, WeakMember>(heap);
  HeterogeneousConversionTest<UntracedMember, UntracedMember>(heap);
}

template <template <typename> class MemberType,
          template <typename> class PersistentType>
void PersistentConversionTest(cppgc::Heap* heap) {
  {
    PersistentType<GCed> persistent =
        MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType<GCed> member = persistent;
    EXPECT_EQ(persistent.Get(), member.Get());
  }
  {
    PersistentType<DerivedGCed> persistent =
        MakeGarbageCollected<DerivedGCed>(heap->GetAllocationHandle());
    MemberType<GCed> member = persistent;
    EXPECT_EQ(persistent.Get(), member.Get());
  }
  {
    PersistentType<GCed> persistent =
        MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType<GCed> member;
    member = persistent;
    EXPECT_EQ(persistent.Get(), member.Get());
  }
  {
    PersistentType<DerivedGCed> persistent =
        MakeGarbageCollected<DerivedGCed>(heap->GetAllocationHandle());
    MemberType<GCed> member;
    member = persistent;
    EXPECT_EQ(persistent.Get(), member.Get());
  }
}

TEST_F(MemberTest, PersistentConversion) {
  cppgc::Heap* heap = GetHeap();
  PersistentConversionTest<Member, Persistent>(heap);
  PersistentConversionTest<Member, WeakPersistent>(heap);
  PersistentConversionTest<WeakMember, Persistent>(heap);
  PersistentConversionTest<WeakMember, WeakPersistent>(heap);
  PersistentConversionTest<UntracedMember, Persistent>(heap);
  PersistentConversionTest<UntracedMember, WeakPersistent>(heap);
}

template <template <typename> class MemberType1,
          template <typename> class MemberType2>
void EqualityTest(cppgc::Heap* heap) {
  {
    GCed* gced = MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType1<GCed> member1 = gced;
    MemberType2<GCed> member2 = gced;
    EXPECT_TRUE(member1 == member2);
    EXPECT_FALSE(member1 != member2);
    member2 = member1;
    EXPECT_TRUE(member1 == member2);
    EXPECT_FALSE(member1 != member2);
  }
  {
    MemberType1<GCed> member1 =
        MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    MemberType2<GCed> member2 =
        MakeGarbageCollected<GCed>(heap->GetAllocationHandle());
    EXPECT_TRUE(member1 != member2);
    EXPECT_FALSE(member1 == member2);
  }
}

TEST_F(MemberTest, EqualityTest) {
  cppgc::Heap* heap = GetHeap();
  EqualityTest<Member, Member>(heap);
  EqualityTest<Member, WeakMember>(heap);
  EqualityTest<Member, UntracedMember>(heap);
  EqualityTest<WeakMember, Member>(heap);
  EqualityTest<WeakMember, WeakMember>(heap);
  EqualityTest<WeakMember, UntracedMember>(heap);
  EqualityTest<UntracedMember, Member>(heap);
  EqualityTest<UntracedMember, WeakMember>(heap);
  EqualityTest<UntracedMember, UntracedMember>(heap);
}

TEST_F(MemberTest, WriteBarrierTriggered) {
  CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered = 0;
  CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered = 0;
  GCed* gced = MakeGarbageCollected<GCed>(GetAllocationHandle());
  MemberWithCustomBarrier member1 = gced;
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered);
  EXPECT_EQ(0u, CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered);
  member1 = gced;
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered);
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered);
  member1 = nullptr;
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered);
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered);
  MemberWithCustomBarrier member2 = nullptr;
  // No initializing barriers for std::nullptr_t.
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered);
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered);
  member2 = kSentinelPointer;
  EXPECT_EQ(kSentinelPointer, member2.Get());
  EXPECT_EQ(kSentinelPointer, member2);
  // No initializing barriers for pointer sentinel.
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::InitializingWriteBarriersTriggered);
  EXPECT_EQ(1u, CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered);
  member2.Swap(member1);
  EXPECT_EQ(3u, CustomWriteBarrierPolicy::AssigningWriteBarriersTriggered);
}

TEST_F(MemberTest, CheckingPolicy) {
  static constexpr size_t kElements = 64u;
  CustomCheckingPolicy::ChecksTriggered = 0u;

  for (std::size_t i = 0; i < kElements; ++i) {
    CustomCheckingPolicy::Cached.push_back(
        MakeGarbageCollected<GCed>(GetAllocationHandle()));
  }

  MemberWithCustomChecking member;
  for (GCed* item : CustomCheckingPolicy::Cached) {
    member = item;
  }
  EXPECT_EQ(CustomCheckingPolicy::Cached.size(),
            CustomCheckingPolicy::ChecksTriggered);
}
}  // namespace internal
}  // namespace cppgc
