#include "log_impl.h"

#include <library/cpp/logger/stream.h>
#include <library/cpp/logger/log.h>
#include <library/cpp/testing/unittest/registar.h>
#include <util/stream/str.h>
#include <util/string/ascii.h>

namespace {
    // TMockLogBackend: differs from TStreamLogBackend by not using explicit EOL in records
    class TMockLogBackend: public TLogBackend {
    private:
        TStringStream Data;
        char EndOfRecordMark = 0;

    public:
        TMockLogBackend() {
        }

        TMockLogBackend(char eol) {
            SetEol(eol);
        }

        void WriteData(const TLogRecord& rec) override {
            if (EndOfRecordMark) {
                Data << TStringBuf(rec.Data, rec.Len) << EndOfRecordMark;
            } else {
                Data << TStringBuf(rec.Data, rec.Len) << Endl;
            }
        }

        void ReopenLog() override {
            Data.Clear();
        }

    public:
        TString GetData() const {
            return Data.Str();
        }

        void SetEol(char subst) {
            EndOfRecordMark = subst;
        }
    };

    class TMockGlobalLog: public TLog {
    private:
        static TLogBackend* CreateBackend() {
            return new TMockLogBackend('@');
        }

    public:
        TMockGlobalLog()
            : TLog(THolder(CreateBackend()))
        {
        }

        TAutoPtr<TMockLogBackend> DetachBackend() {
            TAutoPtr<TMockLogBackend> old(static_cast<TMockLogBackend*>(ReleaseBackend().Release()));
            ResetBackend(THolder(CreateBackend()));
            return old;
        }

        static TMockGlobalLog& Instance() {
            return *TLoggerOperator<TMockGlobalLog>::Get();
        }
    };
}

class TExtBuilderLogging: public NUnitTest::TTestBase {
    UNIT_TEST_SUITE(TExtBuilderLogging)
    UNIT_TEST(TestGlobalLog);
    UNIT_TEST(TestLocalLog);
    UNIT_TEST(SmokeTestStderr);
    UNIT_TEST_SUITE_END();

    void SubstDigits(TString& text, char with) {
        for (auto p = text.begin(); p != text.end(); ++p) {
            if (IsAsciiDigit(*p)) {
                *p = with;
            } else if (*p == '\n') {
                *p = '$';
            }
        }
    }

    void WriteTestData(TLog& log) {
        // Base syntax (no endl)
        C_LOG(log) << TLOG_CRIT << "li" << "neA";

        // Multi-operation syntax (not recommended)
        {
            auto builder = C_LOG_BUILDER(log, TLOG_DEBUG);
            *builder << "lineB";
            builder.Drop();
        }

        // Base syntax with no priority (INFO is assumed)
        C_LOG(log) << "lineC";
    }

    void TestLocalLog() {
        TAutoPtr<TLogBackend> mockCallBack(new TMockLogBackend());
        THolder<TLog> log = MakeHolder<TLog>(mockCallBack);

        WriteTestData(*log);

        mockCallBack = log->ReleaseBackend();
        TString actual = dynamic_cast<TMockLogBackend&>(*mockCallBack).GetData();
        log.Destroy();

        SubstDigits(actual, '0');
        UNIT_ASSERT_VALUES_EQUAL("log_ut.cpp:00 lineA$log_ut.cpp:00 lineB$log_ut.cpp:00 lineC$", actual);
    }

    template <typename TLogClass>
    void TestGlobalLogImpl() {
        using TBackend = NLoggingImpl::TComponentBackendBase<TLogClass>;
        using TCallback = TMockLogBackend;

        TAutoPtr<TLogBackend> mockCallBack(new TCallback('#'));
        TAutoPtr<TBackend> componentLb = new TBackend("[MySys] ");
        componentLb->ResetCallback(mockCallBack);

        THolder<TLog> log = MakeHolder<TLog>(TAutoPtr<TLogBackend>(componentLb.Release()));
        UNIT_ASSERT(componentLb.Get() == nullptr);

        WriteTestData(*log);

        componentLb = TAutoPtr<TBackend>(dynamic_cast<TBackend*>(log->ReleaseBackend().Release()));
        UNIT_ASSERT(componentLb);
        {
            //1. Check component callback
            TCallback* cb = dynamic_cast<TMockLogBackend*>(componentLb->GetCallback());
            UNIT_ASSERT(cb);
            TString actual = cb->GetData();
            SubstDigits(actual, '0');
            TString expected =
                "log_ut.cpp:00 lineA#"
                "log_ut.cpp:00 lineB#"
                "log_ut.cpp:00 lineC#";

            UNIT_ASSERT_VALUES_EQUAL(expected, actual);
        }

        if constexpr (std::is_same<TLogClass, TMockGlobalLog>::value) {
            //2. Check global log
            TAutoPtr<TMockLogBackend> globalMock = TMockGlobalLog::Instance().DetachBackend();
            UNIT_ASSERT(globalMock);
            TString actual = static_cast<TMockLogBackend&>(*globalMock).GetData();
            SubstDigits(actual, '0');
            //'Expected' has '\n' (substituted by '$') before end of record '@'
            TString expected =
                "CRITICAL_INFO: 0000-00-00 00:00:00.000 +0000 [MySys] log_ut.cpp:00 lineA$@"
                "DEBUG: 0000-00-00 00:00:00.000 +0000 [MySys] log_ut.cpp:00 lineB$@"
                "INFO: 0000-00-00 00:00:00.000 +0000 [MySys] log_ut.cpp:00 lineC$@";
            UNIT_ASSERT_VALUES_EQUAL(expected, actual);
        }
    }

    void TestGlobalLog() {
        TLoggerOperator<TMockGlobalLog>::Set(new TMockGlobalLog);

        // print something, then check global log and callback log
        TestGlobalLogImpl<TMockGlobalLog>();
    }

    void SmokeTestStderr() {
        //
        // try logging to stderr (should not crash)
        //
        if (!TLoggerOperator<TGlobalLog>::Usage()) {
            InitGlobalLog2Console(TLOG_DEBUG);
        }

        // prints something to console (we do not check what is written; we control only the callback log)
        TestGlobalLogImpl<TGlobalLog>();
    }
};

UNIT_TEST_SUITE_REGISTRATION(TExtBuilderLogging);
