/*
 * 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 "ObjectTraversal.hpp"

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

#include "ObjectTestSupport.hpp"
#include "ReferenceOps.hpp"
#include "Types.h"
#include "Utils.hpp"
#include "ObjectOps.hpp"

using namespace kotlin;

using ::testing::_;

namespace {

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

struct Payload {
    mm::RefField field1;
    mm::RefField field2;
    mm::RefField field3;

    static constexpr std::array kFields{
            &Payload::field1,
            &Payload::field2,
            &Payload::field3,
    };
};

using ProcessFunMock = testing::StrictMock<testing::MockFunction<void(mm::RefFieldAccessor)>>;

MATCHER_P(SameAccessor, accessor, "") {
    return arg.direct().location() == accessor.direct().location();
}

} // namespace

TEST(ObjectTraversalTest, TraverseFieldsExceptions) {
    struct CallableWithExceptions {
        void operator()(mm::RefFieldAccessor) noexcept(false) {}
    };
    struct CallableWithoutExceptions {
        void operator()(mm::RefFieldAccessor) noexcept {}
    };

    static_assert(
            noexcept(traverseObjectFields(std::declval<ObjHeader*>(), std::declval<CallableWithoutExceptions>())),
            "Callable is noexcept, so traverse is noexcept");
    static_assert(
            !noexcept(traverseObjectFields(std::declval<ObjHeader*>(), std::declval<CallableWithExceptions>())),
            "Callable is noexcept(false), so traverse is noexcept(false)");
}

TEST(ObjectTraversalTest, TraverseEmptyObjectFields) {
    test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder<EmptyPayload>()};
    test_support::Object<EmptyPayload> object(type.typeInfo());
    ProcessFunMock process;

    EXPECT_CALL(process, Call(_)).Times(0);
    traverseObjectFields(object.header(), process.AsStdFunction());
}

TEST(ObjectTraversalTest, TraverseObjectFields) {
    test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder<Payload>()};
    ObjHeader field1{};
    ObjHeader field3{};
    test_support::Object<Payload> object(type.typeInfo());
    object->field1.direct() = &field1;
    object->field3.direct() = &field3;
    ProcessFunMock process;

    EXPECT_CALL(process, Call(SameAccessor(object->field1.accessor())));
    EXPECT_CALL(process, Call(SameAccessor(object->field2.accessor())));
    EXPECT_CALL(process, Call(SameAccessor(object->field3.accessor())));
    traverseObjectFields(object.header(), process.AsStdFunction());
}

TEST(ObjectTraversalTest, TraverseObjectFieldsWithException) {
    constexpr int kException = 1;

    test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder<Payload>()};
    ObjHeader field1{};
    ObjHeader field2{};
    ObjHeader field3{};
    test_support::Object<Payload> object(type.typeInfo());
    object->field1.direct() = &field1;
    object->field2.direct() = &field2;
    object->field3.direct() = &field3;
    ProcessFunMock process;

    EXPECT_CALL(process, Call(SameAccessor(object->field1.accessor())));
    EXPECT_CALL(process, Call(SameAccessor(object->field2.accessor()))).WillOnce([]() { throw kException; });
    EXPECT_CALL(process, Call(SameAccessor(object->field3.accessor()))).Times(0);
    try {
        traverseObjectFields(object.header(), process.AsStdFunction());
    } catch (int exception) {
        EXPECT_THAT(exception, kException);
    } catch (...) {
        EXPECT_TRUE(false);
    }
}

TEST(ObjectTraversalTest, TraverseEmptyArrayFields) {
    test_support::ObjectArray<0> array;
    ProcessFunMock process;

    EXPECT_CALL(process, Call(_)).Times(0);
    traverseObjectFields(array.header(), process.AsStdFunction());
}

TEST(ObjectTraversalTest, TraverseArrayFields) {
    ObjHeader element1{};
    ObjHeader element3{};
    test_support::ObjectArray<3> array;
    array.elements()[0].direct() = &element1;
    array.elements()[2].direct() = &element3;
    ProcessFunMock process;

    EXPECT_CALL(process, Call(SameAccessor(array.elements()[0].accessor())));
    EXPECT_CALL(process, Call(SameAccessor(array.elements()[1].accessor())));
    EXPECT_CALL(process, Call(SameAccessor(array.elements()[2].accessor())));
    traverseObjectFields(array.header(), process.AsStdFunction());
}

TEST(ObjectTraversalTest, TraverseArrayFieldsWithException) {
    constexpr int kException = 1;

    ObjHeader element1{};
    ObjHeader element2{};
    ObjHeader element3{};
    test_support::ObjectArray<3> array;
    array.elements()[0].direct() = &element1;
    array.elements()[1].direct() = &element2;
    array.elements()[2].direct() = &element3;
    ProcessFunMock process;

    EXPECT_CALL(process, Call(SameAccessor(array.elements()[0].accessor())));
    EXPECT_CALL(process, Call(SameAccessor(array.elements()[1].accessor()))).WillOnce([]() { throw kException; });
    EXPECT_CALL(process, Call(SameAccessor(array.elements()[2].accessor()))).Times(0);
    try {
        traverseObjectFields(array.header(), process.AsStdFunction());
    } catch (int exception) {
        EXPECT_THAT(exception, kException);
    } catch (...) {
        EXPECT_TRUE(false);
    }
}

TEST(ObjectTraversalTest, TraverseRefsExceptions) {
    struct CallableWithExceptions {
        void operator()(ObjHeader*) noexcept(false) {}
    };
    struct CallableWithoutExceptions {
        void operator()(ObjHeader*) noexcept {}
    };

    static_assert(
            noexcept(traverseReferredObjects(std::declval<ObjHeader*>(), std::declval<CallableWithoutExceptions>())),
            "Callable is noexcept, so traverse is noexcept");
    static_assert(
            !noexcept(traverseReferredObjects(std::declval<ObjHeader*>(), std::declval<CallableWithExceptions>())),
            "Callable is noexcept(false), so traverse is noexcept(false)");
}

TEST(ObjectTraversalTest, TraverseEmptyObjectRefs) {
    test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder<EmptyPayload>()};
    test_support::Object<EmptyPayload> object(type.typeInfo());
    testing::StrictMock<testing::MockFunction<void(ObjHeader*)>> process;

    EXPECT_CALL(process, Call(_)).Times(0);
    traverseReferredObjects(object.header(), [&process](ObjHeader* field) { process.Call(field); });
}

TEST(ObjectTraversalTest, TraverseObjectRefs) {
    test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder<Payload>()};
    ObjHeader field1{};
    ObjHeader field3{};
    test_support::Object<Payload> object(type.typeInfo());
    object->field1.direct() = &field1;
    object->field3.direct() = &field3;
    testing::StrictMock<testing::MockFunction<void(ObjHeader*)>> process;

    EXPECT_CALL(process, Call(&field1));
    EXPECT_CALL(process, Call(&field3));
    traverseReferredObjects(object.header(), [&process](ObjHeader* field) { process.Call(field); });
}

TEST(ObjectTraversalTest, TraverseObjectRefsWithException) {
    constexpr int kException = 1;

    test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder<Payload>()};
    ObjHeader field1{};
    ObjHeader field2{};
    ObjHeader field3{};
    test_support::Object<Payload> object(type.typeInfo());
    object->field1.direct() = &field1;
    object->field2.direct() = &field2;
    object->field3.direct() = &field3;
    testing::StrictMock<testing::MockFunction<void(ObjHeader*)>> process;

    EXPECT_CALL(process, Call(&field1));
    EXPECT_CALL(process, Call(&field2)).WillOnce([]() { throw kException; });
    EXPECT_CALL(process, Call(&field3)).Times(0);
    try {
        traverseReferredObjects(object.header(), [&process](ObjHeader* field) { process.Call(field); });
    } catch (int exception) {
        EXPECT_THAT(exception, kException);
    } catch (...) {
        EXPECT_TRUE(false);
    }
}

TEST(ObjectTraversalTest, TraverseEmptyArrayRefs) {
    test_support::ObjectArray<0> array;
    testing::StrictMock<testing::MockFunction<void(ObjHeader*)>> process;

    EXPECT_CALL(process, Call(_)).Times(0);
    traverseReferredObjects(array.header(), [&process](ObjHeader* field) { process.Call(field); });
}

TEST(ObjectTraversalTest, TraverseArrayRefs) {
    ObjHeader element1{};
    ObjHeader element3{};
    test_support::ObjectArray<3> array;
    array.elements()[0].direct() = &element1;
    array.elements()[2].direct() = &element3;
    testing::StrictMock<testing::MockFunction<void(ObjHeader*)>> process;

    EXPECT_CALL(process, Call(&element1));
    EXPECT_CALL(process, Call(&element3));
    traverseReferredObjects(array.header(), [&process](ObjHeader* field) { process.Call(field); });
}

TEST(ObjectTraversalTest, TraverseArrayRefsWithException) {
    constexpr int kException = 1;

    ObjHeader element1{};
    ObjHeader element2{};
    ObjHeader element3{};
    test_support::ObjectArray<3> array;
    array.elements()[0].direct() = &element1;
    array.elements()[1].direct() = &element2;
    array.elements()[2].direct() = &element3;
    testing::StrictMock<testing::MockFunction<void(ObjHeader*)>> process;

    EXPECT_CALL(process, Call(&element1));
    EXPECT_CALL(process, Call(&element2)).WillOnce([]() { throw kException; });
    EXPECT_CALL(process, Call(&element3)).Times(0);
    try {
        traverseReferredObjects(array.header(), [&process](ObjHeader* field) { process.Call(field); });
    } catch (int exception) {
        EXPECT_THAT(exception, kException);
    } catch (...) {
        EXPECT_TRUE(false);
    }
}
