//! Types/fns concerning URLs (see RFC 3986)
#[forbid(deprecated_mode)];

use core::cmp::Eq;
use map::HashMap;
use io::{Reader, ReaderUtil};
use dvec::DVec;
use from_str::FromStr;
use result::{Err, Ok};
use to_str::ToStr;
use to_bytes::IterBytes;

struct Url {
    scheme: ~str,
    user: Option<UserInfo>,
    host: ~str,
    port: Option<~str>,
    path: ~str,
    query: Query,
    fragment: Option<~str>
}

type UserInfo = {
    user: ~str,
    pass: Option<~str>
};

pub type Query = ~[(~str, ~str)];

pub fn Url(scheme: ~str, user: Option<UserInfo>, host: ~str,
       port: Option<~str>, path: ~str, query: Query,
       fragment: Option<~str>) -> Url {
    Url { scheme: move scheme, user: move user, host: move host,
         port: move port, path: move path, query: move query,
         fragment: move fragment }
}

fn UserInfo(user: ~str, pass: Option<~str>) -> UserInfo {
    {user: move user, pass: move pass}
}

fn encode_inner(s: &str, full_url: bool) -> ~str {
    do io::with_str_reader(s) |rdr| {
        let mut out = ~"";

        while !rdr.eof() {
            let ch = rdr.read_byte() as char;
            match ch {
              // unreserved:
              'A' .. 'Z' |
              'a' .. 'z' |
              '0' .. '9' |
              '-' | '.' | '_' | '~' => {
                str::push_char(&mut out, ch);
              }
              _ => {
                  if full_url {
                    match ch {
                      // gen-delims:
                      ':' | '/' | '?' | '#' | '[' | ']' | '@' |

                      // sub-delims:
                      '!' | '$' | '&' | '"' | '(' | ')' | '*' |
                      '+' | ',' | ';' | '=' => {
                        str::push_char(&mut out, ch);
                      }

                      _ => out += #fmt("%%%X", ch as uint)
                    }
                } else {
                    out += #fmt("%%%X", ch as uint);
                }
              }
            }
        }

        out
    }
}

/**
 * Encodes a URI by replacing reserved characters with percent encoded
 * character sequences.
 *
 * This function is compliant with RFC 3986.
 */
pub fn encode(s: &str) -> ~str {
    encode_inner(s, true)
}

/**
 * Encodes a URI component by replacing reserved characters with percent
 * encoded character sequences.
 *
 * This function is compliant with RFC 3986.
 */

fn encode_component(s: &str) -> ~str {
    encode_inner(s, false)
}

fn decode_inner(s: &str, full_url: bool) -> ~str {
    do io::with_str_reader(s) |rdr| {
        let mut out = ~"";

        while !rdr.eof() {
            match rdr.read_char() {
              '%' => {
                let bytes = rdr.read_bytes(2u);
                let ch = uint::parse_bytes(bytes, 16u).get() as char;

                if full_url {
                    // Only decode some characters:
                    match ch {
                      // gen-delims:
                      ':' | '/' | '?' | '#' | '[' | ']' | '@' |

                      // sub-delims:
                      '!' | '$' | '&' | '"' | '(' | ')' | '*' |
                      '+' | ',' | ';' | '=' => {
                        str::push_char(&mut out, '%');
                        str::push_char(&mut out, bytes[0u] as char);
                        str::push_char(&mut out, bytes[1u] as char);
                      }

                      ch => str::push_char(&mut out, ch)
                    }
                } else {
                      str::push_char(&mut out, ch);
                }
              }
              ch => str::push_char(&mut out, ch)
            }
        }

        out
    }
}

/**
 * Decode a string encoded with percent encoding.
 *
 * This will only decode escape sequences generated by encode_uri.
 */
pub fn decode(s: &str) -> ~str {
    decode_inner(s, true)
}

/**
 * Decode a string encoded with percent encoding.
 */
pub fn decode_component(s: &str) -> ~str {
    decode_inner(s, false)
}

fn encode_plus(s: &str) -> ~str {
    do io::with_str_reader(s) |rdr| {
        let mut out = ~"";

        while !rdr.eof() {
            let ch = rdr.read_byte() as char;
            match ch {
              'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '_' | '.' | '-' => {
                str::push_char(&mut out, ch);
              }
              ' ' => str::push_char(&mut out, '+'),
              _ => out += #fmt("%%%X", ch as uint)
            }
        }

        out
    }
}

/**
 * Encode a hashmap to the 'application/x-www-form-urlencoded' media type.
 */
pub fn encode_form_urlencoded(m: HashMap<~str, @DVec<@~str>>) -> ~str {
    let mut out = ~"";
    let mut first = true;

    for m.each |key, values| {
        let key = encode_plus(key);

        for (*values).each |value| {
            if first {
                first = false;
            } else {
                str::push_char(&mut out, '&');
                first = false;
            }

            out += #fmt("%s=%s", key, encode_plus(**value));
        }
    }

    out
}

/**
 * Decode a string encoded with the 'application/x-www-form-urlencoded' media
 * type into a hashmap.
 */
pub fn decode_form_urlencoded(s: ~[u8]) ->
    map::HashMap<~str, @dvec::DVec<@~str>> {
    do io::with_bytes_reader(s) |rdr| {
        let m = HashMap();
        let mut key = ~"";
        let mut value = ~"";
        let mut parsing_key = true;

        while !rdr.eof() {
            match rdr.read_char() {
              '&' | ';' => {
                if key != ~"" && value != ~"" {
                    let values = match m.find(key) {
                      Some(values) => values,
                      None => {
                        let values = @DVec();
                        m.insert(key, values);
                        values
                      }
                    };
                    (*values).push(@value)
                }

                parsing_key = true;
                key = ~"";
                value = ~"";
              }
              '=' => parsing_key = false,
              ch => {
                let ch = match ch {
                  '%' => {
                    uint::parse_bytes(rdr.read_bytes(2u), 16u).get() as char
                  }
                  '+' => ' ',
                  ch => ch
                };

                if parsing_key {
                    str::push_char(&mut key, ch)
                } else {
                    str::push_char(&mut value, ch)
                }
              }
            }
        }

        if key != ~"" && value != ~"" {
            let values = match m.find(key) {
              Some(values) => values,
              None => {
                let values = @DVec();
                m.insert(key, values);
                values
              }
            };
            (*values).push(@value)
        }

        m
    }
}


fn split_char_first(s: &str, c: char) -> (~str, ~str) {
    let len = str::len(s);
    let mut index = len;
    let mut mat = 0;
    do io::with_str_reader(s) |rdr| {
        let mut ch : char;
        while !rdr.eof() {
            ch = rdr.read_byte() as char;
            if ch == c {
                // found a match, adjust markers
                index = rdr.tell()-1;
                mat = 1;
                break;
            }
        }
    }
    if index+mat == len {
        return (str::slice(s, 0, index), ~"");
    } else {
        return (str::slice(s, 0, index),
             str::slice(s, index + mat, str::len(s)));
    }
}

fn userinfo_from_str(uinfo: &str) -> UserInfo {
    let (user, p) = split_char_first(uinfo, ':');
    let pass = if str::len(p) == 0 {
        option::None
    } else {
        option::Some(p)
    };
    return UserInfo(user, pass);
}

pure fn userinfo_to_str(userinfo: UserInfo) -> ~str {
    if option::is_some(&userinfo.pass) {
        return str::concat(~[copy userinfo.user, ~":",
                          option::unwrap(copy userinfo.pass),
                          ~"@"]);
    } else {
        return str::concat(~[copy userinfo.user, ~"@"]);
    }
}

impl UserInfo : Eq {
    pure fn eq(other: &UserInfo) -> bool {
        self.user == (*other).user && self.pass == (*other).pass
    }
    pure fn ne(other: &UserInfo) -> bool { !self.eq(other) }
}

fn query_from_str(rawquery: &str) -> Query {
    let mut query: Query = ~[];
    if str::len(rawquery) != 0 {
        for str::split_char(rawquery, '&').each |p| {
            let (k, v) = split_char_first(*p, '=');
            query.push((decode_component(k), decode_component(v)));
        };
    }
    return query;
}

pub pure fn query_to_str(query: Query) -> ~str {
    let mut strvec = ~[];
    for query.each |kv| {
        let (k, v) = copy *kv;
        // This is really safe...
        unsafe {
          strvec += ~[#fmt("%s=%s",
                           encode_component(k), encode_component(v))];
        }
    };
    return str::connect(strvec, ~"&");
}

// returns the scheme and the rest of the url, or a parsing error
pub fn get_scheme(rawurl: &str) -> result::Result<(~str, ~str), @~str> {
    for str::each_chari(rawurl) |i,c| {
        match c {
          'A' .. 'Z' | 'a' .. 'z' => loop,
          '0' .. '9' | '+' | '-' | '.' => {
            if i == 0 {
                return result::Err(@~"url: Scheme must begin with a letter.");
            }
            loop;
          }
          ':' => {
            if i == 0 {
                return result::Err(@~"url: Scheme cannot be empty.");
            } else {
                return result::Ok((rawurl.slice(0,i),
                                rawurl.slice(i+1,str::len(rawurl))));
            }
          }
          _ => {
            return result::Err(@~"url: Invalid character in scheme.");
          }
        }
    };
    return result::Err(@~"url: Scheme must be terminated with a colon.");
}

enum Input {
    Digit, // all digits
    Hex, // digits and letters a-f
    Unreserved // all other legal characters
}

impl Input : Eq {
    pure fn eq(other: &Input) -> bool {
        match (self, (*other)) {
            (Digit, Digit) => true,
            (Hex, Hex) => true,
            (Unreserved, Unreserved) => true,
            (Digit, _) => false,
            (Hex, _) => false,
            (Unreserved, _) => false
        }
    }
    pure fn ne(other: &Input) -> bool { !self.eq(other) }
}

// returns userinfo, host, port, and unparsed part, or an error
fn get_authority(rawurl: &str) ->
    result::Result<(Option<UserInfo>, ~str, Option<~str>, ~str), @~str> {
    if !str::starts_with(rawurl, ~"//") {
        // there is no authority.
        return result::Ok((option::None, ~"", option::None, rawurl.to_str()));
    }

    enum State {
        Start, // starting state
        PassHostPort, // could be in user or port
        Ip6Port, // either in ipv6 host or port
        Ip6Host, // are in an ipv6 host
        InHost, // are in a host - may be ipv6, but don't know yet
        InPort // are in port
    }

    let len = str::len(rawurl);
    let mut st : State = Start;
    let mut in : Input = Digit; // most restricted, start here.

    let mut userinfo : Option<UserInfo> = option::None;
    let mut host : ~str = ~"";
    let mut port : option::Option<~str> = option::None;

    let mut colon_count = 0;
    let mut pos : uint = 0, begin : uint = 2, end : uint = len;

    for str::each_chari(rawurl) |i,c| {
        if i < 2 { loop; } // ignore the leading //

        // deal with input class first
        match c {
          '0' .. '9' => (),
          'A' .. 'F' | 'a' .. 'f' => {
            if in == Digit {
                in = Hex;
            }
          }
          'G' .. 'Z' | 'g' .. 'z' | '-' | '.' | '_' | '~' | '%' |
          '&' |'\'' | '(' | ')' | '+' | '!' | '*' | ',' | ';' | '=' => {
            in = Unreserved;
          }
          ':' | '@' | '?' | '#' | '/' => {
            // separators, don't change anything
          }
          _ => {
            return result::Err(@~"Illegal character in authority");
          }
        }

        // now process states
        match c {
          ':' => {
            colon_count += 1;
            match st {
              Start => {
                pos = i;
                st = PassHostPort;
              }
              PassHostPort => {
                // multiple colons means ipv6 address.
                if in == Unreserved {
                    return result::Err(
                        @~"Illegal characters in IPv6 address.");
                }
                st = Ip6Host;
              }
              InHost => {
                pos = i;
                // can't be sure whether this is an ipv6 address or a port
                if in == Unreserved {
                    return result::Err(@~"Illegal characters in authority.");
                }
                st = Ip6Port;
              }
              Ip6Port => {
                if in == Unreserved {
                    return result::Err(@~"Illegal characters in authority.");
                }
                st = Ip6Host;
              }
              Ip6Host => {
                if colon_count > 7 {
                    host = str::slice(rawurl, begin, i);
                    pos = i;
                    st = InPort;
                }
              }
              _ => {
                return result::Err(@~"Invalid ':' in authority.");
              }
            }
            in = Digit; // reset input class
          }

          '@' => {
            in = Digit; // reset input class
            colon_count = 0; // reset count
            match st {
              Start => {
                let user = str::slice(rawurl, begin, i);
                userinfo = option::Some({user : user,
                                         pass: option::None});
                st = InHost;
              }
              PassHostPort => {
                let user = str::slice(rawurl, begin, pos);
                let pass = str::slice(rawurl, pos+1, i);
                userinfo = option::Some({user: user,
                                         pass: option::Some(pass)});
                st = InHost;
              }
              _ => {
                return result::Err(@~"Invalid '@' in authority.");
              }
            }
            begin = i+1;
          }

          '?' | '#' | '/' => {
            end = i;
            break;
          }
          _ => ()
        }
        end = i;
    }

    let end = end; // make end immutable so it can be captured

    let host_is_end_plus_one: &fn() -> bool = || {
        end+1 == len
            && !['?', '#', '/'].contains(&(rawurl[end] as char))
    };

    // finish up
    match st {
      Start => {
        if host_is_end_plus_one() {
            host = str::slice(rawurl, begin, end+1);
        } else {
            host = str::slice(rawurl, begin, end);
        }
      }
      PassHostPort | Ip6Port => {
        if in != Digit {
            return result::Err(@~"Non-digit characters in port.");
        }
        host = str::slice(rawurl, begin, pos);
        port = option::Some(str::slice(rawurl, pos+1, end));
      }
      Ip6Host | InHost => {
        host = str::slice(rawurl, begin, end);
      }
      InPort => {
        if in != Digit {
            return result::Err(@~"Non-digit characters in port.");
        }
        port = option::Some(str::slice(rawurl, pos+1, end));
      }
    }

    let rest = if host_is_end_plus_one() { ~"" }
    else { str::slice(rawurl, end, len) };
    return result::Ok((userinfo, host, port, rest));
}


// returns the path and unparsed part of url, or an error
fn get_path(rawurl: &str, authority : bool) ->
    result::Result<(~str, ~str), @~str> {
    let len = str::len(rawurl);
    let mut end = len;
    for str::each_chari(rawurl) |i,c| {
        match c {
          'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '&' |'\'' | '(' | ')' | '.'
          | '@' | ':' | '%' | '/' | '+' | '!' | '*' | ',' | ';' | '='
          | '_' | '-' => {
            loop;
          }
          '?' | '#' => {
            end = i;
            break;
          }
          _ => return result::Err(@~"Invalid character in path.")
        }
    }

    if authority {
        if end != 0 && !str::starts_with(rawurl, ~"/") {
            return result::Err(@~"Non-empty path must begin with\
                               '/' in presence of authority.");
        }
    }

    return result::Ok((decode_component(str::slice(rawurl, 0, end)),
                    str::slice(rawurl, end, len)));
}

// returns the parsed query and the fragment, if present
fn get_query_fragment(rawurl: &str) ->
    result::Result<(Query, Option<~str>), @~str> {
    if !str::starts_with(rawurl, ~"?") {
        if str::starts_with(rawurl, ~"#") {
            let f = decode_component(str::slice(rawurl,
                                                1,
                                                str::len(rawurl)));
            return result::Ok((~[], option::Some(f)));
        } else {
            return result::Ok((~[], option::None));
        }
    }
    let (q, r) = split_char_first(str::slice(rawurl, 1,
                                             str::len(rawurl)), '#');
    let f = if str::len(r) != 0 {
        option::Some(decode_component(r)) } else { option::None };
    return result::Ok((query_from_str(q), f));
}

/**
 * Parse a `str` to a `url`
 *
 * # Arguments
 *
 * `rawurl` - a string representing a full url, including scheme.
 *
 * # Returns
 *
 * a `url` that contains the parsed representation of the url.
 *
 */

pub fn from_str(rawurl: &str) -> result::Result<Url, ~str> {
    // scheme
    let mut schm = get_scheme(rawurl);
    if result::is_err(&schm) {
        return result::Err(copy *result::get_err(&schm));
    }
    let (scheme, rest) = result::unwrap(schm);

    // authority
    let mut auth = get_authority(rest);
    if result::is_err(&auth) {
        return result::Err(copy *result::get_err(&auth));
    }
    let (userinfo, host, port, rest) = result::unwrap(auth);

    // path
    let has_authority = if host == ~"" { false } else { true };
    let mut pth = get_path(rest, has_authority);
    if result::is_err(&pth) {
        return result::Err(copy *result::get_err(&pth));
    }
    let (path, rest) = result::unwrap(pth);

    // query and fragment
    let mut qry = get_query_fragment(rest);
    if result::is_err(&qry) {
        return result::Err(copy *result::get_err(&qry));
    }
    let (query, fragment) = result::unwrap(qry);

    return result::Ok(Url(scheme, userinfo, host,
                       port, path, query, fragment));
}

impl Url : FromStr {
    static fn from_str(s: &str) -> Option<Url> {
        match from_str(s) {
            Ok(move url) => Some(url),
            Err(_) => None
        }
    }
}

/**
 * Format a `url` as a string
 *
 * # Arguments
 *
 * `url` - a url.
 *
 * # Returns
 *
 * a `str` that contains the formatted url. Note that this will usually
 * be an inverse of `from_str` but might strip out unneeded separators.
 * for example, "http://somehost.com?", when parsed and formatted, will
 * result in just "http://somehost.com".
 *
 */
pub pure fn to_str(url: Url) -> ~str {
    let user = if url.user.is_some() {
      userinfo_to_str(option::unwrap(copy url.user))
    } else {
       ~""
    };
    let authority = if str::len(url.host) != 0 {
        str::concat(~[~"//", user, copy url.host])
    } else {
        ~""
    };
    let query = if url.query.len() == 0 {
        ~""
    } else {
        str::concat(~[~"?", query_to_str(url.query)])
    };
    // ugh, this really is safe
    let fragment = if url.fragment.is_some() unsafe {
        str::concat(~[~"#", encode_component(
            option::unwrap(copy url.fragment))])
    } else {
        ~""
    };

    return str::concat(~[copy url.scheme,
                      ~":",
                      authority,
                      copy url.path,
                      query,
                      fragment]);
}

impl Url: to_str::ToStr {
    pub pure fn to_str() -> ~str {
        to_str(self)
    }
}

impl Url : Eq {
    pure fn eq(other: &Url) -> bool {
        self.scheme == (*other).scheme
            && self.user == (*other).user
            && self.host == (*other).host
            && self.port == (*other).port
            && self.path == (*other).path
            && self.query == (*other).query
            && self.fragment == (*other).fragment
    }

    pure fn ne(other: &Url) -> bool {
        !self.eq(other)
    }
}

impl Url: IterBytes {
    pure fn iter_bytes(lsb0: bool, f: to_bytes::Cb) {
        unsafe { self.to_str() }.iter_bytes(lsb0, f)
    }
}

#[cfg(test)]
mod tests {
    #[legacy_exports];
    #[test]
    fn test_split_char_first() {
        let (u,v) = split_char_first(~"hello, sweet world", ',');
        assert u == ~"hello";
        assert v == ~" sweet world";

        let (u,v) = split_char_first(~"hello sweet world", ',');
        assert u == ~"hello sweet world";
        assert v == ~"";
    }

    #[test]
    fn test_get_authority() {
        let (u, h, p, r) = result::unwrap(get_authority(
            ~"//user:pass@rust-lang.org/something"));
        assert u == option::Some({user: ~"user",
                                  pass: option::Some(~"pass")});
        assert h == ~"rust-lang.org";
        assert p.is_none();
        assert r == ~"/something";

        let (u, h, p, r) = result::unwrap(get_authority(
            ~"//rust-lang.org:8000?something"));
        assert u.is_none();
        assert h == ~"rust-lang.org";
        assert p == option::Some(~"8000");
        assert r == ~"?something";

        let (u, h, p, r) = result::unwrap(get_authority(
            ~"//rust-lang.org#blah"));
        assert u.is_none();
        assert h == ~"rust-lang.org";
        assert p.is_none();
        assert r == ~"#blah";

        // ipv6 tests
        let (_, h, _, _) = result::unwrap(get_authority(
            ~"//2001:0db8:85a3:0042:0000:8a2e:0370:7334#blah"));
        assert h == ~"2001:0db8:85a3:0042:0000:8a2e:0370:7334";

        let (_, h, p, _) = result::unwrap(get_authority(
            ~"//2001:0db8:85a3:0042:0000:8a2e:0370:7334:8000#blah"));
        assert h == ~"2001:0db8:85a3:0042:0000:8a2e:0370:7334";
        assert p == option::Some(~"8000");

        let (u, h, p, _) = result::unwrap(get_authority(
            ~"//us:p@2001:0db8:85a3:0042:0000:8a2e:0370:7334:8000#blah"));
        assert u == option::Some({user: ~"us", pass : option::Some(~"p")});
        assert h == ~"2001:0db8:85a3:0042:0000:8a2e:0370:7334";
        assert p == option::Some(~"8000");

        // invalid authorities;
        assert result::is_err(&get_authority(
            ~"//user:pass@rust-lang:something"));
        assert result::is_err(&get_authority(
            ~"//user@rust-lang:something:/path"));
        assert result::is_err(&get_authority(
            ~"//2001:0db8:85a3:0042:0000:8a2e:0370:7334:800a"));
        assert result::is_err(&get_authority(
            ~"//2001:0db8:85a3:0042:0000:8a2e:0370:7334:8000:00"));

        // these parse as empty, because they don't start with '//'
        let (_, h, _, _) = result::unwrap(
            get_authority(~"user:pass@rust-lang"));
        assert h == ~"";
        let (_, h, _, _) = result::unwrap(
            get_authority(~"rust-lang.org"));
        assert h == ~"";

    }

    #[test]
    fn test_get_path() {
        let (p, r) = result::unwrap(get_path(
            ~"/something+%20orother", true));
        assert p == ~"/something+ orother";
        assert r == ~"";
        let (p, r) = result::unwrap(get_path(
            ~"test@email.com#fragment", false));
        assert p == ~"test@email.com";
        assert r == ~"#fragment";
        let (p, r) = result::unwrap(get_path(~"/gen/:addr=?q=v", false));
        assert p == ~"/gen/:addr=";
        assert r == ~"?q=v";

        //failure cases
        assert result::is_err(&get_path(~"something?q", true));

    }

    #[test]
    fn test_url_parse() {
        let url = ~"http://user:pass@rust-lang.org/doc?s=v#something";

        let up = from_str(url);
        let u = result::unwrap(up);
        assert u.scheme == ~"http";
        assert option::unwrap(copy u.user).user == ~"user";
        assert option::unwrap(copy option::unwrap(copy u.user).pass)
            == ~"pass";
        assert u.host == ~"rust-lang.org";
        assert u.path == ~"/doc";
        assert u.query.find(|kv| kv.first() == ~"s").get().second() == ~"v";
        assert option::unwrap(copy u.fragment) == ~"something";
    }

    #[test]
    fn test_url_parse_host_slash() {
        let urlstr = ~"http://0.42.42.42/";
        let url = from_str(urlstr).get();
        #debug("url: %?", url);
        assert url.host == ~"0.42.42.42";
        assert url.path == ~"/";
    }

    #[test]
    fn test_url_with_underscores() {
        let urlstr = ~"http://dotcom.com/file_name.html";
        let url = from_str(urlstr).get();
        #debug("url: %?", url);
        assert url.path == ~"/file_name.html";
    }

    #[test]
    fn test_url_with_dashes() {
        let urlstr = ~"http://dotcom.com/file-name.html";
        let url = from_str(urlstr).get();
        #debug("url: %?", url);
        assert url.path == ~"/file-name.html";
    }

    #[test]
    fn test_no_scheme() {
        assert result::is_err(&get_scheme(~"noschemehere.html"));
    }

    #[test]
    fn test_invalid_scheme_errors() {
        assert result::is_err(&from_str(~"99://something"));
        assert result::is_err(&from_str(~"://something"));
    }

    #[test]
    fn test_full_url_parse_and_format() {
        let url = ~"http://user:pass@rust-lang.org/doc?s=v#something";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_userless_url_parse_and_format() {
        let url = ~"http://rust-lang.org/doc?s=v#something";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_queryless_url_parse_and_format() {
        let url = ~"http://user:pass@rust-lang.org/doc#something";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_empty_query_url_parse_and_format() {
        let url = ~"http://user:pass@rust-lang.org/doc?#something";
        let should_be = ~"http://user:pass@rust-lang.org/doc#something";
        assert to_str(result::unwrap(from_str(url))) == should_be;
    }

    #[test]
    fn test_fragmentless_url_parse_and_format() {
        let url = ~"http://user:pass@rust-lang.org/doc?q=v";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_minimal_url_parse_and_format() {
        let url = ~"http://rust-lang.org/doc";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_scheme_host_only_url_parse_and_format() {
        let url = ~"http://rust-lang.org";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_pathless_url_parse_and_format() {
        let url = ~"http://user:pass@rust-lang.org?q=v#something";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_scheme_host_fragment_only_url_parse_and_format() {
        let url = ~"http://rust-lang.org#something";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_url_component_encoding() {
        let url = ~"http://rust-lang.org/doc%20uments?ba%25d%20=%23%26%2B";
        let u = result::unwrap(from_str(url));
        assert u.path == ~"/doc uments";
        assert u.query.find(|kv| kv.first() == ~"ba%d ")
            .get().second() == ~"#&+";
    }

    #[test]
    fn test_url_without_authority() {
        let url = ~"mailto:test@email.com";
        assert to_str(result::unwrap(from_str(url))) == url;
    }

    #[test]
    fn test_encode() {
        assert encode("") == ~"";
        assert encode("http://example.com") == ~"http://example.com";
        assert encode("foo bar% baz") == ~"foo%20bar%25%20baz";
        assert encode(" ") == ~"%20";
        assert encode("!") == ~"!";
        assert encode("\"") == ~"\"";
        assert encode("#") == ~"#";
        assert encode("$") == ~"$";
        assert encode("%") == ~"%25";
        assert encode("&") == ~"&";
        assert encode("'") == ~"%27";
        assert encode("(") == ~"(";
        assert encode(")") == ~")";
        assert encode("*") == ~"*";
        assert encode("+") == ~"+";
        assert encode(",") == ~",";
        assert encode("/") == ~"/";
        assert encode(":") == ~":";
        assert encode(";") == ~";";
        assert encode("=") == ~"=";
        assert encode("?") == ~"?";
        assert encode("@") == ~"@";
        assert encode("[") == ~"[";
        assert encode("]") == ~"]";
    }

    #[test]
    fn test_encode_component() {
        assert encode_component(~"") == ~"";
        assert encode_component(~"http://example.com") ==
            ~"http%3A%2F%2Fexample.com";
        assert encode_component(~"foo bar% baz") == ~"foo%20bar%25%20baz";
        assert encode_component(~" ") == ~"%20";
        assert encode_component(~"!") == ~"%21";
        assert encode_component(~"#") == ~"%23";
        assert encode_component(~"$") == ~"%24";
        assert encode_component(~"%") == ~"%25";
        assert encode_component(~"&") == ~"%26";
        assert encode_component(~"'") == ~"%27";
        assert encode_component(~"(") == ~"%28";
        assert encode_component(~")") == ~"%29";
        assert encode_component(~"*") == ~"%2A";
        assert encode_component(~"+") == ~"%2B";
        assert encode_component(~",") == ~"%2C";
        assert encode_component(~"/") == ~"%2F";
        assert encode_component(~":") == ~"%3A";
        assert encode_component(~";") == ~"%3B";
        assert encode_component(~"=") == ~"%3D";
        assert encode_component(~"?") == ~"%3F";
        assert encode_component(~"@") == ~"%40";
        assert encode_component(~"[") == ~"%5B";
        assert encode_component(~"]") == ~"%5D";
    }

    #[test]
    fn test_decode() {
        assert decode(~"") == ~"";
        assert decode(~"abc/def 123") == ~"abc/def 123";
        assert decode(~"abc%2Fdef%20123") == ~"abc%2Fdef 123";
        assert decode(~"%20") == ~" ";
        assert decode(~"%21") == ~"%21";
        assert decode(~"%22") == ~"%22";
        assert decode(~"%23") == ~"%23";
        assert decode(~"%24") == ~"%24";
        assert decode(~"%25") == ~"%";
        assert decode(~"%26") == ~"%26";
        assert decode(~"%27") == ~"'";
        assert decode(~"%28") == ~"%28";
        assert decode(~"%29") == ~"%29";
        assert decode(~"%2A") == ~"%2A";
        assert decode(~"%2B") == ~"%2B";
        assert decode(~"%2C") == ~"%2C";
        assert decode(~"%2F") == ~"%2F";
        assert decode(~"%3A") == ~"%3A";
        assert decode(~"%3B") == ~"%3B";
        assert decode(~"%3D") == ~"%3D";
        assert decode(~"%3F") == ~"%3F";
        assert decode(~"%40") == ~"%40";
        assert decode(~"%5B") == ~"%5B";
        assert decode(~"%5D") == ~"%5D";
    }

    #[test]
    fn test_decode_component() {
        assert decode_component(~"") == ~"";
        assert decode_component(~"abc/def 123") == ~"abc/def 123";
        assert decode_component(~"abc%2Fdef%20123") == ~"abc/def 123";
        assert decode_component(~"%20") == ~" ";
        assert decode_component(~"%21") == ~"!";
        assert decode_component(~"%22") == ~"\"";
        assert decode_component(~"%23") == ~"#";
        assert decode_component(~"%24") == ~"$";
        assert decode_component(~"%25") == ~"%";
        assert decode_component(~"%26") == ~"&";
        assert decode_component(~"%27") == ~"'";
        assert decode_component(~"%28") == ~"(";
        assert decode_component(~"%29") == ~")";
        assert decode_component(~"%2A") == ~"*";
        assert decode_component(~"%2B") == ~"+";
        assert decode_component(~"%2C") == ~",";
        assert decode_component(~"%2F") == ~"/";
        assert decode_component(~"%3A") == ~":";
        assert decode_component(~"%3B") == ~";";
        assert decode_component(~"%3D") == ~"=";
        assert decode_component(~"%3F") == ~"?";
        assert decode_component(~"%40") == ~"@";
        assert decode_component(~"%5B") == ~"[";
        assert decode_component(~"%5D") == ~"]";
    }

    #[test]
    fn test_encode_form_urlencoded() {
        let m = HashMap();
        assert encode_form_urlencoded(m) == ~"";

        m.insert(~"", @DVec());
        m.insert(~"foo", @DVec());
        assert encode_form_urlencoded(m) == ~"";

        let m = HashMap();
        m.insert(~"foo", @dvec::from_vec(~[@~"bar", @~"123"]));
        assert encode_form_urlencoded(m) == ~"foo=bar&foo=123";

        let m = HashMap();
        m.insert(~"foo bar", @dvec::from_vec(~[@~"abc", @~"12 = 34"]));
        assert encode_form_urlencoded(m) == ~"foo+bar=abc&foo+bar=12+%3D+34";
    }

    #[test]
    fn test_decode_form_urlencoded() {
        assert decode_form_urlencoded(~[]).size() == 0;

        let s = str::to_bytes(~"a=1&foo+bar=abc&foo+bar=12+%3D+34");
        assert decode_form_urlencoded(s).size() == 2;
    }

}

