// https://github.com/vinniefalco/LuaBridge
// Copyright 2021, Stefan Frings
// SPDX-License-Identifier: MIT

#include "TestBase.h"
#include "TestTypes.h"

#include "LuaBridge/Optional.h"

#include <optional>

template<class T>
struct OptionalTest : TestBase
{
};

TYPED_TEST_CASE_P(OptionalTest);

template<typename T>
std::string toLuaSrcString(T const& value)
{
    return std::to_string(value);
}

template<>
std::string toLuaSrcString(bool const& value)
{
    return value ? "true" : "false";
}

template<>
std::string toLuaSrcString(char const& value)
{
    return "'" + std::string(&value, 1) + "'";
}

template<>
std::string toLuaSrcString(std::string const& value)
{
    return "'" + value + "'";
}

template<typename T>
std::optional<T> optCast(luabridge::LuaRef const& ref)
{
    // NOTE cast to std::optional: https://stackoverflow.com/a/45865802
    return ref.cast<std::optional<T>>();
}

TYPED_TEST_P(OptionalTest, LuaRefPresent)
{
    using Traits = TypeTraits<TypeParam>;

    for (TypeParam const& value : Traits::values())
    {
        std::string const luaSrc = "result = " + toLuaSrcString(value);

        SCOPED_TRACE(luaSrc);
        this->runLua(luaSrc);

        std::optional<TypeParam> const actual = optCast<TypeParam>(this->result());
        ASSERT_TRUE(actual.has_value());
        ASSERT_EQ(value, actual.value());
    }
}

TYPED_TEST_P(OptionalTest, LuaRefNotPresent)
{
    this->runLua("result = nil");

    std::optional<TypeParam> const actual = optCast<TypeParam>(this->result());
    ASSERT_FALSE(actual.has_value());
}

TYPED_TEST_P(OptionalTest, LuaRefIsInstancePresent)
{
    using Traits = TypeTraits<TypeParam>;

    for (TypeParam const& value : Traits::values())
    {
        std::string const luaSrc = "result = " + toLuaSrcString(value);

        SCOPED_TRACE(luaSrc);
        this->runLua(luaSrc);

        luabridge::LuaRef const actualRef = this->result();
        ASSERT_TRUE(actualRef.isInstance<std::optional<TypeParam>>());

        // if isInstance returns true a cast without issues is possible
        std::optional<TypeParam> const actual = optCast<TypeParam>(actualRef);
    }
}

TYPED_TEST_P(OptionalTest, LuaRefIsInstancePresentWrongType)
{
    this->runLua("function func() end; result = func");

    luabridge::LuaRef const actualRef = this->result();
    ASSERT_FALSE(actualRef.isInstance<std::optional<TypeParam>>());
}

TYPED_TEST_P(OptionalTest, LuaRefIsInstanceNotPresent)
{
    this->runLua("result = nil");

    luabridge::LuaRef const actualRef = this->result();
    ASSERT_TRUE(actualRef.isInstance<std::optional<TypeParam>>());

    // if isInstance returns true a cast without issues is possible
    std::optional<TypeParam> const actual = optCast<TypeParam>(actualRef);
}

REGISTER_TYPED_TEST_CASE_P(OptionalTest,
                           LuaRefPresent,
                           LuaRefNotPresent,
                           LuaRefIsInstancePresent,
                           LuaRefIsInstancePresentWrongType,
                           LuaRefIsInstanceNotPresent);

INSTANTIATE_TYPED_TEST_CASE_P(OptionalTest, OptionalTest, TestTypes);

namespace {

struct Data
{
    explicit Data(int i) : i(i) {}

    int i;
};

bool operator==(Data const& lhs, Data const& rhs)
{
    return lhs.i == rhs.i;
}

template<typename T>
bool operator==(std::optional<T> const& lhs, std::optional<T> const& rhs)
{
    if (lhs.has_value() != rhs.has_value())
    {
        return false;
    }

    if (lhs.has_value())
    {
        assert(rhs.has_value());
        return lhs.value() == rhs.value();
    }

    assert(!lhs.has_value());
    assert(!rhs.has_value());
    return true;
}

std::optional<Data> processValue(std::optional<Data> const& data)
{
    return data;
}

std::optional<Data> processPointer(std::optional<const Data*> const& data)
{
    std::optional<Data> result;
    if (data)
    {
        result = **data;
    }
    return result;
}

} // namespace

struct OptionalTests : TestBase
{
};

template<typename T>
void testPassFromLua(OptionalTests const& fixture,
                     std::string const& functionName,
                     std::string const& valueString,
                     std::optional<T> const expected)
{
    fixture.resetResult();

    std::string const luaSrc = "result = " + functionName + "(" + valueString + ")";

    SCOPED_TRACE(luaSrc);
    fixture.runLua(luaSrc);

    std::optional<T> const actual = optCast<T>(fixture.result());
    ASSERT_EQ(expected, actual);
}

TEST_F(OptionalTests, PassFromLua)
{
    luabridge::getGlobalNamespace(L)
        .beginClass<Data>("Data")
        .addConstructor<void (*)(int)>()
        .endClass()
        .addFunction("processValue", &processValue)
        .addFunction("processPointer", &processPointer);

    testPassFromLua<Data>(*this, "processValue", "Data(-1)", Data(-1));
    testPassFromLua<Data>(*this, "processValue", "Data(2)", Data(2));
    testPassFromLua<Data>(*this, "processValue", "nil", std::nullopt);

    testPassFromLua<Data>(*this, "processPointer", "Data(-1)", Data(-1));
    testPassFromLua<Data>(*this, "processPointer", "Data(2)", Data(2));
    testPassFromLua<Data>(*this, "processPointer", "nil", std::nullopt);
}
