//
// Copyright (c) 2019 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/url
//

// Test that header file is self-contained.
#include <boost/url/params_ref.hpp>

#include <boost/url/parse.hpp>
#include <boost/url/parse_query.hpp>
#include <boost/url/url.hpp>
#include <boost/core/detail/static_assert.hpp>
#include <boost/core/ignore_unused.hpp>

#include "test_suite.hpp"

#include <iterator>

#ifdef assert
#undef assert
#endif
#define assert BOOST_TEST

namespace boost {
namespace urls {

BOOST_CORE_STATIC_ASSERT(
    ! std::is_default_constructible<
        params_ref>::value);

BOOST_CORE_STATIC_ASSERT(
    std::is_copy_constructible<
        params_ref>::value);

BOOST_CORE_STATIC_ASSERT(
    std::is_copy_assignable<
        params_ref>::value);

//------------------------------------------------

#define BIGSTR "123456789012345678901234567890"

struct params_ref_test
{
    static
    bool
    is_equal(
        param_view const& p0,
        param_view const& p1)
    {
        return
            p0.key == p1.key &&
            p0.has_value == p1.has_value &&
            (! p0.has_value ||
                p0.value == p1.value);
    }

    // check the sequence
    static
    void
    check(
        params_view const& p,
        std::initializer_list<
            param_pct_view> init)
    {
        if(! BOOST_TEST_EQ(
            p.size(), init.size()))
            return;
        auto it0 = p.begin();
        auto it1 = init.begin();
        auto const end = init.end();
        while(it1 != end)
        {
            BOOST_TEST(is_equal(*it0, *it1));
            auto tmp = it0++;
            BOOST_TEST_EQ(++tmp, it0);
            ++it1;
        }
        // reverse
        if(init.size() > 0)
        {
            it0 = p.end();
            it1 = init.end();
            do
            {
                auto tmp = it0--;
                BOOST_TEST_EQ(--tmp, it0);
                --it1;
                BOOST_TEST(is_equal(*it0, *it1));
            }
            while(it1 != init.begin());
        }
    }

    static
    void
    check(
        url_view const& u,
        std::initializer_list<
            param_pct_view> init)
    {
        BOOST_TEST_EQ(u.params().size(), init.size());
        check(u.params(), init);
    }

    static
    void
    check(
        system::result<url_view> const& r,
        std::initializer_list<
            param_pct_view> init)
    {
        if(! BOOST_TEST(r.has_value()))
            return;
        check(*r, init);
    }

    // check whether modifying s0 via
    // f produces a URL with the query
    // string s1 and params init
    static
    void
    check(
        void(*f)(params_ref),
        core::string_view s0,
        core::string_view s1,
        std::initializer_list<
            param_pct_view> init)
    {
        url u;
        {
            auto rv = parse_uri_reference(s0);
            if(! BOOST_TEST(rv.has_value()))
                return;
            u = *rv;
        }
        params_ref ps(u.params());
        f(ps);
        BOOST_TEST_EQ(u.encoded_query(), s1);
        if(! BOOST_TEST_EQ(
                ps.size(), init.size()))
            return;
        check(ps, init);
        {
            auto rv = parse_query(s1);
            if(! BOOST_TEST(rv.has_value()))
                return;
            check(*rv, init);
        }
    }

    // check whether modifying s0 via
    // f1 and f2 produces a URL with
    // the query string s1 and params init
    static
    void
    check(
        void(*f1)(params_ref),
        void(*f2)(params_ref),
        core::string_view s0,
        core::string_view s1,
        std::initializer_list<
            param_pct_view> init)
    {
        check(f1, s0, s1, init);
        check(f2, s0, s1, init);
    }

    //--------------------------------------------

    static
    void
    assign(
        params_ref& p,
        std::initializer_list<
            param_view> init)
    {
        p.assign(
            init.begin(),
            init.end());
    };

    static
    auto
    append(
        params_ref& p,
        std::initializer_list<
            param_view> init) ->
        params_ref::iterator
    {
        return p.append(
            init.begin(),
            init.end());
    };

    static
    auto
    insert(
        params_ref& p,
        params_ref::iterator before,
        std::initializer_list<
            param_view> init) ->
        params_ref::iterator
    {
        return p.insert(
            before,
            init.begin(),
            init.end());
    };

    static
    auto
    replace(
        params_ref& p,
        params_ref::iterator from,
        params_ref::iterator to,
        std::initializer_list<
            param_view> init) ->
        params_ref::iterator
    {
        return p.replace(
            from,
            to,
            init.begin(),
            init.end());
    };

    //--------------------------------------------

    static
    void
    testSpecial()
    {
        // params_ref(params_ref)
        {
            url u("?key=value");
            params_ref p0 = u.params();
            BOOST_TEST_EQ(&p0.url(), &u);
            params_ref p1(p0);
            BOOST_TEST_EQ(&p0.url(), &p1.url());
            check(p0, { {"key","value"} });
            check(p1, { {"key","value"} });
        }

        // params_ref(params_ref, encoding_opts)
        {
            encoding_opts opt;
            opt.space_as_plus = true;
            url u("?key=my+value");
            params_ref p0 = u.params(opt);
            BOOST_TEST_EQ(&p0.url(), &u);
            opt.space_as_plus = false;
            params_ref p1(p0, opt);
            BOOST_TEST_EQ(&p0.url(), &p1.url());
            check(p0, { {"key","my value"} });
            check(p1, { {"key","my+value"} });
        }

        // operator=(params_ref)
        {
            url u0( "?key=value" );
            url u1;
            params_ref p0 = u0.params();
            params_ref p1 = u1.params();
            p1 = p0;
            BOOST_TEST_NE(&p0.url(), &p1.url());
            check(p0, { {"key","value"} });
            check(p1, { {"key","value"} });
        }

        // operator=(initializer_list)
        {
            url u;
            u.params() = {
                {"first", "John"},
                {"last", "Doe"}};
            check(u.params(),{
                {"first", "John"},
                {"last", "Doe"}});
        }

        // operator params_view
        {
            url u;
            params_view qp = u.params();
            BOOST_TEST_EQ(
                qp.buffer().data(), u.buffer().data());
        }
    }

    static
    void
    testObservers()
    {
        // url()
        {
            url u;
            params_ref qp = u.params();
            BOOST_TEST_EQ(&qp.url(), &u);
        }

        // begin()/end()
        {
            // empty
            check(
                boost::urls::parse_relative_ref(""),
                {});

            // empty with fragment
            check(
                boost::urls::parse_relative_ref("#"),
                {});

            // one empty element
            check(
                boost::urls::parse_relative_ref("?"),
                { { "", no_value } });

            // one empty element with fragment
            check(
                boost::urls::parse_relative_ref("?#"),
                { { "", no_value } });

            // one element with empty value
            check(
                boost::urls::parse_relative_ref("?="),
                { { "", "" } });

            // one element with empty value with fragment
            // issue #864
            check(
                boost::urls::parse_relative_ref("?=#"),
                { { "", "" } });

            // one param
            check(
                boost::urls::parse_relative_ref("?key=value"),
                { { "key", "value" } });

            // two params
            check(
                boost::urls::parse_relative_ref("?key1=value1&key2=value2"),
                { { "key1", "value1" }, { "key2", "value2" } });
        }
    }

    static
    void
    testModifiers()
    {
        // clear()
        {
            auto const f = [](params_ref qp)
            {
                qp.clear();
            };
            check(f, "", "", {});
            check(f, "?", "", {});
            check(f, "?first=John&last=Doe", "", {});
        }

        // assign(initializer_list)
        // assign(FwdIt first, FwdIt)
        {
            auto const f = [](params_ref qp)
            {
                qp.assign({ {"first", nullptr}, {"last",""}, {"full", "John Doe"} });
            };
            auto const g = [](params_ref qp)
            {
                assign(qp, { {"first",nullptr}, {"last",""}, {"full", "John Doe"} });
            };
            check(f, g, "", "first&last=&full=John+Doe",
                { {"first",no_value}, {"last",""}, {"full","John Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                qp.assign({ {BIGSTR, nullptr}, {"last",BIGSTR}, {BIGSTR, BIGSTR} });
            };
            auto const g = [](params_ref qp)
            {
                assign(qp, { {BIGSTR, nullptr}, {"last",BIGSTR}, {BIGSTR, BIGSTR} });
            };
            check(f, g, "", 
                BIGSTR "&last=" BIGSTR "&" BIGSTR "=" BIGSTR,
                { {BIGSTR, nullptr}, {"last",BIGSTR}, {BIGSTR, BIGSTR} });
        }

        // append(param_view)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.append({"=","&#"});
                BOOST_TEST(is_equal(*it, {"=","&#"}));
            };
            check(f, "?", "&%3D=%26%23", { {"",no_value}, {"=","&#"} });
            check(f, "?key=value", "key=value&%3D=%26%23", { {"key","value"}, {"=","&#"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.append({"middle",(*qp.begin()).value});
                BOOST_TEST(is_equal(*it, {"middle","John"}));
            };
            check(f, "?first=John&last=Doe", "first=John&last=Doe&middle=John",
                { {"first","John"}, {"last","Doe"}, {"middle","John"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.append({BIGSTR,BIGSTR});
                BOOST_TEST(is_equal(*it, {BIGSTR,BIGSTR}));
            };
            check(f, "?", "&" BIGSTR "=" BIGSTR, { {"",no_value}, {BIGSTR,BIGSTR} });
            check(f, "?key=value", "key=value&" BIGSTR "=" BIGSTR,
                { {"key","value"}, {BIGSTR,BIGSTR} });
        }

        // append(initializer_list)
        // append(FwdIt first, FwdIt)
        {
            auto const f = [](params_ref qp)
            {
                qp.append({ {"first", nullptr}, {"last",""}, {"full", "John Doe"} });
            };
            auto const g = [](params_ref qp)
            {
                append(qp, { {"first",nullptr}, {"last",""}, {"full", "John Doe"} });
            };
            check(f, g, "", "first&last=&full=John+Doe",
                { {"first",no_value}, {"last",""}, {"full","John Doe"} });
            check(f, g, "?", "&first&last=&full=John+Doe",
                { {"",no_value}, {"first",no_value}, {"last",""}, {"full","John Doe"} });
            check(f, g, "?key=value", "key=value&first&last=&full=John+Doe",
                { {"key","value"}, {"first",no_value}, {"last",""}, {"full","John Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                qp.append({ {BIGSTR, nullptr}, {"last",BIGSTR}, {BIGSTR, BIGSTR} });
            };
            auto const g = [](params_ref qp)
            {
                append(qp, { {BIGSTR, nullptr}, {"last",BIGSTR}, {BIGSTR, BIGSTR} });
            };
            check(f, g, "", BIGSTR "&last=" BIGSTR "&" BIGSTR "=" BIGSTR,
                { {BIGSTR,no_value}, {"last",BIGSTR}, {BIGSTR,BIGSTR} });
        }

        // insert(iterator, param_view)
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.insert(std::next(qp.begin(),0),
                    {"middle",(*qp.begin()).value});
                BOOST_TEST(is_equal(*it, {"middle","John"}));
            };
            check(f, "?first=John&last=Doe", "middle=John&first=John&last=Doe",
                { {"middle","John"}, {"first","John"}, {"last","Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.insert(std::next(qp.begin(),1),
                    {"middle",(*qp.begin()).value});
                BOOST_TEST(is_equal(*it, {"middle","John"}));
            };
            check(f, "?first=John&last=Doe", "first=John&middle=John&last=Doe",
                { {"first","John"}, {"middle","John"}, {"last","Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.insert(std::next(qp.begin(),2),
                    {"middle",(*qp.begin()).value});
                BOOST_TEST(is_equal(*it, {"middle","John"}));
            };
            check(f, "?first=John&last=Doe", "first=John&last=Doe&middle=John",
                { {"first","John"}, {"last","Doe"}, {"middle","John"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),0),
                    {"middle",BIGSTR});
                BOOST_TEST(is_equal(*it, {"middle",BIGSTR}));
            };
            check(f, "?first=John&last=Doe", "middle=" BIGSTR "&first=John&last=Doe",
                { {"middle",BIGSTR}, {"first","John"}, {"last","Doe"} });
        }

        // insert(iterator, initializer_list)
        // insert(iterator, FwdIt first, FwdIt)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),0),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, qp.begin());
            };
            auto const g = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),0),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, qp.begin());
            };
            check(f, g, "?k1&k2=&k3=v3",
                "first=John&last=Doe&k1&k2=&k3=v3",
                { {"first","John"}, {"last","Doe"}, {"k1",no_value}, {"k2",""}, {"k3","v3"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),1),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, std::next(qp.begin(), 1));
            };
            auto const g = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),1),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, std::next(qp.begin(), 1));
            };
            check(f, g, "?k1&k2=&k3=v3",
                "k1&first=John&last=Doe&k2=&k3=v3",
                { {"k1",no_value}, {"first","John"}, {"last","Doe"}, {"k2",""}, {"k3","v3"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),2),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, std::next(qp.begin(), 2));
            };
            auto const g = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),2),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, std::next(qp.begin(), 2));
            };
            check(f, g, "?k1&k2=&k3=v3",
                "k1&k2=&first=John&last=Doe&k3=v3",
                { {"k1",no_value}, {"k2",""}, {"first","John"}, {"last","Doe"}, {"k3","v3"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),3),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, std::next(qp.begin(), 3));
            };
            auto const g = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),3),
                    { {"first","John"}, {"last","Doe"} });
                BOOST_TEST(is_equal(*it, {"first","John"}));
                BOOST_TEST_EQ(it, std::next(qp.begin(), 3));
            };
            check(f, g, "?k1&k2=&k3=v3",
                "k1&k2=&k3=v3&first=John&last=Doe",
                { {"k1",no_value}, {"k2",""}, {"k3","v3"}, {"first","John"}, {"last","Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),0),
                    { {"first",BIGSTR}, {BIGSTR,"Doe"} });
                BOOST_TEST(is_equal(*it, {"first",BIGSTR}));
                BOOST_TEST_EQ(it, qp.begin());
            };
            auto const g = [](params_ref qp)
            {
                auto it = qp.insert(std::next(qp.begin(),0),
                    { {"first",BIGSTR}, {BIGSTR,"Doe"} });
                BOOST_TEST(is_equal(*it, {"first",BIGSTR}));
                BOOST_TEST_EQ(it, qp.begin());
            };
            check(f, g, "?k1&k2=&k3=v3",
                "first=" BIGSTR "&" BIGSTR "=Doe&k1&k2=&k3=v3",
                { {"first",BIGSTR}, {BIGSTR, "Doe"}, {"k1",no_value}, {"k2",""}, {"k3","v3"} });
        }

        // erase(iterator)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.erase(std::next(qp.begin(),0));
                BOOST_TEST(is_equal(*it, {"last","Doe"}));
            };
            check(f, "?first=John&last=Doe", "last=Doe", { {"last","Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.erase(std::next(qp.begin(),1));
                BOOST_TEST_EQ(it, qp.end());
            };
            check(f, "?first=John&last=Doe", "first=John", { {"first","John"} });
        }

        // erase(iterator, iterator)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.erase(
                    std::next(qp.begin(),0),
                    std::next(qp.begin(),2));
                BOOST_TEST(is_equal(*it, {"k2","key"}));
            };
            check(f, "?k0&k1=&k2=key", "k2=key", { {"k2","key"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.erase(
                    std::next(qp.begin(),1),
                    std::next(qp.begin(),3));
                BOOST_TEST_EQ(it, qp.end());
            };
            check(f, "?k0&k1=&k2=key", "k0", { {"k0",no_value} });
        }

        // erase(pct_string_view, ignore_case_param)
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto n = qp.erase((*qp.find_last("k1", ignore_case)).value);
                BOOST_TEST_EQ(n, 2);
            };
            check(f, "?k0&k1=&k2=key&k1=value&k3=4&K1=k1", "k0&k2=key&k3=4&K1=k1",
                { {"k0",no_value}, {"k2","key"}, {"k3","4"}, {"K1","k1"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto n = qp.erase("k1", ignore_case);
                BOOST_TEST_EQ(n, 3);
            };
            check(f, "?k0&k1=&k2=key&k1=value&k3=4&K1=5", "k0&k2=key&k3=4",
                { {"k0",no_value}, {"k2","key"}, {"k3","4"} });
        }

        // replace(iterator, param_view)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.replace(std::next(qp.begin(),0),
                    {"=","&#"});
                BOOST_TEST(is_equal(*it, {"=","&#"}));
            };
            check(f, "?first=John&last=Doe", "%3D=%26%23&last=Doe",
                { {"=","&#"}, {"last","Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.replace(std::next(qp.begin(),1),
                    {"=","&#"});
                BOOST_TEST(is_equal(*it, {"=","&#"}));
            };
            check(f, "?first=John&last=Doe", "first=John&%3D=%26%23",
                { {"first","John"}, {"=","&#"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.replace(std::next(qp.begin(),0),
                    *std::next(qp.begin(),1));
                BOOST_TEST(is_equal(*it, {"last","Doe"}));
            };
            check(f, "?first=John&last=Doe", "last=Doe&last=Doe",
                { {"last","Doe"}, {"last","Doe"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.replace(std::next(qp.begin(),1),
                    *std::next(qp.begin(),0));
                BOOST_TEST(is_equal(*it, {"first","John"}));
            };
            check(f, "?first=John&last=Doe", "first=John&first=John",
                { {"first","John"}, {"first","John"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.replace(std::next(qp.begin(),0),
                    {"=",BIGSTR});
                BOOST_TEST(is_equal(*it, {"=",BIGSTR}));
            };
            check(f, "?first=John&last=Doe", "%3D=" BIGSTR "&last=Doe",
                { {"=",BIGSTR}, {"last","Doe"} });
        }

        // replace(iterator, iterator, initializer_list)
        // replace(iterator, iterator, FwdIt, FwdIt)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.replace(
                    std::next(qp.begin(),0),
                    std::next(qp.begin(),2),
                    {{"=","&#"}});
                BOOST_TEST(is_equal(*it, {"=","&#"}));
            };
            auto const g = [](params_ref qp)
            {
                auto it = replace(qp,
                    std::next(qp.begin(),0),
                    std::next(qp.begin(),2),
                    {{"=","&#"}});
                BOOST_TEST(is_equal(*it, {"=","&#"}));
            };
            check(f, g, "?k0&k1=&k2=key", "%3D=%26%23&k2=key",
                { {"=","&#"}, {"k2","key"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.replace(
                    std::next(qp.begin(),0),
                    std::next(qp.begin(),2),
                    {{"=",BIGSTR}});
                BOOST_TEST(is_equal(*it, {"=",BIGSTR}));
            };
            auto const g = [](params_ref qp)
            {
                auto it = replace(qp,
                    std::next(qp.begin(),0),
                    std::next(qp.begin(),2),
                    {{"=",BIGSTR}});
                BOOST_TEST(is_equal(*it, {"=",BIGSTR}));
            };
            check(f, g, "?k0&k1=&k2=key", "%3D=" BIGSTR "&k2=key",
                { {"=",BIGSTR}, {"k2","key"} });
        }

        // unset(iterator)
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.unset(std::next(qp.begin(),0));
                BOOST_TEST(is_equal(*it, {"k0", no_value}));
            };
            check(f, "?k0&k1=&k2=key", "k0&k1=&k2=key",
                { {"k0",no_value}, {"k1",""}, {"k2","key"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.unset(std::next(qp.begin(),1));
                BOOST_TEST(is_equal(*it, {"k1", no_value}));
            };
            check(f, "?k0&k1=&k2=key", "k0&k1&k2=key",
                { {"k0",no_value}, {"k1",no_value}, {"k2","key"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.unset(std::next(qp.begin(),2));
                BOOST_TEST(is_equal(*it, {"k2", no_value}));
            };
            check(f, "?k0&k1=&k2=key", "k0&k1=&k2",
                { {"k0",no_value}, {"k1",""}, {"k2",no_value} });
        }

        // set(iterator, pct_string_view)
        {
            auto const f = [](params_ref qp)
            {
                // self-intersect
                auto it = qp.set(std::next(qp.begin(),0),
                    (*qp.find("k2")).value);
                BOOST_TEST(is_equal(*it, {"k0", "key"}));
            };
            check(f, "?k0&k1=&k2=key", "k0=key&k1=&k2=key",
                { {"k0","key"}, {"k1",""}, {"k2","key"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.set(std::next(qp.begin(),1), "&#");
                BOOST_TEST(is_equal(*it, {"k1", "&#"}));
            };
            check(f, "?k0&k1=&k2=key", "k0&k1=%26%23&k2=key",
                { {"k0",no_value}, {"k1","&#"}, {"k2","key"} });
        }
        {
            auto const f = [](params_ref qp)
            {
                auto it = qp.set(std::next(qp.begin(),1), BIGSTR);
                BOOST_TEST(is_equal(*it, {"k1", BIGSTR}));
            };
            check(f, "?k0&k1=&k2=key", "k0&k1=" BIGSTR "&k2=key",
                { {"k0",no_value}, {"k1",BIGSTR}, {"k2","key"} });
        }
    }

    static
    void
    testJavadocs()
    {
        // url()
        {
        url u( "?key=value" );

        assert( &u.params().url() == &u );
        }

        // assign(intializer_list)
        {
        url u;

        u.params().assign( {{ "first", "John" }, { "last", "Doe" }} );
        }

        // append(param_view)
        {
        url u;

        u.params().append( { "first", "John" } );
        }

        // append(initializer_list)
        {
        url u;

        u.params().append({ { "first", "John" }, { "last", "Doe" } });
        }

        // erase(iterator)
        {
        url u( "?first=John&last=Doe" );

        params_ref::iterator it = u.params().erase( u.params().begin() );

        assert( u.encoded_query() == "last=Doe" );

        ignore_unused(it);
        }

        // replace(iterator, param_view)
        {
        url u( "?first=John&last=Doe" );

        u.params().replace( u.params().begin(), { "title", "Mr" });

        assert( u.encoded_query() == "title=Mr&last=Doe" );
        }

        // unset(iterator)
        {
        url u( "?first=John&last=Doe" );

        u.params().unset( u.params().begin() );

        assert( u.encoded_query() == "first&last=Doe" );
        }

        // set(iterator, value)
        {
        url u( "?id=42&id=69" );

        u.params().set( u.params().begin(), "none" );

        assert( u.encoded_query() == "id=none&id=69" );
        }

        // set(core::string_view, core::string_view)
        {
        url u( "?id=42&id=69" );

        u.params().set( "id", "none" );

        assert( u.params().count( "id" ) == 1 );
        }
    }

    static
    void
    testSpaceAsPlus()
    {
        // issue #903
        {
            // "=?" in key/values
            {
                // In the general case, normalized URLs
                // always decode unreserved chars and encode
                // reserved chars.
                // However, normalizing the URL query should
                // maintain the decoded and encoded "&=+"
                // because they have different meanings
                // in a query.
                // This isn't optional either because
                // normalization can only mitigate false
                // negatives, but it should eliminate
                // false positives.
                // Making it optional would be allowing
                // a false positive because there's
                // at least one very relevant schema (HTTP)
                // where decoded/encoded "&=+" has different
                // meanings and represent different resources.
                urls::url u("https://a/a");
                params_ref params = u.params();
                params.append({"&=?", "&=?"});
                auto it = params.begin();
                const auto& param = *it;
                BOOST_TEST_EQ(param.key, "&=?");
                BOOST_TEST_EQ(param.value, "&=?");
                BOOST_TEST_EQ(u.buffer(), "https://a/a?%26%3D?=%26=?");
                u.normalize_query();
                BOOST_TEST_EQ(u.buffer(), "https://a/a?%26%3D?=%26=?");
                u.normalize();
                BOOST_TEST_EQ(u.buffer(), "https://a/a?%26%3D?=%26=?");
            }

            // opts.space_as_plus = true
            {
                // The params_ref object represents the decoded
                // query parameters, so appending "+" represents
                // a value that should be decoded as "+" (%2B)
                // and not an encoded "+" that would be decoded
                // as space.
                urls::url u("https://a/a");
                encoding_opts opts;
                opts.space_as_plus = true;
                params_ref params = u.params(opts);
                params.append({"a+b c", "d+e f"});
                auto it = params.begin();
                const auto& param = *it;
                BOOST_TEST_EQ(param.key, "a+b c");
                BOOST_TEST_EQ(param.value, "d+e f");
                BOOST_TEST_EQ(u.buffer(), "https://a/a?a%2Bb+c=d%2Be+f");
                u.normalize_query();
                BOOST_TEST_EQ(u.buffer(), "https://a/a?a%2Bb+c=d%2Be+f");
            }

            // opts.space_as_plus = false
            {
                // The params_ref object represents the decoded
                // query parameters without any special treatment
                // for "+" and space. "+" can remain as is
                // and space is represented as "%20".
                urls::url u("https://a/a");
                encoding_opts opts;
                opts.space_as_plus = false;
                params_ref params = u.params(opts);
                params.append({"a+b c", "d+e f"});
                auto it = params.begin();
                const auto& param = *it;
                BOOST_TEST_EQ(param.key, "a+b c");
                BOOST_TEST_EQ(param.value, "d+e f");
                BOOST_TEST_EQ(u.buffer(), "https://a/a?a+b%20c=d+e%20f");
                u.normalize_query();
                BOOST_TEST_EQ(u.buffer(), "https://a/a?a+b%20c=d+e%20f");
            }

            // comparisons
            {
                // We should not consider two URLs equivalent
                // if the query differs in the way "&=+" is encoded.
                // u1: no space as plus
                url u1("https://a/a?%26%3D?=%26=?&a+b%20c=d+e%20f");
                // u1e: no space as plus, non-separators encoded
                url u1e("https://a/a?%26%3D?=%26=?&%61+%62%20%63=%64+%65%20%66");
                // u2: space as plus
                url u2("https://a/a?%26%3D?=%26=?&a%2Bb+c=d%2Be+f");
                // u2e: space as plus, non-separators encoded
                url u2e("https://a/a?%26%3D?=%26=?&%61%2B%62+%63=%64%2Be+%66");
                // u3: separators decoded too early
                url u3("https://a/a?&=?=&=?&a%2Bb+c=d%2Be+f");
                BOOST_TEST_EQ(u1, u1e);
                BOOST_TEST_NE(u1, u2);
                BOOST_TEST_EQ(u2, u2e);
                BOOST_TEST_NE(u2, u3);
                BOOST_TEST_NE(u1, u3);

                // queries that differ by size
                url u4("https://a/a?a+b%20c=d+e%20f");
                url u4longer("https://a/a?%61+%62%20%63=%64+%65%20%66g");
                BOOST_TEST_NE(u4, u4longer);
                BOOST_TEST_NE(u4longer, u4);
            }

            // append other types of any_param_range
            {
                url u("https://a/a");
                params_ref params = u.params();
                params.append({"a+b c", "d+e f"});
                params.append({{"a+b c", "d+e f"}, {"a+b c", "d+e f"}});

                // include all other forms of any_param_range

                BOOST_TEST_EQ(params.size(), 3);
                BOOST_TEST_EQ(u.buffer(),
                    "https://a/a?a%2Bb+c=d%2Be+f&a%2Bb+c=d%2Be+f&a%2Bb+c=d%2Be+f");
            }

            // when setting the encoded query, %2B should not be encoded
            {
                url u("https://a/a");
                u.set_encoded_query("a+b=a%2Bb");
                BOOST_TEST_EQ(u.buffer(), "https://a/a?a+b=a%2Bb");
            }

            // when setting the decoded query, no space as plus is assumed
            {
                url u("https://a/a");
                u.set_encoded_query("a+b=a%2Bb");
                BOOST_TEST_EQ(u.buffer(), "https://a/a?a+b=a%2Bb");
            }
        }
    }

    static
    void
    testAll()
    {
        testSpecial();
        testObservers();
        testModifiers();
        testJavadocs();
        testSpaceAsPlus();
    }

    void
    run()
    {
        testAll();
    }
};

TEST_SUITE(
    params_ref_test,
    "boost.url.params_ref");

} // urls
} // boost
