#include <balancer/kernel/fs/shared_value.h>
#include <balancer/kernel/fs/shared_loader.h>
#include <balancer/kernel/fs/shared_file_rereader.h>
#include <balancer/kernel/fs/threadedfile.h>
#include <balancer/kernel/testing/file_arbiter_mock.h>
#include <balancer/kernel/thread/threadedqueue.h>

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

#include <util/generic/scope.h>
#include <util/system/tempfile.h>
#include <util/thread/factory.h>

using namespace NSrvKernel;

using ::testing::_;
using ::testing::InvokeWithoutArgs;
using ::testing::Return;
using ::testing::StrEq;

struct TValue {
    using TValuePtr = TSimpleSharedPtr<TValue>;
    static void Parse(TMaybe<TString> data, TValuePtr& current) {
        current = MakeSimpleShared<TValue>(data);
    }
    TMaybe<TString> data;
};

using TTestValue = TSharedValue<TValue>;

Y_UNIT_TEST_SUITE(TSharedFileReloaderTest) {
    constexpr char STRING_SAMPLE[] = "abc";
    constexpr char STRING_SAMPLE2[] = "def";
    constexpr char PATH_SAMPLE[] = "test.txt";

    Y_UNIT_TEST(Test) {
        TContExecutor executor(100000);
        TThreadedQueue queue(&executor, SystemThreadFactory());

        TSharedFileReloader reloader(&executor, &queue, PATH_SAMPLE, TDuration::Seconds(1), MakeHolder<TTestValue>());

        TFileManager::Instance().EnableTesting();

        auto fileArbiterMockPtr = MakeHolder<NSrvKernel::NTesting::TFileArbiterMock>();
        auto& fileArbiterMock = *fileArbiterMockPtr;

        TFileManager::TMockGuard guard{std::move(fileArbiterMockPtr)};

        auto func = [&](TCont* cont) {
            Y_DEFER {
                queue.Dispose();
                executor.Abort();
            };
            ISharedDataLoader* sharedValue = reloader.GetLoader();
            TTestValue* val = dynamic_cast<TTestValue*>(sharedValue);
            UNIT_ASSERT(val);

            // Init invariants
            // --------------------------------------------------------------------------------
            UNIT_ASSERT(!val->Get());
            // --------------------------------------------------------------------------------

            // Situation when we read file first time
            // --------------------------------------------------------------------------------
            TFileStat retFileStat;
            retFileStat.MTime = 1;
            retFileStat.CTime = 1;
            retFileStat.INode = 1;
            retFileStat.Size = 1;
            retFileStat.NLinks = 1;

            EXPECT_CALL(fileArbiterMock, GetStat(StrEq(PATH_SAMPLE), false))
                .Times(1)
                .WillOnce(Return(retFileStat));

            TString input = STRING_SAMPLE;
            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(1)
                .WillOnce(InvokeWithoutArgs([&] {
                    return MakeHolder<TStringInput>(input);
                }));

            reloader.Start();
            cont->SleepT(TDuration::Seconds(1));
            reloader.Stop();

            TSimpleSharedPtr<TValue> got = val->Get();
            UNIT_ASSERT(got->data.Defined());
            UNIT_ASSERT_EQUAL(got->data.GetRef(), STRING_SAMPLE);
            // --------------------------------------------------------------------------------

            // Situation when file was deleted
            // --------------------------------------------------------------------------------
            retFileStat.Size = 0;
            retFileStat.NLinks = 0;

            EXPECT_CALL(fileArbiterMock, GetStat(StrEq(PATH_SAMPLE), false))
                .Times(1)
                .WillOnce(Return(retFileStat));

            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(0);

            reloader.Start();
            cont->SleepT(TDuration::Seconds(1));
            reloader.Stop();

            got = val->Get();
            UNIT_ASSERT(!got->data.Defined());
            // --------------------------------------------------------------------------------

            // Situation when file appeared and its last modification time was increased
            // --------------------------------------------------------------------------------
            retFileStat.MTime = 2;
            retFileStat.Size = 1;
            retFileStat.NLinks = 1;

            EXPECT_CALL(fileArbiterMock, GetStat(StrEq(PATH_SAMPLE), false))
                .Times(1)
                .WillOnce(Return(retFileStat));

            input = STRING_SAMPLE2;
            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(1)
                .WillOnce(InvokeWithoutArgs([&] {
                    return MakeHolder<TStringInput>(input);
                }));

            reloader.Start();
            cont->SleepT(TDuration::Seconds(1));
            reloader.Stop();

            got = val->Get();
            UNIT_ASSERT(got->data.Defined());
            UNIT_ASSERT_EQUAL(got->data.GetRef(), STRING_SAMPLE2);
            // --------------------------------------------------------------------------------

            // Situation when file appeared and its last status change time was increased
            // --------------------------------------------------------------------------------
            retFileStat.CTime = 2;

            EXPECT_CALL(fileArbiterMock, GetStat(StrEq(PATH_SAMPLE), false))
                .Times(1)
                .WillOnce(Return(retFileStat));

            input = STRING_SAMPLE;
            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(1)
                .WillOnce(InvokeWithoutArgs([&] {
                    return MakeHolder<TStringInput>(input);
                }));

            reloader.Start();
            cont->SleepT(TDuration::Seconds(1));
            reloader.Stop();

            got = val->Get();
            UNIT_ASSERT(got->data.Defined());
            UNIT_ASSERT_EQUAL(got->data.GetRef(), STRING_SAMPLE);
            // --------------------------------------------------------------------------------

            // Situation when file appeared and its inode count was changed
            // --------------------------------------------------------------------------------
            retFileStat.INode = 2;

            EXPECT_CALL(fileArbiterMock, GetStat(StrEq(PATH_SAMPLE), false))
                .Times(1)
                .WillOnce(Return(retFileStat));

            input = STRING_SAMPLE;
            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(1)
                .WillOnce(InvokeWithoutArgs([&] {
                    return MakeHolder<TStringInput>(input);
                }));

            reloader.Start();
            cont->SleepT(TDuration::Seconds(1));
            reloader.Stop();

            got = val->Get();
            UNIT_ASSERT(got->data.Defined());
            UNIT_ASSERT_EQUAL(got->data.GetRef(), STRING_SAMPLE);
            // --------------------------------------------------------------------------------

            // Situation when file appeared and its size was changed
            // --------------------------------------------------------------------------------
            retFileStat.Size = 2;

            EXPECT_CALL(fileArbiterMock, GetStat(StrEq(PATH_SAMPLE), false))
                .WillRepeatedly(Return(retFileStat));

            input = STRING_SAMPLE2;
            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(1)
                .WillOnce(InvokeWithoutArgs([&] {
                    return MakeHolder<TStringInput>(input);
                }));

            reloader.Start();
            cont->SleepT(TDuration::Seconds(1));
            // Don't stop reloader to check same FileStat
            got = val->Get();
            UNIT_ASSERT(got->data.Defined());
            UNIT_ASSERT_EQUAL(got->data.GetRef(), STRING_SAMPLE2);
            // --------------------------------------------------------------------------------

            // Situation when file wasn't changed
            // --------------------------------------------------------------------------------
            auto lastPtr = got;

            EXPECT_CALL(fileArbiterMock, GetInput(TString(PATH_SAMPLE)))
                .Times(0);

            cont->SleepT(TDuration::Seconds(1));
            reloader.Stop();

            got = val->Get();
            UNIT_ASSERT(got->data.Defined());
            UNIT_ASSERT_EQUAL(got.Get(), lastPtr.Get());
            // --------------------------------------------------------------------------------
            return 0;
        };
        executor.Execute(func);
    }
}
