/*
 * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
 * that can be found in the LICENSE file.
 */

#include "ExternalRCRefRegistry.hpp"

#include <condition_variable>
#include <mutex>

#include "gmock/gmock.h"
#include "gtest/gtest.h"

#include "ExternalRCRef.hpp"
#include "ObjectTestSupport.hpp"
#include "TestSupport.hpp"
#include "ThreadRegistry.hpp"

using namespace kotlin;

namespace {

class Waiter : private Pinned {
public:
    void allow() noexcept {
        {
            std::unique_lock guard(mutex_);
            allow_ = true;
        }
        cv_.notify_all();
    };

    void wait() noexcept {
        std::unique_lock guard(mutex_);
        cv_.wait(guard, [this] { return allow_; });
    }

private:
    bool allow_ = false;
    std::mutex mutex_;
    std::condition_variable cv_;
};

struct Payload {
    static constexpr test_support::NoRefFields<Payload> kFields = {};
};

test_support::TypeInfoHolder typeInfoHolder{test_support::TypeInfoHolder::ObjectBuilder<Payload>()};

struct HeapObject : test_support::Object<Payload> {
    HeapObject() noexcept : Object(typeInfoHolder.typeInfo()) {}
};

struct PermanentObject : test_support::Object<Payload> {
    PermanentObject() noexcept : Object(typeInfoHolder.typeInfo()) {
        header()->typeInfoOrMeta_ = setPointerBits(header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT);
        RuntimeAssert(header()->permanent(), "Must be permanent");
    }
};

} // namespace

class ExternalRCRefRegistryTest : public testing::Test {
public:
    ~ExternalRCRefRegistryTest() {
        // Clean up safely.
        roots();
        all();
    }

    void publish() noexcept { mm::ThreadRegistry::Instance().CurrentThreadData()->externalRCRefRegistry().publish(); }

    template <typename... Invalidated>
    std::vector<ObjHeader*> all(Invalidated&&... invalidated) noexcept {
        std::set<ObjHeader*> invalidatedSet({std::forward<Invalidated>(invalidated)...});
        std::vector<ObjHeader*> result;
        for (auto obj : mm::ExternalRCRefRegistry::instance().lockForIter()) {
            if (invalidatedSet.find(obj) != invalidatedSet.end()) {
                obj = nullptr;
            }
            result.push_back(obj);
        }
        return result;
    }

    std::vector<ObjHeader*> roots() noexcept {
        std::vector<ObjHeader*> result;
        for (auto* obj : mm::ExternalRCRefRegistry::instance().roots()) {
            result.push_back(obj);
        }
        return result;
    }

    template <typename Traits>
    ObjHeader* tryRef(mm::ExternalRCRef<Traits>& weakRef) noexcept {
        ObjHeader* result;
        return weakRef.tryRef(&result);
    }
};

TEST_F(ExternalRCRefRegistryTest, RegisterRefsWithoutPublish) {
    RunInNewThread([this] {
        HeapObject object1;
        HeapObject object2;
        PermanentObject object3;
        PermanentObject object4;

        auto ref1 = mm::OwningExternalRCRef(object1.header());
        auto ref2 = mm::WeakExternalRCRef(object2.header());
        auto ref3 = mm::OwningExternalRCRef(object3.header());
        auto ref4 = mm::WeakExternalRCRef(object4.header());

        EXPECT_THAT(roots(), testing::UnorderedElementsAre(object1.header()));
        EXPECT_THAT(all(), testing::UnorderedElementsAre());

        EXPECT_THAT(*ref1, object1.header());
        EXPECT_THAT(tryRef(ref1), object1.header());
        EXPECT_THAT(tryRef(ref2), object2.header());
        EXPECT_THAT(*ref3, object3.header());
        EXPECT_THAT(tryRef(ref3), object3.header());
        EXPECT_THAT(tryRef(ref4), object4.header());

        ref1.reset();
        ref2.reset();
        ref3.reset();
        ref4.reset();

        EXPECT_THAT(*ref1, nullptr);
        EXPECT_THAT(tryRef(ref1), nullptr);
        EXPECT_THAT(tryRef(ref2), nullptr);
        EXPECT_THAT(*ref3, nullptr);
        EXPECT_THAT(tryRef(ref3), nullptr);
        EXPECT_THAT(tryRef(ref4), nullptr);

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());

        publish();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
    });
}

TEST_F(ExternalRCRefRegistryTest, RegisterAllRefs) {
    RunInNewThread([this] {
        HeapObject object1;
        HeapObject object2;
        PermanentObject object3;
        PermanentObject object4;

        auto ref1 = mm::OwningExternalRCRef(object1.header());
        auto ref2 = mm::WeakExternalRCRef(object2.header());
        auto ref3 = mm::OwningExternalRCRef(object3.header());
        auto ref4 = mm::WeakExternalRCRef(object4.header());

        EXPECT_THAT(roots(), testing::UnorderedElementsAre(object1.header()));
        EXPECT_THAT(all(), testing::UnorderedElementsAre());

        EXPECT_THAT(*ref1, object1.header());
        EXPECT_THAT(tryRef(ref1), object1.header());
        EXPECT_THAT(tryRef(ref2), object2.header());
        EXPECT_THAT(*ref3, object3.header());
        EXPECT_THAT(tryRef(ref3), object3.header());
        EXPECT_THAT(tryRef(ref4), object4.header());

        publish();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre(object1.header()));
        EXPECT_THAT(all(), testing::UnorderedElementsAre(object1.header(), object2.header()));

        EXPECT_THAT(*ref1, object1.header());
        EXPECT_THAT(tryRef(ref1), object1.header());
        EXPECT_THAT(tryRef(ref2), object2.header());
        EXPECT_THAT(*ref3, object3.header());
        EXPECT_THAT(tryRef(ref3), object3.header());
        EXPECT_THAT(tryRef(ref4), object4.header());

        ref1.reset();
        ref2.reset();
        ref3.reset();
        ref4.reset();

        EXPECT_THAT(*ref1, nullptr);
        EXPECT_THAT(tryRef(ref1), nullptr);
        EXPECT_THAT(tryRef(ref2), nullptr);
        EXPECT_THAT(*ref3, nullptr);
        EXPECT_THAT(tryRef(ref3), nullptr);
        EXPECT_THAT(tryRef(ref4), nullptr);

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
    });
}

TEST_F(ExternalRCRefRegistryTest, InvalidateWeakRef) {
    RunInNewThread([this] {
        HeapObject object;
        auto ref = mm::WeakExternalRCRef(object.header());
        publish();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(object.header()), testing::UnorderedElementsAre(nullptr));
        EXPECT_THAT(tryRef(ref), nullptr);

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre(nullptr));
        EXPECT_THAT(tryRef(ref), nullptr);

        ref.reset();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
    });
}

TEST_F(ExternalRCRefRegistryTest, RetainWeakRef) {
    RunInNewThread([this] {
        HeapObject object;
        auto ref = mm::WeakExternalRCRef(object.header());
        publish();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(tryRef(ref), object.header());

        mm::retainExternalRCRef(ref.get());

        EXPECT_THAT(roots(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(all(), testing::UnorderedElementsAre(object.header()));

        mm::releaseExternalRCRef(ref.get());

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(tryRef(ref), object.header());

        ref.reset();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
    });
}

TEST_F(ExternalRCRefRegistryTest, RetainWeakRefBeforePublish) {
    RunInNewThread([this] {
        HeapObject object;
        auto ref = mm::WeakExternalRCRef(object.header());

        mm::retainExternalRCRef(ref.get());

        EXPECT_THAT(roots(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
        EXPECT_THAT(tryRef(ref), object.header());

        publish();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(all(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(tryRef(ref), object.header());

        mm::releaseExternalRCRef(ref.get());

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre(object.header()));
        EXPECT_THAT(tryRef(ref), object.header());

        ref.reset();

        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
    });
}

TEST_F(ExternalRCRefRegistryTest, StressOwningRef) {
    HeapObject object;
    Waiter waiter;
    std::vector<ScopedThread> mutators;
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        mutators.emplace_back([&, this] {
            ScopedMemoryInit scope;
            waiter.wait();
            auto ref = mm::OwningExternalRCRef(object.header());
            publish();
            ref.reset();
        });
    }
    waiter.allow();
    mutators.clear();
    EXPECT_THAT(roots(), testing::UnorderedElementsAre());
    EXPECT_THAT(all(), testing::UnorderedElementsAre());
}

TEST_F(ExternalRCRefRegistryTest, StressWeakRef) {
    HeapObject object;
    Waiter waiter;
    std::vector<ScopedThread> mutators;
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        mutators.emplace_back([&, this] {
            ScopedMemoryInit scope;
            waiter.wait();
            auto ref = mm::WeakExternalRCRef(object.header());
            publish();
            ref.reset();
        });
    }
    waiter.allow();
    mutators.clear();
    EXPECT_THAT(roots(), testing::UnorderedElementsAre());
    EXPECT_THAT(all(), testing::UnorderedElementsAre());
}

TEST_F(ExternalRCRefRegistryTest, StressWeakRefRetainRelease) {
    RunInNewThread([this] {
        constexpr int kGCCycles = 10000;
        constexpr int kRefsCount = 3;
        HeapObject object;
        KRef obj = object.header();
        Waiter waiter;
        std::atomic<bool> canStop = false;
        std::vector<mm::WeakExternalRCRef> refs;
        for (int i = 0; i < kRefsCount; ++i) {
            refs.emplace_back(mm::WeakExternalRCRef(obj));
        }
        publish();
        std::vector<ScopedThread> mutators;
        mutators.emplace_back([&, this] {
            waiter.wait();
            for (int i = 0; i < kGCCycles; ++i) {
                roots();
                all();
            }
            canStop.store(true, std::memory_order_release);
        });
        for (int i = 0; i < kDefaultThreadCount; ++i) {
            mutators.emplace_back([i, &refs, &waiter, &canStop] {
                ScopedMemoryInit scope;
                waiter.wait();
                auto& ref = refs[i % kRefsCount];
                while (!canStop.load(std::memory_order_acquire)) {
                    mm::retainExternalRCRef(ref.get());
                    mm::releaseExternalRCRef(ref.get());
                }
            });
        }
        waiter.allow();
        mutators.clear();
        for (auto& ref : refs) {
            ref.reset();
        }
        EXPECT_THAT(roots(), testing::UnorderedElementsAre());
        EXPECT_THAT(all(), testing::UnorderedElementsAre());
    });
}
