#include <balancer/kernel/http2/client/http2_request.h>
#include <balancer/kernel/http2/client/http2_backend.h>
#include <balancer/client/ut/util/env.h>
#include <balancer/kernel/http2/client/ut/proto/protocol.grpc.pb.h>
#include <balancer/modules/balancer/module.h>
#include <balancer/kernel/http/parser/request_builder.h>
#include <balancer/kernel/http2/server/common/http2_common.h>
#include <balancer/kernel/http2/server/http2_frame.h>

#include <balancer/server/server.h>

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

#include <contrib/libs/grpc/include/grpcpp/grpcpp.h>

#include <util/system/byteorder.h>

namespace {
template<typename TService>
class TServiceRunner : public TService {
  public:
    using TService::TService;

    ~TServiceRunner() override {
        Server->Shutdown();
    }

    ui16 Run() {
        TString server_address("[::]:0");
        grpc::ServerBuilder builder;
        int port = 0;
        builder.AddListeningPort(server_address, grpc::InsecureServerCredentials(), &port);
        builder.RegisterService(this);
        Server = builder.BuildAndStart();

        return port;
    }

    std::unique_ptr<grpc::Server> Server;
};

class TStreamingSumService : public NProtocol::TServerWithStreaming::Service {
  public:
    grpc::Status Sum(grpc::ServerContext*, ::grpc::ServerReaderWriter<NProtocol::TSumResponse, NProtocol::TSumRequest>* stream) override {
        NProtocol::TSumRequest request;
        while(stream->Read(&request)) {
            NProtocol::TSumResponse response;
            response.SetSum(request.GetA() + request.GetB());
            stream->Write(response);
        }
        return grpc::Status::OK;
    }
};

class TConcatService : public NProtocol::TConcatenator::Service {
  public:
    grpc::Status Concat(::grpc::ServerContext* /*context*/, ::grpc::ServerReaderWriter<::NProtocol::TConcatResponse, ::NProtocol::TConcatRequest>* stream) override {
        NProtocol::TConcatRequest request;
        while(stream->Read(&request)) {
            NProtocol::TConcatResponse response;
            response.SetSum(request.GetA() + request.GetB());
            stream->Write(response);
        }
        return grpc::Status::OK;
    }
};

auto GenerateBackendConfig(ui16 port, int /*attempts*/) {
    NJson::TJsonValue config;
    config["maxlen"] = 65536;
    config["maxreq"] = 65536;

    NJson::TJsonValue& proxy = config["proxy"];
    proxy["cached_ip"] = "127.0.0.1";
    proxy["host"] = "localhost";
    proxy["port"] = port;
    proxy["backend_timeout"] = "5s";
    proxy["connect_timeout"] = "1s";
    proxy["protocols"].AppendValue(NBalancerClient::THttp2Backend::ProtocolName());

    return config;
}

TString GenerateGrpcFrame(NProtoBuf::MessageLite& message) {
    TString data;
    data.reserve(sizeof(ui8)+sizeof(ui32)+message.ByteSize());
    data.append('\0');
    ui32 size = HostToInet(static_cast<ui32>(message.ByteSize()));
    data.append((char*)&size, sizeof(size));
    data.append(message.SerializeAsString());
    return data;
}

bool ExtractMessageFromGrpcFrame(TStringBuf frame, NProtoBuf::MessageLite& message, size_t& offset) {
    frame.Skip(offset);
    bool compressed = *frame.data();
    Y_ENSURE(!compressed);
    frame.Skip(sizeof(ui8));
    ui32 size = InetToHost(*reinterpret_cast<ui32*>(const_cast<TStringBuf::pointer>(frame.data())));
    frame.Skip(sizeof(size));
    bool success = message.ParseFromArray(frame.data(), size);
    frame.Skip(size);
    offset += sizeof(ui8) + sizeof(size) + size;
    return success;
}

bool ReadGrpcFrame(TString& buffer, NSrvKernel::TW2UChannel<TString>& channel, size_t offset) {
    TString chunk;
    while (buffer.size() < offset + 5 && channel.Receive(chunk, TInstant::Max()) == NSrvKernel::EChannelStatus::Success) {
        if (chunk.empty()) {
            break;
        }

        buffer.append(chunk);
    }

    if (buffer.size() < offset + 5) {
        return false;
    }

    ui32 size = InetToHost(*reinterpret_cast<ui32*>(const_cast<TStringBuf::pointer>(buffer.data() + offset + sizeof(ui8))));

    while (buffer.size() < offset + size + 5 && channel.Receive(chunk, TInstant::Max()) == NSrvKernel::EChannelStatus::Success) {
        if (chunk.empty()) {
            break;
        }

        buffer.append(chunk);
    }

    if (buffer.size() < offset + size + 5) {
        return false;
    }

    return true;
}

void SendHttp2RequestAndCheckResponse(ui16 proxyPort, const TStringBuf& expectedResponse) {
    NBalancerClient::NTesting::TEnv env;
    env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
    env.Start();

    auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
    NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
            .Version11()
            .Method(NSrvKernel::EMethod::GET)
            .Path("/path")
            .Header("Host", "somehost");

    NSrvKernel::THTTP2StreamDetails http2;
    request.Props().HTTP2 = &http2;

    auto source = env.CreateSource(GenerateBackendConfig(proxyPort, 1));
    auto context = source->SendRequest(std::move(request), {}, clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), nullptr, nullptr);

    NBalancerClient::TBalancerClientResponse response;
    UNIT_ASSERT_NO_EXCEPTION(NSrvKernel::TryRethrowError(context->GetFuture().ExtractValueSync().GetResponse().AssignTo(response)));
    UNIT_ASSERT_VALUES_EQUAL(response.Data, expectedResponse);
}
}

Y_UNIT_TEST_SUITE(Http2) {
    Y_UNIT_TEST(Grpc) {
        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        TServiceRunner<TStreamingSumService> service;
        auto port = service.Run();

        auto source = env.CreateSource(GenerateBackendConfig(port, 1));

        NProtocol::TSumRequest requestData;
        requestData.SetA(2);
        requestData.SetB(3);

        auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                    .Method(NSrvKernel::EMethod::POST)
                    .Path("/NProtocol.TServerWithStreaming/Sum")
                    .Version11()
                    .Header("te", "trailers")
                    .Header("content-type", "application/grpc+proto")
                    .Header("grpc-encoding", "identity")
                    .Header("grpc-accept-encoding", "identity")
                    .Header("Host", "somehost");

        NSrvKernel::THTTP2StreamDetails http2;
        request.Props().HTTP2 = &http2;

        auto context = source->SendRequest(std::move(request), GenerateGrpcFrame(requestData), clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), nullptr, nullptr);
        auto response = std::move(context->GetFuture().ExtractValueSync().GetResponse().GetOrThrow());
        UNIT_ASSERT_EQUAL(response.HttpResponse.ResponseLine().StatusCode, 200);
        UNIT_ASSERT_EQUAL(response.Trailers.GetFirstValue("grpc-status"), "0");
        TStringBuf buffer{response.Data};
        NProtocol::TSumResponse sum;
        size_t offset = 0;
        UNIT_ASSERT(ExtractMessageFromGrpcFrame(buffer, sum, offset));
        UNIT_ASSERT_EQUAL(offset, buffer.size());
        UNIT_ASSERT_EQUAL(sum.GetSum(), 5);
    }

    Y_UNIT_TEST(GrpcStreaming) {
        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        TServiceRunner<TStreamingSumService> service;
        auto port = service.Run();

        auto source = env.CreateSource(GenerateBackendConfig(port, 1));

        auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                .Method(NSrvKernel::EMethod::POST)
                .Path("/NProtocol.TServerWithStreaming/Sum")
                .Version11()
                .Header("te", "trailers")
                .Header("content-type", "application/grpc+proto")
                .Header("grpc-encoding", "identity")
                .Header("grpc-accept-encoding", "identity")
                .Header("Host", "somehost");

        NSrvKernel::THTTP2StreamDetails http2;
        request.Props().HTTP2 = &http2;

        auto inputChannel = MakeAtomicShared<NSrvKernel::TU2WChannel<TString>>(Max<size_t>());
        auto outputChannel = MakeAtomicShared<NSrvKernel::TW2UChannel<TString>>(Max<size_t>());

        auto context = source->SendRequest(std::move(request), {}, clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), inputChannel, outputChannel);

        size_t offset = 0;
        TString buffer;

        {
            NProtocol::TSumRequest requestData;
            requestData.SetA(1);
            requestData.SetB(2);

            UNIT_ASSERT_EQUAL(inputChannel->Send(GenerateGrpcFrame(requestData), TInstant::Max()),
                              NSrvKernel::EChannelStatus::Success);

            NProtocol::TSumResponse sum;
            UNIT_ASSERT(ReadGrpcFrame(buffer, *outputChannel, offset));
            UNIT_ASSERT(ExtractMessageFromGrpcFrame(buffer, sum, offset));
            UNIT_ASSERT_EQUAL(sum.GetSum(), 3);
        }

        {
            NProtocol::TSumRequest requestData;
            requestData.SetA(2);
            requestData.SetB(3);

            UNIT_ASSERT_EQUAL(inputChannel->Send(GenerateGrpcFrame(requestData), TInstant::Max()), NSrvKernel::EChannelStatus::Success);

            NProtocol::TSumResponse sum;
            UNIT_ASSERT(ReadGrpcFrame(buffer, *outputChannel, offset));
            UNIT_ASSERT(ExtractMessageFromGrpcFrame(buffer, sum, offset));
            UNIT_ASSERT_EQUAL(sum.GetSum(), 5);
        }

        UNIT_ASSERT_EQUAL(offset, buffer.size());

        UNIT_ASSERT_EQUAL(inputChannel->Send(TString{}, TInstant::Max()), NSrvKernel::EChannelStatus::Success);

        auto response = std::move(context->GetFuture().ExtractValueSync().GetResponse().GetOrThrow());
        UNIT_ASSERT_EQUAL(response.Trailers.GetFirstValue("grpc-status"), "0");
    }

    Y_UNIT_TEST(GrpcBigFrames) {
        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        TServiceRunner<TConcatService> service;
        auto port = service.Run();

        auto source = env.CreateSource(GenerateBackendConfig(port, 1));

        TString a{100000, 'a'};
        TString b{"b"};

        NProtocol::TConcatRequest requestData;
        requestData.SetA(a);
        requestData.SetB(b);

        auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                .Method(NSrvKernel::EMethod::POST)
                .Path("/NProtocol.TConcatenator/Concat")
                .Version11()
                .Header("te", "trailers")
                .Header("content-type", "application/grpc+proto")
                .Header("grpc-encoding", "identity")
                .Header("grpc-accept-encoding", "identity")
                .Header("Host", "somehost");

        NSrvKernel::THTTP2StreamDetails http2;
        request.Props().HTTP2 = &http2;

        auto inputChannel = MakeAtomicShared<NSrvKernel::TU2WChannel<TString>>(Max<size_t>());

        auto context = source->SendRequest(std::move(request), {}, clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), inputChannel, nullptr);

        UNIT_ASSERT_EQUAL(inputChannel->Send(GenerateGrpcFrame(requestData), TInstant::Max()), NSrvKernel::EChannelStatus::Success);
        UNIT_ASSERT_EQUAL(inputChannel->Send(TString{}, TInstant::Max()), NSrvKernel::EChannelStatus::Success);

        auto response = std::move(context->GetFuture().ExtractValueSync().GetResponse().GetOrThrow());
        UNIT_ASSERT_EQUAL(response.HttpResponse.ResponseLine().StatusCode, 200);
        UNIT_ASSERT_EQUAL(response.Trailers.GetFirstValue("grpc-status"), "0");
        TStringBuf buffer{response.Data};
        NProtocol::TConcatResponse sum;
        size_t offset = 0;
        UNIT_ASSERT(ExtractMessageFromGrpcFrame(buffer, sum, offset));
        UNIT_ASSERT_EQUAL(offset, buffer.size());
        UNIT_ASSERT_EQUAL(sum.GetSum(), a+b);
    }

    Y_UNIT_TEST(Ping) {
        TThreadPool pool;
        pool.Start(1);

        TSocketHolder listenSocket;
        NSrvKernel::TryRethrowError(NSrvKernel::TcpSocket(AF_INET6, false).AssignTo(listenSocket));
        NSrvKernel::TryRethrowError(NSrvKernel::Bind(listenSocket, {NSrvKernel::Loopback6(), 0}));
        NSrvKernel::TSockAddr eph;
        NSrvKernel::TryRethrowError(NSrvKernel::GetSockName(listenSocket).AssignTo(eph));
        ui16 port = eph.Port();
        NSrvKernel::TryRethrowError(NSrvKernel::Listen(listenSocket, 1));
        NThreading::Async([&]() {
            NSrvKernel::TAcceptResult acceptSocket;
            TryRethrowError(NSrvKernel::Accept(listenSocket, false).AssignTo(acceptSocket));
            TSocket socket{acceptSocket.Conn.Release()};
            auto settingsFrame = NSrvKernel::NHTTP2::TFrameHeading::NewSettings();
            auto data = settingsFrame.Write();
            socket.Send(data->Data(), data->Length());
            Sleep(TDuration::Seconds(5));
        }, pool);


        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        auto source = env.CreateSource(GenerateBackendConfig(port, 1));

        auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                .Version11()
                .Method(NSrvKernel::EMethod::GET)
                .Path("/path")
                .Header("Host", "somehost");

        NSrvKernel::THTTP2StreamDetails http2;
        request.Props().HTTP2 = &http2;

        auto context = source->SendRequest(std::move(request), {}, clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), nullptr, nullptr);
        auto error = context->GetFuture().ExtractValueSync().GetResponse().ReleaseError();
        auto backendError = error.GetAs<NSrvKernel::TBackendError>();
        UNIT_ASSERT(backendError);
        UNIT_ASSERT_STRING_CONTAINS(backendError->InnerError()->what(), "ping failed");
    }

    Y_UNIT_TEST(Proxy) {
        constexpr TStringBuf EXPECTED_PATH = "/path";
        constexpr TStringBuf EXPECTED_RESPONSE = "response";

        TThreadPool pool;
        pool.Start(1);

        TPortManager portManager {true};
        ui16 proxyPort = portManager.GetPort();
        ui16 serverPort = portManager.GetPort();

        NThreading::TPromise<TString> promise = NThreading::NewPromise<TString>();
        NBalancerServer::TStandaloneServer server {
                [&](NBalancerServer::THttpRequestEnv& env) {
                    promise.SetValue(TString{env.Path()});
                    NSrvKernel::TResponse response(200, "Ok");

                    auto reply = env.GetReplyTransport();

                    reply->SendHead(std::move(response));
                    reply->SendData(TStringBuf(EXPECTED_RESPONSE));
                    reply->SendEof();

                    return NSrvKernel::TError {};
                },
                NBalancerServer::TOptions().SetPort(serverPort)
        };

        NThreading::Async([&]() {
            server.Run();
        }, pool);

        NBalancerClient::NTesting::TProxy proxy {NBalancerClient::NTesting::TBalancerProxyInstanceConfig{proxyPort, serverPort, true}};
        proxy.Start();

        SendHttp2RequestAndCheckResponse(proxyPort, EXPECTED_RESPONSE);

        server.Stop();

        UNIT_ASSERT_EQUAL(promise.GetFuture().GetValueSync(), EXPECTED_PATH);
    }

    Y_UNIT_TEST(ProxyHttp2Trailers) {
        constexpr TStringBuf EXPECTED_PATH = "/path";
        constexpr TStringBuf EXPECTED_RESPONSE = "response";
        constexpr TStringBuf EXPECTED_TRAILER_NAME = "testTrailer";
        constexpr TStringBuf EXPECTED_TRAILER_VALUE = "value";

        // Reserve ports for test
        TPortManager portManager {true};
        ui16 proxyPort = portManager.GetPort();
        ui16 serverPort = portManager.GetPort();

        // Setting up server
        TThreadPool pool;
        pool.Start(1);

        NThreading::TPromise<TString> promise = NThreading::NewPromise<TString>();
        NBalancerServer::TStandaloneServer server {
            [&](NBalancerServer::THttpRequestEnv& env) -> NSrvKernel::TError {
                    promise.SetValue(TString{env.Path()});
                    NSrvKernel::TResponse response(200, "Ok");

                    auto reply = env.GetReplyTransport();

                    reply->SendHead(std::move(response));
                    reply->SendData(TStringBuf(EXPECTED_RESPONSE));

                    Y_REQUIRE(env.TrailersAllowed(), yexception{} << "Need TE: trailers header");

                    NSrvKernel::THeaders headers;
                    headers.Add(TString{EXPECTED_TRAILER_NAME}, TString{EXPECTED_TRAILER_VALUE});
                    reply->SendTrailers(std::move(headers));

                    return NSrvKernel::TError {};
                },
                NBalancerServer::TOptions().SetPort(serverPort)
        };

        NThreading::Async([&]() {
            server.Run();
            }, pool);

        // Setting up proxy
        NBalancerClient::NTesting::TProxy proxy {NBalancerClient::NTesting::TBalancerProxyInstanceConfig{proxyPort, serverPort, true}};
        proxy.Start();

        // Create environment for checking response
        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        // Creating http2 request
        auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                .Method(NSrvKernel::EMethod::POST)
                .Path(EXPECTED_PATH)
                .Version11()
                .Header("te", "trailers")
                .Header("Host", "somehost");

        NSrvKernel::THTTP2StreamDetails http2;
        request.Props().HTTP2 = &http2;

        // Sending request
        auto source = env.CreateSource(GenerateBackendConfig(proxyPort, 1));
        auto context = source->SendRequest(std::move(request), {}, clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), nullptr, nullptr);

        NBalancerClient::TBalancerClientResponse response;
        UNIT_ASSERT_NO_EXCEPTION(NSrvKernel::TryRethrowError(context->GetFuture().ExtractValueSync().GetResponse().AssignTo(response)));

        server.Stop();
        pool.Stop();

        // Checking trailers
        UNIT_ASSERT_EQUAL(response.Trailers.GetFirstValue(EXPECTED_TRAILER_NAME), EXPECTED_TRAILER_VALUE);

        UNIT_ASSERT_EQUAL(promise.GetFuture().GetValueSync(), EXPECTED_PATH);
        UNIT_ASSERT_VALUES_EQUAL(response.Data, EXPECTED_RESPONSE);
    }

    Y_UNIT_TEST(ProxyHttp1ClientHttp2BackendNoTrailers) {
        constexpr TStringBuf EXPECTED_PATH = "/path";
        constexpr TStringBuf EXPECTED_RESPONSE = "response";

        // Reserve ports for test
        TPortManager portManager {true};
        ui16 proxyPort = portManager.GetPort();
        ui16 serverPort = portManager.GetPort();

        // Setting up server
        TThreadPool pool;
        pool.Start(1);

        NThreading::TPromise<TString> promise = NThreading::NewPromise<TString>();
        NBalancerServer::TStandaloneServer server {
                [&](NBalancerServer::THttpRequestEnv& env) -> NSrvKernel::TError {
                    promise.SetValue(TString{env.Path()});
                    NSrvKernel::TResponse response(200, "Ok");

                    auto reply = env.GetReplyTransport();

                    Y_REQUIRE(!env.TrailersAllowed(), yexception{} << "Header TE must be filtered");

                    reply->SendHead(std::move(response));
                    reply->SendData(TStringBuf(EXPECTED_RESPONSE));
                    reply->SendEof();

                    return NSrvKernel::TError {};
                },
                NBalancerServer::TOptions().SetPort(serverPort)
        };

        NThreading::Async([&]() {
            server.Run();
        }, pool);

        // Setting up proxy
        NBalancerClient::NTesting::TProxy proxy {NBalancerClient::NTesting::TBalancerProxyInstanceConfig{proxyPort, serverPort, true}};
        proxy.Start();

        // Create environment for checking response
        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        // Creating http1 request
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                .Method(NSrvKernel::EMethod::POST)
                .Path(EXPECTED_PATH)
                .Version11()
                .Header("te", "trailers")
                .Header("Host", "somehost");

        // Sending request
        auto source = env.CreateSource(GenerateBackendConfig(proxyPort, 1));
        auto context = source->SendRequest(std::move(request), {}, nullptr, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), nullptr, nullptr);

        NBalancerClient::TBalancerClientResponse response;
        UNIT_ASSERT_NO_EXCEPTION(NSrvKernel::TryRethrowError(context->GetFuture().ExtractValueSync().GetResponse().AssignTo(response)));

        server.Stop();
        pool.Stop();

        // Checking trailers
        UNIT_ASSERT_EQUAL(true, response.Trailers.Size() == 0);

        UNIT_ASSERT_EQUAL(promise.GetFuture().GetValueSync(), EXPECTED_PATH);
        UNIT_ASSERT_VALUES_EQUAL(response.Data, EXPECTED_RESPONSE);
    }

    Y_UNIT_TEST(RequestTimeout) {
        TThreadPool pool;
        pool.Start(1);

        TPortManager portManager {true};
        ui16 serverPort = portManager.GetPort();

        NBalancerServer::TStandaloneServer server {
                [&](NBalancerServer::THttpRequestEnv&) {
                    RunningCont()->SleepD(TDuration::Seconds(10).ToDeadLine());
                    return NSrvKernel::TError {};
                },
                NBalancerServer::TOptions().SetPort(serverPort)
        };

        NThreading::Async([&]() {
            server.Run();
        }, pool);

        NBalancerClient::NTesting::TEnv env;
        env.MainTask().GetProtocolFactory().RegisterProtocolImpl<NBalancerClient::THttp2Backend>();
        env.Start();

        auto clientRequest = MakeIntrusive<NBalancerClient::THttp2Request>();
        NSrvKernel::TRequest request = NSrvKernel::BuildRequest()
                .Version11()
                .Method(NSrvKernel::EMethod::GET)
                .Path("/path")
                .Header("Host", "somehost");

        NSrvKernel::THTTP2StreamDetails http2;
        request.Props().HTTP2 = &http2;

        auto source = env.CreateSource(GenerateBackendConfig(serverPort, 1));
        auto context = source->SendRequest(std::move(request), {}, clientRequest, MakeHolder<NSrvKernel::TAttemptsHolderBase>(1, 0), nullptr, nullptr);

        auto error = context->GetFuture().ExtractValueSync().GetResponse().ReleaseError();
        auto systemError = error.GetAs<TSystemError>();
        server.Stop();

        UNIT_ASSERT_VALUES_EQUAL(systemError->Status(), ETIMEDOUT);
    }
}
