#include <balancer/client/experimental/requesters/balancer_client/balancer_client.h>

#include "util/server.h"

#include <balancer/client/experimental/util/requester_utils.h>
#include <balancer/client/experimental/util/request_utils.h>
#include <balancer/client/experimental/util/request_context_utils.h>
#include <balancer/client/experimental/base/timeout_error.h>

#include <library/cpp/testing/gtest/gtest.h>

using namespace NHttp;

// TODO:
// кеширование соединений (HTTP/1/2/S)
// параллельные запросы
// большие данные
// вызов read из колбека read
// peek и limit в response stream
// http/2 запрос без body не должен порождать DATA фреймы
// alpn
// ssl sni из Host
// обычные и защищенные соединения не смешиваются в кеше

TEST(Http, Methods) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        response.Headers().Add("Request-Method", ToString(request.Method()));
        request.GetReplyTransport()->SendHead(std::move(response));
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();

    auto methods = {
        EMethod::OPTIONS,
        EMethod::HEAD,
        EMethod::GET,
        EMethod::POST,
        EMethod::PUT,
        EMethod::PATCH,
        EMethod::DELETE,

//        not supported by server
//        EMethod::TRACE,
//        EMethod::CONNECT
    };

    for (auto method: methods) {
        request.Method = method;
        TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
        ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
        ASSERT_EQ(response.Head.Headers.FindHeader("Request-Method")->Value(), ToString(method));
    }
}

TEST(Http, Streaming) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        for (;;) {
            TString buffer;
            buffer.resize(16_KB);
            size_t bytesCount = request.Input()->Read((void*) buffer.data(), buffer.capacity());
            if (bytesCount == 0) {
                request.GetReplyTransport()->SendEof();
                request.GetReplyTransport()->Finish();
                return {};
            }
            buffer.resize(bytesCount);
            request.GetReplyTransport()->SendData(std::move(buffer));
        }
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.ChunkedRequest = true;

    auto context = client.Send(request);
    auto head = NUtils::ReadHead(*context, TInstant::Max()).GetValueSync().Success();
    ASSERT_EQ(head.StatusCode, HTTP_OK);
    for (int i = 0; i < 10; i++) {
        auto message = ToString(i);
        NSrvKernel::TChunkList chunk{message};
        context->Write(chunk);
        auto frame = NUtils::ReadRaw(*context, TInstant::Max()).GetValueSync().Success();
        auto data = std::get<TDataFrame>(std::move(frame.GetRef()));
        auto response = NSrvKernel::StrInplace(data);
        ASSERT_EQ(response, message);
    }
}

TEST(Http, Http2) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        if (request.IsHttp2()) {
            request.GetReplyTransport()->SendData(TStringBuf{"HTTP/2"});
        } else {
            request.GetReplyTransport()->SendData(TStringBuf{"HTTP/1"});
        }
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();

    {
        request.ProtocolVersion = EProtocolVersion::HTTP11;
        TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
        ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
        ASSERT_EQ(ToString(response.Body), "HTTP/1");
    }
    {
        request.ProtocolVersion = EProtocolVersion::HTTP2;
        TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
        ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
        ASSERT_EQ(ToString(response.Body), "HTTP/2");
    }
}

TEST(Http, Timeout) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        Y_UNUSED(NSrvKernel::UniversalSleep(TDuration::Seconds(2)));
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();

    TError error = NUtils::Send(client, request, TInstant::Seconds(1)).Error();
    ASSERT_TRUE(error.GetInfo<TTimeoutError>());
}

TEST(Http, EmptyEndpoint) {
    auto func = []() {
        NRequesters::TBalancerClient client;
        NUtils::Send(client, {}, TInstant::Max());
    };
    ASSERT_DEATH(func(), "endpoint is empty");
}

TEST(Http, Path) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        request.GetReplyTransport()->SendData(request.Path());
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.Path = "/some/path";

    TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
    ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
    ASSERT_EQ(ToString(response.Body), request.Path);
}

TEST(Http, Query) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        request.GetReplyTransport()->SendData(request.Cgi());
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.Query.InsertUnescaped("key", "value");


    TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
    ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
    ASSERT_EQ(ToString(response.Body), "key=value");
}

TEST(Http, Body) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        auto body = request.Input()->ReadAll();
        request.GetReplyTransport()->SendData(body);
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.Body.Push("body");

    TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
    ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
    ASSERT_EQ(ToString(response.Body), request.Body);
}

TEST(Http, Headers) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        response.Headers().Add("Test-Header", request.Headers().GetFirstValue("Test-Header"));
        request.GetReplyTransport()->SendHead(std::move(response));
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.Headers.AddHeader("Test-Header", "Test-Value");

    TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
    ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
    ASSERT_EQ(response.Head.Headers.FindHeader("Test-Header")->Value(), request.Headers.FindHeader("Test-Header")->Value());
}

TEST(Http, Trailer) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        Y_VERIFY(request.TrailersAllowed());
        NSrvKernel::THeaders trailer;
        trailer.Add("Test-Trailer", "Test-Value");
        request.GetReplyTransport()->SendTrailers(std::move(trailer));
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.ProtocolVersion = EProtocolVersion::HTTP2;
    request.Headers.AddHeader("te", "trailers");

    TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
    ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
    ASSERT_EQ(response.Trailer.FindHeader("Test-Trailer")->Value(), "Test-Value");
}

TEST(Http, Cancel) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();

    auto context = client.Send(request);
    context->Cancel();
    auto result = NUtils::ReadHead(*context, TInstant::Max()).GetValueSync();
    ASSERT_TRUE(result.IsError());
}

TEST(Http, Parsing) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
        request.GetReplyTransport()->SendHead(std::move(response));
        auto body = request.Input()->ReadAll();
        request.GetReplyTransport()->SendData(body);
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    class TCustomParser: public IResponseParser {
      public:
        void Parse(IResponseStream& stream, TInstant deadline, THandler callback) noexcept override {
            stream.Read(0, false, deadline, [callback = std::move(callback)](IResponseStream::TReadResult result) {
                if (result.IsError()) {
                    return callback(std::move(result.Error()));
                }

                if (result.Success().Empty()) {
                    callback(std::any{});
                } else if (holds_alternative<TResponseHeadFrame>(result.Success().GetRef())) {
                    callback(std::make_any<TString>("head"));
                } else {
                    callback(std::make_any<TString>("body"));
                }
            });
        }
    };

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.Body.Push("123");

    TCustomParser parser;

    auto context = client.Send(request);
    auto result = NUtils::Read(*context, parser, TInstant::Max()).GetValueSync().Success();
    ASSERT_EQ(std::any_cast<TString>(result), "head");
    result = NUtils::Read(*context, parser, TInstant::Max()).GetValueSync().Success();
    ASSERT_EQ(std::any_cast<TString>(result), "body");
    result = NUtils::Read(*context, parser, TInstant::Max()).GetValueSync().Success();
    ASSERT_FALSE(result.has_value());
}

TEST(Http, ParallelReads) {
    auto func = []() {
        THttpServer::TOptions serverOptions;
        serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
            NSrvKernel::TResponse response;
            response.ResponseLine() = NSrvKernel::TResponseLine{200, "OK"};
            request.GetReplyTransport()->SendHead(std::move(response));
            request.GetReplyTransport()->SendEof();
            return {};
        };
        THttpServer server{serverOptions};
        NRequesters::TBalancerClient client;

        TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();

        auto context = client.Send(request);
        NUtils::ReadHead(*context, TInstant::Max());
        NUtils::ReadRaw(*context, TInstant::Max());
    };
    ASSERT_DEATH(func(), "parallel reads");
}

TEST(Http, ReasonPhrase) {
    THttpServer::TOptions serverOptions;
    serverOptions.Handler = [](NBalancerServer::THttpRequestEnv& request) -> NSrvKernel::TError {
        NSrvKernel::TResponse response;
        response.ResponseLine() = NSrvKernel::TResponseLine{200, "CUSTOM REASON PHRASE"};
        request.GetReplyTransport()->SendHead(std::move(response));
        request.GetReplyTransport()->SendEof();
        return {};
    };
    THttpServer server{serverOptions};
    NRequesters::TBalancerClient client;

    TRequest request = NUtils::BuildRequest("http://127.0.0.1:" + ToString(server.GetPort())).Success();
    request.Body.Push("body");

    TFullResponse response = NUtils::Send(client, request, TInstant::Max()).Success();
    ASSERT_EQ(response.Head.StatusCode, HTTP_OK);
    ASSERT_EQ(response.Head.ReasonPhrase, "CUSTOM REASON PHRASE");
}
