#include <saas/api/clientapi.h>
#include <saas/util/json/json.h>

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

#include <util/stream/output.h>
#include <util/random/random.h>

using namespace NRTY;
using namespace NJson;

//#define COMMON_JSON_UT_DEBUG

namespace {
    const TString Zones[] = {
        "zone1",
        "samplezone"
    };
    const TString SearchAttributes[] = {
        "search_attr1",
        "search_attr2",
        "shared1",
        "shared2"
    };
    const TString GroupAttributes[] = {
        "grp_attr1",
        "shared2",
    };
    const TString Properties[] = {
        "prop1",
        "shared2"
    };
    const TString SpecialKeys[] = {
        "key1",
        "value1"
    };
    const TString FSProperties[] = {
        "fsprop1",
        "fsshared2"
    };
    const TString Factors[] = {
        "erf1",
        "erf2",
        "somefactor",
        "shared1"
    };
    const TString CS[] = {
        "cs",
        "shared2"
    };

    const TString OptionsTypeTests[] = {
        /*SimpleUrlFormat */ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"url\" : \"DOCUMENT_URL\"}] }",
        /*LikeZoneUrlFormat */ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"random\" : {\"value\" : \"DOCUMENT_URL\", \"type\" : \"url\" }}] }",
        /*WithOptionsUrlFormat */ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"random\" : {\"value\" : \"DOCUMENT_URL\", \"options\" : {\"type\" : \"url\"} }}] }"
    };

    const TString TypeFormats[] = {
        /*Compact*/ "{\"action\" : \"delete\", \"prefix\" : 1, \"docs\" : [{\"body\" : \"BODY\", \"manymore\" : { \"value\" : \"42\", \"type\" : \"#uplf\"}}] }",
        /*Long*/ "{\"action\" : \"delete\", \"prefix\" : 1, \"docs\" : [{\"body\" : \"BODY\", \"manymore\" : { \"value\" : \"42\", \"type\" : \"prop,url,factor,search_attr_literal\"}}] }",
        /*Array*/ "{\"action\" : \"delete\", \"prefix\" : 1, \"docs\" : [{\"body\" : \"BODY\", \"manymore\" : { \"value\" : \"42\", \"type\" : [\"prop\",\"url\",\"factor\",\"search_attr_literal\"]}}] }"
    };

    const TString IncorrectJsons[] = {
        /*NoUrlJson*/ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"mime_type\" : \"text/xml\"}] }",
        /*UnknownFieldRoot*/ "{\"action\" : \"add\", \"prefix\" : 1, \"query_del\":\"$remove_all$\"}",
        /*UnknownFieldZone*/ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"url\" : \"DOC_URL\", \"random\" : {\"music\" : \"is magnificent\", \"value\" : \"zone_val\", \"options\" : {\"type\" : \"#zp\"} }}] }",
        /*UnknownFieldDocOptions*/ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"url\" : \"DOC_URL\", \"body\" : \"BODY\", \"options\" : {\"language\" : \"en\", \"spin\" : \"1/2\" }}] }",
        /*UnknownFieldZoneOptions*/ "{\"action\" : \"add\", \"prefix\" : 1, \"docs\" : [{\"url\" : \"DOC_URL\", \"random\" : {\"value\" : \"zone_val\", \"options\" : {\"duality\" : \"simple\", \"type\" : \"#zp\"} }}] }",
        /*UnknownZoneType*/ "{\"action\" : \"delete\", \"prefix\" : 1, \"docs\" : [{\"body\" : \"BODY\", \"manymore\" : { \"value\" : \"42\", \"type\" : \"#a\"}}] }",
        /*IncorrectTimestamp*/ "{\"action\" : \"modify\", \"prefix\" : 1, \"docs\" : [{\"body\" : \"BODY\", \"manymore\" : { \"value\" : \"42\", \"type\" : \"#uplf\"}, \"options\" : {\"modification_timestamp\" : \"199999999999999\"}}] }",
    };
}

Y_UNIT_TEST_SUITE(TestCommonJsonFormatApi) {
    TJsonValue CompactType() {
        return TString("#uplfc");
    }
    TJsonValue LongType() {
        return TString("cs_info,prop,url,factor,search_attr_literal");
    }
    TJsonValue ArrayType() {
        TJsonValue result(JSON_ARRAY);
        result.AppendValue(TString("cs_info"));
        result.AppendValue(TString("search_attr_literal"));
        result.AppendValue(TString("factor"));
        result.AppendValue(TString("url"));
        result.AppendValue(TString("prop"));
        return result;
    }

    typedef TAutoPtr<TAction> TActionPtr;

    TActionPtr GetStuffedDocument() {
        TActionPtr result(new TAction);
        TAction& action = *result;
        TDocument& doc = action.AddDocument();
        doc.SetUrl("sample url");

        for (size_t i = 0; i < Y_ARRAY_SIZE(SearchAttributes); ++i)
            doc.AddAttribute(SearchAttributes[i]).AddValue(SearchAttributes[i]).AddType(TAttributeValue::avtLit);
        for (size_t i = 0; i < Y_ARRAY_SIZE(GroupAttributes); ++i)
            doc.AddAttribute(GroupAttributes[i]).AddValue(42).AddType(TAttributeValue::avtGrp);
        for (size_t i = 0; i < Y_ARRAY_SIZE(Properties); ++i)
            doc.AddAttribute(Properties[i]).AddValue(Properties[i]).AddType(TAttributeValue::avtProp);
        for (size_t i = 0; i < Y_ARRAY_SIZE(SpecialKeys); ++i)
            doc.AddAttribute(Properties[i]).AddValue(Properties[i]).AddType(TAttributeValue::avtSpecKey);
        for (size_t i = 0; i < Y_ARRAY_SIZE(FSProperties); ++i)
            doc.AddAttribute(FSProperties[i]).AddValue(FSProperties[i]).AddType(TAttributeValue::avtFSProp);
        for (size_t i = 0; i < Y_ARRAY_SIZE(Factors); ++i)
            doc.AddAttribute(Factors[i]).AddValue("0.1234").AddType(TAttributeValue::avtFactor);
        for (size_t i = 0; i < Y_ARRAY_SIZE(CS); ++i)
            doc.AddCS(CS[i]).AddFactor("factor1", "432");

        for (size_t i = 0; i < Y_ARRAY_SIZE(Zones); ++i) {
            TZone& zone = doc.AddZone(Zones[i]);
            for (size_t j = 0; j < Y_ARRAY_SIZE(SearchAttributes); ++j)
                zone.AddAttribute(SearchAttributes[j]).AddValue(144).AddType(TAttributeValue::avtInt);
            zone.AddZone("subzone_" + Zones[i]).SetText("hi there");
        }

        NSaas::TAnnBlock annotation = doc.AddAnnotations();
        {
            NSaas::TAnnBlock::TSentence sent = annotation.AddSentence("some text", LANG_ENG);
            {
                NSaas::TAnnBlock::TRegion reg = sent.AddRegion(0);
                reg.AddStream("stream1", "aaa");
                reg.AddStream("stream2", "bbb");
            }
            {
                NSaas::TAnnBlock::TRegion reg = sent.AddRegion(10);
                reg.AddStream("stream1", "ccc");
                reg.AddStream("stream2", "ddd");
            }
        }
        {
            NSaas::TAnnBlock::TSentence sent = annotation.AddSentence("another text", LANG_ENG);
            NSaas::TAnnBlock::TRegion reg = sent.AddRegion(10);
            reg.AddStream("stream1", "2.0f");
            reg.AddStream("stream2", "0.177f");
        }

        NSaas::TGeoBlock geodata = doc.AddGeo();
        {
            NSaas::TGeoBlock::TRectSet r = geodata.AddRectSet("geo_ids");
            r.AddRect(0, 1.5, 0, 3);
            r.AddRect(-180, -90, 180, 90);
            NSaas::TGeoBlock::TPointSet p = geodata.AddPointSet("outlets", /*geodetic=*/true);
            p.AddPoint(/*lon=*/37.6155, /*lat=*/55.7522);
            p.AddPoint(/*lon=*/37.6155, /*lat=*/55.7522);
            geodata.AddPointSet("empty_layer", /*geodetic=*/true);
            NSaas::TGeoBlock::TPointSet p2 = geodata.AddPointSet("custom", /*geodetic=*/false);
            p2.AddPoint(1000, 400);
        }

        {
            doc.AddEmbedding("embedding1", "123456");
            doc.AddEmbedding("embedding2", TStringBuf("a\x12\x20\x40\xff\x00"sv));
            doc.AddEmbedding("embedding3", "123456", "version_123");
            doc.AddEmbedding("embedding3", "1234567", "version_789");
            doc.AddEmbedding("embedding3", "1234567", "version_789", "nonempty_tag");
        }

        return result;
    }

    TActionPtr GetDeleteDocument(bool deleteUrl) {
        TActionPtr result(new TAction);
        TAction& action = *result;
        action.SetActionType(TAction::atDelete);
        if (deleteUrl)
            action.GetDocument().SetUrl("samle_url");
        else
            action.SetRequest("everything");
        return result;
    }

    TActionPtr GetReopen() {
        TActionPtr result(new TAction);
        TAction& action = *result;
        action.SetActionType(TAction::atReopen);
        action.SetPrefix(24);
        return result;
    }

    TActionPtr GetSwitch() {
        TActionPtr result(new TAction);
        TAction& action = *result;
        action.SetActionType(TAction::atSwitchPrefix);
        action.SetPrefix(42);
        return result;
    }

    void SelfConsistencyCheck(TActionPtr ActionPtr, const TString& description) {
        TToJsonContext context(TToJsonContext::COMMON_JSON);
        const TJsonValue& original = ActionPtr->ToJson(context);
        const TJsonValue& reparsed = TAction().ParseFromJson(original).ToJson(context);
#if defined(COMMON_JSON_UT_DEBUG)
        Cerr << "case: " << description << Endl;
        Cerr << "original: " << NUtil::JsonToString(original) << Endl;
        Cerr << "reparsed: " << NUtil::JsonToString(reparsed) << Endl;
#endif
        UNIT_ASSERT_C(reparsed == original, "case :" + description);
    }

    bool IncorrectJson(const TString& string) {
        const TJsonValue& json = NUtil::JsonFromString(string);
        try {
            TAction().ParseFromJson(json);
            return false;
        } catch (const yexception& e) {
#if defined(COMMON_JSON_UT_DEBUG)
            Cerr << "json is incorrect because: " << e.what() << Endl;
#else
            Y_UNUSED(e);
#endif
            return true;
        }
    }

    Y_UNIT_TEST(TestSelfConsistencyCheck) {
        SelfConsistencyCheck(GetStuffedDocument(), "stuffed document");
        SelfConsistencyCheck(GetDeleteDocument(true), "delete document");
        SelfConsistencyCheck(GetDeleteDocument(false), "delete request");
        SelfConsistencyCheck(GetReopen(), "reopen");
        SelfConsistencyCheck(GetSwitch(), "switch prefix");
    }

    Y_UNIT_TEST(TestOptionsFormatsCheck) {
        for (size_t i = 0; i < Y_ARRAY_SIZE(OptionsTypeTests); ++i)
            UNIT_ASSERT_C(TAction().ParseFromJson(NUtil::JsonFromString(OptionsTypeTests[i])).GetDocument().GetUrl() == "DOCUMENT_URL", "case #" + ToString(i));
    }

    Y_UNIT_TEST(TestTypeFormatsCheck) {
        for (size_t i = 0; i < Y_ARRAY_SIZE(TypeFormats); ++i) {
            TAction action;
            action.ParseFromJson(NUtil::JsonFromString(TypeFormats[i]));
            const NRTYServer::TMessage::TDocument& doc = action.ToProtobuf().GetDocument();
            UNIT_ASSERT(doc.GetUrl() == "42");
            UNIT_ASSERT(doc.DocumentPropertiesSize());
            UNIT_ASSERT(doc.HasFactors());
            UNIT_ASSERT(doc.SearchAttributesSize());
        }
    }

    Y_UNIT_TEST(TestIncorrectJson) {
        for (size_t i = 0; i < Y_ARRAY_SIZE(IncorrectJsons); ++i)
            UNIT_ASSERT_C(IncorrectJson(IncorrectJsons[i]), "case #" + ToString(i));
    }

    Y_UNIT_TEST(TestSameNameZoneAndAttr) {
        TAction action;
        action.SetPrefix(1);
        action.SetId(1);
        action.SetActionType(TAction::atAdd);
        TDocument& doc = action.AddDocument();
        TZone& zone = doc.AddZone("psbirth");
        zone.AddSearchAttribute("psage", 20);
        bool failed = false;
        try {
            zone.AddZone("psage").SetText("twenty");
        }
        catch (...) {
            failed = true;
        }
        UNIT_ASSERT(failed);
    }

    Y_UNIT_TEST(TestIntFactor) {
        THashMap<TString, i32> factors {
            { "max", Max<i32>() },
            { "min", Min<i32>() },
            { "one", -1}
        };
        TAction action;
        action.SetPrefix(1);
        action.SetId(1);
        action.SetActionType(TAction::atAdd);
        TDocument& doc = action.AddDocument();
        doc.SetUrl("my url");
        for (const auto& i : factors) {
            doc.AddIntFactor(i.first, i.second);
        }
        TToJsonContext context(TToJsonContext::COMMON_JSON);
        NJson::TJsonValue json = action.ToJson(context);
        for (const auto& i : factors) {
            const auto& f = json["docs"][0][i.first][0];
            auto ivalue = f["value"].GetIntegerRobust();
            UNIT_ASSERT_EQUAL_C(ivalue, i.second, i.first);
            UNIT_ASSERT_EQUAL_C(f["type"].GetStringRobust(), "#F", i.first);
        }
        NRTYServer::TMessage::TDocument protoDoc = TAction().ParseFromJson(json).ToProtobuf().GetDocument();
        for (ui32 i = 0; i < protoDoc.GetFactors().NamesSize(); ++i) {
            TString name = protoDoc.GetFactors().GetNames(i);
            float value = protoDoc.GetFactors().GetValues().GetValues(i).GetValue();
            UNIT_ASSERT_C(factors.contains(name), name);
            i32 iValue;
            memcpy(&iValue, &value, sizeof(iValue));
            UNIT_ASSERT_EQUAL_C(iValue, factors[name], name);
        }
    }
};
