#include "ut_common.h"

#include <balancer/kernel/cookie/cookie.h>

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

#include <util/generic/xrange.h>

using namespace NSrvKernel;
using namespace NSrvKernel::NCookie;

Y_UNIT_TEST_SUITE(TChromiumTest) {
    // https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/net/cookies/parsed_cookie_unittest.cc

    Y_UNIT_TEST(TestEmpty) {
        TestParse("", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse("     ", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse("=", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse("=;", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse(" =;", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse("= ;", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse(" = ;", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse(";", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse(" ;", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse(" ; ", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieEmpty});
        TestParse("\t", TSetCookieSyntaxErrors{ESetCookieSyntaxError::InvalidOctet, ESetCookieSyntaxError::CookieEmpty});
        TestParse("\t;", TSetCookieSyntaxErrors{ESetCookieSyntaxError::InvalidOctet, ESetCookieSyntaxError::CookieEmpty});
        TestParse("\t=\t", TSetCookieSyntaxErrors{ESetCookieSyntaxError::InvalidOctet, ESetCookieSyntaxError::CookieEmpty});
        TestParse("\t=", TSetCookieSyntaxErrors{ESetCookieSyntaxError::InvalidOctet, ESetCookieSyntaxError::CookieEmpty});
        TestParse("=\t", TSetCookieSyntaxErrors{ESetCookieSyntaxError::InvalidOctet, ESetCookieSyntaxError::CookieEmpty});
        TestParse("name=", TSetCookie{.Name="name"});
        // Nameless cookies without '=' behave differently in Safari and Chromium/Firefox.
        // There also can be only one nameless cookie. They are therefore forbidden in balancer.
        TestParse("value", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieNameEmpty});
        TestParse("=value", TSetCookieSyntaxErrors{ESetCookieSyntaxError::CookieNameEmpty});
    }

    Y_UNIT_TEST(TestQuoted) {
        // Quoting the chromium tests source code:
        //      These are some quoting cases which the major browsers all
        //      handle differently.  I've tested Internet Explorer 6, Opera 9.6,
        //      Firefox 3, and Safari Windows 3.2.1.  We originally tried to match
        //      Firefox closely, however we now match Internet Explorer and Safari.

        // Trailing whitespace after a quoted value.  The whitespace after
        // the quote is stripped in all browsers.
        TestParse(
            "FOO=\"zzz \"  ",
            TSetCookie{.Name="FOO", .Value=FromStringBuf("\"zzz \"")},
            "FOO=\"zzz \""
        );
        // Handling a quoted value with a ';', like FOO="zz;pp"  ;
        // IE and Safari: "zz;
        // Firefox and Opera: "zz;pp"
        // Balancer forbids unknown attrs.
        TestParse(
            "FOO=\"zz;pp\" ;",
            TSetCookieSyntaxErrors{ESetCookieSyntaxError::AttrUnknown}
        );
        // Handling a value with multiple quoted parts, like FOO="zzz "   "ppp" ;
        // IE and Safari: "zzz "   "ppp";
        // Firefox: "zzz ";
        // Opera: <rejects cookie>
        TestParse(
            "FOO=\"zzz \"   \"ppp\" ",
            TSetCookie{.Name="FOO", .Value=FromStringBuf("\"zzz \"   \"ppp\"")},
            "FOO=\"zzz \"   \"ppp\""
        );
        // A quote in a value that didn't start quoted.  like FOO=A"B ;
        // IE, Safari, and Firefox: A"B;
        // Opera: <rejects cookie>
        TestParse(
            "FOO=A\"B",
            TSetCookie{.Name="FOO", .Value=FromStringBuf("A\"B")},
            "FOO=A\"B"
        );
        // path parameters aren't supposed to be quoted.  Bug 1261605.
        TestParse(
            "aBc=\"zz;pp\" ; ; path=\"/\"  ; httponly ",
            TSetCookieSyntaxErrors{
                ESetCookieSyntaxError::AttrUnknown,
                ESetCookieSyntaxError::AttrEmpty,
                ESetCookieSyntaxError::PathValueInvalid,
            }
        );
        TestParse(
            "\"BLA\\\"HHH\"=; path=/; secure;",
            TSetCookie{.Name="\"BLA\\\"HHH\"", .Path="/", .Secure=true},
            "\"BLA\\\"HHH\"=; Path=/; Secure"
        );
        TestParse(
            "a=\"B",
            TSetCookie{.Name="a", .Value=FromStringBuf("\"B")}
        );
        TestParse(
            "ANCUUID=\"zohNumRKgI0oxyhSsV3Z7D\"  ; expires=Sun, 18-Apr-2027 21:06:29 GMT ; path=/  ;  ",
            TSetCookie{
                .Name="ANCUUID",
                .Value=FromStringBuf("\"zohNumRKgI0oxyhSsV3Z7D\""),
                .Path="/",
                .Expires=TExpires{.Year=2027, .Month=4, .MDay=18, .Hour=21, .Minute=6, .Second=29}
            },
            "ANCUUID=\"zohNumRKgI0oxyhSsV3Z7D\"; Path=/; Expires=Sun, 18-Apr-2027 21:06:29 GMT"
        );
    }

    Y_UNIT_TEST(TestAttributeCase) {
        TestParse(
            "BLAHHH=; pAth=/; sECuRe; httpONLY; sAmESitE=StrIct",
            TSetCookie{
                .Name="BLAHHH", .Path="/", .SameSite=ESameSite::Strict, .Secure=true, .HttpOnly=true,
            },
            "BLAHHH=; Path=/; SameSite=Strict; Secure; HttpOnly"
        );
    }

    Y_UNIT_TEST(TestWSpace) {
        TestParse(
            "ABC=;  path = /wee",
            TSetCookie{.Name="ABC", .Path="/wee"},
            "ABC=; Path=/wee"
        );
        TestParse(
            "  A  = BC  ;secure;   samesite = lax     ",
            TSetCookie{
                .Name="A", .Value=FromStringBuf("BC"), .SameSite=ESameSite::Lax, .Secure=true
            },
            "A=BC; SameSite=Lax; Secure"
        );
        // We forbid empty attrs in balancer.
        TestParse(
            "  A  = BC  ;secure;;   samesite = lax     ",
            TSetCookieSyntaxErrors{ESetCookieSyntaxError::AttrEmpty}
        );
        TestParse(
            "ANCUUID=zohNumRKgI0oxyhSsV3Z7D  ; expires=Sun, 18-Apr-2027 21:06:29 GMT ; path=/  ;  ",
            TSetCookie{
                .Name="ANCUUID",
                .Value=FromStringBuf("zohNumRKgI0oxyhSsV3Z7D"),
                .Path="/",
                .Expires=TExpires{.Year=2027, .Month=4, .MDay=18, .Hour=21, .Minute=6, .Second=29}
            },
            "ANCUUID=zohNumRKgI0oxyhSsV3Z7D; Path=/; Expires=Sun, 18-Apr-2027 21:06:29 GMT"
        );
        TestParse(
            "  A=== BC  ;secure;   httponly",
            TSetCookie{.Name="A", .Value=FromStringBuf("== BC"), .Secure=true, .HttpOnly=true},
            "A=== BC; Secure; HttpOnly"
        );
    }
//
//    Y_UNIT_TEST(TestTooLong) {
//        TestParse(
//            "a=b" + TString(4086, ' ') + ";secure",
//            TSetCookie{.Name="a", .Value=FromStringBuf("b"), .Secure=true},
//            "a=b; Secure"
//        );
//        TestParse(
//            "a=b" + TString(4087, ' ') + ";secure",
//            TSetCookieSyntaxErrors{ESetCookieSyntaxError::TooLong}
//        );
//    }

    Y_UNIT_TEST(TestInvalidOctet) {
        // Chromium allows cookies with unicode in name or value. We in balancer won't.
        for (auto c : Concatenate(xrange(0, 31), xrange(127, 256))) {
            for (auto nva : {
                TString(TStringBuilder() << "n" << char(c) << "m=vl; Path=/pt"),
                TString(TStringBuilder() << "nm=v" << char(c) << "l; Path=/pt"),
                TString(TStringBuilder() << "nm=vl; Path=/p" << char(c) << "t"),
            }) {
                TestParse(nva, TSetCookieSyntaxErrors{ESetCookieSyntaxError::InvalidOctet});
            }
        }
        for (auto c : xrange(33, 127)) {
            if (char(c) == ';') {
                continue;
            }
            if (char(c) != '=') {
                auto cc = TString(TStringBuilder() << char(c) << '=');
                TestParse(
                    cc,
                    TSetCookie{.Name=std::string(1, char(c))}
                );
            }
            {
                auto cc = TString(TStringBuilder() << "n=" << char(c));
                TestParse(
                    cc,
                    TSetCookie{.Name="n", .Value=TBlob::FromString(TString{1, char(c)})}
                );
            }
            {
                auto path = "/" + std::string(1, char(c));
                auto cc = TString(TStringBuilder() << "n=; Path=" << path);
                TestParse(
                    cc,
                    TSetCookie{.Name="n", .Path=path}
                );
            }
        }
    }

    Y_UNIT_TEST(TestDuplicateAttrs) {
        // The quote from the chromium tests source code:
        //      Set the domain attribute twice in a cookie line. If the second attribute's
        //      value is empty, it shoud be ignored. This is de facto standard behavior, per https://crbug.com/601786.
        // Instead of replicating this behavior we will just drop the whole cookie.
        TestParse(
            "name=value; domain=foo.com; domain=bar.com",
            TSetCookieSyntaxErrors{ESetCookieSyntaxError::DomainValueConflict}
        );
        TestParse(
            "name=value; domain=foo.com; domain=",
            TSetCookieSyntaxErrors{
                ESetCookieSyntaxError::DomainValueInvalid, ESetCookieSyntaxError::DomainValueConflict
            }
        );
    }

    Y_UNIT_TEST(TestSetSameSite) {
        TestParse(
            "name=value; samesite=strict",
            TSetCookie{.Name="name", .Value=FromStringBuf("value"), .SameSite=ESameSite::Strict},
            "name=value; SameSite=Strict"
        );
        TestParse(
            "name=value; samesite=lAx",
            TSetCookie{.Name="name", .Value=FromStringBuf("value"), .SameSite=ESameSite::Lax},
            "name=value; SameSite=Lax"
        );
        TestParse(
            "name=value; samesite=NONE",
            TSetCookie{.Name="name", .Value=FromStringBuf("value"), .SameSite=ESameSite::None},
            "name=value; SameSite=None"
        );
        TestParse(
            "name=value; samesite=Blah",
            TSetCookieSyntaxErrors{ESetCookieSyntaxError::SameSiteValueInvalid}
        );
        TestParse(
            "name=value; samesite",
            TSetCookieSyntaxErrors{ESetCookieSyntaxError::SameSiteValueInvalid}
        );
    }

    Y_UNIT_TEST(SpecialTokens) {
        // The quote from the chromium tests source code:
        //      Special tokens "secure" and "httponly" should be treated as any other name
        //      when they are in the first position.
        // TODO(velavokr): maybe we should catch cookies with special names anyway?
        TestParse("domain=", TSetCookie{.Name="domain"});
        TestParse("path=", TSetCookie{.Name="path"});
        TestParse("secure=", TSetCookie{.Name="secure"});
        TestParse("httponly=", TSetCookie{.Name="httponly"});
        TestParse("expires=", TSetCookie{.Name="expires"});
        TestParse("max-age=", TSetCookie{.Name="max-age"});
        TestParse("samesite=", TSetCookie{.Name="samesite"});
        TestParse(
            "name=value; secure=xxx",
            TSetCookie{.Name="name", .Value=FromStringBuf("value"), .Secure=true},
            "name=value; Secure"
        );
        TestParse(
            "name=value; httponly=xxx",
            TSetCookie{.Name="name", .Value=FromStringBuf("value"), .HttpOnly=true},
            "name=value; HttpOnly"
        );
    }

    Y_UNIT_TEST(TestRequestCookieParsing) {
        // Simple case.
        TestEveryCookieKV("key=value", TCookieVec{{"key", "value"}});
        // Multiple key/value pairs.
        TestEveryCookieKV("key1=value1; key2=value2", TCookieVec{{"key1", "value1"}, {"key2", "value2"}});
        // Empty value.
        TestEveryCookieKV("key=; otherkey=1234", TCookieVec{{"key", ""}, {"otherkey", "1234"}});
        // Special characters (including equals signs) in value.
        TestEveryCookieKV("key=; a2=s=(./&t=:&u=a#$; a3=+~",
            TCookieVec{{"key", ""}, {"a2", "s=(./&t=:&u=a#$"}, {"a3", "+~"}});
        // Quoted value.
        TestEveryCookieKV("key=\"abcdef\"; otherkey=1234", TCookieVec{{"key", "\"abcdef\""}, {"otherkey", "1234"}});
    }
}
