/*
 * Copyright 2010-2021 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 "RootSet.hpp"

#include <memory>
#include <vector>

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

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

using namespace kotlin;

namespace {

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

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

using Object = test_support::Object<Payload>;

std::unique_ptr<Object> allocateObject() noexcept {
    return std::make_unique<Object>(typeHolder.typeInfo());
}

class Global : private Pinned {
public:
    explicit Global(mm::ThreadData& threadData) noexcept {
        mm::GlobalsRegistry::Instance().RegisterStorageForGlobal(&threadData, &location_);
        location_ = allocateObject().release()->header();
    }

    KRef& operator*() noexcept { return location_; }

    ~Global() {
        // Delete the allocated global.
        std::unique_ptr<Object> obj(&Object::FromObjHeader(location_));
        location_ = nullptr;
    }
private:
    ObjHeader* location_ = nullptr;
};

// TODO: All the test helpers to create the rootset should be abstracted out.

template <size_t LocalsCount>
class StackEntry : private Pinned {
public:
    static_assert(LocalsCount > 0, "Must have at least 1 object on stack");

    explicit StackEntry(mm::ShadowStack& shadowStack) : shadowStack_(shadowStack) {
        objects_.reserve(LocalsCount);
        // Fill `locals_` with some values.
        for (size_t i = 0; i < LocalsCount; ++i) {
            auto object = allocateObject();
            (*this)[i] = object->header();
            objects_.push_back(std::move(object));
        }

        shadowStack_.EnterFrame(data_.data(), 0, kTotalCount);
    }

    ~StackEntry() { shadowStack_.LeaveFrame(data_.data(), 0, kTotalCount); }

    ObjHeader*& operator[](size_t index) { return data_[kFrameOverlayCount + index]; }

private:
    mm::ShadowStack& shadowStack_;
    std::vector<std::unique_ptr<Object>> objects_;

    // The following is what the compiler creates on the stack.
    static inline constexpr int kFrameOverlayCount = sizeof(FrameOverlay) / sizeof(ObjHeader**);
    static inline constexpr int kTotalCount = kFrameOverlayCount + LocalsCount;
    std::array<ObjHeader*, kTotalCount> data_;
};

struct TLSKey {};

} // namespace

TEST(ThreadRootSetTest, Basic) {
    mm::ShadowStack stack;
    StackEntry<2> entry(stack);

    TLSKey key;
    mm::ThreadLocalStorage tls;
    tls.AddRecord(&key, 3);
    tls.Commit();

    mm::ThreadRootSet iter(stack, tls);

    std::vector<mm::ThreadRootSet::Value> actual;
    for (auto object : iter) {
        actual.push_back(object);
    }

    auto asStack = [](ObjHeader*& object) -> mm::ThreadRootSet::Value { return {object, mm::ThreadRootSet::Source::kStack}; };
    auto asTLS = [](ObjHeader*& object) -> mm::ThreadRootSet::Value { return {object, mm::ThreadRootSet::Source::kTLS}; };
    EXPECT_THAT(
            actual,
            testing::ElementsAre(
                    asStack(entry[0]), asStack(entry[1]), asTLS(*tls.Lookup(&key, 0)), asTLS(*tls.Lookup(&key, 1)),
                    asTLS(*tls.Lookup(&key, 2))));
}

TEST(ThreadRootSetTest, Empty) {
    mm::ShadowStack stack;
    mm::ThreadLocalStorage tls;

    mm::ThreadRootSet iter(stack, tls);

    std::vector<mm::ThreadRootSet::Value> actual;
    for (auto object : iter) {
        actual.push_back(object);
    }

    EXPECT_THAT(actual, testing::IsEmpty());
}

TEST(GlobalRootSetTest, Basic) {
    RunInNewThread([](mm::ThreadData& threadData) {
        Global global1(threadData);
        Global global2(threadData);

        mm::ExternalRCRefRegistry externalRCRefsRegistry;
        mm::ExternalRCRefRegistry::ThreadQueue stableRefsProducer(externalRCRefsRegistry);
        auto stableRef1 = allocateObject();
        auto stableRef2 = allocateObject();
        auto stableRef3 = allocateObject();
        mm::OwningExternalRCRef stableRefHandle1(stableRefsProducer.createExternalRCRefImpl(stableRef1->header(), 1).toRaw());
        mm::OwningExternalRCRef stableRefHandle2(stableRefsProducer.createExternalRCRefImpl(stableRef2->header(), 1).toRaw());
        mm::OwningExternalRCRef stableRefHandle3(stableRefsProducer.createExternalRCRefImpl(stableRef3->header(), 1).toRaw());

        threadData.globalsThreadQueue().Publish();
        stableRefsProducer.publish();

        mm::GlobalRootSet iter(mm::GlobalsRegistry::Instance(), externalRCRefsRegistry);

        std::vector<mm::GlobalRootSet::Value> actual;
        for (auto object : iter) {
            actual.push_back(object);
        }

        auto asGlobal = [](Global& global) -> mm::GlobalRootSet::Value { return {*global, mm::GlobalRootSet::Source::kGlobal}; };
        auto asStableRef = [](std::unique_ptr<Object>& object) -> mm::GlobalRootSet::Value { return {object->header(), mm::GlobalRootSet::Source::kStableRef}; };
        EXPECT_THAT(
                actual,
                testing::UnorderedElementsAre(
                        asGlobal(global1), asGlobal(global2), asStableRef(stableRef1), asStableRef(stableRef2), asStableRef(stableRef3)));
        mm::GlobalsRegistry::Instance().ClearForTests();
    });
}

TEST(GlobalRootSetTest, Empty) {
    RunInNewThread([](mm::ThreadData& threadData) {
        mm::GlobalsRegistry globals;
        mm::ExternalRCRefRegistry externalRCRefsRegistry;

        mm::GlobalRootSet iter(globals, externalRCRefsRegistry);

        std::vector<mm::GlobalRootSet::Value> actual;
        for (auto object : iter) {
            actual.push_back(object);
        }

        EXPECT_THAT(actual, testing::IsEmpty());
    });
}
