#include "ysaveload.h"

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

#include <util/memory/pool.h>
#include <util/stream/buffer.h>
#include <util/memory/blob.h>
#include <util/generic/list.h>
#include <util/generic/map.h>
#include <util/generic/hash_multi_map.h>
#include <util/generic/deque.h>
#include <util/generic/string.h>
#include <util/generic/vector.h>
#include <util/generic/buffer.h>
#include <util/generic/hash_set.h>
#include <util/generic/maybe.h>

static inline char* AllocateFromPool(TMemoryPool& pool, size_t len) {
    return (char*)pool.Allocate(len);
}

class TSaveLoadTest: public TTestBase {
    UNIT_TEST_SUITE(TSaveLoadTest);
    UNIT_TEST(TestSaveLoad)
    UNIT_TEST(TestNewStyle)
    UNIT_TEST(TestNewNewStyle)
    UNIT_TEST(TestList)
    UNIT_TEST(TestTuple)
    UNIT_TEST(TestVariant)
    UNIT_TEST(TestOptional)
    UNIT_TEST(TestInheritNonVirtualClass)
    UNIT_TEST(TestInheritVirtualClass)
    UNIT_TEST_SUITE_END();

    struct TSaveHelper {
        inline void Save(IOutputStream* o) const {
            o->Write("qwerty", 7);
        }

        inline void Load(IInputStream* i) {
            char buf[7];

            UNIT_ASSERT_EQUAL(i->Load(buf, 7), 7);
            UNIT_ASSERT_EQUAL(strcmp(buf, "qwerty"), 0);
        }
    };

    struct TNewStyleSaveHelper {
        template <class S>
        inline void SaveLoad(S* s) {
            ::SaveLoad(s, Str);
        }

        TString Str;
    };

    struct TNewNewStyleHelper {
        TString Str;
        ui32 Int;

        Y_SAVELOAD_DEFINE(Str, Int);
    };

private:
    inline void TestNewNewStyle() {
        TString ss;

        {
            TNewNewStyleHelper h;

            h.Str = "qw";
            h.Int = 42;

            TStringOutput so(ss);

            ::Save(&so, h);
        }

        {
            TNewNewStyleHelper h;

            TStringInput si(ss);
            ::Load(&si, h);

            UNIT_ASSERT_EQUAL(h.Str, "qw");
            UNIT_ASSERT_EQUAL(h.Int, 42);
        }
    }

    inline void TestNewStyle() {
        TString ss;

        {
            TNewStyleSaveHelper sh;
            sh.Str = "qwerty";
            TStringOutput so(ss);
            SaveLoad(&so, sh);
        }

        {
            TNewStyleSaveHelper sh;
            TStringInput si(ss);
            SaveLoad(&si, sh);

            UNIT_ASSERT_EQUAL(sh.Str, "qwerty");
        }
    }

    inline void TestSaveLoad() {
        TBufferStream S_;

        //save part
        {
            Save(&S_, (ui8)1);
            Save(&S_, (ui16)2);
            Save(&S_, (ui32)3);
            Save(&S_, (ui64)4);
        }

        {
            TVector<ui16> vec;

            vec.push_back((ui16)1);
            vec.push_back((ui16)2);
            vec.push_back((ui16)4);

            Save(&S_, vec);
        }

        {
            TMap<ui16, ui32> map;

            map[(ui16)1] = 2;
            map[(ui16)2] = 3;
            map[(ui16)3] = 4;

            Save(&S_, map);
        }

        {
            TMultiMap<ui16, ui32> multimap;

            multimap.emplace((ui16)1, 2);
            multimap.emplace((ui16)2, 3);
            multimap.emplace((ui16)2, 4);
            multimap.emplace((ui16)2, 5);
            multimap.emplace((ui16)3, 6);

            Save(&S_, multimap);
        }

        {
            TSaveHelper helper;

            Save(&S_, helper);
        }

        {
            TString val("123456");

            Save(&S_, val);
        }

        {
            TBuffer buf;

            buf.Append("asdf", 4);
            Save(&S_, buf);
        }

        {
            TVector<const char*> vec;

            vec.push_back("1");
            vec.push_back("123");
            vec.push_back("4567");

            Save(&S_, vec);
        }

        {
            TDeque<ui16> deq;

            deq.push_back(1);
            deq.push_back(2);
            deq.push_back(4);
            deq.push_back(5);

            Save(&S_, deq);
        }

        {
            TMaybe<size_t> h(10);
            Save(&S_, h);
        }

        {
            TMaybe<size_t> h(20);
            Save(&S_, h);
        }

        {
            TMaybe<size_t> h;
            Save(&S_, h);
        }

        {
            TMaybe<size_t> h;
            Save(&S_, h);
        }

        {
            THashMultiMap<TString, int> mm;

            mm.insert({"one", 1});
            mm.insert({"two", 2});
            mm.insert({"two", 22});

            Save(&S_, mm);
        }

        //load part
        {
            ui8 val;

            Load(&S_, val);
            UNIT_ASSERT_EQUAL(val, 1);
        }

        {
            ui16 val;

            Load(&S_, val);
            UNIT_ASSERT_EQUAL(val, 2);
        }

        {
            ui32 val;

            Load(&S_, val);
            UNIT_ASSERT_EQUAL(val, 3);
        }

        {
            ui64 val;

            Load(&S_, val);
            UNIT_ASSERT_EQUAL(val, 4);
        }

        {
            TVector<ui16> vec;

            Load(&S_, vec);
            UNIT_ASSERT_EQUAL(vec.size(), 3);
            UNIT_ASSERT_EQUAL(vec[0], 1);
            UNIT_ASSERT_EQUAL(vec[1], 2);
            UNIT_ASSERT_EQUAL(vec[2], 4);
        }

        {
            TMap<ui16, ui32> map;

            Load(&S_, map);
            UNIT_ASSERT_EQUAL(map.size(), 3);
            UNIT_ASSERT_EQUAL(map[(ui16)1], 2);
            UNIT_ASSERT_EQUAL(map[(ui16)2], 3);
            UNIT_ASSERT_EQUAL(map[(ui16)3], 4);
        }

        {
            TMultiMap<ui16, ui32> multimap;

            Load(&S_, multimap);
            UNIT_ASSERT_EQUAL(multimap.size(), 5);
            UNIT_ASSERT_EQUAL(multimap.find((ui16)1)->second, 2);
            UNIT_ASSERT_EQUAL(multimap.find((ui16)3)->second, 6);

            THashSet<ui32> values;
            auto range = multimap.equal_range((ui16)2);
            for (auto i = range.first; i != range.second; ++i) {
                values.insert(i->second);
            }

            UNIT_ASSERT_EQUAL(values.size(), 3);
            UNIT_ASSERT_EQUAL(values.contains(3), true);
            UNIT_ASSERT_EQUAL(values.contains(4), true);
            UNIT_ASSERT_EQUAL(values.contains(5), true);
        }

        {
            TSaveHelper helper;

            Load(&S_, helper);
        }

        {
            TString val;

            Load(&S_, val);
            UNIT_ASSERT_EQUAL(val, "123456");
        }

        {
            TBuffer buf;

            Load(&S_, buf);
            UNIT_ASSERT_EQUAL(buf.size(), 4);
            UNIT_ASSERT_EQUAL(memcmp(buf.data(), "asdf", 4), 0);
        }

        {
            TVector<const char*> vec;
            TMemoryPool pool(1024);

            Load(&S_, vec, pool);

            UNIT_ASSERT_EQUAL(vec.size(), 3);
            UNIT_ASSERT_EQUAL(vec[0], TString("1"));
            UNIT_ASSERT_EQUAL(vec[1], TString("123"));
            UNIT_ASSERT_EQUAL(vec[2], TString("4567"));
        }

        {
            TDeque<ui16> deq;

            Load(&S_, deq);

            UNIT_ASSERT_EQUAL(deq.size(), 4);
            UNIT_ASSERT_EQUAL(deq[0], 1);
            UNIT_ASSERT_EQUAL(deq[1], 2);
            UNIT_ASSERT_EQUAL(deq[2], 4);
            UNIT_ASSERT_EQUAL(deq[3], 5);
        }

        {
            TMaybe<size_t> h(5);
            Load(&S_, h);
            UNIT_ASSERT_EQUAL(*h, 10);
        }

        {
            TMaybe<size_t> h;
            Load(&S_, h);
            UNIT_ASSERT_EQUAL(*h, 20);
        }

        {
            TMaybe<size_t> h;
            UNIT_ASSERT(!h);
            Load(&S_, h);
            UNIT_ASSERT(!h);
        }

        {
            TMaybe<size_t> h(7);
            UNIT_ASSERT(!!h);
            Load(&S_, h);
            UNIT_ASSERT(!h);
        }

        {
            THashMultiMap<TString, int> mm;

            Load(&S_, mm);

            UNIT_ASSERT_EQUAL(mm.size(), 3);
            UNIT_ASSERT_EQUAL(mm.count("one"), 1);
            auto oneIter = mm.equal_range("one").first;
            UNIT_ASSERT_EQUAL(oneIter->second, 1);
            UNIT_ASSERT_EQUAL(mm.count("two"), 2);
            auto twoIter = mm.equal_range("two").first;
            UNIT_ASSERT_EQUAL(twoIter->second, 2);
            UNIT_ASSERT_EQUAL((++twoIter)->second, 22);
        }
    }

    void TestList() {
        TBufferStream s;

        TList<int> list = {0, 1, 10};
        Save(&s, list);

        list.clear();
        Load(&s, list);

        UNIT_ASSERT_VALUES_EQUAL(list.size(), 3);
        UNIT_ASSERT_VALUES_EQUAL(*std::next(list.begin(), 0), 0);
        UNIT_ASSERT_VALUES_EQUAL(*std::next(list.begin(), 1), 1);
        UNIT_ASSERT_VALUES_EQUAL(*std::next(list.begin(), 2), 10);
    }

    void TestTuple() {
        TBufferStream s;

        using TTuple = std::tuple<int, TString, unsigned int>;
        const TTuple toSave{-10, "qwerty", 15};
        Save(&s, toSave);

        TTuple toLoad;
        Load(&s, toLoad);

        UNIT_ASSERT_VALUES_EQUAL(std::get<0>(toLoad), std::get<0>(toSave));
        UNIT_ASSERT_VALUES_EQUAL(std::get<1>(toLoad), std::get<1>(toSave));
        UNIT_ASSERT_VALUES_EQUAL(std::get<2>(toLoad), std::get<2>(toSave));
    }

    template <class TVariant, class T>
    void TestVariantImpl(TVariant& v, const T& expected) {
        v = expected;

        TBufferStream s;
        ::Save(&s, v);
        ::Load(&s, v);
        UNIT_ASSERT_VALUES_EQUAL(std::get<T>(v), expected);
    }

    void TestVariant() {
        std::variant<int, bool, TString, TVector<char>> v(1);
        TestVariantImpl(v, 42);
        TestVariantImpl(v, true);
        TestVariantImpl(v, TString("foo"));
        TestVariantImpl(v, TVector<char>{'b', 'a', 'r'});

        v = TString("baz");
        TBufferStream s;
        ::Save(&s, v);

        std::variant<char, bool> v2 = false;
        UNIT_ASSERT_EXCEPTION(::Load(&s, v2), TLoadEOF);
    }

    template <class T>
    void TestOptionalImpl(const std::optional<T>& v) {
        std::optional<T> loaded;
        TBufferStream s;
        ::Save(&s, v);
        ::Load(&s, loaded);

        UNIT_ASSERT_VALUES_EQUAL(v.has_value(), loaded.has_value());
        if (v.has_value()) {
            UNIT_ASSERT_VALUES_EQUAL(*v, *loaded);
        }
    }

    void TestOptional() {
        TestOptionalImpl(std::optional<ui64>(42ull));
        TestOptionalImpl(std::optional<bool>(true));
        TestOptionalImpl(std::optional<TString>("abacaba"));
        TestOptionalImpl(std::optional<ui64>(std::nullopt));
    }

    //  tests serialization of class with three public string members
    template <class TDerived, class TInterface = TDerived>
    void TestInheritClassImpl() {
        TBufferStream s;
        {
            TDerived v1;
            v1.Str1 = "One";
            v1.Str2 = "Two";
            v1.Str3 = "Three";
            ::Save(&s, static_cast<const TInterface&>(v1));
        }
        {
            TDerived v2;
            ::Load(&s, static_cast<TInterface&>(v2));
            UNIT_ASSERT_VALUES_EQUAL_C(v2.Str1, "One", TypeName<TDerived>() << " via " << TypeName<TInterface>());
            UNIT_ASSERT_VALUES_EQUAL_C(v2.Str2, "Two", TypeName<TDerived>() << " via " << TypeName<TInterface>());
            UNIT_ASSERT_VALUES_EQUAL_C(v2.Str3, "Three", TypeName<TDerived>() << " via " << TypeName<TInterface>());
        }
    }

    void TestInheritNonVirtualClass() {
        struct TBaseNonVirtual {
            TString Str1;
            Y_SAVELOAD_DEFINE(Str1);
        };
        struct TDerivedNonVirtual: TBaseNonVirtual {
            TString Str2;
            TString Str3;
            Y_SAVELOAD_DEFINE(TNonVirtualSaver<TBaseNonVirtual>{this}, Str2, Str3);
        };
        TestInheritClassImpl<TDerivedNonVirtual>();
    }

    void TestInheritVirtualClass() {
        struct IInterface {
            virtual void Save(IOutputStream* out) const = 0;
            virtual void Load(IInputStream* in) = 0;
        };
        struct TBaseVirtual: IInterface {
            TString Str1;
            Y_SAVELOAD_DEFINE_OVERRIDE(Str1);
        };
        struct TDerivedVirtual: TBaseVirtual {
            TString Str2;
            TString Str3;
            Y_SAVELOAD_DEFINE_OVERRIDE(TNonVirtualSaver<TBaseVirtual>{this}, Str2, Str3);
        };
        TestInheritClassImpl<TDerivedVirtual>();
        TestInheritClassImpl<TDerivedVirtual, TBaseVirtual>();
        TestInheritClassImpl<TDerivedVirtual, IInterface>();
    }
};

UNIT_TEST_SUITE_REGISTRATION(TSaveLoadTest);