//! The `PathBuilder` trait used to construct the path used to query endpoints and the types that
//! implement it.

use std::{
    borrow::Cow,
    collections::BTreeSet,
    fmt::{Display, Write},
};

use konst::{iter, slice, string};
use percent_encoding::utf8_percent_encode;
use tracing::warn;

use super::{FeatureFlag, MatrixVersion, SupportedVersions, error::IntoHttpError};
use crate::percent_encode::PATH_PERCENT_ENCODE_SET;

/// Trait implemented by types providing a method to construct the path used to query an endpoint.
///
/// Types implementing this must enforce that all possible paths returned from `select_path()` must
/// contain the same number of variables.
pub trait PathBuilder: Sized {
    /// The input necessary to generate the endpoint URL.
    type Input<'a>;

    /// Pick the right path according to the given input.
    ///
    /// Returns an error if no path could be selected for the given input.
    fn select_path(&self, input: Self::Input<'_>) -> Result<&'static str, IntoHttpError>;

    /// Generate the endpoint URL for this data, considering the given input.
    ///
    /// ## Arguments
    ///
    /// * `input` - The input necessary to select the path.
    /// * `base_url` - The base URL (i.e. the scheme and host) to which the endpoint path will be
    ///   appended. Since all paths begin with a slash, it is not necessary for the this to have a
    ///   trailing slash. If it has one however, it will be ignored.
    /// * `path_args` - The values of the variables in the endpoint's path. The order and number
    ///   must match the order and number of the variables in the path.
    /// * `query_string` - The serialized query string to append to the URL.
    ///
    /// ## Errors
    ///
    /// Returns an error if the `PathBuilder::select_path()` implementation returns an error.
    ///
    /// Panics if the number of `path_args` doesn't match the number of variables in the path
    /// returned by `PathBuilder::select_path()`  must contain the same variables.
    fn make_endpoint_url(
        &self,
        input: Self::Input<'_>,
        base_url: &str,
        path_args: &[&dyn Display],
        query_string: &str,
    ) -> Result<String, IntoHttpError> {
        let path_with_placeholders = self.select_path(input)?;

        let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
        let mut segments = path_with_placeholders.split('/');
        let mut path_args = path_args.iter();

        let first_segment = segments.next().expect("split iterator is never empty");
        assert!(first_segment.is_empty(), "endpoint paths must start with '/'");

        for segment in segments {
            if extract_endpoint_path_segment_variable(segment).is_some() {
                let arg = path_args
                    .next()
                    .expect("number of placeholders must match number of arguments")
                    .to_string();
                let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);

                write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
            } else {
                res.reserve(segment.len() + 1);
                res.push('/');
                res.push_str(segment);
            }
        }

        if !query_string.is_empty() {
            res.push('?');
            res.push_str(query_string);
        }

        Ok(res)
    }

    /// All the possible paths used by the endpoint in canon form.
    ///
    /// This is meant to be used to register paths in server routers.
    fn all_paths(&self) -> impl Iterator<Item = &'static str>;

    /// The list of path parameters in the URL.
    ///
    /// Used for `#[test]`s generated by the API macros.
    #[doc(hidden)]
    fn _path_parameters(&self) -> Vec<&'static str>;
}

/// The complete history of this endpoint as far as Ruma knows, together with all variants on
/// versions stable and unstable.
///
/// The amount and positioning of path variables are the same over all path variants.
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct VersionHistory {
    /// A list of unstable paths over this endpoint's history, mapped to optional unstable
    /// features.
    ///
    /// For endpoint querying purposes, the last item will be used as a fallback.
    unstable_paths: &'static [(Option<&'static str>, &'static str)],

    /// A list of stable paths, mapped to selectors.
    ///
    /// Sorted (ascending) by Matrix version.
    stable_paths: &'static [(StablePathSelector, &'static str)],

    /// The Matrix version that deprecated this endpoint.
    ///
    /// Deprecation often precedes one Matrix version before removal.
    ///
    /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
    /// emit a warning, see the corresponding documentation for more information.
    deprecated: Option<MatrixVersion>,

    /// The Matrix version that removed this endpoint.
    ///
    /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
    /// emit an error, see the corresponding documentation for more information.
    removed: Option<MatrixVersion>,
}

impl VersionHistory {
    /// Constructs an instance of [`VersionHistory`], erroring on compilation if it does not
    /// pass invariants.
    ///
    /// Specifically, this checks the following invariants:
    ///
    /// * Path arguments are equal (in order, amount, and argument name) in all path strings
    /// * In `stable_paths`:
    ///   * Matrix versions are in ascending order
    ///   * No matrix version is referenced twice
    /// * `deprecated`'s version comes after the latest version mentioned in `stable_paths`, except
    ///   for version 1.0, and only if any stable path is defined
    /// * `removed` comes after `deprecated`, or after the latest referenced `stable_paths`, like
    ///   `deprecated`
    ///
    /// ## Arguments
    ///
    /// * `unstable_paths` - List of unstable paths for the endpoint, mapped to optional unstable
    ///   features.
    /// * `stable_paths` - List of stable paths for the endpoint, mapped to selectors.
    /// * `deprecated` - The Matrix version that deprecated the endpoint, if any.
    /// * `removed` - The Matrix version that removed the endpoint, if any.
    pub const fn new(
        unstable_paths: &'static [(Option<&'static str>, &'static str)],
        stable_paths: &'static [(StablePathSelector, &'static str)],
        deprecated: Option<MatrixVersion>,
        removed: Option<MatrixVersion>,
    ) -> Self {
        const fn check_path_args_equal(first: &'static str, second: &'static str) {
            let mut second_iter = string::split(second, "/");

            iter::for_each!(first_s in string::split(first, '/') => {
                if let Some(first_arg) = extract_endpoint_path_segment_variable(first_s) {
                    let second_next_arg: Option<&'static str> = loop {
                        let Some(second_s) = second_iter.next() else {
                            break None;
                        };

                        let maybe_second_arg = extract_endpoint_path_segment_variable(second_s);

                        if let Some(second_arg) = maybe_second_arg {
                            break Some(second_arg);
                        }
                    };

                    if let Some(second_next_arg) = second_next_arg {
                        if !string::eq_str(second_next_arg, first_arg) {
                            panic!("names of endpoint path segment variables do not match");
                        }
                    } else {
                        panic!("counts of endpoint path segment variables do not match");
                    }
                }
            });

            // If second iterator still has some values, empty first.
            while let Some(second_s) = second_iter.next() {
                if extract_endpoint_path_segment_variable(second_s).is_some() {
                    panic!("counts of endpoint path segment variables do not match");
                }
            }
        }

        // The path we're going to use to compare all other paths with
        let ref_path: &str = if let Some((_, s)) = unstable_paths.first() {
            s
        } else if let Some((_, s)) = stable_paths.first() {
            s
        } else {
            panic!("no endpoint paths supplied")
        };

        iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
            check_path_is_valid(unstable_path.1);
            check_path_args_equal(ref_path, unstable_path.1);
        });

        let mut prev_seen_version: Option<MatrixVersion> = None;

        iter::for_each!(version_path in slice::iter(stable_paths) => {
            check_path_is_valid(version_path.1);
            check_path_args_equal(ref_path, version_path.1);

            if let Some(current_version) = version_path.0.version() {
                if let Some(prev_seen_version) = prev_seen_version {
                    let cmp_result = current_version.const_ord(&prev_seen_version);

                    if cmp_result.is_eq() {
                        // Found a duplicate, current == previous
                        panic!("duplicate matrix version in stable paths")
                    } else if cmp_result.is_lt() {
                        // Found an older version, current < previous
                        panic!("stable paths are not in ascending order")
                    }
                }

                prev_seen_version = Some(current_version);
            }
        });

        if let Some(deprecated) = deprecated {
            if let Some(prev_seen_version) = prev_seen_version {
                let ord_result = prev_seen_version.const_ord(&deprecated);
                if !deprecated.is_legacy() && ord_result.is_eq() {
                    // prev_seen_version == deprecated, except for 1.0.
                    // It is possible that an endpoint was both made stable and deprecated in the
                    // legacy versions.
                    panic!("deprecated version is equal to latest stable path version")
                } else if ord_result.is_gt() {
                    // prev_seen_version > deprecated
                    panic!("deprecated version is older than latest stable path version")
                }
            } else {
                panic!("defined deprecated version while no stable path exists")
            }
        }

        if let Some(removed) = removed {
            if let Some(deprecated) = deprecated {
                let ord_result = deprecated.const_ord(&removed);
                if ord_result.is_eq() {
                    // deprecated == removed
                    panic!("removed version is equal to deprecated version")
                } else if ord_result.is_gt() {
                    // deprecated > removed
                    panic!("removed version is older than deprecated version")
                }
            } else {
                panic!("defined removed version while no deprecated version exists")
            }
        }

        Self { unstable_paths, stable_paths, deprecated, removed }
    }

    /// Whether the homeserver advertises support for a path in this [`VersionHistory`].
    ///
    /// Returns `true` if any version or feature in the given [`SupportedVersions`] matches a path
    /// in this history, unless the endpoint was removed.
    ///
    /// Note that this is likely to return false negatives, since some endpoints don't specify a
    /// stable or unstable feature, and homeservers should not advertise support for a Matrix
    /// version unless they support all of its features.
    pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
        match self.versioning_decision_for(&considering.versions) {
            VersioningDecision::Removed => false,
            VersioningDecision::Version { .. } => true,
            VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
        }
    }

    /// Decide which kind of endpoint to use given the supported versions of a homeserver.
    ///
    /// Returns:
    ///
    /// - `Removed` if the endpoint is removed in all supported versions.
    /// - `Version` if the endpoint is stable or deprecated in at least one supported version.
    /// - `Feature` in all other cases, to look if a feature path is supported, or use the last
    ///   unstable path as a fallback.
    ///
    /// If resulting [`VersioningDecision`] is `Stable`, it will also detail if any version denoted
    /// deprecation or removal.
    pub fn versioning_decision_for(
        &self,
        versions: &BTreeSet<MatrixVersion>,
    ) -> VersioningDecision {
        let is_superset_any =
            |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
        let is_superset_all =
            |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));

        // Check if all versions removed this endpoint.
        if self.removed.is_some_and(is_superset_all) {
            return VersioningDecision::Removed;
        }

        // Check if *any* version marks this endpoint as stable.
        if self.added_in().is_some_and(is_superset_any) {
            let all_deprecated = self.deprecated.is_some_and(is_superset_all);

            return VersioningDecision::Version {
                any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
                all_deprecated,
                any_removed: self.removed.is_some_and(is_superset_any),
            };
        }

        VersioningDecision::Feature
    }

    /// Returns the *first* version this endpoint was added in.
    ///
    /// Is `None` when this endpoint is unstable/unreleased.
    pub fn added_in(&self) -> Option<MatrixVersion> {
        self.stable_paths.iter().find_map(|(v, _)| v.version())
    }

    /// Returns the Matrix version that deprecated this endpoint, if any.
    pub fn deprecated_in(&self) -> Option<MatrixVersion> {
        self.deprecated
    }

    /// Returns the Matrix version that removed this endpoint, if any.
    pub fn removed_in(&self) -> Option<MatrixVersion> {
        self.removed
    }

    /// Picks the last unstable path, if it exists.
    pub fn unstable(&self) -> Option<&'static str> {
        self.unstable_paths.last().map(|(_, path)| *path)
    }

    /// Returns all unstable path variants in canon form, with optional corresponding feature.
    pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
        self.unstable_paths.iter().copied()
    }

    /// Returns all version path variants in canon form, with corresponding selector.
    pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
        self.stable_paths.iter().copied()
    }

    /// The path that should be used to query the endpoint, given a set of supported versions.
    ///
    /// Picks the latest path that the versions accept.
    ///
    /// Returns an endpoint in the following format;
    /// - `/_matrix/client/versions`
    /// - `/_matrix/client/hello/{world}` (`{world}` is a path replacement parameter)
    ///
    /// Note: This doesn't handle endpoint removals, check with
    /// [`versioning_decision_for`](VersionHistory::versioning_decision_for) to see if this endpoint
    /// is still available.
    pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
        let version_paths = self
            .stable_paths
            .iter()
            .filter_map(|(selector, path)| selector.version().map(|version| (version, path)));

        // Reverse the iterator, to check the "latest" version first.
        for (ver, path) in version_paths.rev() {
            // Check if any of the versions are equal or greater than the version the path needs.
            if versions.iter().any(|v| v.is_superset_of(ver)) {
                return Some(path);
            }
        }

        None
    }

    /// The path that should be used to query the endpoint, given a list of supported features.
    pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
        let unstable_feature_paths = self
            .unstable_paths
            .iter()
            .filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
        let stable_feature_paths = self
            .stable_paths
            .iter()
            .filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));

        // Reverse the iterator, to check the "latest" features first.
        for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
            // Return the path of the first supported feature.
            if supported_features.iter().any(|supported| supported.as_str() == feature) {
                return Some(path);
            }
        }

        None
    }
}

impl PathBuilder for VersionHistory {
    type Input<'a> = Cow<'a, SupportedVersions>;

    /// Pick the right path according to the given input.
    ///
    /// This will fail if, for every version in `input`;
    /// - The endpoint is too old, and has been removed in all versions.
    ///   ([`EndpointRemoved`](super::error::IntoHttpError::EndpointRemoved))
    /// - The endpoint is too new, and no unstable path is known for this endpoint.
    ///   ([`NoUnstablePath`](super::error::IntoHttpError::NoUnstablePath))
    ///
    /// Finally, this will emit a warning through [`tracing`] if it detects that any version in
    /// `input` has deprecated this endpoint.
    fn select_path(
        &self,
        input: Cow<'_, SupportedVersions>,
    ) -> Result<&'static str, IntoHttpError> {
        match self.versioning_decision_for(&input.versions) {
            VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
                self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
            )),
            VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => {
                if any_removed {
                    if all_deprecated {
                        warn!(
                            "endpoint is removed in some (and deprecated in ALL) \
                             of the following versions: {:?}",
                            input.versions
                        );
                    } else if any_deprecated {
                        warn!(
                            "endpoint is removed (and deprecated) in some of the \
                             following versions: {:?}",
                            input.versions
                        );
                    } else {
                        unreachable!("any_removed implies *_deprecated");
                    }
                } else if all_deprecated {
                    warn!(
                        "endpoint is deprecated in ALL of the following versions: {:?}",
                        input.versions
                    );
                } else if any_deprecated {
                    warn!(
                        "endpoint is deprecated in some of the following versions: {:?}",
                        input.versions
                    );
                }

                Ok(self
                    .version_path(&input.versions)
                    .expect("VersioningDecision::Version implies that a version path exists"))
            }
            VersioningDecision::Feature => self
                .feature_path(&input.features)
                .or_else(|| self.unstable())
                .ok_or(IntoHttpError::NoUnstablePath),
        }
    }

    fn all_paths(&self) -> impl Iterator<Item = &'static str> {
        self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
    }

    fn _path_parameters(&self) -> Vec<&'static str> {
        let path = self.all_paths().next().unwrap();
        path.split('/').filter_map(extract_endpoint_path_segment_variable).collect()
    }
}

/// A versioning "decision" derived from a set of Matrix versions.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[allow(clippy::exhaustive_enums)]
pub enum VersioningDecision {
    /// A feature path should be used, or a fallback.
    Feature,

    /// A path with a Matrix version should be used.
    Version {
        /// If any version denoted deprecation.
        any_deprecated: bool,

        /// If *all* versions denoted deprecation.
        all_deprecated: bool,

        /// If any version denoted removal.
        any_removed: bool,
    },

    /// This endpoint was removed in all versions, it should not be used.
    Removed,
}

/// A selector for a stable path of an endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum StablePathSelector {
    /// The path is available with the given stable feature.
    Feature(&'static str),

    /// The path was added in the given Matrix version.
    Version(MatrixVersion),

    /// The path is available via a stable feature and was added in a Matrix version.
    FeatureAndVersion {
        /// The stable feature that adds support for the path.
        feature: &'static str,
        /// The Matrix version when the path was added.
        version: MatrixVersion,
    },
}

impl StablePathSelector {
    /// The feature that adds support for the path, if any.
    pub const fn feature(self) -> Option<&'static str> {
        match self {
            Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
            _ => None,
        }
    }

    /// The Matrix version when the path was added, if any.
    pub const fn version(self) -> Option<MatrixVersion> {
        match self {
            Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
            _ => None,
        }
    }
}

impl From<MatrixVersion> for StablePathSelector {
    fn from(value: MatrixVersion) -> Self {
        Self::Version(value)
    }
}

/// The endpoint has a single path.
///
/// This means that the endpoint has no path history, or the Matrix spec has no way to manage path
/// history in the API that it is a part of.
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct SinglePath(&'static str);

impl SinglePath {
    /// Construct a new `SinglePath` for the given path.
    pub const fn new(path: &'static str) -> Self {
        check_path_is_valid(path);

        // Check that path variables are valid.
        iter::for_each!(segment in string::split(path, '/') => {
            extract_endpoint_path_segment_variable(segment);
        });

        Self(path)
    }

    /// The path of the endpoint.
    pub fn path(&self) -> &'static str {
        self.0
    }
}

impl PathBuilder for SinglePath {
    type Input<'a> = ();

    fn select_path(&self, _input: ()) -> Result<&'static str, IntoHttpError> {
        Ok(self.0)
    }

    fn all_paths(&self) -> impl Iterator<Item = &'static str> {
        std::iter::once(self.0)
    }

    fn _path_parameters(&self) -> Vec<&'static str> {
        self.0.split('/').filter_map(extract_endpoint_path_segment_variable).collect()
    }
}

/// Check that the given path is valid.
///
/// Panics if the path contains invalid (non-ascii or whitespace) characters.
const fn check_path_is_valid(path: &'static str) {
    iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
        match *path_b {
            0x21..=0x7E => {},
            _ => panic!("path contains invalid (non-ascii or whitespace) characters")
        }
    });
}

/// Extract the variable of the given endpoint path segment.
///
/// The supported syntax for an endpoint path segment variable is `{var}`.
///
/// Returns the name of the variable if one was found in the segment, `None` if no variable was
/// found.
///
/// Panics if:
///
/// * The segment begins with `{` but doesn't end with `}`.
/// * The segment ends with `}` but doesn't begin with `{`.
/// * The segment begins with `:`, which matches the old syntax for endpoint path segment variables.
pub const fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
    if string::starts_with(segment, ':') {
        panic!("endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`");
    }

    if let Some(s) = string::strip_prefix(segment, '{') {
        let var = string::strip_suffix(s, '}')
            .expect("endpoint path segment variable braces mismatch: missing ending `}`");
        return Some(var);
    }

    if string::ends_with(segment, '}') {
        panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
    }

    None
}

#[cfg(test)]
mod tests {
    use std::{
        borrow::Cow,
        collections::{BTreeMap, BTreeSet},
    };

    use assert_matches2::assert_matches;

    use super::{PathBuilder, StablePathSelector, VersionHistory};
    use crate::api::{
        MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
        SupportedVersions,
        error::IntoHttpError,
    };

    fn stable_only_history(
        stable_paths: &'static [(StablePathSelector, &'static str)],
    ) -> VersionHistory {
        VersionHistory { unstable_paths: &[], stable_paths, deprecated: None, removed: None }
    }

    fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
        SupportedVersions {
            versions: versions.iter().copied().collect(),
            features: BTreeSet::new(),
        }
    }

    // TODO add test that can hook into tracing and verify the deprecation warning is emitted

    #[test]
    fn make_simple_endpoint_url() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s")]);

        let url = history
            .make_endpoint_url(
                Cow::Owned(version_only_supported(&[V1_0])),
                "https://example.org",
                &[],
                "",
            )
            .unwrap();
        assert_eq!(url, "https://example.org/s");
    }

    #[test]
    fn make_endpoint_url_with_path_args() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
        let url = history
            .make_endpoint_url(
                Cow::Owned(version_only_supported(&[V1_0])),
                "https://example.org",
                &[&"123"],
                "",
            )
            .unwrap();
        assert_eq!(url, "https://example.org/s/123");
    }

    #[test]
    fn make_endpoint_url_with_path_args_with_dash() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
        let url = history
            .make_endpoint_url(
                Cow::Owned(version_only_supported(&[V1_0])),
                "https://example.org",
                &[&"my-path"],
                "",
            )
            .unwrap();
        assert_eq!(url, "https://example.org/s/my-path");
    }

    #[test]
    fn make_endpoint_url_with_path_args_with_reserved_char() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
        let url = history
            .make_endpoint_url(
                Cow::Owned(version_only_supported(&[V1_0])),
                "https://example.org",
                &[&"#path"],
                "",
            )
            .unwrap();
        assert_eq!(url, "https://example.org/s/%23path");
    }

    #[test]
    fn make_endpoint_url_with_query() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/")]);
        let url = history
            .make_endpoint_url(
                Cow::Owned(version_only_supported(&[V1_0])),
                "https://example.org",
                &[],
                "foo=bar",
            )
            .unwrap();
        assert_eq!(url, "https://example.org/s/?foo=bar");
    }

    #[test]
    #[should_panic]
    fn make_endpoint_url_wrong_num_path_args() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
        _ = history.make_endpoint_url(
            Cow::Owned(version_only_supported(&[V1_0])),
            "https://example.org",
            &[],
            "",
        );
    }

    const EMPTY: VersionHistory =
        VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };

    #[test]
    fn select_version() {
        let version_supported = version_only_supported(&[V1_0, V1_1]);
        let superset_supported = version_only_supported(&[V1_1]);

        // With version only.
        let hist =
            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
        assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
        assert!(hist.is_supported(&version_supported));
        assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
        assert!(hist.is_supported(&superset_supported));

        // With feature and version.
        let hist = VersionHistory {
            stable_paths: &[(
                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
                "/s",
            )],
            ..EMPTY
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
        assert!(hist.is_supported(&version_supported));
        assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
        assert!(hist.is_supported(&superset_supported));

        // Select latest stable version.
        let hist = VersionHistory {
            stable_paths: &[
                (StablePathSelector::Version(V1_0), "/s_v1"),
                (StablePathSelector::Version(V1_1), "/s_v2"),
            ],
            ..EMPTY
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s_v2"));
        assert!(hist.is_supported(&version_supported));

        // With unstable feature.
        let unstable_supported = SupportedVersions {
            versions: [V1_0].into(),
            features: ["org.boo.unstable".into()].into(),
        };
        let hist = VersionHistory {
            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
            stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
            ..EMPTY
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&unstable_supported)), Ok("/s"));
        assert!(hist.is_supported(&unstable_supported));
    }

    #[test]
    fn select_stable_feature() {
        let supported = SupportedVersions {
            versions: [V1_1].into(),
            features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
        };

        // With feature only.
        let hist = VersionHistory {
            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
            stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
            ..EMPTY
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
        assert!(hist.is_supported(&supported));

        // With feature and version.
        let hist = VersionHistory {
            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
            stable_paths: &[(
                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
                "/s",
            )],
            ..EMPTY
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
        assert!(hist.is_supported(&supported));
    }

    #[test]
    fn select_unstable_feature() {
        let supported = SupportedVersions {
            versions: [V1_1].into(),
            features: ["org.boo.unstable".into()].into(),
        };

        let hist = VersionHistory {
            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
            stable_paths: &[(
                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
                "/s",
            )],
            ..EMPTY
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
        assert!(hist.is_supported(&supported));
    }

    #[test]
    fn select_unstable_fallback() {
        let supported = version_only_supported(&[V1_0]);
        let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
        assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
        assert!(!hist.is_supported(&supported));
    }

    #[test]
    fn select_r0() {
        let supported = version_only_supported(&[V1_0]);
        let hist =
            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
        assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/r"));
        assert!(hist.is_supported(&supported));
    }

    #[test]
    fn select_removed_err() {
        let supported = version_only_supported(&[V1_3]);
        let hist = VersionHistory {
            stable_paths: &[
                (StablePathSelector::Version(V1_0), "/r"),
                (StablePathSelector::Version(V1_1), "/s"),
            ],
            unstable_paths: &[(None, "/u")],
            deprecated: Some(V1_2),
            removed: Some(V1_3),
        };
        assert_matches!(
            hist.select_path(Cow::Borrowed(&supported)),
            Err(IntoHttpError::EndpointRemoved(V1_3))
        );
        assert!(!hist.is_supported(&supported));
    }

    #[test]
    fn partially_removed_but_stable() {
        let supported = version_only_supported(&[V1_2]);
        let hist = VersionHistory {
            stable_paths: &[
                (StablePathSelector::Version(V1_0), "/r"),
                (StablePathSelector::Version(V1_1), "/s"),
            ],
            unstable_paths: &[],
            deprecated: Some(V1_2),
            removed: Some(V1_3),
        };
        assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
        assert!(hist.is_supported(&supported));
    }

    #[test]
    fn no_unstable() {
        let supported = version_only_supported(&[V1_0]);
        let hist =
            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
        assert_matches!(
            hist.select_path(Cow::Borrowed(&supported)),
            Err(IntoHttpError::NoUnstablePath)
        );
        assert!(!hist.is_supported(&supported));
    }

    #[test]
    fn version_literal() {
        const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");

        assert_eq!(LIT, V1_0);
    }

    #[test]
    fn parse_as_str_sanity() {
        let version = MatrixVersion::try_from("r0.5.0").unwrap();
        assert_eq!(version, V1_0);
        assert_eq!(version.as_str(), None);

        let version = MatrixVersion::try_from("v1.1").unwrap();
        assert_eq!(version, V1_1);
        assert_eq!(version.as_str(), Some("v1.1"));
    }

    #[test]
    fn supported_versions_from_parts() {
        let empty_features = BTreeMap::new();

        let none = &[];
        let none_supported = SupportedVersions::from_parts(none, &empty_features);
        assert_eq!(none_supported.versions, BTreeSet::new());
        assert_eq!(none_supported.features, BTreeSet::new());

        let single_known = &["r0.6.0".to_owned()];
        let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
        assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
        assert_eq!(single_known_supported.features, BTreeSet::new());

        let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
        let multiple_known_supported =
            SupportedVersions::from_parts(multiple_known, &empty_features);
        assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
        assert_eq!(multiple_known_supported.features, BTreeSet::new());

        let single_unknown = &["v0.0".to_owned()];
        let single_unknown_supported =
            SupportedVersions::from_parts(single_unknown, &empty_features);
        assert_eq!(single_unknown_supported.versions, BTreeSet::new());
        assert_eq!(single_unknown_supported.features, BTreeSet::new());

        let mut features = BTreeMap::new();
        features.insert("org.bar.enabled_1".to_owned(), true);
        features.insert("org.bar.disabled".to_owned(), false);
        features.insert("org.bar.enabled_2".to_owned(), true);

        let features_supported = SupportedVersions::from_parts(single_known, &features);
        assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
        assert_eq!(
            features_supported.features,
            ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
        );
    }

    #[test]
    fn supported_versions_from_parts_order() {
        let empty_features = BTreeMap::new();

        let sorted = &[
            "r0.0.1".to_owned(),
            "r0.5.0".to_owned(),
            "r0.6.0".to_owned(),
            "r0.6.1".to_owned(),
            "v1.1".to_owned(),
            "v1.2".to_owned(),
        ];
        let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
        assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));

        let sorted_reverse = &[
            "v1.2".to_owned(),
            "v1.1".to_owned(),
            "r0.6.1".to_owned(),
            "r0.6.0".to_owned(),
            "r0.5.0".to_owned(),
            "r0.0.1".to_owned(),
        ];
        let sorted_reverse_supported =
            SupportedVersions::from_parts(sorted_reverse, &empty_features);
        assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));

        let random_order = &[
            "v1.1".to_owned(),
            "r0.6.1".to_owned(),
            "r0.5.0".to_owned(),
            "r0.6.0".to_owned(),
            "r0.0.1".to_owned(),
            "v1.2".to_owned(),
        ];
        let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
        assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
    }

    #[test]
    #[should_panic]
    fn make_endpoint_url_with_path_args_old_syntax() {
        let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
        let url = history
            .make_endpoint_url(
                Cow::Owned(version_only_supported(&[V1_0])),
                "https://example.org",
                &[&"123"],
                "",
            )
            .unwrap();
        assert_eq!(url, "https://example.org/s/123");
    }
}
