#include <boost/container/flat_map.hpp>
#include <sdbusplus/unpack_properties.hpp>

#include <gmock/gmock.h>

namespace sdbusplus
{

struct ThrowingUnpack
{
    template <typename... Args>
    bool operator()(Args&&... args) const
    {
        unpackProperties(std::forward<Args>(args)...);
        return false;
    }
};

struct NonThrowingUnpack
{
    struct UnpackError
    {
        UnpackError(sdbusplus::UnpackErrorReason r, const std::string& p) :
            reason(r), property(p)
        {}
        sdbusplus::UnpackErrorReason reason;
        std::string property;
    };

    template <typename... Args>
    std::optional<UnpackError> operator()(Args&&... args) const
    {
        std::optional<UnpackError> error;
        unpackPropertiesNoThrow(
            [&error](sdbusplus::UnpackErrorReason reason,
                     const std::string& property) {
                error.emplace(reason, property);
            },
            std::forward<Args>(args)...);
        return error;
    }
};

template <typename A, typename B>
struct TestingTypes
{
    using SystemUnderTest = A;
    using Container = B;
};

using VariantType = std::variant<std::string, uint32_t, float, double>;
using ContainerTypes = testing::Types<
    TestingTypes<NonThrowingUnpack,
                 std::vector<std::pair<std::string, VariantType>>>,
    TestingTypes<ThrowingUnpack,
                 std::vector<std::pair<std::string, VariantType>>>>;

template <typename Exception, typename F>
std::optional<Exception> captureException(F&& code)
{
    try
    {
        code();
    }
    catch (const Exception& e)
    {
        return e;
    }

    return std::nullopt;
}

template <typename Params>
struct UnpackPropertiesTest : public testing::Test
{
    void SetUp() override
    {
        using namespace std::string_literals;

        data.insert(data.end(),
                    std::make_pair("Key-1"s, VariantType("string"s)));
        data.insert(data.end(), std::make_pair("Key-2"s, VariantType(42.f)));
        data.insert(data.end(), std::make_pair("Key-3"s, VariantType(15.)));
    }

    typename Params::Container data;
    typename Params::SystemUnderTest unpackPropertiesCall;
};

TYPED_TEST_SUITE(UnpackPropertiesTest, ContainerTypes);

TYPED_TEST(UnpackPropertiesTest, returnsValueWhenKeyIsPresentAndTypeMatches)
{
    using namespace testing;

    std::string val1;
    float val2 = 0.f;
    double val3 = 0.;

    EXPECT_FALSE(this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-2",
                                            val2, "Key-3", val3));

    ASSERT_THAT(val1, Eq("string"));
    ASSERT_THAT(val2, FloatEq(42.f));
    ASSERT_THAT(val3, DoubleEq(15.));
}

TYPED_TEST(UnpackPropertiesTest,
           unpackDoesntChangeOriginalDataWhenPassedAsNonConstReference)
{
    using namespace testing;

    std::string val1, val2;

    EXPECT_FALSE(this->unpackPropertiesCall(this->data, "Key-1", val1));
    EXPECT_FALSE(this->unpackPropertiesCall(this->data, "Key-1", val2));

    ASSERT_THAT(val1, Eq("string"));
    ASSERT_THAT(val2, Eq("string"));
}

TYPED_TEST(UnpackPropertiesTest, doesntReportMissingPropertyForOptional)
{
    using namespace testing;
    using namespace std::string_literals;

    std::optional<std::string> val1;
    std::optional<std::string> val4;

    EXPECT_FALSE(
        this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-4", val4));

    ASSERT_THAT(val1, Eq("string"));
    ASSERT_THAT(val4, Eq(std::nullopt));
}

TYPED_TEST(UnpackPropertiesTest, setPresentPointersOnSuccess)
{
    using namespace testing;
    using namespace std::string_literals;

    const std::string* val1 = nullptr;
    const float* val2 = nullptr;
    const double* val3 = nullptr;
    const std::string* val4 = nullptr;

    EXPECT_FALSE(
        this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-2", val2,
                                   "Key-3", val3, "Key-4", val4));

    ASSERT_TRUE(val1 && val2 && val3);
    ASSERT_TRUE(!val4);

    ASSERT_THAT(*val1, Eq("string"));
    ASSERT_THAT(*val2, FloatEq(42.f));
    ASSERT_THAT(*val3, DoubleEq(15.));
}

template <typename Params>
struct UnpackPropertiesThrowingTest : public UnpackPropertiesTest<Params>
{};

using ContainerTypesThrowing = testing::Types<TestingTypes<
    ThrowingUnpack, std::vector<std::pair<std::string, VariantType>>>>;

TYPED_TEST_SUITE(UnpackPropertiesThrowingTest, ContainerTypesThrowing);

TYPED_TEST(UnpackPropertiesThrowingTest, throwsErrorWhenKeyIsMissing)
{
    using namespace testing;

    std::string val1;
    float val2 = 0.f;
    double val3 = 0.;

    auto error = captureException<exception::UnpackPropertyError>([&] {
        this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-4", val2,
                                   "Key-3", val3);
    });

    ASSERT_TRUE(error);
    ASSERT_THAT(error->reason, Eq(UnpackErrorReason::missingProperty));
    ASSERT_THAT(error->propertyName, Eq("Key-4"));
}

TYPED_TEST(UnpackPropertiesThrowingTest, throwsErrorWhenTypeDoesntMatch)
{
    using namespace testing;

    std::string val1;
    std::string val2;
    double val3 = 0.;

    auto error = captureException<exception::UnpackPropertyError>([&] {
        this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-2", val2,
                                   "Key-3", val3);
    });

    ASSERT_TRUE(error);
    ASSERT_THAT(error->reason, Eq(UnpackErrorReason::wrongType));
    ASSERT_THAT(error->propertyName, Eq("Key-2"));
}

TYPED_TEST(UnpackPropertiesThrowingTest, throwsErrorWhenOptionalTypeDoesntMatch)
{
    using namespace testing;

    std::optional<std::string> val1;
    std::optional<std::string> val2;

    auto error = captureException<exception::UnpackPropertyError>([&] {
        this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-2", val2);
    });

    ASSERT_TRUE(error);
    ASSERT_THAT(error->reason, Eq(UnpackErrorReason::wrongType));
    ASSERT_THAT(error->propertyName, Eq("Key-2"));
}

template <typename Params>
struct UnpackPropertiesNonThrowingTest : public UnpackPropertiesTest<Params>
{};

using ContainerTypesNonThrowing = testing::Types<TestingTypes<
    NonThrowingUnpack, std::vector<std::pair<std::string, VariantType>>>>;

TYPED_TEST_SUITE(UnpackPropertiesNonThrowingTest, ContainerTypesNonThrowing);

TYPED_TEST(UnpackPropertiesNonThrowingTest, ErrorWhenKeyIsMissing)
{
    using namespace testing;

    std::string val1;
    float val2 = 0.f;
    double val3 = 0.;

    auto badProperty = this->unpackPropertiesCall(this->data, "Key-1", val1,
                                                  "Key-4", val2, "Key-3", val3);

    ASSERT_TRUE(badProperty);
    EXPECT_THAT(badProperty->reason, Eq(UnpackErrorReason::missingProperty));
    EXPECT_THAT(badProperty->property, Eq("Key-4"));
}

TYPED_TEST(UnpackPropertiesNonThrowingTest, ErrorWhenTypeDoesntMatch)
{
    using namespace testing;

    std::string val1;
    std::string val2;
    double val3 = 0.;

    auto badProperty = this->unpackPropertiesCall(this->data, "Key-1", val1,
                                                  "Key-2", val2, "Key-3", val3);

    ASSERT_TRUE(badProperty);
    EXPECT_THAT(badProperty->reason, Eq(UnpackErrorReason::wrongType));
    EXPECT_THAT(badProperty->property, Eq("Key-2"));
}

TYPED_TEST(UnpackPropertiesNonThrowingTest, ErrorWhenOptionalTypeDoesntMatch)
{
    using namespace testing;

    std::optional<std::string> val1;
    std::optional<std::string> val2;

    auto badProperty =
        this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-2", val2);

    ASSERT_TRUE(badProperty);
    EXPECT_THAT(badProperty->reason, Eq(UnpackErrorReason::wrongType));
    EXPECT_THAT(badProperty->property, Eq("Key-2"));
}

template <typename Params>
struct UnpackPropertiesTest_ForVector : public UnpackPropertiesTest<Params>
{};

using ContainerTypesVector = testing::Types<
    TestingTypes<NonThrowingUnpack,
                 std::vector<std::pair<std::string, VariantType>>>,
    TestingTypes<ThrowingUnpack,
                 std::vector<std::pair<std::string, VariantType>>>>;

TYPED_TEST_SUITE(UnpackPropertiesTest_ForVector, ContainerTypesVector);

TYPED_TEST(UnpackPropertiesTest_ForVector, silentlyDiscardsDuplicatedKeyInData)
{
    using namespace testing;
    using namespace std::string_literals;

    std::string val1;
    float val2 = 0.f;
    double val3 = 0.;

    this->data.insert(this->data.end(),
                      std::make_pair("Key-1"s, VariantType("string2"s)));

    EXPECT_FALSE(this->unpackPropertiesCall(this->data, "Key-1", val1, "Key-2",
                                            val2, "Key-3", val3));

    ASSERT_THAT(val1, Eq("string"));
    ASSERT_THAT(val2, FloatEq(42.f));
    ASSERT_THAT(val3, DoubleEq(15.));
}

} // namespace sdbusplus