#include "apps/opencs/model/world/infocollection.hpp"

#include "components/esm3/esmreader.hpp"
#include "components/esm3/esmwriter.hpp"
#include "components/esm3/formatversion.hpp"
#include "components/esm3/loaddial.hpp"
#include "components/esm3/loadinfo.hpp"

#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <algorithm>
#include <array>
#include <sstream>
#include <vector>

namespace CSMWorld
{
    inline std::ostream& operator<<(std::ostream& stream, const Record<Info>* value)
    {
        return stream << "&Record{.mState=" << value->mState << ", .mId=" << value->get().mId << "}";
    }

    namespace
    {
        using namespace ::testing;

        struct DialInfoData
        {
            ESM::DialInfo mValue;
            bool mDeleted = false;

            void save(ESM::ESMWriter& writer) const { mValue.save(writer, mDeleted); }
        };

        template <class T>
        struct DialogueData
        {
            ESM::Dialogue mDialogue;
            std::vector<T> mInfos;
        };

        DialogueData<ESM::DialInfo> generateDialogueWithInfos(
            std::size_t infoCount, std::string_view dialogueId = "dialogue")
        {
            DialogueData<ESM::DialInfo> result;

            result.mDialogue.blank();
            result.mDialogue.mId = ESM::RefId::stringRefId(dialogueId);
            result.mDialogue.mStringId = dialogueId;

            for (std::size_t i = 0; i < infoCount; ++i)
            {
                ESM::DialInfo& info = result.mInfos.emplace_back();
                info.blank();
                info.mId = ESM::RefId::stringRefId("info" + std::to_string(i));
            }

            if (infoCount >= 2)
            {
                result.mInfos[0].mNext = result.mInfos[1].mId;
                result.mInfos[infoCount - 1].mPrev = result.mInfos[infoCount - 2].mId;
            }

            for (std::size_t i = 1; i < infoCount - 1; ++i)
            {
                result.mInfos[i].mPrev = result.mInfos[i - 1].mId;
                result.mInfos[i].mNext = result.mInfos[i + 1].mId;
            }

            return result;
        }

        template <class Infos>
        std::unique_ptr<std::stringstream> saveDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos)
        {
            auto stream = std::make_unique<std::stringstream>();

            ESM::ESMWriter writer;
            writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion);
            writer.save(*stream);

            writer.startRecord(ESM::REC_DIAL);
            dialogue.save(writer);
            writer.endRecord(ESM::REC_DIAL);

            for (const auto& info : infos)
            {
                writer.startRecord(ESM::REC_INFO);
                info.save(writer);
                writer.endRecord(ESM::REC_INFO);
            }

            return stream;
        }

        void loadDialogueWithInfos(bool base, std::unique_ptr<std::stringstream> stream, InfoCollection& infoCollection,
            InfoOrderByTopic& infoOrder)
        {
            ESM::ESMReader reader;
            reader.open(std::move(stream), "test");

            ASSERT_TRUE(reader.hasMoreRecs());
            ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_DIAL);
            reader.getRecHeader();
            bool isDeleted;
            ESM::Dialogue dialogue;
            dialogue.load(reader, isDeleted);

            while (reader.hasMoreRecs())
            {
                ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO);
                reader.getRecHeader();
                infoCollection.load(reader, base, dialogue, infoOrder);
            }
        }

        template <class Infos>
        void saveAndLoadDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos, bool base,
            InfoCollection& infoCollection, InfoOrderByTopic& infoOrder)
        {
            loadDialogueWithInfos(base, saveDialogueWithInfos(dialogue, infos), infoCollection, infoOrder);
        }

        template <class T>
        void saveAndLoadDialogueWithInfos(
            const DialogueData<T>& data, bool base, InfoCollection& infoCollection, InfoOrderByTopic& infoOrder)
        {
            saveAndLoadDialogueWithInfos(data.mDialogue, data.mInfos, base, infoCollection, infoOrder);
        }

        MATCHER_P(InfoId, v, "")
        {
            return arg.mId == v;
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldAddRecord)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            ESM::DialInfo info;
            info.blank();
            info.mId = ESM::RefId::stringRefId("info0");

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            EXPECT_EQ(collection.getSize(), 1);
            ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            const Record<Info>& record = collection.getRecord(0);
            ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
            EXPECT_EQ(record.mBase.mTopicId, dialogue.mId);
            EXPECT_EQ(record.mBase.mOriginalId, info.mId);
            EXPECT_EQ(record.mBase.mId, ESM::RefId::stringRefId("dialogue#info0"));

            ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId)));
            EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId)));
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldAddRecordAndMarkModifiedOnlyWhenNotBase)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            ESM::DialInfo info;
            info.blank();
            info.mId = ESM::RefId::stringRefId("info0");

            const bool base = false;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            EXPECT_EQ(collection.getSize(), 1);
            ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            const Record<Info>& record = collection.getRecord(0);
            ASSERT_EQ(record.mState, RecordBase::State_ModifiedOnly);
            EXPECT_EQ(record.mModified.mTopicId, dialogue.mId);
            EXPECT_EQ(record.mModified.mOriginalId, info.mId);
            EXPECT_EQ(record.mModified.mId, ESM::RefId::stringRefId("dialogue#info0"));

            ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId)));
            EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId)));
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecord)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            ESM::DialInfo info;
            info.blank();
            info.mId = ESM::RefId::stringRefId("info0");

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            ESM::DialInfo updatedInfo = info;
            updatedInfo.mActor = ESM::RefId::stringRefId("newActor");

            saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, base, collection, infoOrder);

            ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            const Record<Info>& record = collection.getRecord(0);
            ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
            EXPECT_EQ(record.mBase.mActor, ESM::RefId::stringRefId("newActor"));

            ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId)));
            EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId)));
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecordAndMarkModifiedWhenNotBase)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            ESM::DialInfo info;
            info.blank();
            info.mId = ESM::RefId::stringRefId("info0");

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            ESM::DialInfo updatedInfo = info;
            updatedInfo.mActor = ESM::RefId::stringRefId("newActor");

            saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, false, collection, infoOrder);

            ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            const Record<Info>& record = collection.getRecord(0);
            ASSERT_EQ(record.mState, RecordBase::State_Modified);
            EXPECT_EQ(record.mModified.mActor, ESM::RefId::stringRefId("newActor"));

            ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId)));
            EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mId)));
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldSkipAbsentDeletedRecord)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            DialInfoData info;
            info.mValue.blank();
            info.mValue.mId = ESM::RefId::stringRefId("info0");
            info.mDeleted = true;

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            EXPECT_EQ(collection.getSize(), 0);

            ASSERT_THAT(infoOrder, ElementsAre());
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldRemovePresentDeletedBaseRecord)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            DialInfoData info;
            info.mValue.blank();
            info.mValue.mId = ESM::RefId::stringRefId("info0");

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            info.mDeleted = true;

            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            EXPECT_EQ(collection.getSize(), 0);

            ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId)));
            EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre());
        }

        TEST(CSMWorldInfoCollectionTest, loadShouldMarkAsDeletedNotBaseRecord)
        {
            ESM::Dialogue dialogue;
            dialogue.blank();
            dialogue.mId = ESM::RefId::stringRefId("dialogue");
            dialogue.mStringId = "Dialogue";

            DialInfoData info;
            info.mValue.blank();
            info.mValue.mId = ESM::RefId::stringRefId("info0");

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);

            info.mDeleted = true;

            saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, false, collection, infoOrder);

            EXPECT_EQ(collection.getSize(), 1);
            EXPECT_EQ(
                collection.getRecord(ESM::RefId::stringRefId("dialogue#info0")).mState, RecordBase::State_Deleted);

            ASSERT_THAT(infoOrder, ElementsAre(Key(dialogue.mId)));
            EXPECT_THAT(infoOrder.find(dialogue.mId)->second.getOrderedInfo(), ElementsAre(InfoId(info.mValue.mId)));
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldOrderRecordsBasedOnPrev)
        {
            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.getSize(), 3);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldOrderRecordsBasedOnPrevWhenReversed)
        {
            DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            std::reverse(data.mInfos.begin(), data.mInfos.end());

            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;
            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.getSize(), 3);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordBasedOnPrev)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo newInfo;
            newInfo.blank();
            newInfo.mId = ESM::RefId::stringRefId("newInfo");
            newInfo.mPrev = data.mInfos[1].mId;
            newInfo.mNext = ESM::RefId::stringRefId("invalid");

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.getSize(), 4);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordToFrontWhenPrevIsEmpty)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo newInfo;
            newInfo.blank();
            newInfo.mId = ESM::RefId::stringRefId("newInfo");
            newInfo.mNext = ESM::RefId::stringRefId("invalid");

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.getSize(), 4);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordToBackWhenPrevIsNotFound)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo newInfo;
            newInfo.blank();
            newInfo.mId = ESM::RefId::stringRefId("newInfo");
            newInfo.mPrev = ESM::RefId::stringRefId("invalid");

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.getSize(), 4);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 3);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldMoveBackwardUpdatedRecordBasedOnPrev)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo updatedInfo = data.mInfos[2];
            updatedInfo.mPrev = data.mInfos[0].mId;
            updatedInfo.mNext = ESM::RefId::stringRefId("invalid");

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldMoveForwardUpdatedRecordBasedOnPrev)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo updatedInfo = data.mInfos[0];
            updatedInfo.mPrev = data.mInfos[1].mId;
            updatedInfo.mNext = ESM::RefId::stringRefId("invalid");

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldMoveToFrontUpdatedRecordWhenPrevIsEmpty)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo updatedInfo = data.mInfos[2];
            updatedInfo.mPrev = ESM::RefId();
            updatedInfo.mNext = ESM::RefId::stringRefId("invalid");

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 0);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldMoveToBackUpdatedRecordWhenPrevIsNotFound)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);

            saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);

            ESM::DialInfo updatedInfo = data.mInfos[0];
            updatedInfo.mPrev = ESM::RefId::stringRefId("invalid");
            updatedInfo.mNext = ESM::RefId();

            saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1);
        }

        TEST(CSMWorldInfoCollectionTest, sortShouldProvideStableOrderByTopic)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue2"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info0")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info1")), 3);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue2#info0")), 4);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue2#info1")), 5);
        }

        TEST(CSMWorldInfoCollectionTest, getAppendIndexShouldReturnFirstIndexAfterInfoTopic)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_EQ(collection.getAppendIndex(ESM::RefId::stringRefId("dialogue0#info2")), 2);
        }

        TEST(CSMWorldInfoCollectionTest, reorderRowsShouldFailWhenOutOfBounds)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder);

            EXPECT_FALSE(collection.reorderRows(5, {}));
        }

        TEST(CSMWorldInfoCollectionTest, reorderRowsShouldFailWhenAppliedToDifferentTopics)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue0"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "dialogue1"), base, collection, infoOrder);

            EXPECT_FALSE(collection.reorderRows(0, { 0, 1, 2 }));
        }

        TEST(CSMWorldInfoCollectionTest, reorderRowsShouldSucceedWhenAppliedToOneTopic)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(3, "dialogue0"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(3, "dialogue1"), base, collection, infoOrder);

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 1);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info2")), 2);

            EXPECT_TRUE(collection.reorderRows(1, { 1, 0 }));

            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 2);
            EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info2")), 1);
        }

        MATCHER_P(RecordPtrIdIs, v, "")
        {
            return v == arg->get().mId;
        }

        TEST(CSMWorldInfoCollectionTest, getInfosByTopicShouldReturnRecordsGroupedByTopic)
        {
            const bool base = true;
            InfoOrderByTopic infoOrder;
            InfoCollection collection;

            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "d0"), base, collection, infoOrder);
            saveAndLoadDialogueWithInfos(generateDialogueWithInfos(2, "d1"), base, collection, infoOrder);

            collection.sort(infoOrder);

            EXPECT_THAT(collection.getInfosByTopic(),
                UnorderedElementsAre(Pair("d0", ElementsAre(RecordPtrIdIs("d0#info0"), RecordPtrIdIs("d0#info1"))),
                    Pair("d1", ElementsAre(RecordPtrIdIs("d1#info0"), RecordPtrIdIs("d1#info1")))));
        }
    }
}