//
// 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/url.hpp>

#include <boost/url/encode.hpp>
#include <boost/url/parse.hpp>

#include "test_suite.hpp"

#include <algorithm>
#include <iomanip>
#include <sstream>

#ifdef BOOST_TEST_CSTR_EQ
#undef BOOST_TEST_CSTR_EQ
#define BOOST_TEST_CSTR_EQ(expr1,expr2) \
    BOOST_TEST_EQ( boost::urls::detail::to_sv(expr1), boost::urls::detail::to_sv(expr2) )
#endif

/*  Legend

    '#' 0x23    ':' 0x3a
    '%' 0x25    '@' 0x40
    '&' 0x26    '[' 0x5b
    '=' 0x3d    ']' 0x5d
*/

namespace boost {
namespace urls {

// Transparent equal_to
template <class T = void>
struct equal_to {
    bool operator()( const T& lhs, const T& rhs ) const
    {
        return lhs == rhs;
    }
};

template <>
struct equal_to<void> {
    template <class T1, class T2>
    bool operator()( const T1& lhs, const T2& rhs ) const
    {
        return lhs == rhs;
    }
};

struct url_test
{
    template<class Segments>
    static
    void
    equal(
        Segments const& segs,
        std::initializer_list<
            core::string_view> init)
    {
        if(! BOOST_TEST(segs.size() ==
            init.size()))
            return;
        BOOST_TEST(std::equal(
            segs.begin(),
            segs.end(),
            init.begin(),
            equal_to<>{}));
    }

    static
    void
    equal(
        url& u,
        std::initializer_list<
            core::string_view> init)
    {
        url_view const& uv = u;
        equal(u.segments(), init);
        equal(u.encoded_segments(), init);
        equal(uv.segments(), init);
        equal(uv.encoded_segments(), init);
    }

    template<class F>
    static
    void
    modify(
        core::string_view before,
        core::string_view after,
        F&& f)
    {
        url u(before);
        f(u);
        auto s = u.buffer();
        BOOST_TEST_EQ(s, after);
    }

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

    void
    testSpecial()
    {
        // max_size()
        {
            BOOST_TEST_GT(url::max_size(), 0u);

            url u;
            BOOST_TEST_GT(u.max_size(), 0u);
        }

        // copy
        {
            url u = parse_uri_reference("x://y/z?q#f").value();
            url u2(u);
            BOOST_TEST_EQ(u2.buffer(), u.buffer());
        }
        {
            url u = parse_uri_reference("x://y/z?q#f").value();
            url u2 = parse_relative_ref("./").value();
            u2 = u;
            BOOST_TEST_EQ(u2.buffer(), u.buffer());
        }

        // move
        {
            url u = parse_uri_reference("x://y/z?q#f").value();
            url u2(std::move(u));
            BOOST_TEST(u.empty());
            BOOST_TEST_EQ(u2.buffer(), "x://y/z?q#f");
        }
        {
            url u = parse_uri_reference("x://y/z?q#f").value();
            url u2 = parse_relative_ref("./").value();
            u2 = std::move(u);
            BOOST_TEST(u.empty());
            BOOST_TEST_EQ(u2.buffer(), "x://y/z?q#f");
        }

        // url(core::string_view)
        {
            url u("http://example.com/path/to/file.txt?#");
        }
    }

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

    void
    testCapacity()
    {
        // capacity
        {
            url u;
            BOOST_TEST_EQ(u.capacity(), 0u);
            BOOST_TEST(u.empty());
        }

        // reserve
        {
            url u;
            u.reserve(0);
            BOOST_TEST_GE(u.capacity(), 0);
            BOOST_TEST_EQ(u.c_str()[0], '\0');
        }
        {
            url u;
            u.reserve(32);
            BOOST_TEST_GE(u.capacity(), 32u);
            u.reserve(16);
            BOOST_TEST_GE(u.capacity(), 16u);
            u.reserve(64);
            BOOST_TEST_GE(u.capacity(), 64u);
            u = url_view("http://example.com/path/to/file.txt?k=v");
            u.reserve(128);
            BOOST_TEST_GE(u.capacity(), 128u);
        }

        // clear
        {
            url u = parse_uri(
                "http://example.com/index.htm?q#f").value();
            BOOST_TEST_GT(u.capacity(), 0u);
            BOOST_TEST(! u.empty());
            u.clear();
            BOOST_TEST_GT(u.capacity(), 0u);
            BOOST_TEST(u.empty());
            BOOST_TEST_EQ(u.size(), 0u);
        }
    }

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

    void
    testOrigin()
    {
        auto const remove = [](
            core::string_view s1, core::string_view s2)
        {
            url u = parse_uri_reference(s1).value();
            BOOST_TEST_CSTR_EQ(u.remove_origin().buffer(), s2);
            BOOST_TEST(u.encoded_origin().empty());
            BOOST_TEST(! u.has_authority());
        };

        remove("", "");
        remove("w", "w");
        remove("w/", "w/");
        remove("/", "/");
        remove("/x", "/x");
        remove("/x/", "/x/");
        remove("/x/?#", "/x/?#");
        remove("w:", "");
        remove("w::", "%3A");
        remove("w::/:", "%3A/:");
        remove("x://y//z", "/.//z");
        remove("http://user:pass@example.com:80/path/to/file.txt",
               "/path/to/file.txt");

        {
            // issue #394
            url u( "http://www.example.com//kyle:xy" );
            u.remove_origin();
            BOOST_TEST_CSTR_EQ( u.buffer(), "/.//kyle:xy" );
        }

        {
            // issue #446
            boost::urls::url u;
            u.set_scheme("https")
                .set_host("special-api.com")
                .set_port("443")
                .set_path("/prefix/api/v1");
            u.segments().push_back("applications");
            u.segments().push_back("01");
            u.segments().push_back("devices");
            u.params().append({"since", "2022-01-01"});
            BOOST_TEST_EQ(
                u.buffer(),
                "https://special-api.com:443/prefix/api/v1/applications/01/devices?since=2022-01-01");
            auto f = [](url_view u) {
                BOOST_TEST_EQ(
                    u.buffer(),
                    "https://special-api.com/some/path");
            };
            f(url{"https://"}.set_host("special-api.com").set_path("some/path"));
        }

        {
            // issue #919
            core::string_view s = "http://[fe80::1%25eth0]/";
            url u0 = parse_uri(s).value();
            BOOST_TEST_EQ(u0.host_type(), host_type::ipv6);

            // Round-trip ipv6_address
            ipv6_address a = u0.host_ipv6_address();
            u0.set_host_ipv6(a);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Round-trip zone_id
            std::string z = u0.zone_id();
            u0.set_zone_id(z);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Round-trip encoded_zone_id
            pct_string_view ez = u0.encoded_zone_id();
            u0.set_encoded_zone_id(ez);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Round-trip host
            std::string h = u0.host();
            u0.set_host(h);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Round-trip encoded host
            pct_string_view eh = u0.encoded_host();
            u0.set_encoded_host(eh);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Round-trip host_address
            std::string ha = u0.host_address();
            u0.set_host_address(ha);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Round-trip encoded_host_address
            pct_string_view eha = u0.encoded_host_address();
            u0.set_encoded_host_address(eha);
            BOOST_TEST_CSTR_EQ(u0.buffer(), s);

            // Copy constructor
            url u1b(s);
            BOOST_TEST_EQ(u0, u1b);
        }
    }

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

    void
    testPath()
    {
        // set_path_absolute
        {
            url u;
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST(u.set_path_absolute(false));
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "");
            BOOST_TEST(u.set_path_absolute(true));
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "/");
        }
        {
            url u = parse_relative_ref("/").value();
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST(u.set_path_absolute(true));
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "/");
            BOOST_TEST(u.set_path_absolute(false));
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "");
        }
        {
            url u = parse_relative_ref("//").value();
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST(u.set_path_absolute(true));
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "///");
            BOOST_TEST(u.set_path_absolute(false));
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "//");
        }
        {
            url u = parse_relative_ref("//x/y").value();
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST(! u.set_path_absolute(false));
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "//x/y");
        }
        {
            url u = parse_uri("x:y").value();
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST(u.set_path_absolute(false));
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST(u.set_path_absolute(true));
            BOOST_TEST(u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "x:/y");
            BOOST_TEST(u.set_path_absolute(false));
            BOOST_TEST(! u.is_path_absolute());
            BOOST_TEST_EQ(u.buffer(), "x:y");
        }
        {
            url u( "//x" );
            BOOST_TEST( ! u.is_path_absolute() );
            u.set_path("");
            BOOST_TEST( ! u.is_path_absolute() );
            BOOST_TEST_EQ(u.encoded_path(), "");
            u.set_path_absolute(true);
            BOOST_TEST( u.is_path_absolute() );
            BOOST_TEST_EQ(u.encoded_path(), "/");
            u.set_path("");
            BOOST_TEST( ! u.is_path_absolute() );
            BOOST_TEST_EQ(u.encoded_path(), "");
        }
        {
            // issue 390
            url u( "/kyle:xy" );
            u.set_path_absolute( false );
            BOOST_TEST_EQ( u.buffer(), "./kyle:xy" );
        }
        {
            // issue 674
            {
                auto ok = [](core::string_view u0, core::string_view p)
                {
                    urls::url u(u0);
                     u.set_encoded_path(p);
                    BOOST_TEST_CSTR_EQ(u.buffer(), p);
                    u.set_path(p);
                    BOOST_TEST_CSTR_EQ(u.buffer(), p);
                    u.normalize();
                    BOOST_TEST_CSTR_EQ(u.buffer(), p);
                };
                ok("/", "/");
                ok("/", "");
                ok("", "/");
                ok("", "");
            }
            {
                urls::url u;
                BOOST_TEST_EQ(u.encoded_segments().size(), 0);
                u.set_path("/");
                BOOST_TEST_EQ(u.encoded_segments().size(), 0);
                u.set_path("/./");
                BOOST_TEST_EQ(u.encoded_segments().size(), 1);
                BOOST_TEST_CSTR_EQ(u.buffer(), "/./");
                u.normalize();
                BOOST_TEST_CSTR_EQ(u.buffer(), "/");
                BOOST_TEST_CSTR_EQ(u.encoded_target(), "/");
                BOOST_TEST_EQ(u.encoded_segments().size(), 0);
            }
            // path normalization does not encode "/"
            {
                core::string_view s = "/a%2Fb/";
                urls::url u(s);
                u.normalize();
                BOOST_TEST_CSTR_EQ(u.buffer(), s);
            }
        }

        // set_encoded_path
        {
            // empty
            url u = parse_uri("x://y/path/to/file.txt?q#f").value();
            u.set_encoded_path("");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "");
            BOOST_TEST_CSTR_EQ(u.buffer(), "x://y?q#f");
        }
        {
            // path-abempty
            url u = parse_uri("x://y/path/to/file.txt?q#f").value();
            u.set_encoded_path("/x");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "/x");
            BOOST_TEST_CSTR_EQ(u.buffer(), "x://y/x?q#f");
            u.set_encoded_path("x/");
            BOOST_TEST_CSTR_EQ(u.buffer(), "x://y/x/?q#f");
        }
        {
            // path-absolute
            url u = parse_relative_ref("/path/to/file.txt").value();
            u.set_encoded_path("/home/file.txt");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "/home/file.txt");
            BOOST_TEST_CSTR_EQ(u.buffer(), "/home/file.txt");
            u.set_encoded_path("//home/file.txt");
            BOOST_TEST_CSTR_EQ(u.buffer(), "/.//home/file.txt");
            equal(u, { "", "home", "file.txt" });
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "/.//home/file.txt");
            BOOST_TEST_THROWS(u.set_encoded_path("/home/%ile.txt"),
                system::system_error);
        }
        {
            // path-rootless
            url u = parse_uri("x:mailto").value();
            u.set_encoded_path("file.txt");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "file.txt");
            BOOST_TEST_CSTR_EQ(u.buffer(), "x:file.txt");
            u.set_encoded_path(":file.txt");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), ":file.txt");
            BOOST_TEST_CSTR_EQ(u.buffer(), "x::file.txt");
            // to path-absolute
            u.set_encoded_path("/file.txt");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "/file.txt");
            BOOST_TEST_CSTR_EQ(u.buffer(), "x:/file.txt");
        }
        {
            // path-noscheme
            url u = parse_relative_ref("mailto").value();
            u.set_encoded_path("file.txt");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "file.txt");
            BOOST_TEST_CSTR_EQ(u.buffer(), "file.txt");
            u.set_encoded_path(":file.txt");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "%3Afile.txt");
            u.set_encoded_path("http:index.htm");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "http%3Aindex.htm");
        }
        {
            // multiple empty segments with host
            url u = parse_uri_reference("file:///unicorn").value();
            u.set_encoded_path("//\\/");
            BOOST_TEST_CSTR_EQ(u, "file:////%5C/");
            BOOST_TEST_CSTR_EQ(u.encoded_path(), "//%5C/");
        }

        // set_encoded_path
        {
            auto const check =
            [&](core::string_view s0,
                core::string_view arg,
                core::string_view match)
            {
                url u = parse_uri_reference(s0).value();
                u.set_encoded_path(arg);
                BOOST_TEST_CSTR_EQ(
                    u.buffer(), match);
            };
            check(
                "",
                "path/to/file.txt",
                "path/to/file.txt");
            check(
                "",
                "/path/to/file.txt",
                "/path/to/file.txt");
            check(
                "",
                "//index.htm",
                "/.//index.htm");
            check(
                "http://example.com?q#f",
                "path/to/file.txt",
                "http://example.com/path/to/file.txt?q#f");
            check(
                "http://example.com?q#f",
                "/path/to/file.txt",
                "http://example.com/path/to/file.txt?q#f");
            check(
                "x",
                "http:path/to/file.",
                "http%3Apath/to/file.");
            check(
                "x:",
                "y:z/",
                "x:y:z/");
        }

        // set_path
        {
            auto const check =
            [&](core::string_view s0,
                core::string_view arg,
                core::string_view match)
            {
                url u = parse_uri_reference(s0).value();
                u.set_path(arg);
                BOOST_TEST_CSTR_EQ(u.buffer(), match);
            };
            check(
                "",
                "",
                "");
            check(
                "",
                "path/to/file.txt",
                "path/to/file.txt");
            check(
                "",
                "/path/to/file.txt",
                "/path/to/file.txt");
            check(
                "",
                "/path%2Fto%2Ffile.txt",
                "/path%252Fto%252Ffile.txt");
            check(
                "",
                "//index.htm",
                "/.//index.htm");
            check(
                "http://example.com?q#f",
                "path/to/file.txt",
                "http://example.com/path/to/file.txt?q#f");
            check(
                "http://example.com?q#f",
                "/path/to/file.txt",
                "http://example.com/path/to/file.txt?q#f");
            check(
                "x",
                "http:path/to/file.",
                "http%3Apath/to/file.");
            check(
                "x:",
                "y:z/",
                "x:y:z/");
            check(
                "x:y:z/",
                "",
                "x:");
            check(
                "x:y:z/",
                "abc",
                "x:abc");
        }

        // self-intersection
        modify(
            "?/a/b/c",
            "/a/b/c?/a/b/c",
            [](url_base& u)
            {
                u.set_encoded_path(u.encoded_query());
            });
        modify(
            "?/a/b/c",
            "/a/b/c?/a/b/c",
            [](url_base& u)
            {
                u.set_path(u.encoded_query());
            });
        modify(
            "?/a/b/c",
            "/a/b/c?/a/b/c",
            [](url_base& u)
            {
                u.set_path(u.query());
            });

        // crash
        {
            url u;
            u.set_path("");
            BOOST_TEST(u.empty());
        }
    }

    void
    testFragment()
    {
        // has_fragment
        {
            {
                url u;
                BOOST_TEST(! u.has_fragment());
            }
            {
                url u("#");
                BOOST_TEST(u.has_fragment());
            }
            {
                url u("#x");
                BOOST_TEST(u.has_fragment());
            }
        }

        // remove_fragment
        {
            {
                url u;
                u.remove_fragment();
                BOOST_TEST(! u.has_fragment());
            }
            {
                url u("#");
                u.remove_fragment();
                BOOST_TEST(! u.has_fragment());
            }
            {
                url u("#x");
                u.remove_fragment();
                BOOST_TEST(! u.has_fragment());
            }
        }

        // set_encoded_fragment
        {
            {
                url u;
                u.set_encoded_fragment("");
                BOOST_TEST(u.has_fragment());
                BOOST_TEST_CSTR_EQ(u.buffer(), "#");
                BOOST_TEST_CSTR_EQ(u.encoded_fragment(), "");
            }
            {
                url u;
                u.set_encoded_fragment("x");
                BOOST_TEST(u.has_fragment());
                BOOST_TEST_CSTR_EQ(u.buffer(), "#x");
                BOOST_TEST_CSTR_EQ(u.encoded_fragment(), "x");
            }
            {
                url u;
                u.set_encoded_fragment("%41");
                BOOST_TEST(u.has_fragment());
                BOOST_TEST_CSTR_EQ(u.buffer(), "#%41");
                BOOST_TEST_CSTR_EQ(u.encoded_fragment(), "%41");
                BOOST_TEST_CSTR_EQ(u.fragment(), "A");
            }
            {
                url u;
                BOOST_TEST_THROWS(
                    u.set_encoded_fragment("%%"),
                    std::exception);
                BOOST_TEST_THROWS(
                    u.set_encoded_fragment("%fg"),
                    std::exception);
            }
        }

        // set_fragment
        {
            auto good = [](
                core::string_view f, core::string_view h, core::string_view ef)
            {
                url u;
                u.set_fragment(f);
                BOOST_TEST(u.has_fragment());
                BOOST_TEST_CSTR_EQ(u.buffer(), h);
                BOOST_TEST_CSTR_EQ(u.encoded_fragment(), ef);
                BOOST_TEST_CSTR_EQ(u.fragment(), f);
            };

            good("", "#", "");
            good("x", "#x", "x");
            good("%41", "#%2541", "%2541");
            good("%%fg", "#%25%25fg", "%25%25fg");
            good("{}", "#%7B%7D", "%7B%7D");
        }

        // self-intersection
        modify(
            "?abracadabra",
            "?abracadabra#abracadabra",
            [](url_base& u)
            {
                u.set_encoded_fragment(
                    u.encoded_query());
            });
        modify(
            "?abracadabra",
            "?abracadabra#abracadabra",
            [](url_base& u)
            {
                u.set_fragment(
                    u.encoded_query());
            });
        modify(
            "?abracadabra",
            "?abracadabra#abracadabra",
            [](url_base& u)
            {
                u.set_fragment(
                    u.query());
            });
    }

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

    template<class F>
    static
    void
    perform(
        core::string_view s0,
        core::string_view s1,
        std::initializer_list<
            core::string_view> init,
        F const& f)
    {
        url u = parse_uri_reference(s0).value();
        f(u);
        equal(u.segments(), init);
        equal(u.encoded_segments(), init);
        BOOST_TEST_CSTR_EQ(u.buffer(), s1);
    }

    template<class F>
    static
    void
    perform(
        core::string_view s0,
        core::string_view s1,
        std::initializer_list<
            core::string_view> dec_init,
        std::initializer_list<
            core::string_view> enc_init,
        F const& f)
    {
        url u = parse_uri_reference(s0).value();
        f(u);
        equal(u.segments(), dec_init);
        equal(u.encoded_segments(), enc_init);
        BOOST_TEST_CSTR_EQ(u.buffer(), s1);
    }

    void
    testSegments()
    {
        auto const check = [](
            core::string_view s,
            std::initializer_list<
                core::string_view> init,
            bool abs)
        {
            url u =
                parse_uri_reference(
                    s).value();
            url_view const& uv = u;
            BOOST_TEST(
                u.is_path_absolute() == abs);
            BOOST_TEST(
                uv.is_path_absolute() == abs);
            equal(uv.segments(), init);
            equal(uv.encoded_segments(), init);
            equal(u.segments(), init);
            equal(u.encoded_segments(), init);
        };

        auto const abs = [&check](
            core::string_view s,
            std::initializer_list<
                core::string_view> init)
        {
            check(s, init, true);
        };

        auto const rel = [&check](
            core::string_view s,
            std::initializer_list<
                core::string_view> init)
        {
            check(s, init, false);
        };

        auto const assign = [](
            core::string_view s0,
            core::string_view s1,
            std::initializer_list<
                core::string_view> init)
        {
            url u0 = parse_uri_reference(s0).value();
            {
                url u(u0);
                u.segments() = init;
                equal(u.segments(), init);
                //equal(u.encoded_segments(), init);
                BOOST_TEST_CSTR_EQ(u.buffer(), s1);
            }
        };

        rel("", {});
        rel("./", { "" });
        rel("././", { ".", "" });
        rel("index.htm", { "index.htm" });
        rel("path/to/file.txt", { "path", "to", "file.txt" });
        rel("//example.com", {} );
        rel("x:y:z", { "y:z" });
        rel("x:y:z/", { "y:z", "" });
        rel("./y:z", { "y:z" });
        rel("./y:z/", { "y:z", "" });

        abs("/", {});
        abs("/./", { "" });
        abs("/././", { ".", "" });
        abs("//example.com/", {} );
        abs("//example.com/./", { "" } );
        abs("/index.htm", { "index.htm" });
        abs("/home/", { "home", "" });
        abs("//x//", { "", "" });
        abs("/.//", { "", "" });
        abs("//x/y", { "y" });
        abs("/././/", { ".", "", "" });
        abs("/.//", { "", "" });
        abs("x:/.//", { "", "" });

        assign( "", "./", { "" });
        assign( "/", "/./", { "" });
        assign( "//x", "//x/./", { "" });
        assign( "//x/", "//x/./", { "" });
        assign( "", "x", { "x" });
        assign( "/", "/x", { "x" });
        assign( "", "x/y/z", { "x", "y", "z" });
        assign( "/", "/x/y/z", { "x", "y", "z" });
        assign( "/", "/.", { "." });
        assign( "/", "/././", { ".", "" });
        assign( "/", "/././/", { ".", "", "" });
        assign( "//x/", "//x/.", { "." });
        assign( "//x/", "//x/././", { ".", "" });
        assign( "//x/", "//x/././/", { ".", "", "" });

        perform( "/", "/", {}, [](url& u) { u.segments().clear(); });
        perform( "/", "/", {}, [](url& u) { u.encoded_segments().clear(); });
        perform( "//x/", "//x", {}, [](url& u) { u.segments().clear(); });
        perform( "//x/", "//x", {}, [](url& u) { u.encoded_segments().clear(); });
        perform( "/x", "/x/y", { "x", "y" }, [](url& u) { u.segments().push_back("y"); });
        perform( "/x", "/x/y", { "x", "y" }, [](url& u) { u.encoded_segments().push_back("y"); });
        perform( "/x/", "/x//y", { "x", "", "y" }, [](url& u) { u.segments().push_back("y"); });
        perform( "/x/", "/x//y", { "x", "", "y" }, [](url& u) { u.encoded_segments().push_back("y"); });
        perform( "//x//", "/.//", { "", "" }, [](url& u) { u.remove_authority(); });
        perform( "x:y:z", "y%3Az", { "y:z" }, { "y%3Az" }, [](url& u) { u.remove_scheme(); });
        perform( "x:y:z/", "y%3Az/", { "y:z", "" }, { "y%3Az", "" }, [](url& u) { u.remove_scheme(); });
        perform( "./y:z", "x:y:z", { "y:z" }, [](url& u) { u.set_scheme("x"); });
        perform( "./y:z/", "x:y:z/", { "y:z", "" }, [](url& u) { u.set_scheme("x"); });
        perform( "y", "//x/y", { "y" }, [](url& u) { u.set_encoded_authority("x"); });
        perform( "//x/y", "/y", { "y" }, [](url& u) { u.remove_authority(); });
        perform( "y", "//x:1/y", { "y" }, [](url& u) { u.set_encoded_authority("x:1"); });
        perform( "/y", "//x:1/y", { "y" }, [](url& u) { u.set_encoded_authority("x:1"); });
        perform( "x:", "x:y", { "y" }, [](url& u) { u.segments().push_back("y"); });
        perform( "x:", "x:y", { "y" }, [](url& u) { u.encoded_segments().push_back("y"); });
        perform( "/.//", "x:/.//", { "", "" }, [](url& u) { u.set_scheme("x"); });

        perform( "//x/y/z", "//x/z", { "z" }, [](url& u) {
            u.segments().erase(u.segments().begin());
            });

        perform( "//x", "//x/", {}, [](url& u) {
            BOOST_TEST(u.set_path_absolute(true));
            });
        
        perform( "//x/", "//x", {}, [](url& u) {
            BOOST_TEST(u.set_path_absolute(false));
            });
        
        perform( "//x/y", "//x/y", { "y" }, [](url& u) {
            BOOST_TEST(! u.set_path_absolute(false));
            });

        perform( "//x/y", "//x/y", { "y" }, [](url& u) {
            BOOST_TEST(u.set_path_absolute(true));
            });

        perform( "x:", "x:/y", { "y" }, [](url& u) {
            BOOST_TEST(u.set_path_absolute(true));
            u.encoded_segments().push_back("y");
            });

        // issue #921: path %2F round-trip
        {
            url u("https://example.com/a/b/c/d%2Fe%2Ff/g/h");
            BOOST_TEST_CSTR_EQ(u.buffer(), "https://example.com/a/b/c/d%2Fe%2Ff/g/h");
            BOOST_TEST(u.encoded_path() == "/a/b/c/d%2Fe%2Ff/g/h");
            BOOST_TEST(u.path() == "/a/b/c/d/e/f/g/h");

            // set_encoded_path with encoded value (should round-trip correctly)
            u.set_encoded_path(u.encoded_path());
            BOOST_TEST_CSTR_EQ(u.buffer(), "https://example.com/a/b/c/d%2Fe%2Ff/g/h");
            BOOST_TEST(u.encoded_path() == "/a/b/c/d%2Fe%2Ff/g/h");
            BOOST_TEST(u.path() == "/a/b/c/d/e/f/g/h");

            // set_path with decoded value (impossible to round-trip)
            u.set_path(u.path());
            BOOST_TEST_CSTR_EQ(u.buffer(), "https://example.com/a/b/c/d/e/f/g/h");
            BOOST_TEST(u.encoded_path() == "/a/b/c/d/e/f/g/h");
            BOOST_TEST(u.path() == "/a/b/c/d/e/f/g/h");
        }
    }

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

    void
    testResolution()
    {
        auto ub = parse_uri(
            "http://a/b/c/d;p?q").value();

        auto const check = [&ub](
            core::string_view r,
            core::string_view m)
        {
            auto ur =
                parse_uri_reference(r).value();
            url u = parse_uri(
                "z://y:x@p.q:69/x/f?q#f" ).value();
            system::result<void> rv = resolve(ub, ur, u);
            if(! BOOST_TEST( rv.has_value() ))
                return;
            BOOST_TEST_CSTR_EQ(u.buffer(), m);

            // in place resolution
            url base( ub );
            rv = base.resolve( ur );
            if(! BOOST_TEST( rv.has_value() ))
                return;
            BOOST_TEST_CSTR_EQ(base.buffer(), m);
        };

        check("g:h"          , "g:h");
        check("g"            , "http://a/b/c/g");
        check("./g"          , "http://a/b/c/g");
        check("g/"           , "http://a/b/c/g/");
        check("/g"           , "http://a/g");
        check("//g"          , "http://g");
        check("//g?q#f"      , "http://g?q#f");
        check("//g/a/../a"   , "http://g/a");
        check("?y"           , "http://a/b/c/d;p?y");
        check("g?y"          , "http://a/b/c/g?y");
        check("#s"           , "http://a/b/c/d;p?q#s");
        check("g#s"          , "http://a/b/c/g#s");
        check("g?y#s"        , "http://a/b/c/g?y#s");
        check(";x"           , "http://a/b/c/;x");
        check("g;x"          , "http://a/b/c/g;x");
        check("g;x?y#s"      , "http://a/b/c/g;x?y#s");

        check(""             , "http://a/b/c/d;p?q");
        check("."            , "http://a/b/c/");
        check("./"           , "http://a/b/c/");
        check(".."           , "http://a/b/");
        check("%2E%2E"       , "http://a/b/");
        check("../"          , "http://a/b/");
        check("../g"         , "http://a/b/g");
        check("../.."        , "http://a/");
        check("../../"       , "http://a/");
        check("../../g"      , "http://a/g");

        /*  Errata 4547
            https://www.rfc-editor.org/errata/eid4547
        */
        // Original says (ignore extra ".."):
        // check("../../../g",    "http://a/g");
        // check("../../../../g", "http://a/g");
        // With Errata 4547, it should be (include unmatched ".."):
        check("../../../g",    "http://a/../g");
        check("../../../../g", "http://a/../../g");

        check("/./g"         , "http://a/g");
        check("/./g?q#f"     , "http://a/g?q#f");

        // Original says:
        // check("/../g"        , "http://a/g");
        // With Errata 4547, it should be:
        check("/../g"        , "http://a/../g");

        check("g."           , "http://a/b/c/g.");
        check(".g"           , "http://a/b/c/.g");
        check("g.."          , "http://a/b/c/g..");
        check("..g"          , "http://a/b/c/..g");
                             
        check("./../g"       , "http://a/b/g");
        check("./g/."        , "http://a/b/c/g/");
        check("g/./h"        , "http://a/b/c/g/h");
        check("g/../h"       , "http://a/b/c/h");
        check("g;x=1/./y"    , "http://a/b/c/g;x=1/y");
        check("g;x=1/../y"   , "http://a/b/c/y");
                             
        check("g?y/./x"      , "http://a/b/c/g?y/./x");
        check("g?y/../x"     , "http://a/b/c/g?y/../x");
        check("g#s/./x"      , "http://a/b/c/g#s/./x");
        check("g#s/../x"     , "http://a/b/c/g#s/../x");

        {
            url u("path/to/file.txt");
            system::result<void> r = u.resolve(url_view("g/../h"));
            BOOST_TEST(r.has_error());
            BOOST_TEST(r.error() == error::not_a_base);
        }

        // Multiple ".."
        auto const check_base = [](
            core::string_view b,
            core::string_view r,
            core::string_view e)
        {
            auto ub = parse_uri_reference(b).value();
            auto ur = parse_uri_reference(r).value();
            url u = parse_uri(
                "z://y:x@p.q:69/x/f?q#f" ).value();
            system::result<void> rv = resolve(ub, ur, u);
            if (!BOOST_TEST( rv.has_value() ))
                return;
            BOOST_TEST_CSTR_EQ(u.buffer(), e);

            // in place resolution
            url base( ub );
            rv = base.resolve( ur );
            if (!BOOST_TEST( rv.has_value() ))
                return;
            BOOST_TEST_CSTR_EQ(base.buffer(), e);
        };

        // Issue #808
        check_base("scheme:a/b/c", "../../../..", "scheme:../..");
        check_base("scheme:a/b/c", "../../../../", "scheme:../../");
        check_base("scheme:a/b/c/", "../../../..", "scheme:..");
        check_base("scheme:a/b/c/", "../../../../", "scheme:../");
        check_base("scheme:/a/b/c", "../../../..", "scheme:/../..");
        check_base("scheme:/a/b/c", "../../../../", "scheme:/../../");
        check_base("scheme:/a/b/c/", "../../../..", "scheme:/..");
        check_base("scheme:/a/b/c/", "../../../../", "scheme:/../");

        // resolve self
        {
            {
                url u("https://example.com/one/../two%2F..%2Fthree");
                url eu(u);
                system::result<void> r = u.resolve(u);
                BOOST_TEST(r.has_value());
                eu.normalize();
                BOOST_TEST_EQ(u, eu);
            }

            {
                url u("//example.com/one/../two%2F..%2Fthree");
                url u1(u);
                system::result<void> r = u1.resolve(u1);
                BOOST_TEST(r.has_error());
                BOOST_TEST(r.error() == error::not_a_base);
            }
        }

        // copying authority depends on same scheme
        {
            {
                url u("http://auth/path");
                url ref("http:path2");
                BOOST_TEST(u.resolve(ref));
                BOOST_TEST_CSTR_EQ(u, "http://auth/path2");
            }
            {
                url u("http://auth/path");
                url ref("https:path2");
                BOOST_TEST(u.resolve(ref));
                BOOST_TEST_CSTR_EQ(u, "https:path2");
            }
        }

        // issue #920
        {
            url u("https://www.example.org/path/index.html?a%20b=5%206&x%20y=34#frag");
            url ref("?asdf%20qwer=1%202%20");
            BOOST_TEST(u.resolve(ref));
            BOOST_TEST_CSTR_EQ(u.buffer(), "https://www.example.org/path/index.html?asdf%20qwer=1%202%20");
            BOOST_TEST(!u.has_fragment());
        }
    }

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

    void
    testOstream()
    {
        {
            url u = parse_uri(
                "http://example.com/index.htm?q#f").value();
            std::stringstream ss;
            ss << u;
            BOOST_TEST(ss.str() ==
                "http://example.com/index.htm?q#f");
        }
        {
            std::stringstream ss;
            ss <<
                std::setfill('*') <<
                std::left <<
                std::setw(11) <<
                parse_uri("http://x").value();
            BOOST_TEST_EQ(ss.str(), "http://x***");
        }
    }

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

    void
    testNormalize()
    {
        // normalize
        {
            auto check = [](core::string_view before,
                            core::string_view after)
            {
                url u1 = parse_uri_reference(before).value();
                url_view u2 = parse_uri_reference(after).value();
                BOOST_TEST_EQ(u1.compare(u2), 0);
                BOOST_TEST_EQ(u1, u2);
                u1.normalize();
                BOOST_TEST_EQ(u1.buffer(), after);
                std::hash<url_view> h;
                BOOST_TEST_EQ(h(u1), h(u2));
                h = std::hash<url_view>(10);
                BOOST_TEST_EQ(h(u1), h(u2));
            };

            check("HtTp://cPpAlLiAnCe.oRG/",
                  "http://cppalliance.org/");
            check("http://%2a%2b%2C%2f%3A.org/",
                  "http://%2A%2B%2C%2F%3A.org/");
            check("http://%63%70%70%61%6c%6Ci%61n%63e.org/",
                  "http://cppalliance.org/");
            check("http://%43%70%50%61%6c%6Ci%61n%43e.org/",
                  "http://cppalliance.org/");
            check("http://cppalliance.org/a/b/c/./../../g",
                  "http://cppalliance.org/a/g");
            check("http://cppalliance.org/aa/bb/cc/./../../gg",
                  "http://cppalliance.org/aa/gg");
            check("http://cppalliance.org/a/b/../../g",
                  "http://cppalliance.org/g");
            check("http://cppalliance.org/a/b/../../../g",
                  "http://cppalliance.org/../g");
            check("http://cppalliance.org/a/b/../../../../g",
                  "http://cppalliance.org/../../g");
            check("http://cppalliance.org/..",
                  "http://cppalliance.org/..");
            check("http://cppalliance.org?%61=b",
                  "http://cppalliance.org?a=b");
            // issue 396
            check("/./my:sharona",
                  "/my:sharona");
            check("/.//my:sharona",
                  "/.//my:sharona");
            check("/././/my:sharona",
                  "/.//my:sharona");
            check(".//my:sharona",
                  ".//my:sharona");
            check("././/my:sharona",
                  ".//my:sharona");
            // issue 395
            check("./my:sharona",
                  "my%3Asharona");
            check("././my:sharona",
                  "my%3Asharona");
            // issue 382
            check("./my:sha:rona",
                  "my%3Asha%3Arona");
            check("././my:sha:rona",
                  "my%3Asha%3Arona");
            // issue 391
            check("my%3Asharona",
                  "my%3Asharona");
            // issue 579
            check("https://www.boost.org/doc/../%69%6e%64%65%78%20file.html",
                  "https://www.boost.org/index%20file.html");
            // issue 646
            BOOST_TEST_NE(
                url("https://@www.boost.org/"),
                url("https://www.boost.org/"));
            BOOST_TEST_NE(
                url("https://:@www.boost.org/"),
                url("https://@www.boost.org/"));
            // issue 818
            check("HtTp://cppalliance.org/%2F",
                  "http://cppalliance.org/%2F");

        }

        // normalize path
        {
            auto check = [](core::string_view p,
                            core::string_view e) {
                // normalize
                url u1 = parse_relative_ref(p).value();
                u1.normalize_path();
                BOOST_TEST_EQ(u1.encoded_path(), e);
                url u2 = parse_relative_ref(e).value();
                BOOST_TEST_EQ(u1.compare(u2), 0);
                BOOST_TEST_EQ(u1, u2);

                // hash
                std::hash<url_view> h;
                BOOST_TEST_EQ(h(u1), h(u2));
                h = std::hash<url_view>(10);
                BOOST_TEST_EQ(h(u1), h(u2));
            };

            check("/a/b/c/./../../g", "/a/g");
            check("/aa/bb/cc/./../../gg", "/aa/gg");
            check("../a/b/c/./../../g", "../a/g");
            check("./a/b/c/./../../g", "a/g");
            check(".././a/b/c/./../../g", "../a/g");
            check("%2E%2E/./a/b/c/./../../g", "../a/g");
            check("%2e%2E/./a/b/c/./../../g", "../a/g");
            check("/a/b/../../g", "/g");
            check("/a/b/../../../g", "/../g");
            check("mid/content=5/../6", "mid/6");
            check("mid/content=5/../6/.", "mid/6/");
            check("mid/content=5/../6/..", "mid/");
            check("a/../a"       , "a");
            check("a/.."       , "");
            check("/..", "/..");
            check(".", "");
            check("..", "..");
            check("", "");
        }

        // inequality
        {
            auto check = [](core::string_view e1,
                            core::string_view e2,
                            int cmp) {
                url_view u1 = parse_uri(e1).value();
                url_view u2 = parse_uri(e2).value();
                BOOST_TEST_EQ(u1.compare(u2), cmp);
                BOOST_TEST_EQ(u2.compare(u1), -cmp);
                if (cmp != 0)
                {
                    BOOST_TEST_NE(u1, u2);
                    BOOST_TEST_EQ((u1 < u2), (cmp < 0));
                    BOOST_TEST_EQ((u1 <= u2), (cmp <= 0));
                    BOOST_TEST_EQ((u1 > u2), (cmp > 0));
                    BOOST_TEST_EQ((u1 >= u2), (cmp >= 0));
                    std::hash<url_view> h;
                    BOOST_TEST_NE(h(u1), h(u2));
                    h = std::hash<url_view>(10);
                    BOOST_TEST_NE(h(u1), h(u2));
                }
                else
                {
                    BOOST_TEST_EQ(u1, u2);
                    std::hash<url_view> h;
                    BOOST_TEST_EQ(h(u1), h(u2));
                    h = std::hash<url_view>(10);
                    BOOST_TEST_EQ(h(u1), h(u2));
                }
            };

            check("http://cppalliance.org", "https://cppalliance.org", -1);
            check("https://cppalliance.org", "httpz://cppalliance.org", -1);
            check("http://boost.org", "http://cppalliance.org", -1);
            check("http://boost.orgg", "http://boost.org", +1);
            check("http://cppalliance.org/%2E%2E/./b/b/c/./../../g", "http://cppalliance.org/../a/g", +1);
            check("http://alice@cppalliance.org", "http://bob@cppalliance.org", -1);
            check("http://alice:passwd@cppalliance.org", "http://alice:pass@cppalliance.org", 1);
            check("http://alice:pass1@cppalliance.org", "http://alice:pass2@cppalliance.org", -1);
            check("http://cppalliance.org", "http://cppalliance.org:81", -1);
            check("http://cppalliance.org:80", "http://cppalliance.org:81", -1);
            check("http://cppalliance.org?l=v", "http://cppalliance.org?k=v", 1);
            check("http://cppalliance.org?%6C=v", "http://cppalliance.org?k=v", 1);
            check("http://cppalliance.org#frag", "http://cppalliance.org#glob", -1);
            check("http://cppalliance.org#fra", "http://cppalliance.org#frag", -1);
            check("http://cppalliance.org#frag", "http://cppalliance.org#fra", 1);
            // issue 653
            check("http://httpbin.org/redirect/10", "http://httpbin.org/get", +1);
            check("http://httpbin.org/redirect/10//10", "http://httpbin.org/11/../get", +1);
            // issue 818
            check("http://cppalliance.org:00", "http://cppalliance.org:10", -1);
            check("http://cppalliance.org:10", "http://cppalliance.org:00", +1);
            check("http://cppalliance.org:10", "http://cppalliance.org:10", 0);
            check("http://cppalliance.org:10", "http://cppalliance.org:100", -1);
            check("http://cppalliance.org:100", "http://cppalliance.org:10", +1);
            check("http://cppalliance.org:100", "http://cppalliance.org:10", +1);
        }

        // path inequality
        {
            auto check = [](core::string_view e1,
                            core::string_view e2,
                            int cmp) {
                url_view u1 = parse_relative_ref(e1).value();
                url_view u2 = parse_relative_ref(e2).value();
                BOOST_TEST_EQ(u1.compare(u2), cmp);
                BOOST_TEST_EQ(u2.compare(u1), -cmp);
                std::hash<url_view> h;
                BOOST_TEST_NE(int(h(u1) == h(u2)), cmp);
                h = std::hash<url_view>(10);
                BOOST_TEST_NE(int(h(u1) == h(u2)), cmp);
            };

            check("a/g", "/../g", 1);
            check("./a/b/c/./../../g", "/a/b/../../../g", 1);
            check("%2E/a/b/c/./../../g", "/a/b/../../../g", 1);
            check("/../g", "a/g", -1);
            check("/a/b/../../../g", "./a/b/c/./../../g", -1);
            check("../g", "a/g", -1);
            check("a/b/../../../g", "./a/b/c/./../../g", -1);
            check("a/b/../../../%67", "./a/b/c/./../../g", -1);
            check("/aa/g", "/aa/gg", -1);
            check("../a/b", "..%2Fa/b", 1);
            check("../a/b", "%2E%2E%2Fa/b", 1);
            check("../a/b", "%2E%2E/a/b", 0);
        }

        // resolve preconditions
        {
            // base != ref / base.has_scheme()
            {
                url_view base = parse_uri("http://www.example.com/user/").value();
                url_view ref = parse_relative_ref("./../user/./27/../35").value();
                url dest;
                resolve(base, ref, dest);
                BOOST_TEST_CSTR_EQ(dest.buffer(), "http://www.example.com/user/35");
            }

            // base == ref
            // dest becomes `url(base).normalize_path()`
            {
                url_view base = parse_uri("http://www.example.com/user/").value();
                url dest;
                resolve(base, base, dest);
                BOOST_TEST_CSTR_EQ(dest.buffer(), "http://www.example.com/user/");
            }

            // dest == ref
            // dest becomes `url(base).normalize_path()`
            {
                url base = parse_uri("http://www.example.com/user/").value();
                resolve(base, base, base);
                BOOST_TEST_CSTR_EQ(base.buffer(), "http://www.example.com/user/");
            }
        }

        // complete string comparison
        {
            url_view u("https://user:p%61ss@www.%65xample.com:443/p%61th/to/page?k%65y=h%65llo%20world#fr%61gment");
            BOOST_TEST_EQ(u, url_view("https://user:pass@www.example.com:443/path/to/page?key=hello%20world#fragment"));
        }
    }

    void
    testSwap()
    {
        url u1( "http://a.com");
        url u2( "http://b.com");
        swap(u1, u2);
        BOOST_TEST_EQ(u1.buffer(), "http://b.com");
        BOOST_TEST_EQ(u2.buffer(), "http://a.com");

        swap(u1, u1);
        BOOST_TEST_EQ(u1.buffer(), "http://b.com");
    }

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

    void
    testNull()
    {
        url_view u("http://h%00s/pa%00th?qu%00ry#fr%00ag");

        auto stl_equal =
            [](std::string const& s0, std::string const& s1)
        {
        // only std::equal works when the string contains NUL
        if (s0.size() != s1.size())
            return false;
        return std::equal(s0.begin(), s0.end(), s1.begin());
        };

        std::string h = u.host();
        BOOST_TEST_NE(h, "h");
        BOOST_TEST_NE(h, "h\0s");
        std::string eh("h\0s");
        BOOST_TEST_NE(h, eh);
        BOOST_TEST_NOT(stl_equal(h, eh));
        eh = {"h\0s", 3};
        BOOST_TEST_EQ(h, eh);
        BOOST_TEST(stl_equal(h, eh));

        std::string p = u.path();
        BOOST_TEST_NE(p, "/pa");
        BOOST_TEST_NE(p, "/pa\0th");
        std::string ep("/pa\0th");
        BOOST_TEST_NE(p, ep);
        BOOST_TEST_NOT(stl_equal(p, ep));
        ep = {"/pa\0th", 6};
        BOOST_TEST_EQ(p, ep);
        BOOST_TEST(stl_equal(p, ep));

        std::string q = u.query();
        BOOST_TEST_NE(q, "qu");
        BOOST_TEST_NE(q, "qu\0ry");
        std::string eq("qu\0ry");
        BOOST_TEST_NE(q, eq);
        BOOST_TEST_NOT(stl_equal(q, eq));
        eq = {"qu\0ry", 5};
        BOOST_TEST_EQ(q, eq);
        BOOST_TEST(stl_equal(q, eq));

        std::string f = u.fragment();
        BOOST_TEST_NE(f, "fr");
        BOOST_TEST_NE(f, "fr\0ag");
        std::string ef("fr\0ag");
        BOOST_TEST_NE(f, ef);
        BOOST_TEST_NOT(stl_equal(f, ef));
        ef = {"fr\0ag", 5};
        BOOST_TEST_EQ(f, ef);
        BOOST_TEST(stl_equal(f, ef));
    }

    void
    testConst()
    {
        auto f1 = [](segments_view /*seg*/) {};
        auto f2 = [](params_view /*ps*/) {};
        auto f3 = [](segments_encoded_view /*seg*/) {};
        auto f4 = [](params_encoded_view /*ps*/) {};

        const url u("/foobar");
        f1(u.segments());
        f2(u.params());
        f3(u.encoded_segments());
        f4(u.encoded_params());
    }

    void
    run()
    {
        testSpecial();
        testCapacity();
        testOrigin();
        testPath();
        testFragment();
        testSegments();
        testResolution();
        testOstream();
        testNormalize();
        testSwap();
        testNull();
        testConst();
    }
};

TEST_SUITE(url_test, "boost.url.url");

} // urls
} // boost
