//! Components.
#![allow(dead_code)]

use core::fmt;

use iri_string::build::Builder;

/// Test case.
#[derive(Debug, Clone, Copy)]
pub struct TestCase<'a> {
    /// Test case name.
    pub name: Option<&'a str>,
    /// Test case description.
    pub description: Option<&'a str>,
    /// Composed string.
    pub composed: &'a str,
    /// Components.
    pub components: Components<'a>,
    /// Normalized string as URI.
    pub normalized_uri: &'a str,
    /// Normalized string as IRI.
    pub normalized_iri: &'a str,
    /// Normalized (by WHATWG-like algorithm) string as URI.
    pub normalized_uri_whatwg_like: Option<&'a str>,
    /// Normalized (by WHATWG-like algorithm) string as IRI.
    pub normalized_iri_whatwg_like: Option<&'a str>,
    /// Different IRIs.
    pub different_iris: &'a [&'a str],
}

impl TestCase<'_> {
    #[inline]
    #[must_use]
    pub fn is_uri_class(&self) -> bool {
        self.composed.is_ascii()
    }

    #[inline]
    #[must_use]
    pub const fn is_iri_class(&self) -> bool {
        true
    }

    #[inline]
    #[must_use]
    pub const fn is_absolute(&self) -> bool {
        self.components.is_absolute()
    }

    #[inline]
    #[must_use]
    pub const fn is_absolute_without_fragment(&self) -> bool {
        self.components.is_absolute_without_fragment()
    }

    #[inline]
    #[must_use]
    pub const fn is_relative(&self) -> bool {
        self.components.is_relative()
    }

    #[inline]
    #[must_use]
    pub fn is_rfc3986_normalizable(&self) -> bool {
        match self.normalized_iri.find('/') {
            Some(pos) => !self.normalized_iri[(pos + 1)..].starts_with("./"),
            None => true,
        }
    }
}

/// Components.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Components<'a> {
    /// `scheme`.
    pub scheme: Option<&'a str>,
    /// User part (string before the first colon) of `userinfo`.
    ///
    /// Note that `host` should also be `Some(_)` if this is `Some(_)`.
    pub user: Option<&'a str>,
    /// Password part (string after the first colon) of `userinfo`.
    ///
    /// Note that `host` should also be `Some(_)` if this is `Some(_)`.
    pub password: Option<&'a str>,
    /// `host`.
    pub host: Option<&'a str>,
    /// `port`.
    ///
    /// Note that `host` should also be `Some(_)` if this is `Some(_)`.
    pub port: Option<&'a str>,
    /// `path`.
    pub path: &'a str,
    /// `query`.
    pub query: Option<&'a str>,
    /// `fragment`.
    pub fragment: Option<&'a str>,
}

impl<'a> Components<'a> {
    #[inline]
    #[must_use]
    const fn const_default() -> Self {
        Self {
            scheme: None,
            user: None,
            password: None,
            host: None,
            port: None,
            path: "",
            query: None,
            fragment: None,
        }
    }

    pub fn feed_builder(&self, builder: &mut Builder<'a>, clean: bool) {
        if let Some(scheme) = self.scheme {
            builder.scheme(scheme);
        } else if clean {
            builder.unset_scheme();
        }

        if let Some(host) = self.host {
            if self.user.is_some() || self.password.is_some() {
                builder.userinfo((self.user.unwrap_or(""), self.password));
            } else if clean {
                builder.unset_userinfo();
            }

            builder.host(host);

            if let Some(port) = self.port {
                builder.port(port);
            } else if clean {
                builder.unset_port();
            }
        } else if clean {
            builder.unset_authority();
        }

        builder.path(self.path);

        if let Some(query) = self.query {
            builder.query(query);
        } else if clean {
            builder.unset_query();
        }

        if let Some(fragment) = self.fragment {
            builder.fragment(fragment);
        } else if clean {
            builder.unset_fragment();
        }
    }

    #[inline]
    #[must_use]
    pub const fn is_absolute(&self) -> bool {
        self.scheme.is_some()
    }

    #[inline]
    #[must_use]
    pub const fn is_absolute_without_fragment(&self) -> bool {
        self.scheme.is_some() && self.fragment.is_none()
    }

    #[inline]
    #[must_use]
    pub const fn is_relative(&self) -> bool {
        self.scheme.is_none()
    }
}

impl fmt::Display for Components<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(scheme) = self.scheme {
            write!(f, "{scheme}:")?;
        }

        assert!(
            self.host.is_some()
                || (self.user.is_none() && self.password.is_none() && self.port.is_none()),
            "`user`, `password`, and `port` requires `host` to be present"
        );
        if let Some(host) = self.host {
            if let Some(user) = self.user {
                f.write_str(user)?;
            }
            if let Some(password) = self.password {
                write!(f, ":{password}")?;
            }
            if self.user.is_some() || self.password.is_some() {
                write!(f, "@")?;
            }
            f.write_str(host)?;
            if let Some(port) = self.port {
                write!(f, ":{port}")?;
            }
        }

        f.write_str(self.path)?;

        if let Some(query) = self.query {
            write!(f, "#{query}")?;
        }

        if let Some(fragment) = self.fragment {
            write!(f, "#{fragment}")?;
        }

        Ok(())
    }
}

macro_rules! components {
    () => {
        Components::default()
    };
    ($($field:ident: $expr:expr),* $(,)?) => {
        Components {
            $( $field: components!(@field; $field: $expr) ),*,
            .. Components::const_default()
        }
    };
    (@field; path: $expr:expr) => {
        $expr
    };
    (@field; $field:ident: None) => {
        None
    };
    (@field; $field:ident: $expr:expr) => {
        Some($expr)
    };
}

macro_rules! test_case {
    // Name.
    (@field=name; name: $value:expr, $($rest:tt)*) => {
        $value
    };
    // Description.
    (@field=description; description: $value:expr, $($rest:tt)*) => {
        Some($value)
    };
    (@field=description;) => {
        None
    };
    // Composed.
    (@field=composed; composed: $value:expr, $($rest:tt)*) => {
        $value
    };
    // Components.
    (@field=components; components: { $($toks:tt)* }, $($rest:tt)*) => {
        components! { $($toks)* }
    };
    // Normalized URI.
    (@field=normalized_uri; normalized_uri: $value:expr, $($rest:tt)*) => {
        $value
    };
    // Normalized IRI.
    (@field=normalized_iri; normalized_iri: $value:expr, $($rest:tt)*) => {
        $value
    };
    // Normalized URI (WHATWG-like).
    (@field=normalized_uri_whatwg_like; normalized_uri_whatwg_like: $value:expr, $($rest:tt)*) => {
        Some($value)
    };
    (@field=normalized_uri_whatwg_like;) => {
        None
    };
    // Normalized IRI (WHATWG-like).
    (@field=normalized_iri_whatwg_like; normalized_iri_whatwg_like: $value:expr, $($rest:tt)*) => {
        Some($value)
    };
    (@field=normalized_iri_whatwg_like;) => {
        None
    };
    // Different IRIs.
    (@field=different_iris; different_iris: $value:expr, $($rest:tt)*) => {
        $value
    };
    (@field=different_iris;) => {
        &[]
    };
    // Fallback.
    (@field=$name:ident; $field:ident: { $($toks:tt)* }, $($rest:tt)*) => {
        test_case!(@field=$name; $($rest)*)
    };
    // Fallback.
    (@field=$name:ident; $field:ident: $value:expr, $($rest:tt)*) => {
        test_case!(@field=$name; $($rest)*)
    };
    ($($args:tt)*) => {
        TestCase {
            name: Some(test_case!(@field=name; $($args)*)),
            description: test_case!(@field=description; $($args)*),
            composed: test_case!(@field=composed; $($args)*),
            components: test_case!(@field=components; $($args)*),
            normalized_uri: test_case!(@field=normalized_uri; $($args)*),
            normalized_iri: test_case!(@field=normalized_iri; $($args)*),
            normalized_uri_whatwg_like: test_case!(@field=normalized_uri_whatwg_like; $($args)*),
            normalized_iri_whatwg_like: test_case!(@field=normalized_iri_whatwg_like; $($args)*),
            different_iris: test_case!(@field=different_iris; $($args)*),
        }
    };
}

macro_rules! test_cases {
    ($({$($toks:tt)*}),* $(,)?) => {
        &[ $( test_case! { $($toks)* } ),* ]
    }
}

#[allow(clippy::needless_update)] // For `components!` macro.
pub static TEST_CASES: &[TestCase<'static>] = test_cases![
    {
        name: "typical example URI",
        composed: "http://example.com/",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/",
        },
        normalized_uri: "http://example.com/",
        normalized_iri: "http://example.com/",
    },
    {
        name: "typical example URI with user and password",
        composed: "http://user:password@example.com/",
        components: {
            scheme: "http",
            user: "user",
            password: "password",
            host: "example.com",
            path: "/",
        },
        normalized_uri: "http://user:password@example.com/",
        normalized_iri: "http://user:password@example.com/",
    },
    {
        name: "URI with ASCII-only hostname with capital letters",
        description: "ASCII-only hostname should be normalized to lower letters",
        composed: "http://usER:passWORD@eXAMPLe.CoM/",
        components: {
            scheme: "http",
            user: "usER",
            password: "passWORD",
            host: "eXAMPLe.CoM",
            path: "/",
        },
        normalized_uri: "http://usER:passWORD@example.com/",
        normalized_iri: "http://usER:passWORD@example.com/",
    },
    {
        name: "IRI with non-ASCII hostname with capital letters",
        description: "hostname with non-ASCII characters should not be normalized to lower letters",
        composed: "http://usER:passWORD@\u{03B1}.CoM/",
        components: {
            scheme: "http",
            user: "usER",
            password: "passWORD",
            host: "\u{03B1}.CoM",
            path: "/",
        },
        // The RFC 3986 (not 3987) spec is ambiguous: if the host contains percent-encoded
        // non-ASCII characters, should other part of the host be lowercased?
        // In this crate for now, the operations is implemented based on RFC 3987, i.e.
        // even URI type internally checks whether the percent-encoded characters
        // would be decoded to ASCII or not.
        normalized_uri: "http://usER:passWORD@%CE%B1.CoM/",
        normalized_iri: "http://usER:passWORD@\u{03B1}.CoM/",
    },
    {
        name: "URI with all components set",
        composed: "http://user:password@example.com:80/path/to/somewhere?query#fragment",
        components: {
            scheme: "http",
            user: "user",
            password: "password",
            host: "example.com",
            port: "80",
            path: "/path/to/somewhere",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "http://user:password@example.com:80/path/to/somewhere?query#fragment",
        normalized_iri: "http://user:password@example.com:80/path/to/somewhere?query#fragment",
    },
    {
        name: "URI that cannot be normalized by pure RFC 3986",
        composed: "scheme:/.//not-a-host",
        components: {
            scheme: "scheme",
            path: "/.//not-a-host",
        },
        normalized_uri: "scheme:/.//not-a-host",
        normalized_iri: "scheme:/.//not-a-host",
    },
    {
        name: "URI that cannot be normalized by pure RFC 3986",
        composed: "scheme:..///not-a-host",
        components: {
            scheme: "scheme",
            path: "..///not-a-host",
        },
        normalized_uri: "scheme:/.//not-a-host",
        normalized_iri: "scheme:/.//not-a-host",
        normalized_uri_whatwg_like: "scheme:..///not-a-host",
        normalized_iri_whatwg_like: "scheme:..///not-a-host",
    },
    {
        name: "Relative URI reference as a relative path `..`",
        description: "Relative path without scheme and authority should not be normalized",
        composed: "..",
        components: {
            path: "..",
        },
        normalized_uri: "..",
        normalized_iri: "..",
    },
    {
        name: "Relative URI reference as a relative path",
        description: "Relative path without scheme and authority should not be normalized",
        composed: "../foo/..",
        components: {
            path: "../foo/..",
        },
        normalized_uri: "../foo/..",
        normalized_iri: "../foo/..",
    },
    {
        name: "Relative URI reference as a relative path",
        description: "Relative path without scheme and authority should not be normalized",
        composed: "foo/../p%61th",
        components: {
            path: "foo/../p%61th",
        },
        normalized_uri: "foo/../path",
        normalized_iri: "foo/../path",
    },
    {
        name: "Relative path in an absolute URI",
        composed: "scheme:foo/../p%61th",
        components: {
            scheme: "scheme",
            path: "foo/../p%61th",
        },
        normalized_uri: "scheme:/path",
        normalized_iri: "scheme:/path",
        normalized_uri_whatwg_like: "scheme:foo/../path",
        normalized_iri_whatwg_like: "scheme:foo/../path",
    },
    {
        name: "Non-normalized URI",
        composed: "HTTPs://EXaMPLE.COM/pA/Th?Query#Frag",
        components: {
            scheme: "HTTPs",
            host: "EXaMPLE.COM",
            path: "/pA/Th",
            query: "Query",
            fragment: "Frag",
        },
        normalized_uri: "https://example.com/pA/Th?Query#Frag",
        normalized_iri: "https://example.com/pA/Th?Query#Frag",
        different_iris: &[
            "https://example.com/pa/th?Query#Frag",
            "https://example.com/pA/Th?query#Frag",
            "https://example.com/pA/Th?Query#frag",
        ],
    },
    {
        name: "UUID URN",
        composed: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        components: {
            scheme: "urn",
            path: "uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        },
        normalized_uri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        normalized_iri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        different_iris: &[
            "urn:UUID:7f1450df-6678-465b-a881-188f9b6ec822",
            "urn:uuid:7F1450DF-6678-465B-A881-188F9B6EC822",
        ],
    },
    {
        name: "UUID URN",
        composed: "URN:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        components: {
            scheme: "URN",
            path: "uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        },
        normalized_uri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        normalized_iri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        different_iris: &[
            "urn:UUID:7f1450df-6678-465b-a881-188f9b6ec822",
            "urn:uuid:7F1450DF-6678-465B-A881-188F9B6EC822",
        ],
    },
    {
        name: "UUID URN",
        composed: "URN:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        components: {
            scheme: "URN",
            path: "uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        },
        normalized_uri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        normalized_iri: "urn:uuid:7f1450df-6678-465b-a881-188f9b6ec822",
        different_iris: &[
            "urn:UUID:7f1450df-6678-465b-a881-188f9b6ec822",
            "urn:uuid:7F1450DF-6678-465B-A881-188F9B6EC822",
        ],
    },
    {
        name: "IRI with percent-encoded unreserved characters and non-valid UTF-8 bytes",
        composed: "http://example.com/?a=%CE%B1&b=%CE%CE%B1%B1",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/",
            query: "a=%CE%B1&b=%CE%CE%B1%B1",
        },
        normalized_uri: "http://example.com/?a=%CE%B1&b=%CE%CE%B1%B1",
        normalized_iri: "http://example.com/?a=\u{03B1}&b=%CE\u{03B1}%B1",
    },
    {
        name: "not ASCII-only host",
        composed: "SCHEME://Alpha%ce%b1/",
        components: {
            scheme: "SCHEME",
            host: "Alpha%ce%b1",
            path: "/",
        },
        normalized_uri: "scheme://Alpha%CE%B1/",
        normalized_iri: "scheme://Alpha\u{03B1}/",
    },
    {
        name: "URI with percent-encoded unreserevd and reserved characters",
        description: "Tilde character (0x7e) is unreserved and bang (0x21) is reserved",
        composed: "http://example.com/%7E%41%73%63%69%69%21",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/%7E%41%73%63%69%69%21",
        },
        normalized_uri: "http://example.com/~Ascii%21",
        normalized_iri: "http://example.com/~Ascii%21",
    },
    {
        name: "not ASCII-only host",
        description: "Plus character (0x2B) is reserved (sub-delim), so it should not be decoded in host part",
        composed: "SCHEME://PLUS%2bPLUS/",
        components: {
            scheme: "SCHEME",
            host: "PLUS%2bPLUS",
            path: "/",
        },
        normalized_uri: "scheme://plus%2Bplus/",
        normalized_iri: "scheme://plus%2Bplus/",
    },
    {
        name: "empty port",
        // <https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.3>:
        //
        // > URI producers and normalizers should omit the port component
        // > and its ":" delimiter if port is empty or if its value would
        // > be the same as that of the scheme's default.
        description: "According to RFC 3986 section 3.2.3, empty port should be omitted by normalization",
        composed: "https://example.com:/",
        components: {
            scheme: "https",
            host: "example.com",
            port: "",
            path: "/",
        },
        normalized_uri: "https://example.com/",
        normalized_iri: "https://example.com/",
    },
    {
        name: "URI with a dot-dot segment",
        composed: "http://example.com/a/b/c/%2e%2e/d/e",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/a/b/c/%2e%2e/d/e",
        },
        normalized_uri: "http://example.com/a/b/d/e",
        normalized_iri: "http://example.com/a/b/d/e",
    },
    {
        name: "URI with a dot-dot segment",
        composed: "http://example.com/a/b/c/%2E%2E/d/e",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/a/b/c/%2E%2E/d/e",
        },
        normalized_uri: "http://example.com/a/b/d/e",
        normalized_iri: "http://example.com/a/b/d/e",
    },
    {
        name: "URI with a dot-dot segment",
        composed: "http://example.com/a/b/c/../d/e",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/a/b/c/../d/e",
        },
        normalized_uri: "http://example.com/a/b/d/e",
        normalized_iri: "http://example.com/a/b/d/e",
    },
    {
        name: "URI with a dot-dot segment",
        composed: "http://example.com/a/b/c/.%2e/d/e",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/a/b/c/.%2e/d/e",
        },
        normalized_uri: "http://example.com/a/b/d/e",
        normalized_iri: "http://example.com/a/b/d/e",
    },
    {
        name: "URI with dot segments",
        composed: "http://example.com/a/./././././b/c/.%2e/d/e",
        components: {
            scheme: "http",
            host: "example.com",
            path: "/a/./././././b/c/.%2e/d/e",
        },
        normalized_uri: "http://example.com/a/b/d/e",
        normalized_iri: "http://example.com/a/b/d/e",
    },
    // START: Combination.
    {
        name: "Empty relative IRI",
        composed: "",
        components: {
            path: "",
        },
        normalized_uri: "",
        normalized_iri: "",
    },
    {
        name: "Combination: fragment",
        composed: "#fragment",
        components: {
            fragment: "fragment",
        },
        normalized_uri: "#fragment",
        normalized_iri: "#fragment",
    },
    {
        name: "Combination: query",
        composed: "?query",
        components: {
            query: "query",
        },
        normalized_uri: "?query",
        normalized_iri: "?query",
    },
    {
        name: "Combination: query+fragment",
        composed: "?query#fragment",
        components: {
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "?query#fragment",
        normalized_iri: "?query#fragment",
    },
    {
        name: "Combination: path",
        composed: "/pa/th",
        components: {
            path: "/pa/th",
        },
        normalized_uri: "/pa/th",
        normalized_iri: "/pa/th",
    },
    {
        name: "Combination: path+fragment",
        composed: "/pa/th#fragment",
        components: {
            path: "/pa/th",
            fragment: "fragment",
        },
        normalized_uri: "/pa/th#fragment",
        normalized_iri: "/pa/th#fragment",
    },
    {
        name: "Combination: path+query",
        composed: "/pa/th?query",
        components: {
            path: "/pa/th",
            query: "query",
        },
        normalized_uri: "/pa/th?query",
        normalized_iri: "/pa/th?query",
    },
    {
        name: "Combination: path+query+fragment",
        composed: "/pa/th?query#fragment",
        components: {
            path: "/pa/th",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "/pa/th?query#fragment",
        normalized_iri: "/pa/th?query#fragment",
    },
    {
        name: "Combination: authority",
        composed: "//authority",
        components: {
            host: "authority",
        },
        normalized_uri: "//authority",
        normalized_iri: "//authority",
    },
    {
        name: "Combination: authority+fragment",
        composed: "//authority#fragment",
        components: {
            host: "authority",
            fragment: "fragment",
        },
        normalized_uri: "//authority#fragment",
        normalized_iri: "//authority#fragment",
    },
    {
        name: "Combination: authority+query",
        composed: "//authority?query",
        components: {
            host: "authority",
            query: "query",
        },
        normalized_uri: "//authority?query",
        normalized_iri: "//authority?query",
    },
    {
        name: "Combination: authority+query+fragment",
        composed: "//authority?query#fragment",
        components: {
            host: "authority",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "//authority?query#fragment",
        normalized_iri: "//authority?query#fragment",
    },
    {
        name: "Combination: authority+path",
        composed: "//authority/pa/th",
        components: {
            host: "authority",
            path: "/pa/th",
        },
        normalized_uri: "//authority/pa/th",
        normalized_iri: "//authority/pa/th",
    },
    {
        name: "Combination: authority+path+fragment",
        composed: "//authority/pa/th#fragment",
        components: {
            host: "authority",
            path: "/pa/th",
            fragment: "fragment",
        },
        normalized_uri: "//authority/pa/th#fragment",
        normalized_iri: "//authority/pa/th#fragment",
    },
    {
        name: "Combination: authority+path+query",
        composed: "//authority/pa/th?query",
        components: {
            host: "authority",
            path: "/pa/th",
            query: "query",
        },
        normalized_uri: "//authority/pa/th?query",
        normalized_iri: "//authority/pa/th?query",
    },
    {
        name: "Combination: authority+path+query+fragment",
        composed: "//authority/pa/th?query#fragment",
        components: {
            host: "authority",
            path: "/pa/th",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "//authority/pa/th?query#fragment",
        normalized_iri: "//authority/pa/th?query#fragment",
    },
    {
        name: "Combination: scheme",
        composed: "scheme:",
        components: {
            scheme: "scheme",
        },
        normalized_uri: "scheme:",
        normalized_iri: "scheme:",
    },
    {
        name: "Combination: scheme+fragment",
        composed: "scheme:#fragment",
        components: {
            scheme: "scheme",
            fragment: "fragment",
        },
        normalized_uri: "scheme:#fragment",
        normalized_iri: "scheme:#fragment",
    },
    {
        name: "Combination: scheme+query",
        composed: "scheme:?query",
        components: {
            scheme: "scheme",
            query: "query",
        },
        normalized_uri: "scheme:?query",
        normalized_iri: "scheme:?query",
    },
    {
        name: "Combination: scheme+query+fragment",
        composed: "scheme:?query#fragment",
        components: {
            scheme: "scheme",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "scheme:?query#fragment",
        normalized_iri: "scheme:?query#fragment",
    },
    {
        name: "Combination: scheme+path",
        composed: "scheme:/pa/th",
        components: {
            scheme: "scheme",
            path: "/pa/th",
        },
        normalized_uri: "scheme:/pa/th",
        normalized_iri: "scheme:/pa/th",
    },
    {
        name: "Combination: scheme+path+fragment",
        composed: "scheme:/pa/th#fragment",
        components: {
            scheme: "scheme",
            path: "/pa/th",
            fragment: "fragment",
        },
        normalized_uri: "scheme:/pa/th#fragment",
        normalized_iri: "scheme:/pa/th#fragment",
    },
    {
        name: "Combination: scheme+path+query",
        composed: "scheme:/pa/th?query",
        components: {
            scheme: "scheme",
            path: "/pa/th",
            query: "query",
        },
        normalized_uri: "scheme:/pa/th?query",
        normalized_iri: "scheme:/pa/th?query",
    },
    {
        name: "Combination: scheme+path+query+fragment",
        composed: "scheme:/pa/th?query#fragment",
        components: {
            scheme: "scheme",
            path: "/pa/th",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "scheme:/pa/th?query#fragment",
        normalized_iri: "scheme:/pa/th?query#fragment",
    },
    {
        name: "Combination: scheme+authority",
        composed: "scheme://authority",
        components: {
            scheme: "scheme",
            host: "authority",
        },
        normalized_uri: "scheme://authority",
        normalized_iri: "scheme://authority",
    },
    {
        name: "Combination: scheme+authority+fragment",
        composed: "scheme://authority#fragment",
        components: {
            scheme: "scheme",
            host: "authority",
            fragment: "fragment",
        },
        normalized_uri: "scheme://authority#fragment",
        normalized_iri: "scheme://authority#fragment",
    },
    {
        name: "Combination: scheme+authority+query",
        composed: "scheme://authority?query",
        components: {
            scheme: "scheme",
            host: "authority",
            query: "query",
        },
        normalized_uri: "scheme://authority?query",
        normalized_iri: "scheme://authority?query",
    },
    {
        name: "Combination: scheme+authority+query+fragment",
        composed: "scheme://authority?query#fragment",
        components: {
            scheme: "scheme",
            host: "authority",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "scheme://authority?query#fragment",
        normalized_iri: "scheme://authority?query#fragment",
    },
    {
        name: "Combination: scheme+authority+path",
        composed: "scheme://authority/pa/th",
        components: {
            scheme: "scheme",
            host: "authority",
            path: "/pa/th",
        },
        normalized_uri: "scheme://authority/pa/th",
        normalized_iri: "scheme://authority/pa/th",
    },
    {
        name: "Combination: scheme+authority+path+fragment",
        composed: "scheme://authority/pa/th#fragment",
        components: {
            scheme: "scheme",
            host: "authority",
            path: "/pa/th",
            fragment: "fragment",
        },
        normalized_uri: "scheme://authority/pa/th#fragment",
        normalized_iri: "scheme://authority/pa/th#fragment",
    },
    {
        name: "Combination: scheme+authority+path+query",
        composed: "scheme://authority/pa/th?query",
        components: {
            scheme: "scheme",
            host: "authority",
            path: "/pa/th",
            query: "query",
        },
        normalized_uri: "scheme://authority/pa/th?query",
        normalized_iri: "scheme://authority/pa/th?query",
    },
    {
        name: "Combination: scheme+authority+path+query+fragment",
        composed: "scheme://authority/pa/th?query#fragment",
        components: {
            scheme: "scheme",
            host: "authority",
            path: "/pa/th",
            query: "query",
            fragment: "fragment",
        },
        normalized_uri: "scheme://authority/pa/th?query#fragment",
        normalized_iri: "scheme://authority/pa/th?query#fragment",
    },
    // END: Combination.
    {
        name: "1 slash following to the scheme",
        composed: "scheme:/",
        components: {
            scheme: "scheme",
            path: "/",
        },
        normalized_uri: "scheme:/",
        normalized_iri: "scheme:/",
    },
    {
        name: "2 slashes following to the scheme",
        composed: "scheme://",
        components: {
            scheme: "scheme",
            host: "",
        },
        normalized_uri: "scheme://",
        normalized_iri: "scheme://",
    },
    {
        name: "3 slashes following to the scheme",
        composed: "scheme:///",
        components: {
            scheme: "scheme",
            host: "",
            path: "/",
        },
        normalized_uri: "scheme:///",
        normalized_iri: "scheme:///",
    },
    {
        name: "4 slashes following to the scheme",
        composed: "scheme:////",
        components: {
            scheme: "scheme",
            host: "",
            path: "//",
        },
        normalized_uri: "scheme:////",
        normalized_iri: "scheme:////",
    },
    {
        name: "5 slashes following to the scheme",
        composed: "scheme://///",
        components: {
            scheme: "scheme",
            host: "",
            path: "///",
        },
        normalized_uri: "scheme://///",
        normalized_iri: "scheme://///",
    },
    {
        name: "1 slash",
        composed: "/",
        components: {
            path: "/",
        },
        normalized_uri: "/",
        normalized_iri: "/",
    },
    {
        name: "2 slash",
        composed: "//",
        components: {
            host: "",
        },
        normalized_uri: "//",
        normalized_iri: "//",
    },
    {
        name: "3 slash",
        composed: "///",
        components: {
            host: "",
            path: "/",
        },
        normalized_uri: "///",
        normalized_iri: "///",
    },
    {
        name: "4 slash",
        composed: "////",
        components: {
            host: "",
            path: "//",
        },
        normalized_uri: "////",
        normalized_iri: "////",
    },
    {
        name: "5 slash",
        composed: "/////",
        components: {
            host: "",
            path: "///",
        },
        normalized_uri: "/////",
        normalized_iri: "/////",
    },
    {
        name: "IPv4 address",
        composed: "//192.0.2.0",
        components: {
            host: "192.0.2.0",
        },
        normalized_uri: "//192.0.2.0",
        normalized_iri: "//192.0.2.0",
    },
    {
        name: "IPv4 address with port",
        composed: "//192.0.2.0:80",
        components: {
            host: "192.0.2.0",
            port: "80",
        },
        normalized_uri: "//192.0.2.0:80",
        normalized_iri: "//192.0.2.0:80",
    },
    {
        name: "IPv4 address",
        composed: "//255.255.255.255",
        components: {
            host: "255.255.255.255",
        },
        normalized_uri: "//255.255.255.255",
        normalized_iri: "//255.255.255.255",
    },
    {
        name: "IPv4 address with port",
        composed: "//255.255.255.255:65536",
        components: {
            host: "255.255.255.255",
            port: "65536",
        },
        normalized_uri: "//255.255.255.255:65536",
        normalized_iri: "//255.255.255.255:65536",
    },
    {
        name: "IPv4 address",
        composed: "//0.0.0.0",
        components: {
            host: "0.0.0.0",
        },
        normalized_uri: "//0.0.0.0",
        normalized_iri: "//0.0.0.0",
    },
    {
        name: "IPv4 address with port",
        composed: "//0.0.0.0:0",
        components: {
            host: "0.0.0.0",
            port: "0",
        },
        normalized_uri: "//0.0.0.0:0",
        normalized_iri: "//0.0.0.0:0",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:db8::]",
        components: {
            host: "[2001:db8::]",
        },
        normalized_uri: "//[2001:db8::]",
        normalized_iri: "//[2001:db8::]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:db8::]:80",
        components: {
            host: "[2001:db8::]",
            port: "80",
        },
        normalized_uri: "//[2001:db8::]:80",
        normalized_iri: "//[2001:db8::]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::]",
        components: {
            host: "[2001:0db8::]",
        },
        normalized_uri: "//[2001:0db8::]",
        normalized_iri: "//[2001:0db8::]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::]:80",
        components: {
            host: "[2001:0db8::]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::]:80",
        normalized_iri: "//[2001:0db8::]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8:0:0:0:0:0:ffff]",
        components: {
            host: "[2001:0db8:0:0:0:0:0:ffff]",
        },
        normalized_uri: "//[2001:0db8:0:0:0:0:0:ffff]",
        normalized_iri: "//[2001:0db8:0:0:0:0:0:ffff]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8:0:0:0:0:0:ffff]:80",
        components: {
            host: "[2001:0db8:0:0:0:0:0:ffff]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0:0:0:0:0:ffff]:80",
        normalized_iri: "//[2001:0db8:0:0:0:0:0:ffff]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]",
        components: {
            host: "[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]",
        },
        normalized_uri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]",
        normalized_iri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]:80",
        components: {
            host: "[2001:0DB8:0000:0000:0000:000A:BCDE:FFFF]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]:80",
        normalized_iri: "//[2001:0db8:0000:0000:0000:000a:bcde:ffff]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::]",
        components: {
            host: "[2001:0db8::]",
        },
        normalized_uri: "//[2001:0db8::]",
        normalized_iri: "//[2001:0db8::]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::]:80",
        components: {
            host: "[2001:0db8::]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::]:80",
        normalized_iri: "//[2001:0db8::]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0DB8:0:0:0:0::1]",
        components: {
            host: "[2001:0DB8:0:0:0:0::1]",
        },
        normalized_uri: "//[2001:0db8:0:0:0:0::1]",
        normalized_iri: "//[2001:0db8:0:0:0:0::1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0DB8:0:0:0:0::1]:80",
        components: {
            host: "[2001:0DB8:0:0:0:0::1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0:0:0:0::1]:80",
        normalized_iri: "//[2001:0db8:0:0:0:0::1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::89ab:cdef:89AB:CDEF]",
        components: {
            host: "[2001:0db8::89ab:cdef:89AB:CDEF]",
        },
        normalized_uri: "//[2001:0db8::89ab:cdef:89ab:cdef]",
        normalized_iri: "//[2001:0db8::89ab:cdef:89ab:cdef]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::89ab:cdef:89AB:CDEF]:80",
        components: {
            host: "[2001:0db8::89ab:cdef:89AB:CDEF]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::89ab:cdef:89ab:cdef]:80",
        normalized_iri: "//[2001:0db8::89ab:cdef:89ab:cdef]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::1]",
        components: {
            host: "[2001:0db8::1]",
        },
        normalized_uri: "//[2001:0db8::1]",
        normalized_iri: "//[2001:0db8::1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::1]:80",
        components: {
            host: "[2001:0db8::1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::1]:80",
        normalized_iri: "//[2001:0db8::1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8:0::1]",
        components: {
            host: "[2001:0db8:0::1]",
        },
        normalized_uri: "//[2001:0db8:0::1]",
        normalized_iri: "//[2001:0db8:0::1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8:0::1]:80",
        components: {
            host: "[2001:0db8:0::1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0::1]:80",
        normalized_iri: "//[2001:0db8:0::1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8:0:0::1]",
        components: {
            host: "[2001:0db8:0:0::1]",
        },
        normalized_uri: "//[2001:0db8:0:0::1]",
        normalized_iri: "//[2001:0db8:0:0::1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8:0:0::1]:80",
        components: {
            host: "[2001:0db8:0:0::1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0:0::1]:80",
        normalized_iri: "//[2001:0db8:0:0::1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8:0:0:0::1]",
        components: {
            host: "[2001:0db8:0:0:0::1]",
        },
        normalized_uri: "//[2001:0db8:0:0:0::1]",
        normalized_iri: "//[2001:0db8:0:0:0::1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8:0:0:0::1]:80",
        components: {
            host: "[2001:0db8:0:0:0::1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0:0:0::1]:80",
        normalized_iri: "//[2001:0db8:0:0:0::1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8:0:0:0:0::1]",
        components: {
            host: "[2001:0db8:0:0:0:0::1]",
        },
        normalized_uri: "//[2001:0db8:0:0:0:0::1]",
        normalized_iri: "//[2001:0db8:0:0:0:0::1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8:0:0:0:0::1]:80",
        components: {
            host: "[2001:0db8:0:0:0:0::1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8:0:0:0:0::1]:80",
        normalized_iri: "//[2001:0db8:0:0:0:0::1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::0:1]",
        components: {
            host: "[2001:0db8::0:1]",
        },
        normalized_uri: "//[2001:0db8::0:1]",
        normalized_iri: "//[2001:0db8::0:1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::0:1]:80",
        components: {
            host: "[2001:0db8::0:1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::0:1]:80",
        normalized_iri: "//[2001:0db8::0:1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::0:0:1]",
        components: {
            host: "[2001:0db8::0:0:1]",
        },
        normalized_uri: "//[2001:0db8::0:0:1]",
        normalized_iri: "//[2001:0db8::0:0:1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::0:0:1]:80",
        components: {
            host: "[2001:0db8::0:0:1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::0:0:1]:80",
        normalized_iri: "//[2001:0db8::0:0:1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::0:0:0:1]",
        components: {
            host: "[2001:0db8::0:0:0:1]",
        },
        normalized_uri: "//[2001:0db8::0:0:0:1]",
        normalized_iri: "//[2001:0db8::0:0:0:1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::0:0:0:1]:80",
        components: {
            host: "[2001:0db8::0:0:0:1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::0:0:0:1]:80",
        normalized_iri: "//[2001:0db8::0:0:0:1]:80",
    },
    {
        name: "IPv6 address",
        composed: "//[2001:0db8::0:0:0:0:1]",
        components: {
            host: "[2001:0db8::0:0:0:0:1]",
        },
        normalized_uri: "//[2001:0db8::0:0:0:0:1]",
        normalized_iri: "//[2001:0db8::0:0:0:0:1]",
    },
    {
        name: "IPv6 address with port",
        composed: "//[2001:0db8::0:0:0:0:1]:80",
        components: {
            host: "[2001:0db8::0:0:0:0:1]",
            port: "80",
        },
        normalized_uri: "//[2001:0db8::0:0:0:0:1]:80",
        normalized_iri: "//[2001:0db8::0:0:0:0:1]:80",
    },
    {
        name: "IPvFuture address",
        composed: "//[v9999.this-is-future-version-of-ip-address:::::::::]",
        components: {
            host: "[v9999.this-is-future-version-of-ip-address:::::::::]",
        },
        normalized_uri: "//[v9999.this-is-future-version-of-ip-address:::::::::]",
        normalized_iri: "//[v9999.this-is-future-version-of-ip-address:::::::::]",
    },
    {
        name: "IPvFuture address with port",
        composed: "//[v9999.this-is-future-version-of-ip-address:::::::::]:80",
        components: {
            host: "[v9999.this-is-future-version-of-ip-address:::::::::]",
            port: "80",
        },
        normalized_uri: "//[v9999.this-is-future-version-of-ip-address:::::::::]:80",
        normalized_iri: "//[v9999.this-is-future-version-of-ip-address:::::::::]:80",
    },
    {
        name: "Too large port",
        description: "RFC 3986 accepts `*DIGIT` as `port` component",
        composed: "//localhost:999999999",
        components: {
            host: "localhost",
            port: "999999999",
        },
        normalized_uri: "//localhost:999999999",
        normalized_iri: "//localhost:999999999",
    },
    {
        name: "Port only",
        description: "`host` can be empty",
        composed: "//:999999999",
        components: {
            host: "",
            port: "999999999",
        },
        normalized_uri: "//:999999999",
        normalized_iri: "//:999999999",
    },
    {
        name: "Trailing slash should remain after normalization",
        description: "Trailing slash should remain after normalization if the path ends with slash",
        composed: "https://example.com/../../",
        components: {
            scheme: "https",
            host: "example.com",
            path: "/../../",
        },
        normalized_uri: "https://example.com/",
        normalized_iri: "https://example.com/",
    },
    {
        name: "Slash should remain",
        description: "Slash should remain after normalization if the IRI ends with a dot segment",
        composed: "https://example.com/..",
        components: {
            scheme: "https",
            host: "example.com",
            path: "/..",
        },
        normalized_uri: "https://example.com/",
        normalized_iri: "https://example.com/",
    },
    {
        name: "Slash should remain",
        description: "Slash should remain after normalization if the IRI ends with a dot segment",
        composed: "https://example.com/.",
        components: {
            scheme: "https",
            host: "example.com",
            path: "/.",
        },
        normalized_uri: "https://example.com/",
        normalized_iri: "https://example.com/",
    },
    {
        name: "WHATWG URL Standard serialization",
        composed: "scheme:/a/b/../..//c",
        components: {
            scheme: "scheme",
            path: "/a/b/../..//c",
        },
        normalized_uri: "scheme:/.//c",
        normalized_iri: "scheme:/.//c",
    },
    {
        name: "WHATWG URL Standard serialization",
        composed: "scheme:/a/b/../..//c",
        components: {
            scheme: "scheme",
            path: "/a/b/../..//c",
        },
        normalized_uri: "scheme:/.//c",
        normalized_iri: "scheme:/.//c",
    },
];
