#include "test.h"

#define TEST(name) TEST_CASE("cookie-jar: " name, "[cookie_jar]")

auto now     = panda::date::Date::now();
auto past    = now - 3600;
auto ancient = now - 3600 * 5;
auto future  = now + 3600;

TEST("add cookie") {
    CookieJarSP jar(new CookieJar());
    auto& dc = jar->domain_cookies;
    URISP origin(new URI("https://www.perl.org/my-path"));

    Response::Cookie coo("v");
    coo.domain("crazypanda.ru");
    coo.path("/p");

    SECTION("no domain -> get it from origin") {
        coo.domain("");
        coo.path("/p");
        jar->add("k", coo, origin);
        CHECK(dc.size() == 1);
        CHECK(dc[".www.perl.org"][0].host_only() == true);
    }

    SECTION("session cookie is added") {
        jar->add("k", coo, origin);
        CHECK(dc.size() == 1);
        CHECK(dc.count(".crazypanda.ru") == 1);
        CHECK(dc[".crazypanda.ru"][0].name() == "k");
        Response::Cookie &coo = dc[".crazypanda.ru"][0]; /* downcast */
        CHECK(coo.to_string("k") == coo.to_string("k"));
    }

    SECTION("non-expired cookie is added") {
        coo.expires(future);
        jar->add("k", coo, origin, now);
        CHECK(dc.size() == 1);
    }

    SECTION("expired cookie isn't added") {
        coo.expires(past);
        jar->add("k", coo, origin, now);
        CHECK(dc.size() == 0);
    }

    SECTION("2 cookies on same domain with differen paths are added") {
        jar->add("k", coo, origin);
        coo.path("/p2");
        jar->add("k", coo, origin);
        CHECK(dc.size() == 1);
        auto& cookies = dc[".crazypanda.ru"];
        CHECK(cookies.size() == 2);
    }

    SECTION("2 cookies on different domains are addred") {
        jar->add("k", coo, origin);
        coo.domain("example.org");
        jar->add("k", coo, origin);
        CHECK(dc.size() == 2);
    }

    SECTION("updated") {
        jar->add("k", coo, origin);
        coo.value("v2");
        jar->add("k", coo, origin);
        auto& cookies = dc[".crazypanda.ru"];
        CHECK(cookies.size() == 1);
        CHECK(cookies[0].value() == "v2");
    }

    SECTION("remove cookie") {
        jar->add("k", coo, origin);
        coo.expires(past);
        jar->add("k", coo, origin, now);
        CHECK(dc.size() == 0);
    }

    SECTION("remove cookie") {
        coo.path("");
        jar->add("k", coo, origin);
        jar->add("k", coo, origin, past);
        REQUIRE(dc.size() == 1);
        auto& cookies = dc[".crazypanda.ru"];
        CHECK(cookies.at(0).path() == "/my-path");
    }
}

TEST("remove cookies") {
    CookieJarSP jar(new CookieJar());
    auto& dc = jar->domain_cookies;
    URISP origin(new URI("https://www.perl.org/my-path"));

    Response::Cookie coo("v");
    coo.domain("crazypanda.ru");
    coo.path("/p");
    jar->add("c1", coo, origin);

    Response::Cookie coo2("v");
    coo.domain("crazypanda.ru");
    coo.path("/");
    jar->add("c2", coo, origin);

    Response::Cookie coo3("v");
    coo.domain("poker.crazypanda.ru");
    coo.path("/perl");
    jar->add("c3", coo, origin);

    Response::Cookie coo4("v");
    coo.domain("poker.crazypanda.ru");
    coo.path("/cpp");
    jar->add("c4", coo, origin);

    REQUIRE(!dc.empty());

    SECTION("by domain") {
        SECTION("all") {
            auto cookies = jar->remove("crazypanda.ru");
            CHECK(dc.empty());
            CHECK(cookies.size() == 4);
        }

        SECTION("patrial") {
            auto cookies = jar->remove("poker.crazypanda.ru");
            CHECK(!dc.empty());
            CHECK(cookies.size() == 2);
            CHECK(cookies[0].name() == "c3");
            CHECK(cookies[1].name() == "c4");
        }

        SECTION("none") {
            auto cookies = jar->remove("panda.ru");
            CHECK(!dc.empty());
            CHECK(cookies.size() == 0);
        }

        SECTION("strict") {
            auto cookies = jar->remove(".crazypanda.ru");
            CHECK(!dc.empty());
            CHECK(cookies.size() == 2);
            CHECK(cookies[0].name() == "c1");
            CHECK(cookies[1].name() == "c2");
        }
    }

    SECTION("by name") {
        auto cookies = jar->remove("", "c4");
        CHECK(!dc.empty());
        CHECK(cookies.size() == 1);
        CHECK(cookies[0].name() == "c4");
    }

    SECTION("by path") {
        SECTION("prefix") {
            auto cookies = jar->remove("", "", "/p");
            CHECK(!dc.empty());
            CHECK(cookies.size() == 2);
            CHECK(cookies[0].name() == "c3");
            CHECK(cookies[1].name() == "c1");
        }

        SECTION("full path") {
            auto cookies = jar->remove("", "", "/perl");
            CHECK(!dc.empty());
            CHECK(cookies.size() == 1);
            CHECK(cookies[0].name() == "c3");
        }
    }

    SECTION("all") {
        auto cookies = jar->remove();
        CHECK(dc.empty());
        CHECK(cookies.size() == 4);
    }

    SECTION("full match") {
        auto cookies = jar->remove(".poker.crazypanda.ru", "c3", "/perl");
        CHECK(!dc.empty());
        CHECK(cookies.size() == 1);
        CHECK(cookies[0].name() == "c3");
    }
}

TEST("find/match cookie") {
    CookieJarSP jar(new CookieJar());

    URISP origin(new URI("https://perl.perl.org/"));

    Response::Cookie coo1("v1");
    coo1.domain("crazypanda.ru");
    coo1.path("/p1");
    coo1.expires(now);

    Response::Cookie coo2("v2");
    coo2.domain("crazypanda.ru");
    coo2.path("/p1/p2");
    coo2.expires(now);

    Response::Cookie coo3("v3");
    coo3.domain("perl.crazypanda.ru");
    coo3.path("/pp3");
    coo3.expires(past);
    coo3.secure(true);

    jar->add("k1", coo1, origin, ancient);
    jar->add("k2", coo2, origin, ancient);
    jar->add("k3", coo3, origin, ancient);

    SECTION("prepreq"){
        auto& dc = jar->domain_cookies;
        auto cookies = &dc.at(".crazypanda.ru");
        REQUIRE(cookies->size() == 2);
        REQUIRE(cookies->at(0).name() == "k1");
        REQUIRE(cookies->at(1).name() == "k2");

        cookies = &dc.at(".perl.crazypanda.ru");
        REQUIRE(cookies->size() == 1);
        REQUIRE(cookies->at(0).name() == "k3");
    }

    SECTION("find nothing (path mismatch)") {
        auto cookies = jar->find(URISP{new URI("http://crazypanda.ru/404")}, past);
        REQUIRE(cookies.size() == 0);
    }

    SECTION("find nothing (domain mismatch)") {
        auto cookies = jar->find(URISP{new URI("http://example.org/")});
        REQUIRE(cookies.size() == 0);
    }

    SECTION("find most precise") {
        auto cookies = jar->find(URISP{new URI("http://crazypanda.ru/p1/p2")}, past);
        REQUIRE(cookies.size() == 1);
        auto& c = cookies[0];
        CHECK(c.name() == "k2");
        CHECK(c.value() == "v2");
    }

    SECTION("2 of 3 match (by path)") {
        auto cookies = jar->find(URISP{new URI("http://crazypanda.ru/p1")}, past);
        REQUIRE(cookies.size() == 2);
        CHECK(cookies[0].name() == "k2");
        CHECK(cookies[1].name() == "k1");
    }

    SECTION("2 of 3 match (by domain)") {
        auto cookies = jar->find(URISP{new URI("https://crazypanda.ru/p")}, past);
        REQUIRE(cookies.size() == 2);
        CHECK(cookies[0].name() == "k2");
        CHECK(cookies[1].name() == "k1");
    }

    SECTION("3 of 3 match (by domain)") {
        auto cookies = jar->find(URISP{new URI("https://perl.crazypanda.ru/")}, past);
        REQUIRE(cookies.size() == 3);
        CHECK(cookies[0].name() == "k2");
        CHECK(cookies[1].name() == "k3");
        CHECK(cookies[2].name() == "k1");
    }

    SECTION("2 of 3 match (by domain, and security)") {
        auto cookies = jar->find(URISP{new URI("http://perl.crazypanda.ru/")}, past);
        REQUIRE(cookies.size() == 2);
        CHECK(cookies[0].name() == "k2");
        CHECK(cookies[1].name() == "k1");
    }

    SECTION("3 of 3 match (by subdomain)") {
        auto cookies = jar->find(URISP{new URI("https://cpp.and.perl.crazypanda.ru/")}, past);
        REQUIRE(cookies.size() == 3);
        CHECK(cookies[0].name() == "k2");
        CHECK(cookies[1].name() == "k3");
        CHECK(cookies[2].name() == "k1");
    }

    SECTION("3 of 3 match by subdomain, but mismatch my date") {
        auto cookies = jar->find(URISP{new URI("https://cpp.and.perl.crazypanda.ru/")}, future);
        REQUIRE(cookies.size() == 0);
    }

    SECTION("3 of 3 match by subdomain, 2 match my date") {
        auto cookies = jar->find(URISP{new URI("https://perl.crazypanda.ru/")}, now);
        REQUIRE(cookies.size() == 2);
        CHECK(cookies[0].name() == "k2");
        CHECK(cookies[1].name() == "k1");
    }

    SECTION("same-site policy") {
        CookieJarSP jar(new CookieJar());
        REQUIRE(jar->domain_cookies.size() == 0);

        URISP origin(new URI("https://my.crazypanda.ru/"));

        Response::Cookie coo1("v1");
        coo1.domain("crazypanda.ru");
        coo1.path("/p1");
        coo1.expires(future);

        Response::Cookie coo4("v4");
        coo4.domain("crazypanda.ru");
        coo4.path("/cpp");
        coo4.expires(future);
        coo4.same_site(Response::Cookie::SameSite::Strict);

        Response::Cookie coo5("v5");
        coo5.domain("crazypanda.ru");
        coo5.path("/cpp");
        coo5.expires(future);
        coo5.same_site(Response::Cookie::SameSite::Lax);

        jar->add("k1", coo1, origin, ancient);
        jar->add("k4", coo4, origin, ancient);
        jar->add("k5", coo5, origin, ancient);

        auto& dc = jar->domain_cookies;
        REQUIRE(dc.at(".crazypanda.ru").size() == 3);

        SECTION("request matches origin") {
            auto cookies = jar->find(origin, now);
            REQUIRE(cookies.size() == 3);
        }

        SECTION("different site") {
            auto cookies = jar->find(URISP{new URI("https://public.crazypanda.ru/")}, past);
            REQUIRE(cookies.size() == 1);
            CHECK(cookies[0].name() == "k1");
        }

        SECTION("different site, lax context") {
            auto cookies = jar->find(URISP{new URI("https://public.crazypanda.ru/")}, past, true);
            REQUIRE(cookies.size() == 2);
            CHECK(cookies[0].name() == "k5");
            CHECK(cookies[1].name() == "k1");
        }

        SECTION("subdomain") {
            auto cookies = jar->find(URISP{new URI("https://static.my.crazypanda.ru/")}, past);
            REQUIRE(cookies.size() == 3);
        }
    }

    SECTION("session cookies") {
        CookieJarSP jar(new CookieJar());
        Response::Cookie coo1("v1");
        coo1.domain("crazypanda.ru");
        coo1.path("/p1");
        jar->add("k1", coo1, origin);
        auto cookies = jar->find(URISP{new URI("https://games.crazypanda.ru/")});
        REQUIRE(cookies.size() == 1);
    }

    SECTION("host-only cookies (missing domain)") {
        CookieJarSP jar(new CookieJar());
        auto origin = URISP{new URI("https://ya.ru/")};
        Response::Cookie coo1("v1");
        jar->add("k1", coo1, origin);
        auto cookies = jar->find(origin);
        REQUIRE(cookies.size() == 1);

        cookies = jar->find(URISP{new URI("https://www.ya.ru/")});
        REQUIRE(cookies.size() == 0);
    }
}

TEST("cookies collection from the request") {
    CookieJarSP jar(new CookieJar());

    URISP req_uri = new URI("http://games.crazypanda.ru/hello/world");
    auto res = Response::Builder()
            .cookie("c1", Response::Cookie("v1"))
            .cookie("c2", Response::Cookie("v2").domain("crazypanda.ru").path("/hi"))
            .cookie("c3", Response::Cookie("v3").domain("google.com"))
            .build();


    SECTION("same origin -> 2 cookies") {
        jar->collect(*res, req_uri);
        auto cookies = jar->find(URISP{new URI("http://games.crazypanda.ru")});
        REQUIRE(cookies.size() == 2);
        CHECK(cookies[0].name() == "c1");
        CHECK(cookies[1].name() == "c2");
    }

    SECTION("differnt subdomain -> 1 cookie") {
        jar->collect(*res, req_uri);
        auto cookies = jar->find(URISP{new URI("http://ww.games.crazypanda.ru")});
        REQUIRE(cookies.size() == 1);
        CHECK(cookies[0].name() == "c2");
    }

    SECTION("ignore predicate") {
        CookieJar::ignore_fn fn([](auto&, auto&){ return true; });
        jar->set_ignore(fn);
        jar->collect(*res, req_uri);
        auto cookies = jar->find(URISP{new URI("http://games.crazypanda.ru")});
        REQUIRE(cookies.size() == 0);
    }
}

TEST("cookies population to thr response") {
    CookieJarSP jar(new CookieJar());
    URISP uri(new URI("https://crazypanda.ru/"));
    Response::Cookie coo1("v1");
    jar->add("k1", coo1, uri);

    auto req = Request::Builder().uri(uri).build();
    REQUIRE(req->cookies.size() == 0);
    jar->populate(*req);
    REQUIRE(req->cookies.size() == 1);
    CHECK(req->cookies.get("k1") == "v1");
}

TEST("(de)serialization") {
    URISP origin(new URI("https://www.tut.by"));
    CookieJarSP jar(new CookieJar());

    SECTION("single cookie serialization") {
        auto& dc = jar->domain_cookies;
        Response::Cookie coo("v");
        coo.domain("tut.by");
        coo.path("/news");
        jar->add("k", coo, origin);

        REQUIRE(dc.size() == 1);
        REQUIRE(dc.count(".tut.by") == 1);
        CHECK(dc[".tut.by"][0].name() == "k");

        auto &jcoo = dc[".tut.by"][0];
        REQUIRE(jcoo.to_string() == "{\"key\":\"k\", \"value\":\"v\", \"domain\":\"tut.by\", \"path\":\"/news\"}");

        CHECK(jar->to_string(true) == "[\n{\"key\":\"k\", \"value\":\"v\", \"domain\":\"tut.by\", \"path\":\"/news\"}]");
        CHECK(jar->to_string(false) == "[\n]");

        CookieJar::DomainCookies dc2;
        REQUIRE(CookieJar::parse_cookies(jar->to_string(true), dc2) == std::error_code());
        REQUIRE(dc2[".tut.by"].size() == 1);
        CHECK(dc2[".tut.by"][0].to_string() == jcoo.to_string());
    }

    SECTION("by date filtration") {
        panda::time::tzset("Europe/Moscow");
        panda::date::Date expires(2020, 05, 18, 5);
        auto past = expires - 3600;
        auto future = expires + 3600;

        Response::Cookie coo("v");
        coo.domain("tut.by");
        coo.path("/news");
        coo.expires(expires);
        jar->add("k", coo, origin, past);

        CHECK(jar->to_string(false, past) == "[\n{\"key\":\"k\", \"value\":\"v\", \"domain\":\"tut.by\", \"path\":\"/news\", \"expires\":\"1589767200\"}]");
        CHECK(jar->to_string(false, future) == "[\n]");
    }

    SECTION("samesite & origin") {
        Response::Cookie coo("v");
        coo.domain("tut.by");
        coo.same_site(Response::Cookie::SameSite::Strict);
        jar->add("k", coo, origin);
        CHECK(jar->to_string(true) == "[\n{\"key\":\"k\", \"value\":\"v\", \"domain\":\"tut.by\", \"path\":\"/\", \"same_site\":\"S\", \"origin\":\"https://www.tut.by\"}]");
    }

    SECTION("samesite & origin") {
        Response::Cookie coo("v");
        jar->add("k", coo, origin);
        CHECK(jar->to_string(true) == "[\n{\"key\":\"k\", \"value\":\"v\", \"domain\":\"www.tut.by\", \"path\":\"/\", \"host_only\":\"1\"}]");
    }

    SECTION("parsing") {
        string data = R"DATA([
{"key":"k1", "value":"v1", "domain":"tut.by", "path":"/", "same_site":"S", "origin":"https://www.tut.by", "same_site":"S"},
{"key":"k2", "value":"v2", "domain":"ya.ru", "path":"/", "host_only":"1"}])DATA";
        CookieJarSP jar(new CookieJar(data));
        auto& dc = jar->domain_cookies;

        REQUIRE(dc[".tut.by"].size() == 1);
        auto& c1 = dc[".tut.by"][0];
        CHECK(c1.name() == "k1");
        CHECK(c1.value() == "v1");
        CHECK(c1.domain() == "tut.by");
        CHECK(c1.same_site() == Response::Cookie::SameSite::Strict);
        CHECK(c1.origin()->to_string() == "https://www.tut.by");

        REQUIRE(dc[".ya.ru"].size() == 1);
        auto& c2 = dc[".ya.ru"][0];
        CHECK(c2.name() == "k2");
        CHECK(c2.value() == "v2");
        CHECK(c2.domain() == "ya.ru");
        CHECK(c2.host_only());
    }
}