#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/serialization.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/tests/small/object.h>

#include <library/cpp/testing/unittest/registar.h>

#include <chrono>
#include <optional>
#include <string_view>

namespace maps::mrc::yt::tests {

Y_UNIT_TEST_SUITE(serialization_tests) {

template<typename T>
bool equal(const T& lhs, const T& rhs);

bool equal(const geolib3::LinearRing2& lhs, const geolib3::LinearRing2& rhs)
{
    if (lhs.pointsNumber() != rhs.pointsNumber()) {
        return false;
    }

    for (size_t i = 0; i < lhs.pointsNumber(); ++i) {
        if (!equal(lhs.pointAt(i), rhs.pointAt(i))) {
            return false;
        }
    }

    return true;
}

bool equal(const geolib3::Polygon2& lhs, const geolib3::Polygon2& rhs)
{
    if (lhs.interiorRingsNumber() != rhs.interiorRingsNumber()) {
        return false;
    }

    if (!equal(lhs.exteriorRing(), rhs.exteriorRing())) {
        return false;
    }

    for (size_t i = 0; i < lhs.interiorRingsNumber(); ++i) {
        if (!equal(lhs.interiorRingAt(i), rhs.interiorRingAt(i))) {
            return false;
        }
    }

    return true;
}

bool equal(const geolib3::Polyline2& lhs, const geolib3::Polyline2& rhs)
{
    return lhs.points() == rhs.points();
}

template<typename T>
bool equal(const FeatureWithImage<T>& lhs, const FeatureWithImage<T>& rhs)
{
    // skip image exact comparison, because of jpg compression
    return equal(lhs.feature, rhs.feature) && equal(lhs.image.size(), rhs.image.size());
}

template<class T, std::size_t... I>
bool equalTuplesImpl(const T& lhs, const T& rhs, std::index_sequence<I...>)
{
    return (equal(std::get<I>(lhs), std::get<I>(rhs)) && ...);
}

template<class ...Ts>
bool equalTuples(const std::tuple<Ts...>& lhs, const std::tuple<Ts...>& rhs)
{
    return equalTuplesImpl(lhs, rhs, std::index_sequence_for<Ts...>{});
}

template<typename T>
bool equal(const T& lhs, const T& rhs)
{
    if constexpr (introspection::is_introspectable_v<T>) {
        return equalTuples(introspection::introspect(lhs), introspection::introspect(rhs));
    } else {
        return lhs == rhs;
    }
}

template<typename Entity>
bool test(const Entity& object)
{
    const auto node = serialize(object);
    const auto deserialized = deserialize<Entity>(node);

    return equal(object, deserialized);
}

Y_UNIT_TEST(base_types)
{
    UNIT_ASSERT(test<bool>(false));
    UNIT_ASSERT(test<bool>(true));

    UNIT_ASSERT(test<char>('5'));
    UNIT_ASSERT(test<unsigned char>('5'));

    UNIT_ASSERT(test<int8_t>(-5));
    UNIT_ASSERT(test<uint8_t>(5));

    UNIT_ASSERT(test<int16_t>(-3));
    UNIT_ASSERT(test<uint16_t>(4));

    UNIT_ASSERT(test<int32_t>(-3));
    UNIT_ASSERT(test<uint32_t>(4));

    UNIT_ASSERT(test<int64_t>(-1));
    UNIT_ASSERT(test<uint64_t>(2));

    UNIT_ASSERT(test<float>(-1.0));
    UNIT_ASSERT(test<double>(1.0));

    UNIT_ASSERT(test<std::string>("Hello world!"));

    {
        const std::string_view value{"string view"};
        UNIT_ASSERT(value == deserialize<std::string>(serialize(value)));
    }
}

Y_UNIT_TEST(wrong_enum_value_deserialization)
{
    UNIT_ASSERT_EXCEPTION(
        deserialize<SomeEnum>(NYT::TNode(TString("wrong_value"))),
        maps::RuntimeError
    );
}

Y_UNIT_TEST(complex_types)
{
    UNIT_ASSERT(test(SomeEnum::One));

    UNIT_ASSERT(test(traffic_signs::TrafficSign::InformationParking));

    UNIT_ASSERT(test(chrono::parseSqlDateTime("2015-06-16 15:25:21.662649+03")));
    UNIT_ASSERT(test(
        std::chrono::time_point<
            std::chrono::system_clock,
            std::chrono::minutes>(std::chrono::minutes(20))));

    UNIT_ASSERT(test(std::chrono::seconds(10)));
    UNIT_ASSERT(test(std::chrono::minutes(60)));
    UNIT_ASSERT(test(std::chrono::minutes(-60)));

    UNIT_ASSERT(test<Bytes>({0, 1, 2}));

    UNIT_ASSERT(test(json::Value::fromString(R"({"a": 1, "b":[1, 2]})")));

    UNIT_ASSERT(test(common::ImageOrientation{common::Rotation::CW_0}));

    UNIT_ASSERT(test(common::ImageBox{0, 0, 100, 100}));

    UNIT_ASSERT(test(geolib3::Heading{31}));
}

Y_UNIT_TEST(compound_types)
{
    UNIT_ASSERT(test<std::optional<int>>(1));
    UNIT_ASSERT(test<std::optional<int>>(std::nullopt));

    UNIT_ASSERT(test<std::vector<int>>({1, 2, 3, 4}));
    UNIT_ASSERT(test<std::vector<float>>({1.0, 2.0, 3.0}));
    UNIT_ASSERT(test<std::vector<std::string>>({"hello", "world"}));
    UNIT_ASSERT(test<std::vector<std::optional<int>>>({std::nullopt}));
}

Y_UNIT_TEST(geolib_types)
{
    UNIT_ASSERT(test<geolib3::Point2>({1.0, 2.0}));

    UNIT_ASSERT(
        test<geolib3::LinearRing2>(
            geolib3::LinearRing2(
                geolib3::PointsVector{{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}, {0.0, 0.0}}
            )
        )
    );

    UNIT_ASSERT(
        test<geolib3::Polyline2>(
            geolib3::Polyline2(
                geolib3::PointsVector{{1.0, 2.0}, {3.0, 4.0}}
            )
        )
    );

    UNIT_ASSERT(
        test<geolib3::Polygon2>(
            geolib3::Polygon2(
                geolib3::LinearRing2(
                    geolib3::PointsVector{{0.0, 0.0}, {3.0, 0.0}, {3.0, 3.0}, {0.0, 3.0}, {0.0, 0.0}}
                ),
                {
                    geolib3::LinearRing2(
                        geolib3::PointsVector{{1.1, 1.1}, {2.0, 1.0}, {2.0, 2.0}, {1.0, 2.0}, {1.0, 1.0}}
                    ),
                }
            )
        )
    );
}

Y_UNIT_TEST(table_serializabel_object)
{
    const mrc::db::Object object{1, "a"};
    UNIT_ASSERT(test(object));
}

Y_UNIT_TEST(serializable_object)
{
    const mrc::Object object{1, "a"};
    UNIT_ASSERT(test(object));
}

} // Y_UNIT_TEST_SUITE

} // namespace maps::mrc::yt::tests
