#include "metric_registry.h"

#include <library/cpp/monlib/encode/protobuf/protobuf.h>
#include <library/cpp/monlib/encode/json/json.h>
#include <library/cpp/resource/resource.h>

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

#include <util/stream/str.h>

using namespace NMonitoring;

template<>
void Out<NMonitoring::NProto::TSingleSample::ValueCase>(IOutputStream& os, NMonitoring::NProto::TSingleSample::ValueCase val) {
    switch (val) {
    case NMonitoring::NProto::TSingleSample::ValueCase::kInt64:
        os << "Int64";
        break;
    case NMonitoring::NProto::TSingleSample::ValueCase::kUint64:
        os << "Uint64";
        break;
    case NMonitoring::NProto::TSingleSample::ValueCase::kHistogram:
        os << "Histogram";
        break;
    case NMonitoring::NProto::TSingleSample::ValueCase::kFloat64:
        os << "Float64";
        break;
    case NMonitoring::NProto::TSingleSample::ValueCase::kSummaryDouble:
        os << "DSummary";
        break;
    case NMonitoring::NProto::TSingleSample::ValueCase::kLogHistogram:
        os << "LogHistogram";
        break;
    case NMonitoring::NProto::TSingleSample::ValueCase::VALUE_NOT_SET:
        os << "NOT SET";
        break;
    }
}

Y_UNIT_TEST_SUITE(TMetricRegistryTest) {
    Y_UNIT_TEST(Gauge) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        TGauge* g = registry.Gauge({{"my", "gauge"}});

        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 0.0, 1E-6);
        g->Set(12.34);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 12.34, 1E-6);

        double val;

        val = g->Add(1.2);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 13.54, 1E-6);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), val, 1E-6);

        val = g->Add(-3.47);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 10.07, 1E-6);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), val, 1E-6);
    }

    Y_UNIT_TEST(LazyGauge) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        double val = 0.0;
        TLazyGauge* g = registry.LazyGauge({{"my", "lazyGauge"}}, [&val](){return val;});

        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 0.0, 1E-6);
        val = 12.34;
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 12.34, 1E-6);

        val += 1.2;
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 13.54, 1E-6);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), val, 1E-6);

        val += -3.47;
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), 10.07, 1E-6);
        UNIT_ASSERT_DOUBLES_EQUAL(g->Get(), val, 1E-6);
    }

    Y_UNIT_TEST(IntGauge) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        TIntGauge* g = registry.IntGauge({{"my", "gauge"}});

        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 0);

        i64 val;

        val = g->Inc();
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 1);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);

        val = g->Dec();
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 0);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);

        val = g->Add(1);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 1);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);

        val = g->Add(2);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 3);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);

        val = g->Add(-5);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), -2);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);
    }

    Y_UNIT_TEST(LazyIntGauge) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        i64 val = 0;
        TLazyIntGauge* g = registry.LazyIntGauge({{"my", "gauge"}}, [&val](){return val;});

        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 0);
        val += 1;
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 1);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);

        val -= 1;
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), 0);
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);

        val = 42;
        UNIT_ASSERT_VALUES_EQUAL(g->Get(), val);
    }

    Y_UNIT_TEST(Counter) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        TCounter* c = registry.Counter({{"my", "counter"}});

        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 0);
        UNIT_ASSERT_VALUES_EQUAL(c->Inc(), 1);
        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 1);
        UNIT_ASSERT_VALUES_EQUAL(c->Add(10), 11);
        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 11);
    }

    Y_UNIT_TEST(LazyCounter) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        ui64 val = 0;

        TLazyCounter* c = registry.LazyCounter({{"my", "counter"}}, [&val](){return val;});

        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 0);
        val = 42;
        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 42);
    }

    Y_UNIT_TEST(LazyRate) {
        TMetricRegistry registry(TLabels{{"common", "label"}});
        ui64 val = 0;

        TLazyRate* r = registry.LazyRate({{"my", "rate"}}, [&val](){return val;});

        UNIT_ASSERT_VALUES_EQUAL(r->Get(), 0);
        val = 42;
        UNIT_ASSERT_VALUES_EQUAL(r->Get(), 42);
    }

    Y_UNIT_TEST(DoubleCounter) {
        TMetricRegistry registry(TLabels{{"common", "label"}});

        TCounter* c = registry.Counter({{"my", "counter"}});
        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 0);
        c->Add(10);

        c = registry.Counter({{"my", "counter"}});
        UNIT_ASSERT_VALUES_EQUAL(c->Get(), 10);
    }

    Y_UNIT_TEST(Sample) {
        TMetricRegistry registry(TLabels{{"common", "label"}});

        TGauge* g = registry.Gauge({{"my", "gauge"}});
        g->Set(12.34);

        TCounter* c = registry.Counter({{"my", "counter"}});
        c->Add(10);

        NProto::TSingleSamplesList samples;
        auto encoder = EncoderProtobuf(&samples);
        auto now = TInstant::Now();
        registry.Accept(now, encoder.Get());

        UNIT_ASSERT_VALUES_EQUAL(samples.SamplesSize(), 2);
        UNIT_ASSERT_VALUES_EQUAL(samples.CommonLabelsSize(), 1);
        {
            const NProto::TLabel& label = samples.GetCommonLabels(0);
            UNIT_ASSERT_STRINGS_EQUAL(label.GetName(), "common");
            UNIT_ASSERT_STRINGS_EQUAL(label.GetValue(), "label");
        }


        for (const NProto::TSingleSample& sample : samples.GetSamples()) {
            UNIT_ASSERT_VALUES_EQUAL(sample.LabelsSize(), 1);
            UNIT_ASSERT_VALUES_EQUAL(sample.GetTime(), now.MilliSeconds());

            if (sample.GetMetricType() == NProto::GAUGE) {
                UNIT_ASSERT_VALUES_EQUAL(sample.GetValueCase(), NProto::TSingleSample::kFloat64);
                UNIT_ASSERT_DOUBLES_EQUAL(sample.GetFloat64(), 12.34, 1E-6);

                const NProto::TLabel& label = sample.GetLabels(0);
                UNIT_ASSERT_STRINGS_EQUAL(label.GetName(), "my");
                UNIT_ASSERT_STRINGS_EQUAL(label.GetValue(), "gauge");
            } else if (sample.GetMetricType() == NProto::COUNTER) {
                UNIT_ASSERT_VALUES_EQUAL(sample.GetValueCase(), NProto::TSingleSample::kUint64);
                UNIT_ASSERT_VALUES_EQUAL(sample.GetUint64(), 10);

                const NProto::TLabel& label = sample.GetLabels(0);
                UNIT_ASSERT_STRINGS_EQUAL(label.GetName(), "my");
                UNIT_ASSERT_STRINGS_EQUAL(label.GetValue(), "counter");
            } else {
                UNIT_FAIL("unexpected sample type");
            }
        }
    }

    Y_UNIT_TEST(Histograms) {
        TMetricRegistry registry(TLabels{{"common", "label"}});

        THistogram* h1 = registry.HistogramCounter(
                {{"sensor", "readTimeMillis"}},
                ExponentialHistogram(5, 2));

        THistogram* h2 = registry.HistogramRate(
                {{"sensor", "writeTimeMillis"}},
                ExplicitHistogram({1, 5, 15, 20, 25}));

        for (i64 i = 0; i < 100; i++) {
            h1->Record(i);
            h2->Record(i);
        }

        TStringStream ss;
        {
            auto encoder = EncoderJson(&ss, 2);
            registry.Accept(TInstant::Zero(), encoder.Get());
        }
        ss << '\n';

        UNIT_ASSERT_NO_DIFF(ss.Str(), NResource::Find("/histograms.json"));
    }

    Y_UNIT_TEST(StreamingEncoderTest) {
        const TString expected {
            "{\"commonLabels\":{\"common\":\"label\"},"
            "\"sensors\":[{\"kind\":\"GAUGE\",\"labels\":{\"my\":\"gauge\"},\"value\":12.34}]}"
        };

        TMetricRegistry registry(TLabels{{"common", "label"}});

        TGauge* g = registry.Gauge({{"my", "gauge"}});
        g->Set(12.34);

        TStringStream os;
        auto encoder = EncoderJson(&os);
        registry.Accept(TInstant::Zero(), encoder.Get());

        UNIT_ASSERT_STRINGS_EQUAL(os.Str(), expected);
    }

    Y_UNIT_TEST(CreatingSameMetricWithDifferentTypesShouldThrow) {
        TMetricRegistry registry;

        registry.Gauge({{"foo", "bar"}});
        UNIT_ASSERT_EXCEPTION(registry.Counter({{"foo", "bar"}}), yexception);

        registry.HistogramCounter({{"bar", "baz"}}, nullptr);
        UNIT_ASSERT_EXCEPTION(registry.HistogramRate({{"bar", "baz"}}, nullptr), yexception);
    }

    Y_UNIT_TEST(EncodeRegistryWithCommonLabels) {
        TMetricRegistry registry(TLabels{{"common", "label"}});

        TGauge* g = registry.Gauge({{"my", "gauge"}});
        g->Set(12.34);

        // Append() adds common labels to each metric, allowing to combine
        // several metric registries in one resulting blob
        {
            TStringStream os;
            auto encoder = EncoderJson(&os);
            encoder->OnStreamBegin();
            registry.Append(TInstant::Zero(), encoder.Get());
            encoder->OnStreamEnd();

            UNIT_ASSERT_STRINGS_EQUAL(
                    os.Str(),
                    "{\"sensors\":[{\"kind\":\"GAUGE\",\"labels\":{\"common\":\"label\",\"my\":\"gauge\"},\"value\":12.34}]}");
        }

        // Accept() adds common labels to the beginning of the blob
        {
            TStringStream os;
            auto encoder = EncoderJson(&os);
            registry.Accept(TInstant::Zero(), encoder.Get());

            UNIT_ASSERT_STRINGS_EQUAL(
                    os.Str(),
                    "{\"commonLabels\":{\"common\":\"label\"},"
                    "\"sensors\":[{\"kind\":\"GAUGE\",\"labels\":{\"my\":\"gauge\"},\"value\":12.34}]}");
        }
    }

    Y_UNIT_TEST(MetricsRegistryClear) {
        TMetricRegistry registry;
        registry.Gauge({{"some", "label"}})->Add(1);

        NProto::TSingleSamplesList samples;
        auto encoder = EncoderProtobuf(&samples);
        registry.Accept(TInstant::Now(), encoder.Get());

        UNIT_ASSERT(samples.SamplesSize() == 1);

        samples = {};
        registry.Clear();
        registry.Accept(TInstant::Now(), encoder.Get());

        UNIT_ASSERT(samples.SamplesSize() == 0);
    }
}