#include <balancer/kernel/http/parser/http.h>

#include <balancer/kernel/http/parser/tests/util/custom_chunks_emitter_io.h>

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

#include <util/generic/xrange.h>

using namespace NSrvKernel;

Y_UNIT_TEST_SUITE(THttpResponseTest) {

    void DoTestAcceptResponseLine(TString line, int status, int maj, int min, TString reason) {
        TResponse response;
        line += "\r\n";
        TError error = response.Parse(line);
        UNIT_ASSERT_C(!error, line.Quote());
        UNIT_ASSERT_VALUES_EQUAL((int)response.ResponseLine().StatusCode, status);
        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().MajorVersion, maj);
        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().MinorVersion, min);
        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().Reason.AsStringBuf(), reason);
    }

    void DoTestRejectResponseLine(TString line, int status) {
        TResponse response;
        line += "\r\n";
        TError error = response.Parse(line);
        UNIT_ASSERT_C(error, line.Quote());
        UNIT_ASSERT_VALUES_EQUAL_C((int)error.GetAs<THttpError>()->Code(), status, line.Quote());
    }

    Y_UNIT_TEST(TestEmptyReasonPhraseLine) {
        DoTestAcceptResponseLine("HTTP/1.1 204\r\n", 204, 1, 1, "");
        DoTestAcceptResponseLine("HTTP/1.1 204 \r\n", 204, 1, 1, "");
    }

    Y_UNIT_TEST(TestNonEmptyReasonPhraseLine) {
        DoTestAcceptResponseLine("HTTP/1.1 204     OK  \r\n", 204, 1, 1, "OK  ");
    }

    Y_UNIT_TEST(TestVersions) {
        DoTestAcceptResponseLine("HTTP/1.0 100\r\n", 100, 1, 0, "");
        DoTestAcceptResponseLine("HTTP/1.1 100\r\n", 100, 1, 1, "");

        DoTestRejectResponseLine("HTTP/1.2 100\r\n", 400);
        DoTestRejectResponseLine("HTTP/0.9 100\r\n", 400);
    }

    Y_UNIT_TEST(TestMalformed) {
        DoTestRejectResponseLine("HTTP/0 100\r\n", 400);
        DoTestRejectResponseLine("HTTP/2 100\r\n", 400);
        DoTestRejectResponseLine("HTTP 100\r\n", 400);
        DoTestRejectResponseLine("HTTP/ 100\r\n", 400);
        DoTestRejectResponseLine("HHHH/1.1 100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 600\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 1000\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 99\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 9\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 \r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1\r\n", 400);
        DoTestRejectResponseLine("\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1\t100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1\r100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1\n100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 \r100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 \n100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1\r\n100\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 \r10\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 \n10\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1  10\r\n", 400);
        DoTestRejectResponseLine(" HTTP/1.1 100\r\n", 400);
        DoTestRejectResponseLine("\rHTTP/1.1 100\r\n", 400);
        DoTestRejectResponseLine("\nHTTP/1.1 100\r\n", 400);
        DoTestRejectResponseLine("\r\nHTTP/1.1 100\r\n", 400);
    }

    Y_UNIT_TEST(TestCodes) {
        for (auto code : {100, 199, 200, 299, 300, 399, 400, 499, 500, 599}) {
            DoTestAcceptResponseLine("HTTP/1.1 " + ToString(code) + "\r\n", code, 1, 1, "");
        }
    }

    Y_UNIT_TEST(TestAsciiReasonPhraseLine) {
        for (size_t n : xrange(1, 10)) {
            for (char c : xrange(32, 127)) {
                TString reason;
                reason.resize(n, c);
                TString expectedReason = reason;
                if (c == 32) {
                    expectedReason = "";
                }
                DoTestAcceptResponseLine("HTTP/1.1 204 " + reason + "\r\n", 204, 1, 1, expectedReason);
            }

            {
                // TAB is also required by RFC 2616
                TString reason;
                reason.resize(n, '\t');
                DoTestAcceptResponseLine("HTTP/1.1 204 " + reason + "\r\n", 204, 1, 1, reason);
            }

            for (char c : xrange(0, 32)) {
                if (c == '\t') {
                    continue;
                }
                TString reason;
                reason.resize(n, c);
                DoTestRejectResponseLine("HTTP/1.1 204 " + reason + "\r\n", 400);
            }

            for (char c : xrange(127, 128)) {
                TString reason;
                reason.resize(n, c);
                DoTestRejectResponseLine("HTTP/1.1 204 " + reason + "\r\n", 400);
            }

            for (char c : xrange(128, 256)) {
                TString reason;
                reason.resize(n, c);
                DoTestAcceptResponseLine("HTTP/1.1 204 " + reason + "\r\n", 204, 1, 1, reason);
            }
        }
    }

    Y_UNIT_TEST(TestSpecificAsciiReasonPhrases) {
        DoTestAcceptResponseLine("HTTP/1.1 404 can not find graph \"foo\"\r\n", 404, 1, 1, "can not find graph \"foo\"");
        DoTestRejectResponseLine("HTTP/1.1 500 \r\n\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 500 \r\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 500 \n\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 500\r\n\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 500\r\r\n", 400);
        DoTestRejectResponseLine("HTTP/1.1 500\n\r\n", 400);
    }

    Y_UNIT_TEST(TestResponse) {
        TResponse response;
        TChunkList unparsed;
        TString line = "HTTP/1.1 302 Found\r\n"
                       "date: Mon, 03 Feb 2020 11:49:18 GMT\r\n"
                       "location: https://yandex.st/lego/_/pDu9OWAQKB0s2J9IojKpiS_Eho.ico\r\n"
                       "content-encoding: br\r\n"
                       "content-type: text/html; charset=iso-8859-1\r\n"
                       "connection: Close\r\n"
                       "content-length: 185\r\n"
                       "vary: Accept-Encoding\r\n\r\n";

        TCustomChunksEmitterIo stringEmitter(std::move(line));

        TError error = response.Read(&stringEmitter, unparsed, TInstant::Max());
        UNIT_ASSERT_C(!error, error->what());

        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().StatusCode, 302);
        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().MajorVersion, 1);
        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().MajorVersion, 1);
        UNIT_ASSERT_VALUES_EQUAL(response.ResponseLine().Reason.AsStringBuf(), "Found");

        const auto& headers = response.Headers();
        UNIT_ASSERT_VALUES_EQUAL(headers.Size(), 5);
        UNIT_ASSERT_VALUES_EQUAL(headers.GetFirstValue("date"), "Mon, 03 Feb 2020 11:49:18 GMT");
        UNIT_ASSERT_VALUES_EQUAL(headers.GetFirstValue("location"),
                                 "https://yandex.st/lego/_/pDu9OWAQKB0s2J9IojKpiS_Eho.ico");
        UNIT_ASSERT_VALUES_EQUAL(headers.GetFirstValue("content-encoding"), "br");
        UNIT_ASSERT_VALUES_EQUAL(headers.GetFirstValue("content-type"), "text/html; charset=iso-8859-1");
        UNIT_ASSERT_VALUES_EQUAL(headers.GetFirstValue("vary"), "Accept-Encoding");

        const auto& props = response.Props();
        UNIT_ASSERT_VALUES_EQUAL(*props.ContentLength, 185);
        UNIT_ASSERT(!props.KeepAlive);
        UNIT_ASSERT(!props.ExplicitKeepAliveHeader);
        UNIT_ASSERT(props.ExplicitConnectionHeader);
        UNIT_ASSERT(!props.UpgradeRequested);
    }

    Y_UNIT_TEST(TestCtrFromStatusAndReason) {
        const int code = 10;
        TMaybe<TResponse> serverResponse;
        {
            TString temp = "bad gateway";
            serverResponse.ConstructInPlace(code, std::move(temp));
        }
        UNIT_ASSERT_VALUES_EQUAL(serverResponse->ResponseLine().StatusCode, code);
        UNIT_ASSERT_VALUES_EQUAL(serverResponse->ResponseLine().Reason.AsStringBuf(), "bad gateway");
    }
};
