#include <memory>
#include <utility>
#include <gtest/gtest.h>
#include <gmock/gmock.h>

namespace orxonox
{
    namespace
    {
        class TestClass
        {
            public:
                TestClass() : value_(0) {}
                virtual ~TestClass() {}

                void setValue(int value) { this->value_ = value; }
                int getValue() const { return this->value_; }

            private:
                int value_;
        };

        class TestChildClass : public TestClass
        {
            public:
                TestChildClass() {}
                virtual ~TestChildClass() {}
        };

        class TestChildClassMock : public TestChildClass
        {
            public:
                TestChildClassMock() {}
                ~TestChildClassMock() { objectDestroyed(); }

                MOCK_METHOD0(objectDestroyed, void());
        };
    }

    TEST(SharedPtr, ConstructorDefault)
    {
        std::shared_ptr<TestClass> test;
        EXPECT_EQ(nullptr, test.get());
    }

    TEST(SharedPtr, ConstructorPointer)
    {
        TestClass* pointer = new TestClass();

        std::shared_ptr<TestClass> test(pointer);
        EXPECT_EQ(pointer, test.get());
        EXPECT_EQ(pointer, &(*test));
    }

    TEST(SharedPtr, ConstructorChildPointer)
    {
        TestChildClass* pointer = new TestChildClass();

        std::shared_ptr<TestClass> test(pointer);
        EXPECT_EQ(pointer, test.get());
    }

    TEST(SharedPtr, ConstructorOther)
    {
        TestClass* pointer = new TestClass();

        std::shared_ptr<TestClass> other(pointer);
        EXPECT_EQ(pointer, other.get());

        std::shared_ptr<TestClass> test = other;
        EXPECT_EQ(pointer, test.get());
    }

    TEST(SharedPtr, ConstructorOtherChild)
    {
        TestChildClass* pointer = new TestChildClass();

        std::shared_ptr<TestChildClass> other(pointer);
        EXPECT_EQ(pointer, other.get());

        std::shared_ptr<TestClass> test = other;
        EXPECT_EQ(pointer, test.get());
    }

    TEST(SharedPtr, Assign)
    {
        TestClass* pointer = new TestClass();

        std::shared_ptr<TestClass> other(pointer);
        EXPECT_EQ(pointer, other.get());

        std::shared_ptr<TestClass> test;
        EXPECT_EQ(nullptr, test.get());

        test = other;
        EXPECT_EQ(pointer, test.get());
    }

    TEST(SharedPtr, AssignChild)
    {
        TestChildClass* pointer = new TestChildClass();

        std::shared_ptr<TestChildClass> other(pointer);
        EXPECT_EQ(pointer, other.get());

        std::shared_ptr<TestClass> test;
        EXPECT_EQ(nullptr, test.get());

        test = other;
        EXPECT_EQ(pointer, test.get());
    }

    TEST(SharedPtr, Cast)
    {
        TestChildClass* pointer = new TestChildClass();

        std::shared_ptr<TestChildClass> other(pointer);
        EXPECT_EQ(pointer, other.get());

        std::shared_ptr<TestClass> test = std::static_pointer_cast<TestClass>(other);
        EXPECT_EQ(pointer, test.get());
    }

    TEST(SharedPtr, Access)
    {
        TestClass* pointer = new TestClass();

        std::shared_ptr<TestClass> test(pointer);
        EXPECT_EQ(pointer, test.get());

        EXPECT_EQ(0, test->getValue());
        test->setValue(5);
        EXPECT_EQ(5, test->getValue());
        EXPECT_EQ(5, (*test).getValue());
    }

    TEST(SharedPtr, Boolean)
    {
        std::shared_ptr<TestClass> test;
        EXPECT_EQ(nullptr, test.get());
        EXPECT_FALSE(test);

        TestClass* pointer = new TestClass();

        test.reset(pointer);
        EXPECT_EQ(pointer, test.get());
        EXPECT_TRUE(static_cast<bool>(test));
    }

    TEST(SharedPtr, Swap)
    {
        TestClass* pointer1 = new TestClass();
        TestClass* pointer2 = new TestClass();

        std::shared_ptr<TestClass> test1(pointer1);
        std::shared_ptr<TestClass> test2(pointer2);

        EXPECT_EQ(pointer1, test1.get());
        EXPECT_EQ(pointer2, test2.get());

        test1.swap(test2);

        EXPECT_EQ(pointer2, test1.get());
        EXPECT_EQ(pointer1, test2.get());
    }

    TEST(SharedPtr, ObjectDestroyedOnePointer)
    {
        TestChildClassMock* pointer = new TestChildClassMock();
        std::shared_ptr<TestClass> test(pointer);

        EXPECT_CALL(*pointer, objectDestroyed()).Times(1);
    }

    TEST(SharedPtr, ObjectDestroyedManyPointers)
    {
        TestChildClassMock* pointer = new TestChildClassMock();

        std::shared_ptr<TestClass> test(pointer);
        std::vector<std::shared_ptr<TestClass>> tests;
        for (size_t i = 0; i < 100; ++i)
            tests.push_back(std::shared_ptr<TestClass>(test));

        EXPECT_CALL(*pointer, objectDestroyed()).Times(1);
    }

    TEST(SharedPtr, TestConstructors)
    {
        {
        TestChildClassMock* pointer = new TestChildClassMock();

        // default
        std::shared_ptr<TestChildClass> sharedPtr1;
        EXPECT_EQ(nullptr, sharedPtr1.get());

        // pointer
        std::shared_ptr<TestChildClass> sharedPtr2(pointer);
        EXPECT_EQ(pointer, sharedPtr2.get());

        // copy
        std::shared_ptr<TestChildClass> sharedPtr3(sharedPtr2);
        EXPECT_EQ(pointer, sharedPtr3.get());

        // move
        std::shared_ptr<TestChildClass> sharedPtr4(std::move(sharedPtr3));
        EXPECT_EQ(pointer, sharedPtr4.get());

        // other
        std::shared_ptr<TestClass> sharedPtr5(sharedPtr4);
        EXPECT_EQ(pointer, sharedPtr5.get());

        EXPECT_CALL(*pointer, objectDestroyed()).Times(1);
        }
    }

    TEST(SharedPtr, TestConstructors2)
    {
        {
        TestChildClassMock* pointer = new TestChildClassMock();

        // default
        std::shared_ptr<TestChildClass> sharedPtr1;
        EXPECT_EQ(nullptr, sharedPtr1.get());

        // pointer
        std::shared_ptr<TestChildClass> sharedPtr2(pointer);
        EXPECT_EQ(pointer, sharedPtr2.get());

        // copy
        std::shared_ptr<TestChildClass> sharedPtr3 = sharedPtr2;
        EXPECT_EQ(pointer, sharedPtr3.get());

        // move
        std::shared_ptr<TestChildClass> sharedPtr4 = std::move(sharedPtr3);
        EXPECT_EQ(pointer, sharedPtr4.get());

        // other
        std::shared_ptr<TestClass> sharedPtr5 = sharedPtr4;
        EXPECT_EQ(pointer, sharedPtr5.get());

        EXPECT_CALL(*pointer, objectDestroyed()).Times(1);
        }
    }

    TEST(SharedPtr, TestAssignments)
    {
        TestChildClassMock* pointer = new TestChildClassMock();

        std::shared_ptr<TestChildClass> sharedPtr1(pointer);
        EXPECT_EQ(pointer, sharedPtr1.get());

        // copy
        std::shared_ptr<TestChildClass> sharedPtr2;
        sharedPtr2 = sharedPtr1;
        EXPECT_EQ(pointer, sharedPtr2.get());

        // move
        std::shared_ptr<TestChildClass> sharedPtr3;
        sharedPtr3 = std::move(sharedPtr2);
        EXPECT_EQ(pointer, sharedPtr3.get());

        // other
        std::shared_ptr<TestClass> sharedPtr4;
        sharedPtr4 = sharedPtr3;
        EXPECT_EQ(pointer, sharedPtr4.get());

        EXPECT_CALL(*pointer, objectDestroyed()).Times(1);
    }
}
