/*
 *  Copyright (c) 2021, The OpenThread Authors.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

#include "test_platform.h"

#include <string.h>

#include <openthread/config.h>

#include "test_util.hpp"
#include "common/code_utils.hpp"
#include "common/owned_ptr.hpp"
#include "common/retain_ptr.hpp"

namespace ot {

class TestObject : public RetainCountable
{
public:
    TestObject(void)
        : mWasFreed(false)
    {
    }

    void     Free(void) { mWasFreed = true; }
    void     ResetTestFlags(void) { mWasFreed = false; }
    uint16_t GetRetainCount(void) const { return RetainCountable::GetRetainCount(); }
    bool     WasFreed(void) const { return mWasFreed && (GetRetainCount() == 0); }

private:
    bool mWasFreed;
};

static constexpr uint16_t kSkipRetainCountCheck = 0xffff;

template <typename PointerType>
void VerifyPointer(const PointerType &aPointer,
                   const TestObject * aObject,
                   uint16_t           aRetainCount = kSkipRetainCountCheck)
{
    if (aObject == nullptr)
    {
        VerifyOrQuit(aPointer.IsNull());
        VerifyOrQuit(aPointer.Get() == nullptr);
        VerifyOrQuit(aPointer == nullptr);
    }
    else
    {
        VerifyOrQuit(!aPointer.IsNull());
        VerifyOrQuit(aPointer.Get() == aObject);
        VerifyOrQuit(aPointer == aObject);
        VerifyOrQuit(aPointer != nullptr);

        VerifyOrQuit(!aPointer->WasFreed());
        VerifyOrQuit(!(*aPointer).WasFreed());

        if (aRetainCount != kSkipRetainCountCheck)
        {
            VerifyOrQuit(aObject->GetRetainCount() == aRetainCount);
        }
    }

    VerifyOrQuit(aPointer == aPointer);
}

void TestOwnedPtr(void)
{
    TestObject obj1;
    TestObject obj2;
    TestObject obj3;

    printf("\n====================================================================================\n");
    printf("Testing `OwnedPtr`\n");

    printf("\n - Default constructor (null pointer)");

    {
        OwnedPtr<TestObject> ptr;

        VerifyPointer(ptr, nullptr);
    }

    printf("\n - Constructor taking ownership of an object");
    obj1.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - Move constructor taking over from another");
    obj1.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr1(&obj1);
        OwnedPtr<TestObject> ptr2(ptr1.PassOwnership());

        VerifyPointer(ptr1, nullptr);
        VerifyPointer(ptr2, &obj1);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - `Free()` method");
    obj1.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1);

        ptr.Free();

        VerifyOrQuit(obj1.WasFreed());
        VerifyPointer(ptr, nullptr);

        ptr.Free();

        VerifyOrQuit(obj1.WasFreed());
        VerifyPointer(ptr, nullptr);
    }

    printf("\n - `Reset()` method");
    obj1.ResetTestFlags();
    obj2.ResetTestFlags();
    obj3.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1);

        ptr.Reset(&obj2);

        VerifyOrQuit(obj1.WasFreed());
        VerifyOrQuit(!obj2.WasFreed());
        VerifyPointer(ptr, &obj2);

        ptr.Reset();
        VerifyOrQuit(obj2.WasFreed());
        VerifyPointer(ptr, nullptr);

        ptr.Reset(&obj3);
        VerifyPointer(ptr, &obj3);
    }

    VerifyOrQuit(obj1.WasFreed());
    VerifyOrQuit(obj2.WasFreed());
    VerifyOrQuit(obj3.WasFreed());

    printf("\n - Self `Reset()`");
    obj1.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr1(&obj1);
        OwnedPtr<TestObject> ptr2;

        VerifyPointer(ptr1, &obj1);

        ptr1.Reset(&obj1);
        VerifyPointer(ptr1, &obj1);

        ptr2.Reset(nullptr);
        VerifyPointer(ptr2, nullptr);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - Move assignment (operator `=`)");
    obj1.ResetTestFlags();
    obj2.ResetTestFlags();
    obj3.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr1(&obj1);
        OwnedPtr<TestObject> ptr2(&obj2);
        OwnedPtr<TestObject> ptr3(&obj3);

        VerifyPointer(ptr1, &obj1);
        VerifyPointer(ptr2, &obj2);
        VerifyPointer(ptr3, &obj3);

        // Move from non-null (ptr1) to non-null (ptr2)
        ptr2 = ptr1.PassOwnership();
        VerifyPointer(ptr1, nullptr);
        VerifyPointer(ptr2, &obj1);
        VerifyOrQuit(!obj1.WasFreed());
        VerifyOrQuit(obj2.WasFreed());

        // Move from null (ptr1) to non-null (ptr3)
        ptr3 = ptr1.PassOwnership();
        VerifyPointer(ptr1, nullptr);
        VerifyPointer(ptr3, nullptr);
        VerifyOrQuit(obj3.WasFreed());

        // Move from non-null (ptr2) to null (ptr1)
        ptr1 = ptr2.PassOwnership();
        VerifyPointer(ptr1, &obj1);
        VerifyPointer(ptr2, nullptr);
        VerifyOrQuit(!obj1.WasFreed());

        // Move from null (ptr2) to null (ptr3)
        ptr3 = ptr2.PassOwnership();
        VerifyPointer(ptr2, nullptr);
        VerifyPointer(ptr3, nullptr);
        VerifyOrQuit(!obj1.WasFreed());
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - Self move assignment (operator `=`)");
    obj1.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr1(&obj1);
        OwnedPtr<TestObject> ptr2;

        VerifyPointer(ptr1, &obj1);
        VerifyPointer(ptr2, nullptr);

        // Move from non-null (ptr1) to itself
        ptr1 = ptr1.PassOwnership();
        VerifyPointer(ptr1, &obj1);

        // Move from null (ptr2) to itself
        ptr2 = ptr2.PassOwnership();
        VerifyPointer(ptr2, nullptr);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - `Release()` method");
    obj1.ResetTestFlags();

    {
        OwnedPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1);

        VerifyOrQuit(ptr.Release() == &obj1);
        VerifyOrQuit(!obj1.WasFreed());
        VerifyPointer(ptr, nullptr);

        VerifyOrQuit(ptr.Release() == nullptr);
        VerifyOrQuit(!obj1.WasFreed());
        VerifyPointer(ptr, nullptr);
    }

    printf("\n\n-- PASS\n");
}

void TestRetainPtr(void)
{
    TestObject obj1;
    TestObject obj2;
    TestObject obj3;

    printf("\n====================================================================================\n");
    printf("Testing `RetainPtr`\n");

    VerifyOrQuit(obj1.GetRetainCount() == 0);
    VerifyOrQuit(obj2.GetRetainCount() == 0);
    VerifyOrQuit(obj3.GetRetainCount() == 0);

    printf("\n - Default constructor (null pointer)");

    {
        RetainPtr<TestObject> ptr;

        VerifyPointer(ptr, nullptr);
    }

    printf("\n - Constructor taking over management of an object");
    obj1.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1, 1);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - Two constructed `RetainPtr`s of the same object");
    obj1.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr1(&obj1);
        RetainPtr<TestObject> ptr2(&obj1);

        VerifyPointer(ptr1, &obj1, 2);
        VerifyPointer(ptr2, &obj1, 2);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - Copy constructor");
    obj1.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr1(&obj1);
        RetainPtr<TestObject> ptr2(ptr1);

        VerifyPointer(ptr1, &obj1, 2);
        VerifyPointer(ptr2, &obj1, 2);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - `Reset()` method");
    obj1.ResetTestFlags();
    obj2.ResetTestFlags();
    obj3.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1, 1);

        ptr.Reset(&obj2);
        VerifyOrQuit(obj1.WasFreed());
        VerifyOrQuit(!obj2.WasFreed());
        VerifyPointer(ptr, &obj2, 1);

        ptr.Reset();
        VerifyOrQuit(obj2.WasFreed());
        VerifyPointer(ptr, nullptr);

        ptr.Reset(&obj3);
        VerifyPointer(ptr, &obj3, 1);
    }

    VerifyOrQuit(obj1.WasFreed());
    VerifyOrQuit(obj2.WasFreed());
    VerifyOrQuit(obj3.WasFreed());

    printf("\n - Self `Reset()`");
    obj1.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr1(&obj1);
        RetainPtr<TestObject> ptr2;

        VerifyPointer(ptr1, &obj1, 1);

        ptr1.Reset(&obj1);
        VerifyPointer(ptr1, &obj1, 1);

        ptr2.Reset(nullptr);
        VerifyPointer(ptr2, nullptr);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - Assignment `=`");
    obj1.ResetTestFlags();
    obj2.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr1(&obj1);
        RetainPtr<TestObject> ptr2(&obj2);
        RetainPtr<TestObject> ptr3;

        VerifyPointer(ptr1, &obj1, 1);
        VerifyPointer(ptr2, &obj2, 1);
        VerifyPointer(ptr3, nullptr);

        VerifyOrQuit(ptr1 != ptr2);
        VerifyOrQuit(ptr1 != ptr3);
        VerifyOrQuit(ptr2 != ptr3);

        // Set from non-null (ptr1) to non-null (ptr2)
        ptr2 = ptr1;
        VerifyPointer(ptr1, &obj1, 2);
        VerifyPointer(ptr2, &obj1, 2);
        VerifyOrQuit(obj2.WasFreed());
        VerifyOrQuit(ptr1 == ptr2);

        // Set from null (ptr3) to non-null (ptr1)
        ptr1 = ptr3;
        VerifyPointer(ptr1, nullptr);
        VerifyPointer(ptr3, nullptr);
        VerifyPointer(ptr2, &obj1, 1);
        VerifyOrQuit(ptr1 == ptr3);

        // Move from null (ptr1) to null (ptr3)
        ptr3 = ptr1;
        VerifyPointer(ptr1, nullptr);
        VerifyPointer(ptr3, nullptr);
        VerifyOrQuit(ptr1 == ptr3);

        // Move from non-null (ptr2) to null (ptr3)
        ptr3 = ptr2;
        VerifyPointer(ptr2, &obj1, 2);
        VerifyPointer(ptr3, &obj1, 2);
        VerifyOrQuit(ptr2 == ptr3);
    }

    VerifyOrQuit(obj1.WasFreed());
    VerifyOrQuit(obj2.WasFreed());

    printf("\n - Self assignment `=`");
    obj1.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr1(&obj1);
        RetainPtr<TestObject> ptr2;

        VerifyPointer(ptr1, &obj1, 1);
        VerifyPointer(ptr2, nullptr);

        // Set from non-null (ptr1) to itself. We use `*(&ptr1) to
        // silence `clang` error/warning not allowing explicit self
        // assignment to same variable.
        ptr1 = *(&ptr1);
        VerifyPointer(ptr1, &obj1, 1);

        // Set from null (ptr2) to itself
        ptr2 = *(&ptr2);
        VerifyPointer(ptr2, nullptr);
    }

    VerifyOrQuit(obj1.WasFreed());

    printf("\n - `Release()` method");
    obj1.ResetTestFlags();

    {
        RetainPtr<TestObject> ptr(&obj1);

        VerifyPointer(ptr, &obj1, 1);

        VerifyOrQuit(ptr.Release() == &obj1);
        VerifyPointer(ptr, nullptr);

        VerifyOrQuit(ptr.Release() == nullptr);
        VerifyPointer(ptr, nullptr);
    }

    VerifyOrQuit(!obj1.WasFreed());
    VerifyOrQuit(obj1.GetRetainCount() == 1);

    printf("\n\n-- PASS\n");
}

} // namespace ot

int main(void)
{
    ot::TestOwnedPtr();
    ot::TestRetainPtr();
    printf("\nAll tests passed.\n");
    return 0;
}
