// Copyright 2022 The Pigweed Authors
//
// Licensed 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
//
//     https://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 "pw_intrusive_ptr/intrusive_ptr.h"

#include <stdint.h>

#include <utility>

#include "pw_unit_test/framework.h"

namespace pw {
namespace {

class TestItem : public RefCounted<TestItem> {
 public:
  TestItem() { ++instance_counter; }

  explicit TestItem(int32_t f) : TestItem() { first = f; }

  explicit TestItem(int64_t s) : TestItem() { second = s; }

  TestItem(int32_t f, int64_t s) : TestItem() {
    first = f;
    second = s;
  }

  TestItem(const TestItem&) : TestItem() {}
  TestItem(TestItem&&) noexcept : TestItem() {}

  TestItem& operator=(const TestItem& other) {
    if (&other != this) {
      ++instance_counter;
    }
    return *this;
  }

  TestItem& operator=(TestItem&& other) noexcept {
    if (&other != this) {
      ++instance_counter;
    }
    return *this;
  }

  virtual ~TestItem() { --instance_counter; }

  inline static int32_t instance_counter = 0;

  int32_t first = 0;
  int64_t second = 1;
};

class TestItemDerived : public TestItem {
 public:
  TestItemDerived() { ++derived_instance_counter; }
  TestItemDerived(const TestItemDerived&) : TestItemDerived() {}
  TestItemDerived(TestItemDerived&&) noexcept : TestItemDerived() {}

  TestItemDerived& operator=(const TestItemDerived& other) {
    if (&other != this) {
      ++derived_instance_counter;
    }
    return *this;
  }

  TestItemDerived& operator=(TestItemDerived&& other) noexcept {
    if (&other != this) {
      ++derived_instance_counter;
    }
    return *this;
  }

  ~TestItemDerived() override { --derived_instance_counter; }

  inline static int32_t derived_instance_counter = 0;
};

struct FreeTestItem {
  void AddRef() const { ++instance_counter; }

  bool ReleaseRef() const { return --instance_counter < 1; }

  mutable int32_t instance_counter = 0;
};

class IntrusivePtrTest : public ::testing::Test {
 protected:
  void SetUp() override {
    TestItem::instance_counter = 0;
    TestItemDerived::derived_instance_counter = 0;
  }
};

TEST_F(IntrusivePtrTest, DeletingLastPtrDeletesTheObject) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, AssigningToNullptrDeletesTheObject) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  EXPECT_EQ(TestItem::instance_counter, 1);
  ptr = nullptr;
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, AssigningToEmptyPtrDeletesTheObject) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  IntrusivePtr<TestItem> empty;
  EXPECT_EQ(TestItem::instance_counter, 1);
  ptr = empty;
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, SwapWithNullptrKeepsTheObject) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  IntrusivePtr<TestItem> empty;
  EXPECT_EQ(TestItem::instance_counter, 1);

  ptr.swap(empty);
  EXPECT_EQ(TestItem::instance_counter, 1);
  EXPECT_EQ(ptr, nullptr);

  empty = nullptr;
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, CopyingPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2(ptr);
      EXPECT_EQ(TestItem::instance_counter, 1);
    }

    // We still have a ptr here.
    EXPECT_EQ(TestItem::instance_counter, 1);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, MovingPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2(std::move(ptr));
      EXPECT_EQ(TestItem::instance_counter, 1);
    }

    // ptr was moved away, object should be deleted.
    EXPECT_EQ(TestItem::instance_counter, 0);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, CopyAssigningPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);

    {
      auto ptr_2 = ptr;
      EXPECT_EQ(TestItem::instance_counter, 1);
    }

    // We still have a ptr here.
    EXPECT_EQ(TestItem::instance_counter, 1);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, MoveAssigningPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);

    {
      auto ptr_2 = std::move(ptr);
      EXPECT_EQ(TestItem::instance_counter, 1);
    }

    // ptr was moved away, object should be deleted.
    EXPECT_EQ(TestItem::instance_counter, 0);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, CopyingPtrToBaseClassPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
    EXPECT_EQ(TestItem::instance_counter, 1);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2(ptr);
      EXPECT_EQ(TestItem::instance_counter, 1);
      EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
    }

    // We still have a ptr here.
    EXPECT_EQ(TestItem::instance_counter, 1);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
  EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
}

TEST_F(IntrusivePtrTest, MovingPtrToBaseClassPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
    EXPECT_EQ(TestItem::instance_counter, 1);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2(std::move(ptr));
      EXPECT_EQ(TestItem::instance_counter, 1);
      EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
    }

    // ptr was moved away, object should be deleted.
    EXPECT_EQ(TestItem::instance_counter, 0);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
  EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
}

TEST_F(IntrusivePtrTest, CopyAssigningPtrToBaseClassPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
    EXPECT_EQ(TestItem::instance_counter, 1);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2 = ptr;
      EXPECT_EQ(TestItem::instance_counter, 1);
      EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
    }

    // We still have a ptr here.
    EXPECT_EQ(TestItem::instance_counter, 1);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
  EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
}

TEST_F(IntrusivePtrTest, MoveAssigningPtrToBaseClassPtrDoesntCreateNewObjects) {
  {
    IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
    EXPECT_EQ(TestItem::instance_counter, 1);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2 = std::move(ptr);
      EXPECT_EQ(TestItem::instance_counter, 1);
      EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
    }

    // ptr was moved away, object should be deleted.
    EXPECT_EQ(TestItem::instance_counter, 0);
    EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
  EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
}

TEST_F(IntrusivePtrTest, CopyAssigningPtrDeletesOldObjectIfLast) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2(new TestItem());
      EXPECT_EQ(TestItem::instance_counter, 2);

      ptr_2 = ptr;

      // Old object in ptr_2 should be removed.
      EXPECT_EQ(TestItem::instance_counter, 1);
    }

    // We still have a ptr here.
    EXPECT_EQ(TestItem::instance_counter, 1);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

TEST_F(IntrusivePtrTest, MoveAssigningPtrDeletesOldObjectIfLast) {
  {
    IntrusivePtr<TestItem> ptr(new TestItem());
    EXPECT_EQ(TestItem::instance_counter, 1);

    {
      IntrusivePtr<TestItem> ptr_2(new TestItem());
      EXPECT_EQ(TestItem::instance_counter, 2);

      ptr_2 = std::move(ptr);

      // Old object in ptr_2 should be removed.
      EXPECT_EQ(TestItem::instance_counter, 1);
    }

    // ptr was moved away, object should be deleted.
    EXPECT_EQ(TestItem::instance_counter, 0);
  }
  EXPECT_EQ(TestItem::instance_counter, 0);
}

// Comparison tests use operators directly to cover == and != and both
// argument orders.
TEST_F(IntrusivePtrTest, PtrsWithDifferentObjectsAreNotEqual) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  IntrusivePtr<TestItem> ptr_2(new TestItem());

  EXPECT_FALSE(ptr == ptr_2);
  EXPECT_FALSE(ptr_2 == ptr);
  EXPECT_TRUE(ptr != ptr_2);
  EXPECT_TRUE(ptr_2 != ptr);
}

TEST_F(IntrusivePtrTest, PtrsWithSameObjectsAreEqual) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  auto ptr_2 = ptr;

  EXPECT_TRUE(ptr == ptr_2);
  EXPECT_TRUE(ptr_2 == ptr);
  EXPECT_FALSE(ptr != ptr_2);
  EXPECT_FALSE(ptr_2 != ptr);
}

TEST_F(IntrusivePtrTest, FilledPtrIsNotEqualToEmptyPtr) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  IntrusivePtr<TestItem> empty;

  EXPECT_FALSE(ptr == empty);
  EXPECT_FALSE(empty == ptr);
  EXPECT_TRUE(ptr != empty);
  EXPECT_TRUE(empty != ptr);
}

TEST_F(IntrusivePtrTest, FilledPtrIsNotEqualToNullptr) {
  IntrusivePtr<TestItem> ptr(new TestItem());

  EXPECT_FALSE(ptr == nullptr);
  EXPECT_FALSE(nullptr == ptr);
  EXPECT_TRUE(ptr != nullptr);
  EXPECT_TRUE(nullptr != ptr);
}

TEST_F(IntrusivePtrTest, EmptyPtrIsEqualToNullptr) {
  IntrusivePtr<TestItem> empty;

  EXPECT_TRUE(empty == nullptr);
  EXPECT_TRUE(nullptr == empty);
  EXPECT_FALSE(empty != nullptr);
  EXPECT_FALSE(nullptr != empty);
}

TEST_F(IntrusivePtrTest, PtrsWithDifferentObjectsReturnDifferentPointers) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  IntrusivePtr<TestItem> ptr_2(new TestItem());

  EXPECT_NE(ptr.get(), ptr_2.get());
}

TEST_F(IntrusivePtrTest, PtrsWithSameObjectsReturnSamePointer) {
  IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
  auto ptr_2 = ptr;
  IntrusivePtr<TestItem> ptr_3 = ptr;

  EXPECT_EQ(ptr.get(), ptr_2.get());
  EXPECT_EQ(static_cast<TestItem*>(ptr.get()), ptr_3.get());
}

TEST_F(IntrusivePtrTest, EmptyPtrReturnsNullptr) {
  IntrusivePtr<TestItem> empty;

  EXPECT_EQ(empty.get(), nullptr);
}

TEST_F(IntrusivePtrTest, ConstifyWorks) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  IntrusivePtr<const TestItem> ptr_2 = ptr;

  EXPECT_EQ(TestItem::instance_counter, 1);
  EXPECT_EQ(ptr.get(), ptr_2.get());
}

TEST_F(IntrusivePtrTest, NonRefCountedObjectWorks) {
  // Compilation test only.
  IntrusivePtr<FreeTestItem> empty;
  IntrusivePtr<FreeTestItem> free(new FreeTestItem);
  IntrusivePtr<FreeTestItem> free_2(free);
  IntrusivePtr<FreeTestItem> free_3(std::move(free));
}

TEST_F(IntrusivePtrTest, MakeRefCounted) {
  auto ptr_1 = MakeRefCounted<TestItem>();
  EXPECT_EQ(TestItem::instance_counter, 1);
  EXPECT_EQ(ptr_1->first, 0);
  EXPECT_EQ(ptr_1->second, 1);

  auto ptr_2 = MakeRefCounted<TestItem>(int32_t(42));
  EXPECT_EQ(TestItem::instance_counter, 2);
  EXPECT_EQ(ptr_2->first, 42);
  EXPECT_EQ(ptr_2->second, 1);

  auto ptr_3 = MakeRefCounted<TestItem>(int64_t(2));
  EXPECT_EQ(TestItem::instance_counter, 3);
  EXPECT_EQ(ptr_3->first, 0);
  EXPECT_EQ(ptr_3->second, 2);

  auto ptr_4 = MakeRefCounted<TestItem>(42, 5);
  EXPECT_EQ(TestItem::instance_counter, 4);
  EXPECT_EQ(ptr_4->first, 42);
  EXPECT_EQ(ptr_4->second, 5);
}

TEST_F(IntrusivePtrTest, UseCount) {
  IntrusivePtr<TestItem> ptr(new TestItem());
  EXPECT_EQ(ptr.use_count(), 1);
  {
    IntrusivePtr<TestItem> ptr_copy = ptr;
    EXPECT_EQ(ptr.use_count(), 2);
  }
  EXPECT_EQ(ptr.use_count(), 1);
}

TEST_F(IntrusivePtrTest, UseCountForNullPtr) {
  IntrusivePtr<TestItem> ptr;
  EXPECT_EQ(ptr.use_count(), 0);

  ptr = IntrusivePtr(new TestItem);
  EXPECT_EQ(ptr.use_count(), 1);

  ptr = nullptr;
  EXPECT_EQ(ptr.use_count(), 0);
}

}  // namespace
}  // namespace pw
