#include "mingeo_component.h"
#include <saas/api/action.h>
#include <saas/rtyserver/components/mingeo/ut/component_test.h>
#include <saas/rtyserver/components/l2/l2_memory_manager.h>
#include <saas/rtyserver/config/searcher_config.h>
#include <library/cpp/json/writer/json.h>
#include <library/cpp/json/json_reader.h>
#include <library/cpp/testing/unittest/registar.h>

namespace NRTYServer {
    using TDocument = TMessage::TDocument;
    // mocks "application container" for unit tests
    class TMinGeoComponentTestHelper: public TComponentTestHelper {
    public:
        using TPtr = TIntrusivePtr<TMinGeoComponentTestHelper>;

    public:
        THolder<TMinGeoComponent> Component;
        THolder<TMinGeoParser> Parser;

    public:
        TMinGeoComponentTestHelper(const TComponentTestHelper::TPatch& configPatch) {
            TUnstrictConfig cfg;
            TComponentTestHelper::FillConfig(cfg);
            for (const auto& [path, value] : configPatch)
                cfg.SetValue(path, value);

            cfg.SetValue("Server.Components", "FULLARC,MinGeo");
            cfg.SetValue("Server.ComponentsConfig." FULL_ARCHIVE_COMPONENT_NAME ".ActiveLayers", "MinGeo");
            TAtomicSharedPtr<TDaemonConfig> dae = MakeAtomicShared<TDaemonConfig>(cfg.ToString().c_str(), false);
            THolder<TRTYServerConfig> rtyConfig = MakeHolder<TRTYServerConfig>(dae);
            rtyConfig->InitFromString(cfg.ToString());
            rtyConfig->GetSearcherConfig().Factors = MakeMockRelevConf();
            SetConfig(std::move(rtyConfig));
        }

        static TAtomicSharedPtr<NRTYFactors::TConfig> MakeMockRelevConf() {
            NJsonWriter::TBuf json;
            json.BeginObject().EndObject();
            const TString tmpFile = "relev.tmp";
            {
                TFileOutput o(tmpFile);
                o.SetFinishPropagateMode(true);
                o << json.Str() << Endl;
            }
            return MakeAtomicShared<NRTYFactors::TConfig>(tmpFile.c_str());
        }

        void Start() {
            SetUpGlobals();
            Component = MakeHolder<TMinGeoComponent>(GetConfig());
            Parser = MakeHolder<TMinGeoParser>(Component->GetCore());
        }

        const TGeoTransientEntity& Parse(TParsedDocument& pd, const NRTYServer::TMessage::TDocument& doc) const {
            Y_VERIFY(Parser);

            TDocParseContext dpc;
            TComponentParser::TParsingContext pc(pd, doc, NRTYServer::TMessage::MODIFY_DOCUMENT, dpc);
            Parser->Parse(pc);
            TGeoTransientEntity* e = pd.GetComponentEntity<TGeoTransientEntity>((TString)MinGeoComponentName);
            Y_ENSURE(e != nullptr);
            return *e;
        }
    };

    class TMinGeoComponentPrinter {
    public:
        NJson::TJsonValue ToList(const NRTYGeo::EGeoObjectType kind, const TVector<NRTYGeo::TRawPoint>& data) {
            using namespace NJson;
            TJsonValue dest(EJsonValueType::JSON_ARRAY);
            if (kind != NRTYGeo::EGeoObjectType::MultiRect) {
                for (const TRawPoint& point : data) {
                    TJsonValue ll(EJsonValueType::JSON_ARRAY);
                    ll.AppendValue(point.first);  //lon
                    ll.AppendValue(point.second); //lat
                    dest.AppendValue(std::move(ll));
                }
            } else {
                for (size_t i = 0; i < data.size(); i += 2) {
                    TRawPoint lower = data[i];
                    TJsonValue ll(EJsonValueType::JSON_ARRAY);
                    ll.AppendValue(lower.first);  //lon
                    ll.AppendValue(lower.second); //lat

                    TRawPoint upper = (i + 1 < data.size()) ? data[i + 1] : TRawPoint{0, 0};
                    ll.AppendValue(upper.first);  //lon
                    ll.AppendValue(upper.second); //lat
                    dest.AppendValue(std::move(ll));
                }
            }
            return dest;
        }

        NJson::TJsonValue ToJson(NRTYGeo::TRecord& l2Record) {
            using namespace NRTYGeo;
            using namespace NJson;

            TJsonValue a(EJsonValueType::JSON_ARRAY);
            for (const NRTYGeo::TGeoLayer& o : l2Record.Layers) {
                TJsonValue m(EJsonValueType::JSON_MAP);
                m.InsertValue("kind", (int)o.Object.Kind);
                m.InsertValue("coords", ToList(o.Object.Kind, o.Object.GeoPoints));
                m.InsertValue("layer", (int)o.LayerId);
                a.AppendValue(std::move(m));
            }
            return a;
        }

        static NRTYGeo::TRecord LoadRecord(const TBlob& blob) {
            Y_ENSURE(!blob.IsNull());
            NRTYGeo::TRecord l2Record;
            TMinGeoParser::Load(blob, l2Record);
            return l2Record;
        }

        TString Print(const TGeoTransientEntity& pe) {
            NJsonWriter::TBuf json;

            const TBlob& blob = pe.GetData();
            if (!blob.IsNull()) {
                NRTYGeo::TRecord l2Record = LoadRecord(blob);
                NJson::TJsonValue tmp = ToJson(l2Record);
                json.WriteJsonValue(&tmp);
            } else {
                json.WriteNull();
            }

            return json.Str();
        }
    };

    class TMinGeoBuilderTest: public NUnitTest::TTestBase {
        UNIT_TEST_SUITE(TMinGeoBuilderTest)
        UNIT_TEST(TestParseTextCoords)
        UNIT_TEST(TestParseMsgs)
        UNIT_TEST_SUITE_END();

    private:
        TMinGeoComponentTestHelper::TPtr App;

    private:
        void SetUp() override {
            InitGlobalLog2Console(TLOG_WARNING);
        }

        void TearDown() override {
            if (App) {
                App->TearDownGlobals();
                App.Drop();
            }
        }

    public:
        void TestParseTextCoords() {
            TComponentTestHelper::TPatch cfg;
            App = MakeIntrusive<TMinGeoComponentTestHelper>(cfg);
            App->Start();

            TMinGeoComponentPrinter pri;
            {
                TDocument doc;
                TParsedDocument pd(App->GetConfig());
                const TGeoTransientEntity& e = App->Parse(pd, doc);

                UNIT_ASSERT_VALUES_EQUAL("null", pri.Print(e));
            }
            {
                TDocument doc;
                auto* p = doc.AddDocumentProperties();
                p->SetName("coords");
                p->SetValue("");

                TParsedDocument pd(App->GetConfig());
                UNIT_ASSERT_VALUES_EQUAL("[{\"kind\":1,\"coords\":[],\"layer\":0}]", pri.Print(App->Parse(pd, doc)));
            }
            {
                TDocument doc;
                auto* p = doc.AddDocumentProperties();
                p->SetName("coords");
                p->SetValue("23.1:43.65;1000:43.9;23.1:43.65");

                TParsedDocument pd(App->GetConfig());
                UNIT_ASSERT_VALUES_EQUAL("[{\"kind\":1,\"coords\":[[23.10000038,43.65000153],[1000,43.90000153],[23.10000038,43.65000153]],\"layer\":0}]", pri.Print(App->Parse(pd, doc)));
            }
            {
                TDocument doc;
                auto* p = doc.AddDocumentProperties();
                p->SetName("coords");
                p->SetValue("1,2;3,4;5,6"); // incorrect separator

                TParsedDocument pd(App->GetConfig());
                const TGeoTransientEntity& e = App->Parse(pd, doc); // no exception, just empty coords (why not?)
                NRTYGeo::TRecord l2Record = TMinGeoComponentPrinter::LoadRecord(e.GetData());
                UNIT_ASSERT_VALUES_EQUAL(1, (int)l2Record.Layers.size());
                UNIT_ASSERT_VALUES_EQUAL(0, (int)l2Record.Layers[0].LayerId);
                UNIT_ASSERT_VALUES_EQUAL((int)NRTYGeo::EGeoObjectType::MultiPoint, (int)l2Record.Layers[0].Object.Kind);
                UNIT_ASSERT(l2Record.Layers[0].Object.GeoPoints.empty());
            }
            {
                TDocument doc;
                auto* p = doc.AddDocumentProperties();
                p->SetName("coords");
                p->SetValue(";2:2;;3:1;0:4;-10:-10;;"); // extra delimiters, must be ignored

                TParsedDocument pd(App->GetConfig());
                UNIT_ASSERT_VALUES_EQUAL("[{\"kind\":1,\"coords\":[[2,2],[3,1],[0,4],[-10,-10]],\"layer\":0}]", pri.Print(App->Parse(pd, doc)));
            }
        }

        static NSaas::TAction MakeAction() {
            NSaas::TAction action;
            action.SetActionType(NSaas::TAction::atModify);
            action.AddDocument().SetUrl(" ");
            return action;
        }

        static TDocument ConvertToDoc(NSaas::TAction& msg) {
            // transform TAction to Json format, and parse it back as TMessage. This is to make the test more realistic
            TString strJsonMsg = msg.BuildJsonMessage();
            NJson::TJsonValue jsonMsg;
            Y_ENSURE(NJson::ReadJsonTree(TStringBuf(strJsonMsg), &jsonMsg, /*throwOnError=*/true));

            NSaas::TAction parsedMsg;
            parsedMsg.ParseFromJson(jsonMsg);
            return parsedMsg.ToProtobuf().GetDocument();
        }

        void TestParseMsgs() {
            TComponentTestHelper::TPatch cfg;
            App = MakeIntrusive<TMinGeoComponentTestHelper>(cfg);
            App->Start();

            TMinGeoComponentPrinter pri;
            {
                NSaas::TAction msg = MakeAction();
                NSaas::TGeoBlock& geo = msg.GetDocument().AddGeo();
                geo.AddPointSet("coords", true).AddPoint(2, 2).AddPoint(3, 1).AddPoint(0, 4);

                TParsedDocument pd(App->GetConfig());
                TDocument doc = ConvertToDoc(msg);
                const TGeoTransientEntity& e = App->Parse(pd, doc);

                UNIT_ASSERT_VALUES_EQUAL("[{\"kind\":1,\"coords\":[[2,2],[3,1],[0,4]],\"layer\":0}]", pri.Print(e));
                UNIT_ASSERT(e.BuilderExtension.Defined());

                const size_t invHitsCount = e.BuilderExtension->Keys.size();
                UNIT_ASSERT_VALUES_EQUAL(3, invHitsCount); // a hit for each point is expected
            }
            {
                NSaas::TAction msg = MakeAction();
                msg.GetDocument().AddGeo().AddRectSet("coords").AddRect(0, 1, 19, 20).AddRect(10, 12, 30, 31);

                TParsedDocument pd(App->GetConfig());
                const TGeoTransientEntity& e = App->Parse(pd, ConvertToDoc(msg));

                UNIT_ASSERT_VALUES_EQUAL("[{\"kind\":2,\"coords\":[[0,1,19,20],[10,12,30,31]],\"layer\":0}]", pri.Print(e));
                UNIT_ASSERT(e.BuilderExtension.Defined());
                UNIT_ASSERT_VALUES_EQUAL(2, e.BuilderExtension->Keys.size());
            }
            {
                NSaas::TAction msg = MakeAction();
                msg.GetDocument().AddGeo().AddPointSet("coords", false).AddPoint(22, 33);

                TParsedDocument pd(App->GetConfig());
                const TGeoTransientEntity& e = App->Parse(pd, ConvertToDoc(msg));

                UNIT_ASSERT_VALUES_EQUAL("[{\"kind\":0,\"coords\":[[22,33]],\"layer\":0}]", pri.Print(e));
                UNIT_ASSERT(e.BuilderExtension.Defined());
                UNIT_ASSERT_VALUES_EQUAL(0, e.BuilderExtension->Keys.size()); // no hits
            }
        }
    };
}

UNIT_TEST_SUITE_REGISTRATION(NRTYServer::TMinGeoBuilderTest);
